mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-16 06:03:44 +08:00
v2.0
This commit is contained in:
3
web/src/layout/components/Footer/index.ts
Normal file
3
web/src/layout/components/Footer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import PageFooter from './index.vue';
|
||||
|
||||
export { PageFooter };
|
||||
56
web/src/layout/components/Footer/index.vue
Normal file
56
web/src/layout/components/Footer/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="page-footer">
|
||||
<div class="page-footer-link">
|
||||
<a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 官网 </a>
|
||||
<a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 社区 </a>
|
||||
<a href="https://github.com/jekip/naive-ui-admin/issues" target="_blank"> 交流 </a>
|
||||
</div>
|
||||
<div class="copyright"> naive-ui-admin 1.4 · Made by Ah jung </div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageFooter',
|
||||
components: {},
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-footer {
|
||||
//margin: 28px 0 24px 0;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: #515a6e;
|
||||
}
|
||||
}
|
||||
|
||||
&-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
a:not(:last-child) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: #808695;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
web/src/layout/components/Header/PopoverMessage.vue
Normal file
65
web/src/layout/components/Header/PopoverMessage.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<n-card
|
||||
:content-style="{ padding: '0px' }"
|
||||
:footer-style="{ padding: '0px' }"
|
||||
:bordered="false"
|
||||
:segmented="true"
|
||||
>
|
||||
<div v-if="notificationStore.messages.length > 0">
|
||||
<div
|
||||
class="flex items-center max-w-sm p-1 mx-auto space-x-2 rounded-xl"
|
||||
v-for="(item, index) of notificationStore.messages"
|
||||
:key="index"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<n-icon size="40" color="#f00">
|
||||
<NotificationsCircle />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ item.title }}</div>
|
||||
<n-ellipsis :line-clamp="1" class="text-gray-500">{{ item.content }}</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else description="暂无消息哦~" class="pt-20 pb-20" />
|
||||
<template #footer>
|
||||
<div class="flex justify-evenly">
|
||||
<n-button type="text" @click="onClearMessage">清空提醒</n-button>
|
||||
<n-button type="text" @click="onAllMessage">查看更多</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { NotificationsCircle } from '@vicons/ionicons5';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PopoverMessage',
|
||||
components: { NotificationsCircle },
|
||||
emits: ['clear'],
|
||||
setup(_props, { emit }) {
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
const router = useRouter();
|
||||
|
||||
function onClearMessage() {
|
||||
notificationStore.setMessages([]);
|
||||
emit('clear');
|
||||
}
|
||||
|
||||
function onAllMessage() {
|
||||
router.push({ name: 'apply_notice' });
|
||||
}
|
||||
|
||||
return {
|
||||
onClearMessage,
|
||||
notificationStore,
|
||||
onAllMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
384
web/src/layout/components/Header/ProjectSetting.vue
Normal file
384
web/src/layout/components/Header/ProjectSetting.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
|
||||
<n-drawer-content :title="title" :native-scrollbar="false">
|
||||
<div class="drawer">
|
||||
<n-divider title-placement="center">主题</n-divider>
|
||||
|
||||
<div class="justify-center drawer-setting-item dark-switch">
|
||||
<n-tooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch">
|
||||
<template #checked>
|
||||
<n-icon size="14" color="#ffd93b">
|
||||
<SunnySharp />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #unchecked>
|
||||
<n-icon size="14" color="#ffd93b">
|
||||
<Moon />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<span>{{ designStore.darkTheme ? '深' : '浅' }}色主题</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
|
||||
<n-divider title-placement="center">系统主题</n-divider>
|
||||
|
||||
<div class="drawer-setting-item align-items-top">
|
||||
<span
|
||||
class="theme-item"
|
||||
v-for="(item, index) in appThemeList"
|
||||
:key="index"
|
||||
:style="{ 'background-color': item }"
|
||||
@click="togTheme(item)"
|
||||
>
|
||||
<n-icon size="12" v-if="item === designStore.appTheme">
|
||||
<CheckOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<n-divider title-placement="center">导航栏模式</n-divider>
|
||||
|
||||
<div class="drawer-setting-item align-items-top">
|
||||
<div class="drawer-setting-item-style align-items-top">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/nav-theme-dark.svg"
|
||||
@click="togNavMode('vertical')"
|
||||
alt="左侧菜单模式"
|
||||
/>
|
||||
</template>
|
||||
<span>左侧菜单模式</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'vertical'" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/nav-horizontal.svg"
|
||||
alt="顶部菜单模式"
|
||||
@click="togNavMode('horizontal')"
|
||||
/>
|
||||
</template>
|
||||
<span>顶部菜单模式</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/nav-horizontal-mix.svg"
|
||||
@click="togNavMode('horizontal-mix')"
|
||||
alt="顶部菜单混合模式"
|
||||
/>
|
||||
</template>
|
||||
<span>顶部菜单混合模式</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal-mix'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-divider title-placement="center">导航栏风格</n-divider>
|
||||
|
||||
<div class="drawer-setting-item align-items-top">
|
||||
<div class="drawer-setting-item-style align-items-top">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/nav-theme-dark.svg"
|
||||
alt="暗色侧边栏"
|
||||
@click="togNavTheme('dark')"
|
||||
/>
|
||||
</template>
|
||||
<span>暗色侧边栏</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'dark'" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/nav-theme-light.svg"
|
||||
alt="白色侧边栏"
|
||||
@click="togNavTheme('light')"
|
||||
/>
|
||||
</template>
|
||||
<span>白色侧边栏</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'light'" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img
|
||||
src="~@/assets/images/header-theme-dark.svg"
|
||||
@click="togNavTheme('header-dark')"
|
||||
alt="暗色顶栏"
|
||||
/>
|
||||
</template>
|
||||
<span>暗色顶栏</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'header-dark'" />
|
||||
</div>
|
||||
</div>
|
||||
<n-divider title-placement="center">界面功能</n-divider>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 分割菜单 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch
|
||||
:disabled="settingStore.navMode !== 'horizontal-mix'"
|
||||
v-model:value="settingStore.menuSetting.mixMenu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 固定顶栏 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.headerSetting.fixed" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="drawer-setting-item">-->
|
||||
<!-- <div class="drawer-setting-item-title">-->
|
||||
<!-- 固定侧边栏-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="drawer-setting-item-action">-->
|
||||
<!-- <n-switch v-model:value="settingStore.menuSetting.fixed" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 固定多页签 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.multiTabsSetting.fixed" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-divider title-placement="center">界面显示</n-divider>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 显示重载页面按钮 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.headerSetting.isReload" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 显示面包屑导航 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.crumbsSetting.show" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 显示面包屑显示图标 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.crumbsSetting.showIcon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 显示多页签 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.multiTabsSetting.show" />
|
||||
</div>
|
||||
</div>
|
||||
<!--1.15废弃,没啥用,占用操作空间-->
|
||||
<!-- <div class="drawer-setting-item">-->
|
||||
<!-- <div class="drawer-setting-item-title"> 显示页脚 </div>-->
|
||||
<!-- <div class="drawer-setting-item-action">-->
|
||||
<!-- <n-switch v-model:value="settingStore.showFooter" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<n-divider title-placement="center">动画</n-divider>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 禁用动画 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
<n-switch v-model:value="settingStore.isPageAnimate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<div class="drawer-setting-item-title"> 动画类型 </div>
|
||||
<div class="drawer-setting-item-select">
|
||||
<n-select v-model:value="settingStore.pageAnimateType" :options="animateOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item">
|
||||
<n-alert type="warning" :showIcon="false">
|
||||
<p>{{ alertText }}</p>
|
||||
</n-alert>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, unref, watch, computed } from 'vue';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||
import { CheckOutlined } from '@vicons/antd';
|
||||
import { Moon, SunnySharp } from '@vicons/ionicons5';
|
||||
import { darkTheme } from 'naive-ui';
|
||||
import { animates as animateOptions } from '@/settings/animateSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProjectSetting',
|
||||
components: { CheckOutlined, Moon, SunnySharp },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '项目配置',
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 280,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const settingStore = useProjectSettingStore();
|
||||
const designStore = useDesignSettingStore();
|
||||
const state = reactive({
|
||||
width: props.width,
|
||||
title: props.title,
|
||||
isDrawer: false,
|
||||
placement: 'right',
|
||||
alertText:
|
||||
'该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
|
||||
appThemeList: designStore.appThemeList,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => designStore.darkTheme,
|
||||
(to) => {
|
||||
settingStore.navTheme = to ? 'header-dark' : 'dark';
|
||||
}
|
||||
);
|
||||
|
||||
const directionsOptions = computed(() => {
|
||||
return animateOptions.find((item) => item.value == unref(settingStore.pageAnimateType));
|
||||
});
|
||||
|
||||
function openDrawer() {
|
||||
state.isDrawer = true;
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
state.isDrawer = false;
|
||||
}
|
||||
|
||||
function togNavTheme(theme) {
|
||||
settingStore.navTheme = theme;
|
||||
if (settingStore.navMode === 'horizontal' && ['light'].includes(theme)) {
|
||||
settingStore.navTheme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
function togTheme(color) {
|
||||
designStore.appTheme = color;
|
||||
}
|
||||
|
||||
function togNavMode(mode) {
|
||||
settingStore.navMode = mode;
|
||||
settingStore.menuSetting.mixMenu = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
settingStore,
|
||||
designStore,
|
||||
togNavTheme,
|
||||
togNavMode,
|
||||
togTheme,
|
||||
darkTheme,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
animateOptions,
|
||||
directionsOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.drawer {
|
||||
.n-divider:not(.n-divider--vertical) {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
&-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-style {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-title {
|
||||
flex: 1 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-action {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 2px;
|
||||
margin: 0 5px 5px 0;
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
|
||||
.n-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.align-items-top {
|
||||
align-items: flex-start;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dark-switch .n-switch {
|
||||
::v-deep(.n-switch__rail) {
|
||||
background-color: #000e1c;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
web/src/layout/components/Header/components.ts
Normal file
36
web/src/layout/components/Header/components.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
PoweroffOutlined,
|
||||
GithubOutlined,
|
||||
LockOutlined,
|
||||
ReloadOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
CheckOutlined,
|
||||
BellOutlined,
|
||||
} from '@vicons/antd';
|
||||
|
||||
import { Refresh } from '@vicons/ionicons5';
|
||||
|
||||
export default {
|
||||
SettingOutlined,
|
||||
LockOutlined,
|
||||
GithubOutlined,
|
||||
SearchOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
PoweroffOutlined,
|
||||
ReloadOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
CheckOutlined,
|
||||
BellOutlined,
|
||||
Refresh,
|
||||
};
|
||||
3
web/src/layout/components/Header/index.ts
Normal file
3
web/src/layout/components/Header/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import PageHeader from './index.vue';
|
||||
|
||||
export { PageHeader };
|
||||
606
web/src/layout/components/Header/index.vue
Normal file
606
web/src/layout/components/Header/index.vue
Normal file
@@ -0,0 +1,606 @@
|
||||
<template>
|
||||
<div class="layout-header">
|
||||
<!--顶部菜单-->
|
||||
<div
|
||||
class="layout-header-left"
|
||||
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
|
||||
>
|
||||
<div class="logo" v-if="navMode === 'horizontal'">
|
||||
<img src="~@/assets/images/logo.png" alt="" />
|
||||
<h2 v-show="!collapsed" class="title">HotGo</h2>
|
||||
</div>
|
||||
<AsideMenu
|
||||
v-model:collapsed="collapsed"
|
||||
v-model:location="getMenuLocation"
|
||||
:inverted="getInverted"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</div>
|
||||
<!--左侧菜单-->
|
||||
<div class="layout-header-left" v-else>
|
||||
<!-- 菜单收起 -->
|
||||
<div
|
||||
class="ml-1 layout-header-trigger layout-header-trigger-min"
|
||||
@click="() => $emit('update:collapsed', !collapsed)"
|
||||
>
|
||||
<n-icon size="18" v-if="collapsed">
|
||||
<MenuUnfoldOutlined />
|
||||
</n-icon>
|
||||
<n-icon size="18" v-else>
|
||||
<MenuFoldOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 刷新 -->
|
||||
<div
|
||||
class="mr-1 layout-header-trigger layout-header-trigger-min"
|
||||
v-if="headerSetting.isReload"
|
||||
@click="reloadPage"
|
||||
>
|
||||
<n-icon size="18">
|
||||
<ReloadOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 面包屑 -->
|
||||
<n-breadcrumb v-if="crumbsSetting.show">
|
||||
<template v-for="routeItem in breadcrumbList" :key="routeItem.name">
|
||||
<n-breadcrumb-item>
|
||||
<n-dropdown
|
||||
v-if="routeItem.children.length"
|
||||
:options="routeItem.children"
|
||||
@select="dropdownSelect"
|
||||
>
|
||||
<span class="link-text">
|
||||
<component
|
||||
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
|
||||
:is="routeItem.meta.icon"
|
||||
/>
|
||||
{{ routeItem.meta.title }}
|
||||
</span>
|
||||
</n-dropdown>
|
||||
<span class="link-text" v-else>
|
||||
<component
|
||||
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
|
||||
:is="routeItem.meta.icon"
|
||||
/>
|
||||
{{ routeItem.meta.title }}
|
||||
</span>
|
||||
</n-breadcrumb-item>
|
||||
</template>
|
||||
</n-breadcrumb>
|
||||
</div>
|
||||
<div class="layout-header-right">
|
||||
<!-- <div-->
|
||||
<!-- class="layout-header-trigger layout-header-trigger-min"-->
|
||||
<!-- v-for="item in iconList"-->
|
||||
<!-- :key="item.icon.name"-->
|
||||
<!-- >-->
|
||||
<!-- <n-tooltip placement="bottom">-->
|
||||
<!-- <template #trigger>-->
|
||||
<!-- <n-icon size="18">-->
|
||||
<!-- <component :is="item.icon" v-on="item.eventObject || {}" />-->
|
||||
<!-- </n-icon>-->
|
||||
<!-- </template>-->
|
||||
<!-- <span>{{ item.tips }}</span>-->
|
||||
<!-- </n-tooltip>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div
|
||||
class="layout-header-trigger layout-header-trigger-min"
|
||||
v-for="item in iconList"
|
||||
:key="item.icon.name"
|
||||
>
|
||||
<n-popover
|
||||
placement="bottom"
|
||||
v-if="item.icon === 'BellOutlined'"
|
||||
trigger="click"
|
||||
:width="300"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-badge :value="notificationStore.messages.length" :max="99" processing>
|
||||
<n-icon size="18">
|
||||
<BellOutlined />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</template>
|
||||
<PopoverMessage />
|
||||
</n-popover>
|
||||
|
||||
<div v-else>
|
||||
<n-tooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon size="18">
|
||||
<component :is="item.icon" v-on="item.eventObject || {}" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>{{ item.tips }}</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!--切换全屏-->
|
||||
<div class="layout-header-trigger layout-header-trigger-min">
|
||||
<n-tooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon size="18">
|
||||
<component :is="fullscreenIcon" @click="toggleFullScreen" />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>全屏</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
<!-- 个人中心 -->
|
||||
<div class="layout-header-trigger layout-header-trigger-min">
|
||||
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
|
||||
<div class="avatar">
|
||||
<n-avatar round>
|
||||
{{ username }}
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</n-avatar>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
<!--设置-->
|
||||
<div class="layout-header-trigger layout-header-trigger-min" @click="openSetting">
|
||||
<n-tooltip placement="bottom-end">
|
||||
<template #trigger>
|
||||
<n-icon size="18" style="font-weight: bold">
|
||||
<SettingOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>项目配置</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--项目配置-->
|
||||
<ProjectSetting ref="drawerSetting" />
|
||||
|
||||
<template>
|
||||
<n-notification-provider :max="3" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, computed, unref, watch, h } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import components from './components';
|
||||
import {
|
||||
NDialogProvider,
|
||||
useDialog,
|
||||
useMessage,
|
||||
NAvatar,
|
||||
useNotification,
|
||||
NotificationReactive,
|
||||
} from 'naive-ui';
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { useLockscreenStore } from '@/store/modules/lockscreen';
|
||||
import ProjectSetting from './ProjectSetting.vue';
|
||||
import { AsideMenu } from '@/layout/components/Menu';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
import { NotificationsOutline as NotificationsIcon } from '@vicons/ionicons5';
|
||||
import PopoverMessage from './PopoverMessage.vue';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageHeader',
|
||||
components: { ...components, NDialogProvider, ProjectSetting, AsideMenu, PopoverMessage },
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
},
|
||||
inverted: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore();
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
const useLockscreen = useLockscreenStore();
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
|
||||
useProjectSetting();
|
||||
|
||||
const { username } = userStore?.info || {};
|
||||
|
||||
const drawerSetting = ref();
|
||||
|
||||
const state = reactive({
|
||||
username: username || '',
|
||||
fullscreenIcon: 'FullscreenOutlined',
|
||||
navMode: getNavMode,
|
||||
navTheme: getNavTheme,
|
||||
headerSetting: getHeaderSetting,
|
||||
crumbsSetting: getCrumbsSetting,
|
||||
});
|
||||
|
||||
const getInverted = computed(() => {
|
||||
const navTheme = unref(getNavTheme);
|
||||
return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
|
||||
});
|
||||
|
||||
const mixMenu = computed(() => {
|
||||
return unref(getMenuSetting).mixMenu;
|
||||
});
|
||||
|
||||
const getChangeStyle = computed(() => {
|
||||
const { collapsed } = props;
|
||||
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
|
||||
return {
|
||||
left: collapsed ? `${minMenuWidth}px` : `${menuWidth}px`,
|
||||
width: `calc(100% - ${collapsed ? `${minMenuWidth}px` : `${menuWidth}px`})`,
|
||||
};
|
||||
});
|
||||
|
||||
const getMenuLocation = computed(() => {
|
||||
return 'header';
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const generator: any = (routerMap) => {
|
||||
return routerMap.map((item) => {
|
||||
const currentMenu = {
|
||||
...item,
|
||||
label: item.meta.title,
|
||||
key: item.name,
|
||||
disabled: item.path === '/',
|
||||
};
|
||||
// 是否有子菜单,并递归处理
|
||||
if (item.children && item.children.length > 0) {
|
||||
// Recursion
|
||||
currentMenu.children = generator(item.children, currentMenu);
|
||||
}
|
||||
return currentMenu;
|
||||
});
|
||||
};
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
return generator(route.matched);
|
||||
});
|
||||
|
||||
const dropdownSelect = (key) => {
|
||||
router.push({ name: key });
|
||||
};
|
||||
|
||||
// 刷新页面
|
||||
const reloadPage = () => {
|
||||
router.push({
|
||||
path: '/redirect' + unref(route).fullPath,
|
||||
});
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const doLogout = () => {
|
||||
dialog.info({
|
||||
title: '提示',
|
||||
content: '您确定要退出登录吗',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
userStore.logout().then(() => {
|
||||
message.success('成功退出登录');
|
||||
// 移除标签页
|
||||
localStorage.removeItem(TABS_ROUTES);
|
||||
router
|
||||
.replace({
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: route.fullPath,
|
||||
},
|
||||
})
|
||||
.finally(() => location.reload());
|
||||
});
|
||||
},
|
||||
onNegativeClick: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
// 切换全屏图标
|
||||
const toggleFullscreenIcon = () =>
|
||||
(state.fullscreenIcon =
|
||||
document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined');
|
||||
|
||||
// 监听全屏切换事件
|
||||
document.addEventListener('fullscreenchange', toggleFullscreenIcon);
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 图标列表
|
||||
const iconList = [
|
||||
// {
|
||||
// icon: 'SearchOutlined',
|
||||
// tips: '搜索',
|
||||
// },
|
||||
{
|
||||
icon: 'GithubOutlined',
|
||||
tips: 'github',
|
||||
eventObject: {
|
||||
click: () => window.open('https://github.com/bufanyun/hotgo'),
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'BellOutlined',
|
||||
tips: '系统消息',
|
||||
},
|
||||
{
|
||||
icon: 'LockOutlined',
|
||||
tips: '锁屏',
|
||||
eventObject: {
|
||||
click: () => useLockscreen.setLock(true),
|
||||
},
|
||||
},
|
||||
];
|
||||
const avatarOptions = [
|
||||
{
|
||||
label: '个人设置',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 2,
|
||||
},
|
||||
];
|
||||
|
||||
//头像下拉菜单
|
||||
const avatarSelect = (key) => {
|
||||
switch (key) {
|
||||
case 1:
|
||||
router.push({ name: 'setting_account' });
|
||||
break;
|
||||
case 2:
|
||||
doLogout();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function openSetting() {
|
||||
const { openDrawer } = drawerSetting.value;
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
const notification = useNotification();
|
||||
|
||||
const getMessages = computed(() => {
|
||||
return notificationStore.messages;
|
||||
});
|
||||
const nRef = ref<NotificationReactive | null>(null);
|
||||
// 监听新消息,推送通知
|
||||
watch(
|
||||
getMessages,
|
||||
(newVal, _oldVal) => {
|
||||
if (newVal[0] !== undefined) {
|
||||
let message = newVal[0];
|
||||
nRef.value = notification.create({
|
||||
title: message.title,
|
||||
description: message.description,
|
||||
content: message.content,
|
||||
meta: message.meta,
|
||||
duration: 5000,
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: true,
|
||||
src: '/notification.png',
|
||||
}),
|
||||
onClose: () => {
|
||||
nRef.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
iconList,
|
||||
toggleFullScreen,
|
||||
doLogout,
|
||||
route,
|
||||
dropdownSelect,
|
||||
avatarOptions,
|
||||
getChangeStyle,
|
||||
avatarSelect,
|
||||
breadcrumbList,
|
||||
reloadPage,
|
||||
drawerSetting,
|
||||
openSetting,
|
||||
getInverted,
|
||||
getMenuLocation,
|
||||
mixMenu,
|
||||
NotificationsIcon,
|
||||
PopoverMessage,
|
||||
notificationStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
height: @header-height;
|
||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
||||
transition: all 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
z-index: 11;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-left: 10px;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.ant-breadcrumb span:last-child .link-text) {
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.n-breadcrumb {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
> * {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-trigger {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
.n-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: hsla(0, 0%, 100%, 0.08);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: #515a6e;
|
||||
}
|
||||
}
|
||||
|
||||
&-trigger-min {
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-header-light {
|
||||
background: #fff;
|
||||
color: #515a6e;
|
||||
|
||||
.n-icon {
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.layout-header-left {
|
||||
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
|
||||
color: #515a6e;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-header-trigger {
|
||||
&:hover {
|
||||
background: #f8f8f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-header-fix {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 200px;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
//::v-deep(.menu-server-link) {
|
||||
// color: #515a6e;
|
||||
//
|
||||
// &:hover {
|
||||
// color: #1890ff;
|
||||
// }
|
||||
//}
|
||||
|
||||
.action-items-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
.action-item {
|
||||
min-width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.badge-action-item {
|
||||
cursor: pointer;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-input .n-input__border, .n-input .n-input__state-border) {
|
||||
border: none;
|
||||
border-bottom: 1px solid currentColor;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
border: none !important;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
color: currentColor !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/deep/ sup {
|
||||
top: 1.3em;
|
||||
}
|
||||
</style>
|
||||
3
web/src/layout/components/Logo/index.ts
Normal file
3
web/src/layout/components/Logo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Logo from './index.vue';
|
||||
|
||||
export { Logo };
|
||||
39
web/src/layout/components/Logo/index.vue
Normal file
39
web/src/layout/components/Logo/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img src="~@/assets/images/logo.png" alt="" :class="{ 'mr-2': !collapsed }" />
|
||||
<h2 v-show="!collapsed" class="title">后台管理系统</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Index',
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 30px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
web/src/layout/components/Main/index.ts
Normal file
3
web/src/layout/components/Main/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MainView from './index.vue';
|
||||
|
||||
export { MainView };
|
||||
50
web/src/layout/components/Main/index.vue
Normal file
50
web/src/layout/components/Main/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<RouterView>
|
||||
<template #default="{ Component, route }">
|
||||
<transition :name="getTransitionName" mode="out-in" appear>
|
||||
<keep-alive v-if="keepAliveComponents" :include="keepAliveComponents">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component v-else :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</template>
|
||||
</RouterView>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainView',
|
||||
components: {},
|
||||
props: {
|
||||
notNeedKey: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
animate: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getIsPageAnimate, getPageAnimateType } = useProjectSetting();
|
||||
const asyncRouteStore = useAsyncRouteStore();
|
||||
// 需要缓存的路由组件
|
||||
const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents);
|
||||
|
||||
const getTransitionName = computed(() => {
|
||||
return unref(getIsPageAnimate) ? unref(getPageAnimateType) : '';
|
||||
});
|
||||
|
||||
return {
|
||||
keepAliveComponents,
|
||||
getTransitionName,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
3
web/src/layout/components/Menu/index.ts
Normal file
3
web/src/layout/components/Menu/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import AsideMenu from './index.vue';
|
||||
|
||||
export { AsideMenu };
|
||||
167
web/src/layout/components/Menu/index.vue
Normal file
167
web/src/layout/components/Menu/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<NMenu
|
||||
:options="menus"
|
||||
:inverted="inverted"
|
||||
:mode="mode"
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="20"
|
||||
:indent="24"
|
||||
:expanded-keys="openKeys"
|
||||
:value="getSelectedKeys"
|
||||
@update:value="clickMenuItem"
|
||||
@update:expanded-keys="menuExpanded"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, reactive, computed, watch, toRefs, unref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
|
||||
import { generatorMenu, generatorMenuMix } from '@/utils';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Menu',
|
||||
components: {},
|
||||
props: {
|
||||
mode: {
|
||||
// 菜单模式
|
||||
type: String,
|
||||
default: 'vertical',
|
||||
},
|
||||
collapsed: {
|
||||
// 侧边栏菜单是否收起
|
||||
type: Boolean,
|
||||
},
|
||||
//位置
|
||||
location: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
},
|
||||
emits: ['update:collapsed', 'clickMenuItem'],
|
||||
setup(props, { emit }) {
|
||||
// 当前路由
|
||||
const currentRoute = useRoute();
|
||||
const router = useRouter();
|
||||
const asyncRouteStore = useAsyncRouteStore();
|
||||
const settingStore = useProjectSettingStore();
|
||||
const menus = ref<any[]>([]);
|
||||
const selectedKeys = ref<string>(currentRoute.name as string);
|
||||
const headerMenuSelectKey = ref<string>('');
|
||||
|
||||
const { getNavMode } = useProjectSetting();
|
||||
|
||||
const navMode = getNavMode;
|
||||
|
||||
// 获取当前打开的子菜单
|
||||
const matched = currentRoute.matched;
|
||||
|
||||
const getOpenKeys = matched && matched.length ? matched.map((item) => item.name) : [];
|
||||
|
||||
const state = reactive({
|
||||
openKeys: getOpenKeys,
|
||||
});
|
||||
|
||||
const inverted = computed(() => {
|
||||
return ['dark', 'header-dark'].includes(settingStore.navTheme);
|
||||
});
|
||||
|
||||
const getSelectedKeys = computed(() => {
|
||||
let location = props.location;
|
||||
return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
|
||||
? unref(selectedKeys)
|
||||
: unref(headerMenuSelectKey);
|
||||
});
|
||||
|
||||
// 监听分割菜单
|
||||
watch(
|
||||
() => settingStore.menuSetting.mixMenu,
|
||||
() => {
|
||||
updateMenu();
|
||||
if (props.collapsed) {
|
||||
emit('update:collapsed', !props.collapsed);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听菜单收缩状态
|
||||
// watch(
|
||||
// () => props.collapsed,
|
||||
// (newVal) => {
|
||||
// }
|
||||
// );
|
||||
|
||||
// 跟随页面路由变化,切换菜单选中状态
|
||||
watch(
|
||||
() => currentRoute.fullPath,
|
||||
() => {
|
||||
updateMenu();
|
||||
const matched = currentRoute.matched;
|
||||
state.openKeys = matched.map((item) => item.name);
|
||||
const activeMenu: string = (currentRoute.meta?.activeMenu as string) || '';
|
||||
selectedKeys.value = activeMenu ? (activeMenu as string) : (currentRoute.name as string);
|
||||
}
|
||||
);
|
||||
|
||||
function updateMenu() {
|
||||
if (!settingStore.menuSetting.mixMenu) {
|
||||
menus.value = generatorMenu(asyncRouteStore.getMenus);
|
||||
} else {
|
||||
//混合菜单
|
||||
const firstRouteName: string = (currentRoute.matched[0].name as string) || '';
|
||||
menus.value = generatorMenuMix(asyncRouteStore.getMenus, firstRouteName, props.location);
|
||||
const activeMenu: string = currentRoute?.matched[0].meta?.activeMenu as string;
|
||||
headerMenuSelectKey.value = (activeMenu ? activeMenu : firstRouteName) || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 点击菜单
|
||||
function clickMenuItem(key: string) {
|
||||
if (/http(s)?:/.test(key)) {
|
||||
window.open(key);
|
||||
} else {
|
||||
router.push({ name: key });
|
||||
}
|
||||
emit('clickMenuItem' as any, key);
|
||||
}
|
||||
|
||||
//展开菜单
|
||||
function menuExpanded(openKeys: string[]) {
|
||||
if (!openKeys) return;
|
||||
const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1);
|
||||
const isExistChildren = findChildrenLen(latestOpenKey as string);
|
||||
state.openKeys = isExistChildren ? (latestOpenKey ? [latestOpenKey] : []) : openKeys;
|
||||
}
|
||||
|
||||
//查找是否存在子路由
|
||||
function findChildrenLen(key: string) {
|
||||
if (!key) return false;
|
||||
const subRouteChildren: string[] = [];
|
||||
for (const { children, key } of unref(menus)) {
|
||||
if (children && children.length) {
|
||||
subRouteChildren.push(key as string);
|
||||
}
|
||||
}
|
||||
return subRouteChildren.includes(key);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateMenu();
|
||||
});
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
inverted,
|
||||
menus,
|
||||
selectedKeys,
|
||||
headerMenuSelectKey,
|
||||
getSelectedKeys,
|
||||
clickMenuItem,
|
||||
menuExpanded,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
3
web/src/layout/components/TagsView/index.ts
Normal file
3
web/src/layout/components/TagsView/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TabsView from './index.vue';
|
||||
|
||||
export { TabsView };
|
||||
675
web/src/layout/components/TagsView/index.vue
Normal file
675
web/src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,675 @@
|
||||
<template>
|
||||
<div
|
||||
class="tabs-view"
|
||||
:class="{
|
||||
'tabs-view-fix': multiTabsSetting.fixed,
|
||||
'tabs-view-fixed-header': isMultiHeaderFixed,
|
||||
'tabs-view-default-background': getDarkTheme === false,
|
||||
'tabs-view-dark-background': getDarkTheme === true,
|
||||
}"
|
||||
:style="getChangeStyle"
|
||||
>
|
||||
<div class="tabs-view-main">
|
||||
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': scrollable }">
|
||||
<span
|
||||
class="tabs-card-prev"
|
||||
:class="{ 'tabs-card-prev-hide': !scrollable }"
|
||||
@click="scrollPrev"
|
||||
>
|
||||
<n-icon size="16" color="#515a6e">
|
||||
<LeftOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
<span
|
||||
class="tabs-card-next"
|
||||
:class="{ 'tabs-card-next-hide': !scrollable }"
|
||||
@click="scrollNext"
|
||||
>
|
||||
<n-icon size="16" color="#515a6e">
|
||||
<RightOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
<div ref="navScroll" class="tabs-card-scroll">
|
||||
<Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
|
||||
<template #item="{ element }">
|
||||
<div
|
||||
:id="`tag${element.fullPath.split('/').join('\/')}`"
|
||||
class="tabs-card-scroll-item"
|
||||
:class="{ 'active-item': activeKey === element.path }"
|
||||
@click.stop="goPage(element)"
|
||||
@contextmenu="handleContextMenu($event, element)"
|
||||
>
|
||||
<span>{{ element.meta.title }}</span>
|
||||
<n-icon size="14" @click.stop="closeTabItem(element)" v-if="!element.meta.affix">
|
||||
<CloseOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs-close">
|
||||
<n-dropdown
|
||||
trigger="hover"
|
||||
@select="closeHandleSelect"
|
||||
placement="bottom-end"
|
||||
:options="TabsMenuOptions"
|
||||
>
|
||||
<div class="tabs-close-btn">
|
||||
<n-icon size="16" color="#515a6e">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
<n-dropdown
|
||||
:show="showDropdown"
|
||||
:x="dropdownX"
|
||||
:y="dropdownY"
|
||||
@clickoutside="onClickOutside"
|
||||
placement="bottom-start"
|
||||
@select="closeHandleSelect"
|
||||
:options="TabsMenuOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
computed,
|
||||
ref,
|
||||
toRefs,
|
||||
unref,
|
||||
provide,
|
||||
watch,
|
||||
onMounted,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { storage } from '@/utils/Storage';
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
import { useTabsViewStore } from '@/store/modules/tabsView';
|
||||
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
|
||||
import { RouteItem } from '@/store/modules/tabsView';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import {
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
ColumnWidthOutlined,
|
||||
MinusOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
} from '@vicons/antd';
|
||||
import { renderIcon } from '@/utils';
|
||||
import elementResizeDetectorMaker from 'element-resize-detector';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { useGo } from '@/hooks/web/usePage';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabsView',
|
||||
components: {
|
||||
DownOutlined,
|
||||
CloseOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
Draggable,
|
||||
},
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { getDarkTheme, getAppTheme } = useDesignSetting();
|
||||
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting, getIsMobile } =
|
||||
useProjectSetting();
|
||||
const settingStore = useProjectSettingStore();
|
||||
|
||||
const message = useMessage();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabsViewStore = useTabsViewStore();
|
||||
const asyncRouteStore = useAsyncRouteStore();
|
||||
const navScroll: any = ref(null);
|
||||
const navWrap: any = ref(null);
|
||||
const isCurrent = ref(false);
|
||||
const go = useGo();
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const getCardColor = computed(() => {
|
||||
return themeVars.value.cardColor;
|
||||
});
|
||||
|
||||
const getBaseColor = computed(() => {
|
||||
return themeVars.value.textColor1;
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
activeKey: route.fullPath,
|
||||
scrollable: false,
|
||||
dropdownX: 0,
|
||||
dropdownY: 0,
|
||||
showDropdown: false,
|
||||
isMultiHeaderFixed: false,
|
||||
multiTabsSetting: getMultiTabsSetting,
|
||||
});
|
||||
|
||||
// 获取简易的路由对象
|
||||
const getSimpleRoute = (route): RouteItem => {
|
||||
const { fullPath, hash, meta, name, params, path, query } = route;
|
||||
return { fullPath, hash, meta, name, params, path, query };
|
||||
};
|
||||
|
||||
const isMixMenuNoneSub = computed(() => {
|
||||
const mixMenu = settingStore.menuSetting.mixMenu;
|
||||
const currentRoute = useRoute();
|
||||
const navMode = unref(getNavMode);
|
||||
if (unref(navMode) != 'horizontal-mix') return true;
|
||||
return !(unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot);
|
||||
});
|
||||
|
||||
//动态组装样式 菜单缩进
|
||||
const getChangeStyle = computed(() => {
|
||||
const { collapsed } = props;
|
||||
const navMode = unref(getNavMode);
|
||||
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
|
||||
const { fixed }: any = unref(getMultiTabsSetting);
|
||||
let lenNum =
|
||||
navMode === 'horizontal' || !isMixMenuNoneSub.value
|
||||
? '0px'
|
||||
: collapsed
|
||||
? `${minMenuWidth}px`
|
||||
: `${menuWidth}px`;
|
||||
|
||||
if (getIsMobile.value) {
|
||||
return {
|
||||
left: '0px',
|
||||
width: '100%',
|
||||
};
|
||||
}
|
||||
return {
|
||||
left: lenNum,
|
||||
width: `calc(100% - ${!fixed ? '0px' : lenNum})`,
|
||||
};
|
||||
});
|
||||
|
||||
//tags 右侧下拉菜单
|
||||
const TabsMenuOptions = computed(() => {
|
||||
const isDisabled = unref(tabsList).length <= 1;
|
||||
return [
|
||||
{
|
||||
label: '刷新当前',
|
||||
key: '1',
|
||||
icon: renderIcon(ReloadOutlined),
|
||||
},
|
||||
{
|
||||
label: `关闭当前`,
|
||||
key: '2',
|
||||
disabled: unref(isCurrent) || isDisabled,
|
||||
icon: renderIcon(CloseOutlined),
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: '3',
|
||||
disabled: isDisabled,
|
||||
icon: renderIcon(ColumnWidthOutlined),
|
||||
},
|
||||
{
|
||||
label: '关闭全部',
|
||||
key: '4',
|
||||
disabled: isDisabled,
|
||||
icon: renderIcon(MinusOutlined),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
let cacheRoutes: RouteItem[] = [];
|
||||
const simpleRoute = getSimpleRoute(route);
|
||||
try {
|
||||
const routesStr = storage.get(TABS_ROUTES) as string | null | undefined;
|
||||
cacheRoutes = routesStr ? JSON.parse(routesStr) : [simpleRoute];
|
||||
} catch (e) {
|
||||
cacheRoutes = [simpleRoute];
|
||||
}
|
||||
|
||||
// 将最新的路由信息同步到 localStorage 中
|
||||
const routes = router.getRoutes();
|
||||
cacheRoutes.forEach((cacheRoute) => {
|
||||
const route = routes.find((route) => route.path === cacheRoute.path);
|
||||
if (route) {
|
||||
cacheRoute.meta = route.meta || cacheRoute.meta;
|
||||
cacheRoute.name = (route.name || cacheRoute.name) as string;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化标签页
|
||||
tabsViewStore.initTabs(cacheRoutes);
|
||||
|
||||
//监听滚动条
|
||||
function onScroll(e) {
|
||||
let scrollTop =
|
||||
e.target.scrollTop ||
|
||||
document.documentElement.scrollTop ||
|
||||
window.pageYOffset ||
|
||||
document.body.scrollTop; // 滚动条偏移量
|
||||
state.isMultiHeaderFixed = !!(
|
||||
!getHeaderSetting.value.fixed &&
|
||||
getMultiTabsSetting.value.fixed &&
|
||||
scrollTop >= 64
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll, true);
|
||||
|
||||
// 移除缓存组件名称
|
||||
const delKeepAliveCompName = () => {
|
||||
if (route.meta.keepAlive) {
|
||||
const name = router.currentRoute.value.matched.find((item) => item.name == route.name)
|
||||
?.components?.default.name;
|
||||
if (name) {
|
||||
asyncRouteStore.keepAliveComponents = asyncRouteStore.keepAliveComponents.filter(
|
||||
(item) => item != name
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 标签页列表
|
||||
const tabsList: any = computed(() => tabsViewStore.tabsList);
|
||||
const whiteList: string[] = [
|
||||
PageEnum.BASE_LOGIN_NAME,
|
||||
PageEnum.REDIRECT_NAME,
|
||||
PageEnum.ERROR_PAGE_NAME,
|
||||
];
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(to) => {
|
||||
if (whiteList.includes(route.name as string)) return;
|
||||
state.activeKey = to;
|
||||
tabsViewStore.addTabs(getSimpleRoute(route));
|
||||
updateNavScroll(true);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 在页面关闭或刷新之前,保存数据
|
||||
window.addEventListener('beforeunload', () => {
|
||||
storage.set(TABS_ROUTES, JSON.stringify(tabsList.value));
|
||||
});
|
||||
|
||||
// 关闭当前页面
|
||||
const removeTab = (route) => {
|
||||
if (tabsList.value.length === 1) {
|
||||
return message.warning('这已经是最后一页,不能再关闭了!');
|
||||
}
|
||||
delKeepAliveCompName();
|
||||
tabsViewStore.closeCurrentTab(route);
|
||||
// 如果关闭的是当前页
|
||||
if (state.activeKey === route.fullPath) {
|
||||
const currentRoute = tabsList.value[Math.max(0, tabsList.value.length - 1)];
|
||||
state.activeKey = currentRoute.fullPath;
|
||||
router.push(currentRoute);
|
||||
}
|
||||
updateNavScroll();
|
||||
};
|
||||
|
||||
// 刷新页面
|
||||
const reloadPage = () => {
|
||||
delKeepAliveCompName();
|
||||
router.push({
|
||||
path: '/redirect' + unref(route).fullPath,
|
||||
});
|
||||
};
|
||||
|
||||
// 注入刷新页面方法
|
||||
provide('reloadPage', reloadPage);
|
||||
|
||||
// 关闭左侧
|
||||
const closeLeft = (route) => {
|
||||
tabsViewStore.closeLeftTabs(route);
|
||||
state.activeKey = route.fullPath;
|
||||
router.replace(route.fullPath);
|
||||
updateNavScroll();
|
||||
};
|
||||
|
||||
// 关闭右侧
|
||||
const closeRight = (route) => {
|
||||
tabsViewStore.closeRightTabs(route);
|
||||
state.activeKey = route.fullPath;
|
||||
router.replace(route.fullPath);
|
||||
updateNavScroll();
|
||||
};
|
||||
|
||||
// 关闭其他
|
||||
const closeOther = (route) => {
|
||||
tabsViewStore.closeOtherTabs(route);
|
||||
state.activeKey = route.fullPath;
|
||||
router.replace(route.fullPath);
|
||||
updateNavScroll();
|
||||
};
|
||||
|
||||
// 关闭全部
|
||||
const closeAll = () => {
|
||||
tabsViewStore.closeAllTabs();
|
||||
router.replace(PageEnum.BASE_HOME);
|
||||
updateNavScroll();
|
||||
};
|
||||
|
||||
//tab 操作
|
||||
const closeHandleSelect = (key) => {
|
||||
switch (key) {
|
||||
//刷新
|
||||
case '1':
|
||||
reloadPage();
|
||||
break;
|
||||
//关闭
|
||||
case '2':
|
||||
removeTab(route);
|
||||
break;
|
||||
//关闭其他
|
||||
case '3':
|
||||
closeOther(route);
|
||||
break;
|
||||
//关闭所有
|
||||
case '4':
|
||||
closeAll();
|
||||
break;
|
||||
}
|
||||
updateNavScroll();
|
||||
state.showDropdown = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value 要滚动到的位置
|
||||
* @param amplitude 每次滚动的长度
|
||||
*/
|
||||
function scrollTo(value: number, amplitude: number) {
|
||||
const currentScroll = navScroll.value.scrollLeft;
|
||||
const scrollWidth =
|
||||
(amplitude > 0 && currentScroll + amplitude >= value) ||
|
||||
(amplitude < 0 && currentScroll + amplitude <= value)
|
||||
? value
|
||||
: currentScroll + amplitude;
|
||||
navScroll.value && navScroll.value.scrollTo(scrollWidth, 0);
|
||||
if (scrollWidth === value) return;
|
||||
return window.requestAnimationFrame(() => scrollTo(value, amplitude));
|
||||
}
|
||||
|
||||
function scrollPrev() {
|
||||
const containerWidth = navScroll.value.offsetWidth;
|
||||
const currentScroll = navScroll.value.scrollLeft;
|
||||
|
||||
if (!currentScroll) return;
|
||||
const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0;
|
||||
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
|
||||
}
|
||||
|
||||
function scrollNext() {
|
||||
const containerWidth = navScroll.value.offsetWidth;
|
||||
const navWidth = navScroll.value.scrollWidth;
|
||||
const currentScroll = navScroll.value.scrollLeft;
|
||||
|
||||
if (navWidth - currentScroll <= containerWidth) return;
|
||||
const scrollLeft =
|
||||
navWidth - currentScroll > containerWidth * 2
|
||||
? currentScroll + containerWidth
|
||||
: navWidth - containerWidth;
|
||||
scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param autoScroll 是否开启自动滚动功能
|
||||
*/
|
||||
async function updateNavScroll(autoScroll?: boolean) {
|
||||
await nextTick();
|
||||
if (!navScroll.value) return;
|
||||
const containerWidth = navScroll.value.offsetWidth;
|
||||
const navWidth = navScroll.value.scrollWidth;
|
||||
|
||||
if (containerWidth < navWidth) {
|
||||
state.scrollable = true;
|
||||
if (autoScroll) {
|
||||
let tagList = navScroll.value.querySelectorAll('.tabs-card-scroll-item') || [];
|
||||
[...tagList].forEach((tag: HTMLElement) => {
|
||||
// fix SyntaxError
|
||||
if (tag.id === `tag${state.activeKey.split('/').join('\/')}`) {
|
||||
tag.scrollIntoView && tag.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
state.scrollable = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
updateNavScroll(true);
|
||||
}
|
||||
|
||||
function handleContextMenu(e, item) {
|
||||
e.preventDefault();
|
||||
isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path;
|
||||
state.showDropdown = false;
|
||||
nextTick().then(() => {
|
||||
state.showDropdown = true;
|
||||
state.dropdownX = e.clientX;
|
||||
state.dropdownY = e.clientY;
|
||||
});
|
||||
}
|
||||
|
||||
function onClickOutside() {
|
||||
state.showDropdown = false;
|
||||
}
|
||||
|
||||
//tags 跳转页面
|
||||
function goPage(e) {
|
||||
const { fullPath } = e;
|
||||
if (fullPath === route.fullPath) return;
|
||||
state.activeKey = fullPath;
|
||||
go(e, true);
|
||||
}
|
||||
|
||||
//删除tab
|
||||
function closeTabItem(e) {
|
||||
const { fullPath } = e;
|
||||
const routeInfo = tabsList.value.find((item) => item.fullPath == fullPath);
|
||||
removeTab(routeInfo);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onElementResize();
|
||||
});
|
||||
|
||||
function onElementResize() {
|
||||
let observer;
|
||||
observer = elementResizeDetectorMaker();
|
||||
observer.listenTo(navWrap.value, handleResize);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
navWrap,
|
||||
navScroll,
|
||||
route,
|
||||
tabsList,
|
||||
goPage,
|
||||
closeTabItem,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
closeAll,
|
||||
reloadPage,
|
||||
getChangeStyle,
|
||||
TabsMenuOptions,
|
||||
closeHandleSelect,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
handleContextMenu,
|
||||
onClickOutside,
|
||||
getDarkTheme,
|
||||
getAppTheme,
|
||||
getCardColor,
|
||||
getBaseColor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tabs-view {
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
display: flex;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&-main {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
|
||||
.tabs-card {
|
||||
-webkit-box-flex: 1;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.tabs-card-prev,
|
||||
.tabs-card-next {
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
line-height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
.n-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-card-prev {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tabs-card-next {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tabs-card-next-hide,
|
||||
.tabs-card-prev-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-scroll {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&-item {
|
||||
background: v-bind(getCardColor);
|
||||
color: v-bind(getBaseColor);
|
||||
height: 32px;
|
||||
padding: 6px 16px 4px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
|
||||
span {
|
||||
float: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.n-icon {
|
||||
height: 22px;
|
||||
width: 21px;
|
||||
margin-right: -6px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
color: #808695;
|
||||
|
||||
&:hover {
|
||||
color: #515a6e !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 21px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active-item {
|
||||
color: v-bind(getAppTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-card-scrollable {
|
||||
padding: 0 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-close {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
background: var(--color);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
//margin-right: 10px;
|
||||
|
||||
&-btn {
|
||||
color: var(--color);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-view-default-background {
|
||||
background: #f5f7f9;
|
||||
}
|
||||
|
||||
.tabs-view-dark-background {
|
||||
background: #101014;
|
||||
}
|
||||
|
||||
.tabs-view-fix {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
padding: 6px 19px 6px 10px;
|
||||
left: 200px;
|
||||
}
|
||||
|
||||
.tabs-view-fixed-header {
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
277
web/src/layout/index.vue
Normal file
277
web/src/layout/index.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<n-layout class="layout" :position="fixedMenu" has-sider>
|
||||
<n-layout-sider
|
||||
v-if="
|
||||
!isMobile && isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')
|
||||
"
|
||||
show-trigger="bar"
|
||||
@collapse="collapsed = true"
|
||||
:position="fixedMenu"
|
||||
@expand="collapsed = false"
|
||||
:collapsed="collapsed"
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="leftMenuWidth"
|
||||
:native-scrollbar="false"
|
||||
:inverted="inverted"
|
||||
class="layout-sider"
|
||||
>
|
||||
<Logo :collapsed="collapsed" />
|
||||
<AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
|
||||
</n-layout-sider>
|
||||
|
||||
<n-drawer
|
||||
v-model:show="showSideDrawder"
|
||||
:width="menuWidth"
|
||||
:placement="'left'"
|
||||
class="layout-side-drawer"
|
||||
>
|
||||
<Logo :collapsed="collapsed" />
|
||||
<AsideMenu @clickMenuItem="collapsed = false" />
|
||||
</n-drawer>
|
||||
|
||||
<n-layout :inverted="inverted">
|
||||
<n-layout-header :inverted="getHeaderInverted" :position="fixedHeader">
|
||||
<PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout-content
|
||||
class="layout-content"
|
||||
:class="{ 'layout-default-background': getDarkTheme === false }"
|
||||
>
|
||||
<div
|
||||
class="layout-content-main"
|
||||
:class="{
|
||||
'layout-content-main-fix': fixedMulti,
|
||||
'fluid-header': fixedHeader === 'static',
|
||||
}"
|
||||
>
|
||||
<TabsView v-if="isMultiTabs" v-model:collapsed="collapsed" />
|
||||
<div
|
||||
class="main-view"
|
||||
:class="{
|
||||
'main-view-fix': fixedMulti,
|
||||
noMultiTabs: !isMultiTabs,
|
||||
'mt-3': !isMultiTabs,
|
||||
}"
|
||||
>
|
||||
<MainView />
|
||||
</div>
|
||||
</div>
|
||||
<!--1.15废弃,没啥用,占用操作空间-->
|
||||
<!-- <NLayoutFooter v-if="getShowFooter">-->
|
||||
<!-- <PageFooter />-->
|
||||
<!-- </NLayoutFooter>-->
|
||||
</n-layout-content>
|
||||
<n-back-top :right="100" />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref, computed, onMounted } from 'vue';
|
||||
import { Logo } from './components/Logo';
|
||||
import { TabsView } from './components/TagsView';
|
||||
import { MainView } from './components/Main';
|
||||
import { AsideMenu } from './components/Menu';
|
||||
import { PageHeader } from './components/Header';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
import { useLoadingBar } from 'naive-ui';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
const {
|
||||
// getShowFooter,
|
||||
getNavMode,
|
||||
getNavTheme,
|
||||
getHeaderSetting,
|
||||
getMenuSetting,
|
||||
getMultiTabsSetting,
|
||||
} = useProjectSetting();
|
||||
|
||||
const settingStore = useProjectSettingStore();
|
||||
|
||||
const navMode = getNavMode;
|
||||
|
||||
const collapsed = ref<boolean>(false);
|
||||
|
||||
const { mobileWidth, menuWidth } = unref(getMenuSetting);
|
||||
|
||||
const isMobile = computed<boolean>({
|
||||
get: () => settingStore.getIsMobile,
|
||||
set: (val) => settingStore.setIsMobile(val),
|
||||
});
|
||||
|
||||
const fixedHeader = computed(() => {
|
||||
const { fixed } = unref(getHeaderSetting);
|
||||
return fixed ? 'absolute' : 'static';
|
||||
});
|
||||
|
||||
const isMixMenuNoneSub = computed(() => {
|
||||
const mixMenu = settingStore.menuSetting.mixMenu;
|
||||
const currentRoute = useRoute();
|
||||
if (unref(navMode) != 'horizontal-mix') return true;
|
||||
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const fixedMenu = computed(() => {
|
||||
const { fixed } = unref(getHeaderSetting);
|
||||
return fixed ? 'absolute' : 'static';
|
||||
});
|
||||
|
||||
const isMultiTabs = computed(() => {
|
||||
return unref(getMultiTabsSetting).show;
|
||||
});
|
||||
|
||||
const fixedMulti = computed(() => {
|
||||
return unref(getMultiTabsSetting).fixed;
|
||||
});
|
||||
|
||||
const inverted = computed(() => {
|
||||
return ['dark', 'header-dark'].includes(unref(getNavTheme));
|
||||
});
|
||||
|
||||
const getHeaderInverted = computed(() => {
|
||||
const navTheme = unref(getNavTheme);
|
||||
return ['light', 'header-dark'].includes(navTheme) ? unref(inverted) : !unref(inverted);
|
||||
});
|
||||
|
||||
const leftMenuWidth = computed(() => {
|
||||
const { minMenuWidth, menuWidth } = unref(getMenuSetting);
|
||||
return collapsed.value ? minMenuWidth : menuWidth;
|
||||
});
|
||||
|
||||
// const getChangeStyle = computed(() => {
|
||||
// const { minMenuWidth, menuWidth } = unref(getMenuSetting);
|
||||
// return {
|
||||
// 'padding-left': collapsed.value ? `${minMenuWidth}px` : `${menuWidth}px`,
|
||||
// };
|
||||
// });
|
||||
|
||||
const getMenuLocation = computed(() => {
|
||||
return 'left';
|
||||
});
|
||||
|
||||
// 控制显示或隐藏移动端侧边栏
|
||||
const showSideDrawder = computed({
|
||||
get: () => isMobile.value && collapsed.value,
|
||||
set: (val) => (collapsed.value = val),
|
||||
});
|
||||
|
||||
//判断是否触发移动端模式
|
||||
const checkMobileMode = () => {
|
||||
if (document.body.clientWidth <= mobileWidth) {
|
||||
isMobile.value = true;
|
||||
} else {
|
||||
isMobile.value = false;
|
||||
}
|
||||
collapsed.value = false;
|
||||
};
|
||||
|
||||
const watchWidth = () => {
|
||||
const Width = document.body.clientWidth;
|
||||
if (Width <= 950) {
|
||||
collapsed.value = true;
|
||||
} else collapsed.value = false;
|
||||
|
||||
checkMobileMode();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkMobileMode();
|
||||
window.addEventListener('resize', watchWidth);
|
||||
//挂载在 window 方便与在js中使用
|
||||
window['$loading'] = useLoadingBar();
|
||||
window['$loading'].finish();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.layout-side-drawer {
|
||||
background-color: rgb(0, 20, 40);
|
||||
|
||||
.layout-sider {
|
||||
min-height: 100vh;
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
position: relative;
|
||||
z-index: 13;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: auto;
|
||||
|
||||
&-default-background {
|
||||
background: #f5f7f9;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
min-height: 100vh;
|
||||
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||
position: relative;
|
||||
z-index: 13;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.layout-sider-fix {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-right-fix {
|
||||
overflow-x: hidden;
|
||||
padding-left: 200px;
|
||||
min-height: 100vh;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.n-layout-header.n-layout-header--absolute-positioned {
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.n-layout-footer {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content-main {
|
||||
margin: 0 10px 10px;
|
||||
position: relative;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.layout-content-main-fix {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.fluid-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main-view-fix {
|
||||
padding-top: 44px;
|
||||
}
|
||||
|
||||
.noMultiTabs {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
3
web/src/layout/parentLayout.vue
Normal file
3
web/src/layout/parentLayout.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
Reference in New Issue
Block a user