mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 22:18:07 +08:00
parent
a3312331d2
commit
5a8d6db43e
@ -19,6 +19,7 @@ type SettingInfo struct {
|
|||||||
BindAddress string `json:"bindAddress"`
|
BindAddress string `json:"bindAddress"`
|
||||||
PanelName string `json:"panelName"`
|
PanelName string `json:"panelName"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
|
MenuTabs string `json:"menuTabs"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
DefaultNetwork string `json:"defaultNetwork"`
|
DefaultNetwork string `json:"defaultNetwork"`
|
||||||
LastCleanTime string `json:"lastCleanTime"`
|
LastCleanTime string `json:"lastCleanTime"`
|
||||||
|
@ -80,6 +80,7 @@ func Init() {
|
|||||||
migrations.NewMonitorDB,
|
migrations.NewMonitorDB,
|
||||||
migrations.AddNoAuthSetting,
|
migrations.AddNoAuthSetting,
|
||||||
migrations.UpdateXpackHideMenu,
|
migrations.UpdateXpackHideMenu,
|
||||||
|
migrations.AddMenuTabsSetting,
|
||||||
})
|
})
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
|
@ -128,3 +128,13 @@ var UpdateXpackHideMenu = &gormigrate.Migration{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddMenuTabsSetting = &gormigrate.Migration{
|
||||||
|
ID: "20240415-add-menu-tabs-setting",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(&model.Setting{Key: "MenuTabs", Value: "disable"}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ export namespace Setting {
|
|||||||
|
|
||||||
panelName: string;
|
panelName: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
menuTabs: string;
|
||||||
language: string;
|
language: string;
|
||||||
defaultNetwork: string;
|
defaultNetwork: string;
|
||||||
lastCleanTime: string;
|
lastCleanTime: string;
|
||||||
|
@ -352,6 +352,9 @@ const message = {
|
|||||||
tabs: {
|
tabs: {
|
||||||
more: 'More',
|
more: 'More',
|
||||||
hide: 'Hide',
|
hide: 'Hide',
|
||||||
|
close: 'Close',
|
||||||
|
closeLeft: 'Close left',
|
||||||
|
closeRight: 'Close right',
|
||||||
closeCurrent: 'Close current',
|
closeCurrent: 'Close current',
|
||||||
closeOther: 'Close other',
|
closeOther: 'Close other',
|
||||||
closeAll: 'Close All',
|
closeAll: 'Close All',
|
||||||
@ -1228,6 +1231,7 @@ const message = {
|
|||||||
portChange: 'Port change',
|
portChange: 'Port change',
|
||||||
portChangeHelper: 'Modify the service port and restart the service. Do you want to continue?',
|
portChangeHelper: 'Modify the service port and restart the service. Do you want to continue?',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
|
menuTabs: 'Menu tabs',
|
||||||
dark: 'Dark',
|
dark: 'Dark',
|
||||||
light: 'Light',
|
light: 'Light',
|
||||||
auto: 'Follow System',
|
auto: 'Follow System',
|
||||||
|
@ -348,6 +348,9 @@ const message = {
|
|||||||
tabs: {
|
tabs: {
|
||||||
more: '更多',
|
more: '更多',
|
||||||
hide: '收起',
|
hide: '收起',
|
||||||
|
close: '關閉',
|
||||||
|
closeLeft: '關閉左側',
|
||||||
|
closeRight: '關閉右側',
|
||||||
closeCurrent: '關閉當前',
|
closeCurrent: '關閉當前',
|
||||||
closeOther: '關閉其它',
|
closeOther: '關閉其它',
|
||||||
closeAll: '關閉所有',
|
closeAll: '關閉所有',
|
||||||
@ -1165,6 +1168,7 @@ const message = {
|
|||||||
portChange: '端口修改',
|
portChange: '端口修改',
|
||||||
portChangeHelper: '服務端口修改需要重啟服務,是否繼續?',
|
portChangeHelper: '服務端口修改需要重啟服務,是否繼續?',
|
||||||
theme: '主題顏色',
|
theme: '主題顏色',
|
||||||
|
menuTabs: '菜單標簽頁',
|
||||||
componentSize: '組件大小',
|
componentSize: '組件大小',
|
||||||
dark: '暗色',
|
dark: '暗色',
|
||||||
light: '亮色',
|
light: '亮色',
|
||||||
|
@ -348,6 +348,9 @@ const message = {
|
|||||||
tabs: {
|
tabs: {
|
||||||
more: '更多',
|
more: '更多',
|
||||||
hide: '收起',
|
hide: '收起',
|
||||||
|
close: '关闭',
|
||||||
|
closeLeft: '关闭左侧',
|
||||||
|
closeRight: '关闭右侧',
|
||||||
closeCurrent: '关闭当前',
|
closeCurrent: '关闭当前',
|
||||||
closeOther: '关闭其它',
|
closeOther: '关闭其它',
|
||||||
closeAll: '关闭所有',
|
closeAll: '关闭所有',
|
||||||
@ -1166,6 +1169,7 @@ const message = {
|
|||||||
portChange: '端口修改',
|
portChange: '端口修改',
|
||||||
portChangeHelper: '服务端口修改需要重启服务,是否继续?',
|
portChangeHelper: '服务端口修改需要重启服务,是否继续?',
|
||||||
theme: '主题颜色',
|
theme: '主题颜色',
|
||||||
|
menuTabs: '菜单标签页',
|
||||||
componentSize: '组件大小',
|
componentSize: '组件大小',
|
||||||
dark: '暗色',
|
dark: '暗色',
|
||||||
light: '亮色',
|
light: '亮色',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view v-slot="{ Component, route }" :key="key">
|
<router-view v-slot="{ Component, route }" :key="key">
|
||||||
<transition appear name="fade-transform" mode="out-in">
|
<transition appear name="fade-transform" mode="out-in">
|
||||||
<keep-alive :include="cacheRouter">
|
<keep-alive :include="include">
|
||||||
<component :is="Component" :key="route.path"></component>
|
<component :is="Component" :key="route.path"></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
@ -15,4 +15,13 @@ import { computed } from 'vue';
|
|||||||
const key = computed(() => {
|
const key = computed(() => {
|
||||||
return Math.random();
|
return Math.random();
|
||||||
});
|
});
|
||||||
|
const include = computed(() => {
|
||||||
|
return props.keepAlive || cacheRouter;
|
||||||
|
});
|
||||||
|
const props = defineProps({
|
||||||
|
keepAlive: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
<el-scrollbar>
|
<el-scrollbar>
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
:router="true"
|
:router="menuRouter"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
:collapse-transition="false"
|
:collapse-transition="false"
|
||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
|
@select="handleMenuClick"
|
||||||
>
|
>
|
||||||
<SubItem :menuList="routerMenus" />
|
<SubItem :menuList="routerMenus" />
|
||||||
<el-menu-item :index="''">
|
<el-menu-item :index="''">
|
||||||
@ -31,7 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, defineEmits } from 'vue';
|
||||||
import { RouteRecordRaw, useRoute } from 'vue-router';
|
import { RouteRecordRaw, useRoute } from 'vue-router';
|
||||||
import { loadingSvg } from '@/utils/svg';
|
import { loadingSvg } from '@/utils/svg';
|
||||||
import Logo from './components/Logo.vue';
|
import Logo from './components/Logo.vue';
|
||||||
@ -49,6 +50,13 @@ import { getSettingInfo } from '@/api/modules/setting';
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const menuStore = MenuStore();
|
const menuStore = MenuStore();
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
defineProps({
|
||||||
|
menuRouter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
const { meta, path } = route;
|
const { meta, path } = route;
|
||||||
return isString(meta.activeMenu) ? meta.activeMenu : path;
|
return isString(meta.activeMenu) ? meta.activeMenu : path;
|
||||||
@ -79,7 +87,10 @@ const listeningWindow = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
listeningWindow();
|
listeningWindow();
|
||||||
|
const emit = defineEmits(['menuClick']);
|
||||||
|
const handleMenuClick = (path) => {
|
||||||
|
emit('menuClick', path);
|
||||||
|
};
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
ElMessageBox.confirm(i18n.global.t('commons.msg.sureLogOut'), i18n.global.t('commons.msg.infoTitle'), {
|
ElMessageBox.confirm(i18n.global.t('commons.msg.sureLogOut'), i18n.global.t('commons.msg.infoTitle'), {
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
97
frontend/src/layout/components/Tabs/components/TabItem.vue
Normal file
97
frontend/src/layout/components/Tabs/components/TabItem.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<el-tab-pane :name="tabItem.path">
|
||||||
|
<template #label>
|
||||||
|
<el-dropdown
|
||||||
|
size="small"
|
||||||
|
:id="tabItem.path"
|
||||||
|
ref="dropdownRef"
|
||||||
|
trigger="contextmenu"
|
||||||
|
@visible-change="$emit('dropdownVisibleChange', $event, tabItem.path)"
|
||||||
|
>
|
||||||
|
<span class="custom-tabs-label">
|
||||||
|
<el-icon v-if="tabsStore.isShowTabIcon && menuIcon">
|
||||||
|
<el-icon>
|
||||||
|
<SvgIcon :iconName="menuIcon" />
|
||||||
|
</el-icon>
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ menuName }}</span>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="tabsStore.hasCloseDropdown(tabItem.path, 'close')"
|
||||||
|
@click="$emit('closeTab', tabItem.path)"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
{{ $t('tabs.close') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="tabsStore.hasCloseDropdown(tabItem.path, 'left')"
|
||||||
|
@click="$emit('closeTabs', tabItem.path, 'left')"
|
||||||
|
>
|
||||||
|
<el-icon><DArrowLeft /></el-icon>
|
||||||
|
{{ $t('tabs.closeLeft') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="tabsStore.hasCloseDropdown(tabItem.path, 'right')"
|
||||||
|
@click="$emit('closeTabs', tabItem.path, 'right')"
|
||||||
|
>
|
||||||
|
<el-icon><DArrowRight /></el-icon>
|
||||||
|
{{ $t('tabs.closeRight') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="tabsStore.hasCloseDropdown(tabItem.path, 'other')"
|
||||||
|
@click="$emit('closeOtherTabs', tabItem.path)"
|
||||||
|
>
|
||||||
|
<el-icon><More /></el-icon>
|
||||||
|
{{ $t('tabs.closeOther') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { TabsStore } from '@/store';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { Close, DArrowLeft, DArrowRight, More } from '@element-plus/icons-vue';
|
||||||
|
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const tabsStore = TabsStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabItem: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['closeTab', 'closeOtherTabs', 'closeTabs', 'dropdownVisibleChange']);
|
||||||
|
|
||||||
|
const menuName = computed(() => {
|
||||||
|
return i18n.t(props.tabItem.meta.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuIcon = computed(() => {
|
||||||
|
return props.tabItem.meta.icon;
|
||||||
|
});
|
||||||
|
const dropdownRef = ref();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
dropdownRef,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.common-tabs .custom-tabs-label .el-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.common-tabs .custom-tabs-label span {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
83
frontend/src/layout/components/Tabs/index.vue
Normal file
83
frontend/src/layout/components/Tabs/index.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<el-tabs
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-model="tabsStore.activeTabPath"
|
||||||
|
class="common-tabs"
|
||||||
|
type="card"
|
||||||
|
:closable="tabsStore.openedTabs.length > 1"
|
||||||
|
@tab-change="tabChange"
|
||||||
|
@tab-remove="closeTab"
|
||||||
|
>
|
||||||
|
<tabs-view-item
|
||||||
|
v-for="item in tabsStore.openedTabs"
|
||||||
|
ref="tabItems"
|
||||||
|
:key="item.path"
|
||||||
|
:tab-item="item"
|
||||||
|
@close-tab="closeTab"
|
||||||
|
@close-other-tabs="closeOtherTabs"
|
||||||
|
@close-tabs="closeTabs"
|
||||||
|
@dropdown-visible-change="dropdownVisibleChange"
|
||||||
|
/>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { TabsStore } from '@/store';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import TabsViewItem from './components/TabItem.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const tabsStore = TabsStore();
|
||||||
|
const tabItems = ref();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!tabsStore.openedTabs.length) {
|
||||||
|
tabsStore.addTab(route);
|
||||||
|
}
|
||||||
|
tabsStore.activeTabPath = route.path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabChange = (tabPath) => {
|
||||||
|
const tab = tabsStore.findTab(tabPath);
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab);
|
||||||
|
tabsStore.activeTabPath = tab.path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTab = (tabPath) => {
|
||||||
|
const lastTabPath = tabsStore.removeTab(tabPath);
|
||||||
|
if (lastTabPath) {
|
||||||
|
tabChange(lastTabPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeOtherTabs = (tabPath) => {
|
||||||
|
tabsStore.removeOtherTabs(tabPath);
|
||||||
|
tabChange(tabPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTabs = (tabPath, type) => {
|
||||||
|
tabsStore.removeTabs(tabPath, type);
|
||||||
|
tabChange(tabPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownVisibleChange = (visible, tabPath) => {
|
||||||
|
if (visible) {
|
||||||
|
// 关闭其他下拉菜单
|
||||||
|
tabItems.value.forEach(({ dropdownRef }) => {
|
||||||
|
if (dropdownRef.id !== tabPath) {
|
||||||
|
dropdownRef.handleClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,3 +2,4 @@ export { default as Sidebar } from './Sidebar/index.vue';
|
|||||||
export { default as Footer } from './AppFooter.vue';
|
export { default as Footer } from './AppFooter.vue';
|
||||||
export { default as AppMain } from './AppMain.vue';
|
export { default as AppMain } from './AppMain.vue';
|
||||||
export { default as MobileHeader } from './MobileHeader.vue';
|
export { default as MobileHeader } from './MobileHeader.vue';
|
||||||
|
export { default as Tabs } from '@/layout/components/Tabs/index.vue';
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
<div :class="classObj" class="app-wrapper" v-loading="loading" :element-loading-text="loadingText" fullscreen>
|
<div :class="classObj" class="app-wrapper" v-loading="loading" :element-loading-text="loadingText" fullscreen>
|
||||||
<div v-if="classObj.mobile && classObj.openSidebar" class="drawer-bg" @click="handleClickOutside" />
|
<div v-if="classObj.mobile && classObj.openSidebar" class="drawer-bg" @click="handleClickOutside" />
|
||||||
<div class="app-sidebar" v-if="!globalStore.isFullScreen">
|
<div class="app-sidebar" v-if="!globalStore.isFullScreen">
|
||||||
<Sidebar />
|
<Sidebar @menu-click="handleMenuClick" :menu-router="!classObj.openMenuTabs" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<mobile-header v-if="classObj.mobile" />
|
<mobile-header v-if="classObj.mobile" />
|
||||||
<app-main class="app-main" />
|
<Tabs v-if="classObj.openMenuTabs" />
|
||||||
|
<app-main :keep-alive="classObj.openMenuTabs ? tabsStore.cachedTabs : null" class="app-main" />
|
||||||
<Footer class="app-footer" v-if="!globalStore.isFullScreen" />
|
<Footer class="app-footer" v-if="!globalStore.isFullScreen" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -16,17 +16,21 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue';
|
import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue';
|
||||||
import { Sidebar, Footer, AppMain, MobileHeader } from './components';
|
import { Sidebar, Footer, AppMain, MobileHeader, Tabs } from './components';
|
||||||
import useResize from './hooks/useResize';
|
import useResize from './hooks/useResize';
|
||||||
import { GlobalStore, MenuStore } from '@/store';
|
import { GlobalStore, MenuStore, TabsStore } from '@/store';
|
||||||
import { DeviceType } from '@/enums/app';
|
import { DeviceType } from '@/enums/app';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { getLicense, getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
|
import { getLicense, getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
useResize();
|
useResize();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const menuStore = MenuStore();
|
const menuStore = MenuStore();
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
const tabsStore = TabsStore();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -36,12 +40,18 @@ const { switchDark } = useTheme();
|
|||||||
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
let timer: NodeJS.Timer | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!tabsStore.activeTabPath) {
|
||||||
|
handleMenuClick('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
const classObj = computed(() => {
|
const classObj = computed(() => {
|
||||||
return {
|
return {
|
||||||
fullScreen: globalStore.isFullScreen,
|
fullScreen: globalStore.isFullScreen,
|
||||||
hideSidebar: menuStore.isCollapse,
|
hideSidebar: menuStore.isCollapse,
|
||||||
openSidebar: !menuStore.isCollapse,
|
openSidebar: !menuStore.isCollapse,
|
||||||
mobile: globalStore.device === DeviceType.Mobile,
|
mobile: globalStore.device === DeviceType.Mobile,
|
||||||
|
openMenuTabs: globalStore.openMenuTabs,
|
||||||
withoutAnimation: menuStore.withoutAnimation,
|
withoutAnimation: menuStore.withoutAnimation,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -59,6 +69,11 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const handleMenuClick = async (path) => {
|
||||||
|
await router.push({ path: path });
|
||||||
|
tabsStore.addTab(route);
|
||||||
|
tabsStore.activeTabPath = route.path;
|
||||||
|
};
|
||||||
|
|
||||||
const loadDataFromDB = async () => {
|
const loadDataFromDB = async () => {
|
||||||
const res = await getSettingInfo();
|
const res = await getSettingInfo();
|
||||||
@ -66,6 +81,7 @@ const loadDataFromDB = async () => {
|
|||||||
i18n.locale.value = res.data.language;
|
i18n.locale.value = res.data.language;
|
||||||
i18n.warnHtmlMessage = false;
|
i18n.warnHtmlMessage = false;
|
||||||
globalStore.entrance = res.data.securityEntrance;
|
globalStore.entrance = res.data.securityEntrance;
|
||||||
|
globalStore.setOpenMenuTabs(res.data.menuTabs === 'enable');
|
||||||
globalStore.updateLanguage(res.data.language);
|
globalStore.updateLanguage(res.data.language);
|
||||||
globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme });
|
globalStore.setThemeConfig({ ...themeConfig.value, theme: res.data.theme });
|
||||||
globalStore.setThemeConfig({ ...themeConfig.value, panelName: res.data.panelName });
|
globalStore.setThemeConfig({ ...themeConfig.value, panelName: res.data.panelName });
|
||||||
|
@ -2,10 +2,11 @@ import { createPinia } from 'pinia';
|
|||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
import GlobalStore from './modules/global';
|
import GlobalStore from './modules/global';
|
||||||
import MenuStore from './modules/menu';
|
import MenuStore from './modules/menu';
|
||||||
|
import TabsStore from './modules/tabs';
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
pinia.use(piniaPluginPersistedstate);
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
export { GlobalStore, MenuStore };
|
export { GlobalStore, MenuStore, TabsStore };
|
||||||
|
|
||||||
export default pinia;
|
export default pinia;
|
||||||
|
@ -20,6 +20,7 @@ export interface GlobalState {
|
|||||||
language: string; // zh | en | tw
|
language: string; // zh | en | tw
|
||||||
themeConfig: ThemeConfigProp;
|
themeConfig: ThemeConfigProp;
|
||||||
isFullScreen: boolean;
|
isFullScreen: boolean;
|
||||||
|
openMenuTabs: boolean;
|
||||||
isOnRestart: boolean;
|
isOnRestart: boolean;
|
||||||
agreeLicense: boolean;
|
agreeLicense: boolean;
|
||||||
hasNewVersion: boolean;
|
hasNewVersion: boolean;
|
||||||
|
@ -23,6 +23,7 @@ const GlobalStore = defineStore({
|
|||||||
logoWithText: '',
|
logoWithText: '',
|
||||||
favicon: '',
|
favicon: '',
|
||||||
},
|
},
|
||||||
|
openMenuTabs: false,
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
isOnRestart: false,
|
isOnRestart: false,
|
||||||
agreeLicense: false,
|
agreeLicense: false,
|
||||||
@ -39,6 +40,9 @@ const GlobalStore = defineStore({
|
|||||||
}),
|
}),
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
|
setOpenMenuTabs(openMenuTabs: boolean) {
|
||||||
|
this.openMenuTabs = openMenuTabs;
|
||||||
|
},
|
||||||
setScreenFull() {
|
setScreenFull() {
|
||||||
this.isFullScreen = !this.isFullScreen;
|
this.isFullScreen = !this.isFullScreen;
|
||||||
},
|
},
|
||||||
|
142
frontend/src/store/modules/tabs.ts
Normal file
142
frontend/src/store/modules/tabs.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
const TabsStore = defineStore(
|
||||||
|
'TabsStore',
|
||||||
|
() => {
|
||||||
|
const isShowTabIcon = ref(true);
|
||||||
|
// 缓存的KEY,直接给keepalive使用
|
||||||
|
const cachedTabs = ref([]);
|
||||||
|
const openedTabs = ref([]);
|
||||||
|
const activeTabPath = ref('');
|
||||||
|
|
||||||
|
const getActivePath = (path) => {
|
||||||
|
let firstSlashIndex = path.indexOf('/');
|
||||||
|
let lastSlashIndex = path.lastIndexOf('/');
|
||||||
|
if (firstSlashIndex === -1 || firstSlashIndex === lastSlashIndex) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return path.substring(firstSlashIndex, lastSlashIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabIdxByPath = (path) => {
|
||||||
|
return openedTabs.value.findIndex((v) => v.path === path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAllTabs = () => {
|
||||||
|
openedTabs.value = [];
|
||||||
|
cachedTabs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUnActiveTabs = () => {
|
||||||
|
if (openedTabs.value.length) {
|
||||||
|
let idx = getTabIdxByPath(activeTabPath.value);
|
||||||
|
idx = idx > -1 ? idx : 0;
|
||||||
|
const tab = openedTabs.value[idx];
|
||||||
|
removeOtherTabs(tab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findTab = (path) => {
|
||||||
|
const idx = getTabIdxByPath(path);
|
||||||
|
if (idx > -1) {
|
||||||
|
return openedTabs.value[idx];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTab = (tab) => {
|
||||||
|
const idx = getTabIdxByPath(tab.path);
|
||||||
|
if (idx < 0) {
|
||||||
|
openedTabs.value.push(Object.assign({}, tab));
|
||||||
|
addCachedTab(tab.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTab = (path) => {
|
||||||
|
if (openedTabs.value.length > 1) {
|
||||||
|
const idx = getTabIdxByPath(path);
|
||||||
|
if (idx > -1) {
|
||||||
|
removeCachedTab(openedTabs.value[idx].name);
|
||||||
|
openedTabs.value.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return openedTabs.value[openedTabs.value.length - 1].path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOtherTabs = (path) => {
|
||||||
|
const idx = getTabIdxByPath(path);
|
||||||
|
if (idx > -1) {
|
||||||
|
const tab = openedTabs.value[idx];
|
||||||
|
openedTabs.value = [tab];
|
||||||
|
cachedTabs.value = [];
|
||||||
|
cachedTabs.value = [tab.name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTabs = (path, type) => {
|
||||||
|
if (path) {
|
||||||
|
const idx = getTabIdxByPath(path);
|
||||||
|
let removeTabs = [];
|
||||||
|
if (type === 'right') {
|
||||||
|
removeTabs = openedTabs.value.splice(idx + 1);
|
||||||
|
} else if (type === 'left') {
|
||||||
|
removeTabs = openedTabs.value.splice(0, idx);
|
||||||
|
}
|
||||||
|
if (removeTabs.length) {
|
||||||
|
removeTabs.forEach((e) => removeCachedTab(e.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCachedTab = (name) => {
|
||||||
|
if (name && !cachedTabs.value.includes(name)) {
|
||||||
|
cachedTabs.value.push(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCachedTab = (name) => {
|
||||||
|
if (name) {
|
||||||
|
const idx = cachedTabs.value.findIndex((v) => v === name);
|
||||||
|
if (idx > -1) {
|
||||||
|
cachedTabs.value.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasCloseDropdown = (path, type) => {
|
||||||
|
const idx = getTabIdxByPath(path);
|
||||||
|
switch (type) {
|
||||||
|
case 'close':
|
||||||
|
case 'other':
|
||||||
|
return openedTabs.value.length > 1;
|
||||||
|
case 'left':
|
||||||
|
return idx !== 0;
|
||||||
|
case 'right':
|
||||||
|
return idx !== openedTabs.value.length - 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isShowTabIcon,
|
||||||
|
activeTabPath,
|
||||||
|
openedTabs,
|
||||||
|
cachedTabs,
|
||||||
|
addTab,
|
||||||
|
findTab,
|
||||||
|
addCachedTab,
|
||||||
|
removeCachedTab,
|
||||||
|
removeTab,
|
||||||
|
removeTabs,
|
||||||
|
removeOtherTabs,
|
||||||
|
removeAllTabs,
|
||||||
|
removeUnActiveTabs,
|
||||||
|
hasCloseDropdown,
|
||||||
|
getActivePath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TabsStore;
|
@ -202,6 +202,9 @@ const loadDetail = (log: string) => {
|
|||||||
if (log.indexOf('[Theme]') !== -1) {
|
if (log.indexOf('[Theme]') !== -1) {
|
||||||
return log.replace('[Theme]', '[' + i18n.global.t('setting.theme') + ']');
|
return log.replace('[Theme]', '[' + i18n.global.t('setting.theme') + ']');
|
||||||
}
|
}
|
||||||
|
if (log.indexOf('[MenuTabs]') !== -1) {
|
||||||
|
return log.replace('[MenuTabs]', '[' + i18n.global.t('setting.menuTabs') + ']');
|
||||||
|
}
|
||||||
if (log.indexOf('[SessionTimeout]') !== -1) {
|
if (log.indexOf('[SessionTimeout]') !== -1) {
|
||||||
return log.replace('[SessionTimeout]', '[' + i18n.global.t('setting.sessionTimeout') + ']');
|
return log.replace('[SessionTimeout]', '[' + i18n.global.t('setting.sessionTimeout') + ']');
|
||||||
}
|
}
|
||||||
|
@ -153,13 +153,14 @@ import { ref, reactive, onMounted, computed } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { ElForm } from 'element-plus';
|
import type { ElForm } from 'element-plus';
|
||||||
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getLanguage } from '@/api/modules/auth';
|
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getLanguage } from '@/api/modules/auth';
|
||||||
import { GlobalStore, MenuStore } from '@/store';
|
import { GlobalStore, MenuStore, TabsStore } from '@/store';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const menuStore = MenuStore();
|
const menuStore = MenuStore();
|
||||||
|
const tabsStore = TabsStore();
|
||||||
const usei18n = useI18n();
|
const usei18n = useI18n();
|
||||||
|
|
||||||
const errAuthInfo = ref(false);
|
const errAuthInfo = ref(false);
|
||||||
@ -270,6 +271,7 @@ const login = (formEl: FormInstance | undefined) => {
|
|||||||
globalStore.setLogStatus(true);
|
globalStore.setLogStatus(true);
|
||||||
globalStore.setAgreeLicense(true);
|
globalStore.setAgreeLicense(true);
|
||||||
menuStore.setMenuList([]);
|
menuStore.setMenuList([]);
|
||||||
|
tabsStore.removeAllTabs();
|
||||||
MsgSuccess(i18n.global.t('commons.msg.loginSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.loginSuccess'));
|
||||||
router.push({ name: 'home' });
|
router.push({ name: 'home' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -295,6 +297,7 @@ const mfaLogin = async (auto: boolean) => {
|
|||||||
}
|
}
|
||||||
globalStore.setLogStatus(true);
|
globalStore.setLogStatus(true);
|
||||||
menuStore.setMenuList([]);
|
menuStore.setMenuList([]);
|
||||||
|
tabsStore.removeAllTabs();
|
||||||
MsgSuccess(i18n.global.t('commons.msg.loginSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.loginSuccess'));
|
||||||
router.push({ name: 'home' });
|
router.push({ name: 'home' });
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,17 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('setting.menuTabs')" prop="menuTabs">
|
||||||
|
<el-radio-group @change="onSave('MenuTabs', form.menuTabs)" v-model="form.menuTabs">
|
||||||
|
<el-radio-button value="enable">
|
||||||
|
<span>{{ $t('commons.button.enable') }}</span>
|
||||||
|
</el-radio-button>
|
||||||
|
<el-radio-button value="disable">
|
||||||
|
<span>{{ $t('commons.button.disable') }}</span>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item :label="$t('setting.title')" prop="panelName">
|
<el-form-item :label="$t('setting.title')" prop="panelName">
|
||||||
<el-input disabled v-model="form.panelName">
|
<el-input disabled v-model="form.panelName">
|
||||||
<template #append>
|
<template #append>
|
||||||
@ -160,6 +171,7 @@ const form = reactive({
|
|||||||
panelName: '',
|
panelName: '',
|
||||||
systemIP: '',
|
systemIP: '',
|
||||||
theme: '',
|
theme: '',
|
||||||
|
menuTabs: '',
|
||||||
language: '',
|
language: '',
|
||||||
complexityVerification: '',
|
complexityVerification: '',
|
||||||
defaultNetwork: '',
|
defaultNetwork: '',
|
||||||
@ -200,6 +212,7 @@ const search = async () => {
|
|||||||
form.panelName = res.data.panelName;
|
form.panelName = res.data.panelName;
|
||||||
form.systemIP = res.data.systemIP;
|
form.systemIP = res.data.systemIP;
|
||||||
form.theme = res.data.theme;
|
form.theme = res.data.theme;
|
||||||
|
form.menuTabs = res.data.menuTabs;
|
||||||
form.language = res.data.language;
|
form.language = res.data.language;
|
||||||
form.complexityVerification = res.data.complexityVerification;
|
form.complexityVerification = res.data.complexityVerification;
|
||||||
form.defaultNetwork = res.data.defaultNetwork;
|
form.defaultNetwork = res.data.defaultNetwork;
|
||||||
@ -270,6 +283,9 @@ const onSave = async (key: string, val: any) => {
|
|||||||
globalStore.setThemeConfig({ ...themeConfig.value, theme: val });
|
globalStore.setThemeConfig({ ...themeConfig.value, theme: val });
|
||||||
switchDark();
|
switchDark();
|
||||||
}
|
}
|
||||||
|
if (key === 'MenuTabs') {
|
||||||
|
globalStore.setOpenMenuTabs(val === 'enable');
|
||||||
|
}
|
||||||
let param = {
|
let param = {
|
||||||
key: key,
|
key: key,
|
||||||
value: val + '',
|
value: val + '',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user