mirror of
https://github.com/jekip/naive-ui-admin.git
synced 2026-03-01 00:23:11 +08:00
更新0.1.1版本
This commit is contained in:
3
src/layout/components/Footer/index.ts
Normal file
3
src/layout/components/Footer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import PageFooter from './index.vue'
|
||||
|
||||
export { PageFooter }
|
||||
67
src/layout/components/Footer/index.vue
Normal file
67
src/layout/components/Footer/index.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<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 0.1.0 · Made by Ah jung
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GithubOutlined, CopyrightOutlined } from '@vicons/antd'
|
||||
|
||||
export default {
|
||||
name: 'PageFooter',
|
||||
components: { GithubOutlined, CopyrightOutlined },
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-footer {
|
||||
margin: 48px 0 24px 0;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
font-size: 14px;
|
||||
color: #808695;
|
||||
-webkit-transition: all .2s ease-in-out;
|
||||
transition: all .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>
|
||||
324
src/layout/components/Header/ProjectSetting.vue
Normal file
324
src/layout/components/Header/ProjectSetting.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
|
||||
<n-drawer-content :title="title">
|
||||
<div class="drawer">
|
||||
<n-divider title-placement="center">主题</n-divider>
|
||||
|
||||
<div class="drawer-setting-item justify-center dark-switch">
|
||||
<n-tooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<n-switch v-model:value="designStore.darkTheme"/>
|
||||
</template>
|
||||
<span>深色主题</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')"/>
|
||||
</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" @click="togNavMode('horizontal')"/>
|
||||
</template>
|
||||
<span>顶部菜单模式</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'"/>
|
||||
</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" @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" @click="togNavTheme('light')"/>
|
||||
</template>
|
||||
<span>白色侧边栏</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-if="settingStore.navTheme === 'light'"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-setting-item align-items-top">
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img src="~@/assets/images/header-theme-dark.svg" @click="togNavTheme('header-dark')"/>
|
||||
</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 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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<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, watch, createVNode, computed, unref } from 'vue'
|
||||
import { useProjectSettingStore } from "@/store/modules/projectSetting";
|
||||
import { useDesignSettingStore } from "@/store/modules/designSetting";
|
||||
import { CheckOutlined } from '@vicons/antd'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProjectSetting',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '项目配置'
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 280
|
||||
},
|
||||
},
|
||||
components: { CheckOutlined },
|
||||
setup(props, { emit }) {
|
||||
const settingStore = useProjectSettingStore()
|
||||
const designStore = useDesignSettingStore()
|
||||
const { width, title } = props
|
||||
const state = reactive({
|
||||
width,
|
||||
title,
|
||||
isDrawer: false,
|
||||
placement: "right",
|
||||
alertText: '该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
|
||||
appThemeList: designStore.appThemeList
|
||||
})
|
||||
|
||||
watch(
|
||||
() => designStore.darkTheme,
|
||||
(to) => {
|
||||
settingStore.navTheme = to ? 'header-dark': 'dark'
|
||||
}
|
||||
)
|
||||
|
||||
function openDrawer(isDrawer) {
|
||||
state.isDrawer = true
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
state.isDrawer = false
|
||||
}
|
||||
|
||||
function togNavTheme(theme) {
|
||||
settingStore.navTheme = theme
|
||||
if (settingStore.navMode === 'horizontal' && theme === 'light') {
|
||||
designStore.navTheme = 'dark'
|
||||
}
|
||||
}
|
||||
|
||||
function togTheme(color) {
|
||||
designStore.appTheme = color
|
||||
}
|
||||
|
||||
function togNavMode(mode) {
|
||||
settingStore.navMode = mode
|
||||
if (mode === 'horizontal') {
|
||||
settingStore.setNavTheme('light')
|
||||
} else {
|
||||
settingStore.setNavTheme('dark')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
settingStore,
|
||||
designStore,
|
||||
togNavTheme,
|
||||
togNavMode,
|
||||
togTheme,
|
||||
darkTheme,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
}
|
||||
}
|
||||
})
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.n-icon {
|
||||
color: #fff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.align-items-top {
|
||||
align-items: flex-start;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dark-switch .n-switch--active {
|
||||
::v-deep(.n-switch__rail) {
|
||||
background-color: #000e1c;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/layout/components/Header/components.ts
Normal file
34
src/layout/components/Header/components.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NLayout, NAvatar, NMenu, NDropdown, NBreadcrumb, NTooltip } from 'naive-ui'
|
||||
|
||||
import {
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
PoweroffOutlined,
|
||||
GithubOutlined,
|
||||
LockOutlined,
|
||||
ReloadOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
CheckOutlined
|
||||
} from '@vicons/antd'
|
||||
|
||||
export default {
|
||||
SettingOutlined,
|
||||
NDropdown,
|
||||
LockOutlined,
|
||||
GithubOutlined,
|
||||
SearchOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
PoweroffOutlined,
|
||||
ReloadOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
CheckOutlined
|
||||
}
|
||||
3
src/layout/components/Header/index.ts
Normal file
3
src/layout/components/Header/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import PageHeader from './index.vue'
|
||||
|
||||
export { PageHeader }
|
||||
420
src/layout/components/Header/index.vue
Normal file
420
src/layout/components/Header/index.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div class="layout-header" :class="{'layout-header-light':!(navTheme == 'header-dark')}">
|
||||
<!--顶部菜单-->
|
||||
<div class="layout-header-left" v-if="navMode==='horizontal'">
|
||||
<AsideMenu v-model:collapsed="collapsed" mode="horizontal" class="n-menu-horizontal-light"/>
|
||||
</div>
|
||||
<!--左侧菜单-->
|
||||
<div class="layout-header-left" v-else>
|
||||
<!-- 菜单收起 -->
|
||||
<span 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>
|
||||
</span>
|
||||
<!-- 刷新 -->
|
||||
<span class="mr-1 layout-header-trigger layout-header-trigger-min" v-if="headerSetting.isReload"
|
||||
@click="reloadPage">
|
||||
<n-icon size="18">
|
||||
<ReloadOutlined/>
|
||||
</n-icon>
|
||||
</span>
|
||||
<!-- 面包屑 -->
|
||||
<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"></component>
|
||||
{{ routeItem.meta.title }}
|
||||
</span>
|
||||
</n-dropdown>
|
||||
<span class="link-text" v-else>
|
||||
<component v-if="crumbsSetting.showIcon && routeItem.meta.icon" :is="routeItem.meta.icon"></component>
|
||||
{{ routeItem.meta.title }}
|
||||
</span>
|
||||
</n-breadcrumb-item>
|
||||
</template>
|
||||
</n-breadcrumb>
|
||||
</div>
|
||||
<div class="layout-header-right">
|
||||
<span 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>
|
||||
</span>
|
||||
<!--切换全屏-->
|
||||
<span class="layout-header-trigger layout-header-trigger-min">
|
||||
<n-icon size="18">
|
||||
<component :is="fullscreenIcon" @click="toggleFullScreen"/>
|
||||
</n-icon>
|
||||
</span>
|
||||
<!-- 个人中心 -->
|
||||
<span class="layout-header-trigger layout-header-trigger-min">
|
||||
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
|
||||
<div class="avatar">
|
||||
<n-avatar>
|
||||
{{ username }}
|
||||
<template #icon><UserOutlined/></template>
|
||||
</n-avatar>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</span>
|
||||
<!--设置-->
|
||||
<span 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--项目配置-->
|
||||
<ProjectSetting ref="drawerSetting"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, createVNode, ref, computed, unref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import components from './components'
|
||||
import { NDialogProvider, useDialog, useMessage, useNotification } 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";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageHeader',
|
||||
components: { ...components, NDialogProvider, ProjectSetting, AsideMenu },
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore()
|
||||
const useLockscreen = useLockscreenStore()
|
||||
const message = useMessage()
|
||||
const notification = useNotification()
|
||||
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 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 router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const generator: any = (routerMap, parent) => {
|
||||
return routerMap.map((item, key) => {
|
||||
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: any = generator(route.matched)
|
||||
|
||||
const dropdownSelect = (key) => {
|
||||
router.push({ name: key })
|
||||
}
|
||||
|
||||
// 刷新页面
|
||||
const reloadPage = () => {
|
||||
router.push({
|
||||
path: '/redirect' + unref(route).fullPath
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const doLogout = () => {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '您确定要退出登录吗',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
userStore.logout().then((res) => {
|
||||
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/jekip/naive-ui-admin')
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'LockOutlined',
|
||||
tips: '锁屏',
|
||||
eventObject: {
|
||||
click: () => useLockscreen.setLock(true)
|
||||
}
|
||||
}
|
||||
]
|
||||
const avatarOptions = [
|
||||
{
|
||||
label: '个人中心',
|
||||
key: 1
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 2
|
||||
},
|
||||
]
|
||||
|
||||
//头像下拉菜单
|
||||
const avatarSelect = (key) => {
|
||||
switch (key) {
|
||||
case 1:
|
||||
openUserCentre()
|
||||
break;
|
||||
case 2:
|
||||
doLogout()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function openSetting() {
|
||||
const { openDrawer } = drawerSetting.value
|
||||
openDrawer()
|
||||
}
|
||||
|
||||
function openUserCentre() {
|
||||
notification.info({
|
||||
content: '提示',
|
||||
meta: '客官,该功能正在开发中呢...'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
iconList,
|
||||
toggleFullScreen,
|
||||
doLogout,
|
||||
route,
|
||||
dropdownSelect,
|
||||
avatarOptions,
|
||||
getChangeStyle,
|
||||
avatarSelect,
|
||||
breadcrumbList,
|
||||
reloadPage,
|
||||
drawerSetting,
|
||||
openSetting,
|
||||
}
|
||||
}
|
||||
})
|
||||
</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 .2s ease-in-out;
|
||||
width: 100%;
|
||||
z-index: 11;
|
||||
//color: #fff;
|
||||
|
||||
//.n-icon {
|
||||
// color: #fff
|
||||
//}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
::v-deep(.ant-breadcrumb span:last-child .link-text) {
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.n-breadcrumb {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&-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%, .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-router-link) {
|
||||
// color: #515a6e;
|
||||
//
|
||||
// &:hover {
|
||||
// color: #1890ff;
|
||||
// }
|
||||
//}
|
||||
</style>
|
||||
3
src/layout/components/Logo/index.ts
Normal file
3
src/layout/components/Logo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Logo from './index.vue'
|
||||
|
||||
export { Logo }
|
||||
39
src/layout/components/Logo/index.vue
Normal file
39
src/layout/components/Logo/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img src="~@/assets/images/logo.png" alt=""/>
|
||||
<h2 v-show="!collapsed" class="title">NaiveUiAdmin</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 {
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
src/layout/components/Main/index.ts
Normal file
3
src/layout/components/Main/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MainView from './index.vue'
|
||||
|
||||
export { MainView }
|
||||
43
src/layout/components/Main/index.vue
Normal file
43
src/layout/components/Main/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<RouterView>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="zoom-fade" 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 } from 'vue'
|
||||
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainView',
|
||||
components: {},
|
||||
props: {
|
||||
notNeedKey: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
animate: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const asyncRouteStore = useAsyncRouteStore()
|
||||
// 需要缓存的路由组件
|
||||
const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents)
|
||||
return {
|
||||
keepAliveComponents
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
3
src/layout/components/Menu/index.ts
Normal file
3
src/layout/components/Menu/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import AsideMenu from './index.vue'
|
||||
|
||||
export { AsideMenu }
|
||||
91
src/layout/components/Menu/index.vue
Normal file
91
src/layout/components/Menu/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<NMenu :options="menus" :inverted="inverted" :mode="mode" :collapsed="collapsed" :collapsed-width="64"
|
||||
:collapsed-icon-size="20"
|
||||
@update:value="clickMenuItem" :default-expanded-keys="openKeys" v-model:value="selectedKeys">
|
||||
</NMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed, watch, toRefs, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'
|
||||
import { generatorMenu } from '@/utils/index'
|
||||
import { useProjectSettingStore } from "@/store/modules/projectSetting";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Menu',
|
||||
components: {},
|
||||
props: {
|
||||
mode: {
|
||||
// 菜单模式
|
||||
type: String,
|
||||
default: 'vertical'
|
||||
},
|
||||
collapsed: {
|
||||
// 侧边栏菜单是否收起
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
// 当前路由
|
||||
const currentRoute = useRoute()
|
||||
const router = useRouter()
|
||||
const asyncRouteStore = useAsyncRouteStore()
|
||||
const userStore = useUserStore()
|
||||
const settingStore = useProjectSettingStore()
|
||||
const { mode } = props
|
||||
// 获取当前打开的子菜单
|
||||
const getOpenKeys = () => [currentRoute.matched[0]?.name]
|
||||
|
||||
const state = reactive({
|
||||
openKeys: getOpenKeys(),
|
||||
selectedKeys: currentRoute.name,
|
||||
})
|
||||
|
||||
const inverted = computed(() => {
|
||||
return ['dark', 'header-dark'].includes(settingStore.navTheme)
|
||||
})
|
||||
|
||||
const menus = computed(() => {
|
||||
return generatorMenu(asyncRouteStore.getMenus)
|
||||
})
|
||||
|
||||
// 监听菜单收缩状态
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(newVal) => {
|
||||
state.openKeys = newVal ? [] : getOpenKeys()
|
||||
state.selectedKeys = currentRoute.name
|
||||
}
|
||||
)
|
||||
|
||||
// 跟随页面路由变化,切换菜单选中状态
|
||||
watch(
|
||||
() => currentRoute.fullPath,
|
||||
() => {
|
||||
if (currentRoute.name == 'login' || props.collapsed) return
|
||||
state.openKeys = getOpenKeys()
|
||||
state.selectedKeys = currentRoute.name
|
||||
}
|
||||
)
|
||||
|
||||
// 点击菜单
|
||||
const clickMenuItem = (key, item) => {
|
||||
if (/http(s)?:/.test(key)) {
|
||||
window.open(key)
|
||||
} else {
|
||||
router.push({ name: key })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
inverted,
|
||||
menus,
|
||||
mode,
|
||||
clickMenuItem
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
24
src/layout/components/TagsView/components.ts
Normal file
24
src/layout/components/TagsView/components.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
ColumnWidthOutlined,
|
||||
MinusOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Dropdown, Tabs, Card } from 'ant-design-vue'
|
||||
|
||||
export default {
|
||||
[Tabs.name]: Tabs,
|
||||
[Tabs.TabPane.name]: Tabs.TabPane,
|
||||
[Dropdown.name]: Dropdown,
|
||||
[Card.name]: Card,
|
||||
MinusOutlined,
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
ColumnWidthOutlined
|
||||
}
|
||||
3
src/layout/components/TagsView/index.ts
Normal file
3
src/layout/components/TagsView/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TabsView from './index.vue'
|
||||
|
||||
export { TabsView }
|
||||
562
src/layout/components/TagsView/index.vue
Normal file
562
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<div class="tabs-view"
|
||||
:class="{
|
||||
'tabs-view-fix':multiTabsSetting.fixed,
|
||||
'tabs-view-fixed-header':isMultiHeaderFixed,
|
||||
'tabs-view-default-background':getDarkTheme === false
|
||||
}"
|
||||
: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">
|
||||
<div ref="navRef" class="tabs-card-nav" :style="getNavStyle">
|
||||
<Draggable :list="tabsList" animation="300" item-key="fullPath">
|
||||
<template #item="{element}">
|
||||
<div class="tabs-card-scroll-item" @click.stop="goPage(element)" @contextmenu="handleContextMenu">
|
||||
<span>{{ element.meta.title }}</span>
|
||||
<n-icon size="14" @click.stop="closeTabItem(element)">
|
||||
<CloseOutlined/>
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs-close">
|
||||
<n-dropdown trigger="hover" @select="closeHandleSelect" placement="bottom-end" :options="TabsMenuOptions">
|
||||
<div class="tabs-close-btn" @click.prevent>
|
||||
<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, toRaw, 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/src/vuedraggable'
|
||||
import {
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
ColumnWidthOutlined,
|
||||
MinusOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined
|
||||
} from '@vicons/antd'
|
||||
import { renderIcon } from '@/utils/index'
|
||||
import elementResizeDetectorMaker from 'element-resize-detector'
|
||||
import { useProjectSettingStore } from "@/store/modules/projectSetting";
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabsView',
|
||||
components: {
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
ColumnWidthOutlined,
|
||||
MinusOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
Draggable
|
||||
},
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { getDarkTheme } = useDesignSetting()
|
||||
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } = useProjectSetting()
|
||||
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tabsViewStore = useTabsViewStore()
|
||||
const asyncRouteStore = useAsyncRouteStore()
|
||||
const navRef: any = ref(null)
|
||||
const navScroll: any = ref(null)
|
||||
const navWrap: any = ref(null)
|
||||
|
||||
const state = reactive({
|
||||
activeKey: route.fullPath,
|
||||
scrollable: false,
|
||||
navStyle: {
|
||||
transform: ''
|
||||
},
|
||||
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 getChangeStyle = computed(() => {
|
||||
const { collapsed } = props
|
||||
const navMode = unref(getNavMode)
|
||||
const { minMenuWidth, menuWidth }:any = unref(getMenuSetting)
|
||||
const { fixed }:any = unref(getMultiTabsSetting)
|
||||
let lenNum = navMode === 'horizontal' ? '0px' : collapsed ? `${ minMenuWidth }px` : `${ menuWidth }px`
|
||||
return {
|
||||
left: lenNum,
|
||||
width: `calc(100% - ${ !fixed ? '0px' : lenNum })`
|
||||
}
|
||||
})
|
||||
|
||||
//tags 右侧下拉菜单
|
||||
const TabsMenuOptions = [
|
||||
{
|
||||
label: '刷新当前',
|
||||
key: '1',
|
||||
icon: renderIcon(ReloadOutlined)
|
||||
},
|
||||
{
|
||||
label: '关闭当前',
|
||||
key: '2',
|
||||
icon: renderIcon(CloseOutlined)
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: '3',
|
||||
icon: renderIcon(ColumnWidthOutlined)
|
||||
},
|
||||
{
|
||||
label: '关闭全部',
|
||||
key: '4',
|
||||
icon: renderIcon(MinusOutlined)
|
||||
}
|
||||
]
|
||||
|
||||
let routes: RouteItem[] = []
|
||||
|
||||
try {
|
||||
const routesStr = storage.get(TABS_ROUTES) as string | null | undefined
|
||||
routes = routesStr ? JSON.parse(routesStr) : [getSimpleRoute(route)]
|
||||
} catch (e) {
|
||||
routes = [getSimpleRoute(route)]
|
||||
}
|
||||
|
||||
// 初始化标签页
|
||||
tabsViewStore.initTabs(routes)
|
||||
|
||||
|
||||
//监听滚动条
|
||||
function onScroll(e) {
|
||||
let scrollTop = e.target.scrollTop || (document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop); // 滚动条偏移量
|
||||
if (!getHeaderSetting.fixed && getMultiTabsSetting.fixed && scrollTop >= 64) {
|
||||
state.isMultiHeaderFixed = true
|
||||
} else {
|
||||
state.isMultiHeaderFixed = false
|
||||
}
|
||||
}
|
||||
|
||||
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 = ['Redirect', 'login']
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(to) => {
|
||||
if (whiteList.includes(route.name as string) || ['ErrorPage'].includes(route.name as string)) return
|
||||
state.activeKey = to
|
||||
tabsViewStore.addTabs(getSimpleRoute(route))
|
||||
},
|
||||
{ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新页面
|
||||
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)
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
const closeRight = (route) => {
|
||||
tabsViewStore.closeRightTabs(route)
|
||||
state.activeKey = route.fullPath
|
||||
router.replace(route.fullPath)
|
||||
}
|
||||
|
||||
// 关闭其他
|
||||
const closeOther = (route) => {
|
||||
tabsViewStore.closeOtherTabs(route)
|
||||
state.activeKey = route.fullPath
|
||||
router.replace(route.fullPath)
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
const closeAll = () => {
|
||||
localStorage.removeItem('routes')
|
||||
tabsViewStore.closeAllTabs()
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
//tab 操作
|
||||
const closeHandleSelect = (key) => {
|
||||
switch (key) {
|
||||
//刷新
|
||||
case '1':
|
||||
reloadPage()
|
||||
break
|
||||
//关闭
|
||||
case '2':
|
||||
removeTab(route)
|
||||
break
|
||||
//关闭其他
|
||||
case '3':
|
||||
closeOther(route)
|
||||
break
|
||||
//关闭所有
|
||||
case '4':
|
||||
closeAll()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentScrollOffset() {
|
||||
const { navStyle } = state
|
||||
const transform: any = toRaw(navStyle.transform)
|
||||
return transform ? Number(transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1]) : 0
|
||||
}
|
||||
|
||||
function setOffset(value) {
|
||||
state.navStyle.transform = `translateX(-${ value }px)`
|
||||
}
|
||||
|
||||
function scrollPrev() {
|
||||
const containerWidth = navScroll.value.offsetWidth
|
||||
const currentOffset = getCurrentScrollOffset()
|
||||
if (!currentOffset) return
|
||||
let newOffset = currentOffset > containerWidth ? currentOffset - containerWidth : 0
|
||||
setOffset(newOffset)
|
||||
}
|
||||
|
||||
function scrollNext() {
|
||||
const navWidth = navRef.value.offsetWidth
|
||||
const containerWidth = navScroll.value.offsetWidth
|
||||
const currentOffset = getCurrentScrollOffset()
|
||||
if (navWidth - currentOffset <= containerWidth) return
|
||||
|
||||
let newOffset =
|
||||
navWidth - currentOffset > containerWidth * 2
|
||||
? currentOffset + containerWidth
|
||||
: navWidth - containerWidth
|
||||
|
||||
setOffset(newOffset)
|
||||
}
|
||||
|
||||
function updateNavScroll() {
|
||||
const navWidth = navRef.value.offsetWidth
|
||||
const containerWidth = navScroll.value.offsetWidth
|
||||
const currentOffset = getCurrentScrollOffset()
|
||||
if (containerWidth < navWidth) {
|
||||
state.scrollable = true
|
||||
if (navWidth - currentOffset < containerWidth) {
|
||||
setOffset(navWidth - containerWidth)
|
||||
}
|
||||
} else {
|
||||
state.scrollable = false
|
||||
if (currentOffset > 0) {
|
||||
setOffset(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
updateNavScroll()
|
||||
}
|
||||
|
||||
const getNavStyle = computed(() => {
|
||||
return state.navStyle
|
||||
})
|
||||
|
||||
function handleContextMenu(e) {
|
||||
e.preventDefault()
|
||||
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
|
||||
router.push({ path: fullPath })
|
||||
}
|
||||
|
||||
//删除tab
|
||||
function closeTabItem(e) {
|
||||
const { fullPath } = e
|
||||
const routeInfo = tabsList.value.find((item) => item.fullPath == fullPath)
|
||||
removeTab(routeInfo)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let observer
|
||||
observer = elementResizeDetectorMaker()
|
||||
observer.listenTo(navWrap.value, handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
navWrap,
|
||||
navRef,
|
||||
navScroll,
|
||||
route,
|
||||
tabsList,
|
||||
goPage,
|
||||
closeTabItem,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
closeAll,
|
||||
reloadPage,
|
||||
getChangeStyle,
|
||||
TabsMenuOptions,
|
||||
closeHandleSelect,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
getNavStyle,
|
||||
handleContextMenu,
|
||||
onClickOutside,
|
||||
getDarkTheme
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tabs-view {
|
||||
width: 100%;
|
||||
padding: 6px 0px;
|
||||
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 {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.tabs-card-nav {
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
float: left;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
&-item {
|
||||
background: var(--color);
|
||||
color: var(--text-color);
|
||||
height: 32px;
|
||||
padding: 6px 16px 4px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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-fix {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
padding: 6px 19px 6px 10px;
|
||||
left: 200px;
|
||||
}
|
||||
|
||||
.tabs-view-fixed-header {
|
||||
top: 0px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user