This commit is contained in:
孟帅
2022-11-24 23:37:34 +08:00
parent 4ffe54b6ac
commit 29bda0dcdd
1487 changed files with 97869 additions and 96539 deletions

View File

@@ -0,0 +1,3 @@
import PageFooter from './index.vue';
export { PageFooter };

View 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>

View 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>

View 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>

View 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,
};

View File

@@ -0,0 +1,3 @@
import PageHeader from './index.vue';
export { PageHeader };

View 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>

View File

@@ -0,0 +1,3 @@
import Logo from './index.vue';
export { Logo };

View 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>

View File

@@ -0,0 +1,3 @@
import MainView from './index.vue';
export { MainView };

View 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>

View File

@@ -0,0 +1,3 @@
import AsideMenu from './index.vue';
export { AsideMenu };

View 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>

View File

@@ -0,0 +1,3 @@
import TabsView from './index.vue';
export { TabsView };

View 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
View 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>

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>