1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-13 17:24:44 +08:00

feat: 专业版菜单隐藏控制功能实现 (#4392)

This commit is contained in:
John Bro 2024-04-03 14:11:12 +08:00 committed by GitHub
parent 79ca1b4d6b
commit 08da7802c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 315 additions and 2 deletions

View File

@ -54,6 +54,7 @@ type SettingInfo struct {
FileRecycleBin string `json:"fileRecycleBin"`
SnapshotIgnore string `json:"snapshotIgnore"`
XpackHideMenu string `json:"xpackHideMenu"`
}
type SettingUpdate struct {

View File

@ -75,6 +75,7 @@ func Init() {
migrations.AddSnapshotIgnore,
migrations.AddDatabaseIsDelete,
migrations.AddXpackHideMenu,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -25,3 +25,13 @@ var AddDatabaseIsDelete = &gormigrate.Migration{
return nil
},
}
var AddXpackHideMenu = &gormigrate.Migration{
ID: "20240328-add-xpack-hide-menu",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: "{\"id\":\"1\",\"label\":\"/xpack\",\"isCheck\":false,\"title\":\"xpack.menu\",\"children\":[{\"id\":\"2\",\"title\":\"xpack.waf.name\",\"path\":\"/xpack/waf/dashboard\",\"label\":\"Dashboard\",\"isCheck\":false},{\"id\":\"3\",\"title\":\"xpack.tamper.tamper\",\"path\":\"/xpack/tamper\",\"label\":\"Tamper\",\"isCheck\":true},{\"id\":\"4\",\"title\":\"xpack.setting.setting\",\"path\":\"/xpack/setting\",\"label\":\"XSetting\",\"isCheck\":true}]}"}).Error; err != nil {
return err
}
return nil
},
}

View File

@ -46,6 +46,7 @@ export namespace Setting {
weChatVars: string;
dingVars: string;
snapshotIgnore: string;
xpackHideMenu: string;
}
export interface SettingUpdate {
key: string;

View File

@ -1457,6 +1457,13 @@ const message = {
currentVersion: 'Version',
license: 'License',
advancedMenuShow: 'Advanced Menu Display',
showMainAdvancedMenu:
'If only one menu is retained, only the main advanced menu will be displayed in the sidebar',
showAll: 'Show All',
ifShow: 'Whether to Show',
menu: 'Menu',
confirmMessage: 'The page will be refreshed to update the advanced menu list. Continue?',
},
license: {
community: 'Community Edition',

View File

@ -1357,6 +1357,12 @@ const message = {
currentVersion: '當前運行版本',
license: '許可證',
advancedMenuShow: '高級功能選單顯示',
showMainAdvancedMenu: '如果只保留 1 個選單則側邊欄只會顯示高級功能主選單',
showAll: '全部顯示',
ifShow: '是否顯示',
menu: '選單',
confirmMessage: '即將刷新頁面更新高級功能菜單列表是否繼續',
},
license: {
community: '社區版',

View File

@ -1359,6 +1359,12 @@ const message = {
currentVersion: '当前运行版本',
license: '许可证',
advancedMenuShow: '高级功能菜单显示',
showMainAdvancedMenu: '如果只保留 1 个菜单则侧边栏只会显示高级功能主菜单',
showAll: '全部显示',
ifShow: '是否显示',
menu: '菜单',
confirmMessage: '即将刷新页面更新高级功能菜单列表是否继续',
},
license: {
community: '社区版',

View File

@ -44,6 +44,8 @@ import { ElMessageBox } from 'element-plus';
import { GlobalStore, MenuStore } from '@/store';
import { MsgSuccess } from '@/utils/message';
import { isString } from '@vueuse/core';
import { getSettingInfo } from '@/api/modules/setting';
const route = useRoute();
const menuStore = MenuStore();
const globalStore = GlobalStore();
@ -53,9 +55,20 @@ const activeMenu = computed(() => {
});
const isCollapse = computed((): boolean => menuStore.isCollapse);
const routerMenus = computed((): RouteRecordRaw[] => menuStore.menuList);
let routerMenus = computed((): RouteRecordRaw[] => {
return menuStore.menuList.filter((route) => route.meta && !route.meta.hideInSidebar);
});
const screenWidth = ref(0);
interface Node {
id: string;
title: string;
path?: string;
label: string;
isCheck: boolean;
children?: Node[];
}
const listeningWindow = () => {
window.onresize = () => {
return (() => {
@ -85,8 +98,65 @@ const logout = () => {
const systemLogOut = async () => {
await logOutApi();
};
function extractLabels(node: Node, result: string[]): void {
//
if (node.isCheck) {
result.push(node.label);
}
if (node.children) {
for (const childNode of node.children) {
extractLabels(childNode, result);
}
}
}
function getCheckedLabels(json: Node): string[] {
let result: string[] = [];
extractLabels(json, result);
return result;
}
const search = async () => {
const res = await getSettingInfo();
const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedLabels = getCheckedLabels(json);
let rstMenuList: RouteRecordRaw[] = [];
menuStore.menuList.forEach((item) => {
let menuItem = JSON.parse(JSON.stringify(item));
let menuChildren: RouteRecordRaw[] = [];
if (menuItem.path === '/xpack') {
if (checkedLabels.length) {
menuItem.children.forEach((child: any) => {
for (const str of checkedLabels) {
if (child.name === str) {
child.hidden = false;
}
}
if (child.hidden === false) {
menuChildren.push(child);
}
});
menuItem.meta.hideInSidebar = false;
}
menuItem.children = menuChildren as RouteRecordRaw[];
rstMenuList.push(menuItem);
} else {
menuItem.children.forEach((child: any) => {
if (child.hidden == undefined || child.hidden == false) {
menuChildren.push(child);
}
});
menuItem.children = menuChildren as RouteRecordRaw[];
rstMenuList.push(menuItem);
}
});
menuStore.menuList = rstMenuList;
};
onMounted(() => {
menuStore.setMenuList(menuList);
search();
});
</script>

View File

@ -48,7 +48,7 @@ rolesRoutes.forEach((item) => {
let menuItem = JSON.parse(JSON.stringify(item));
let menuChildren: RouteRecordRaw[] = [];
menuItem.children.forEach((child: any) => {
if (child.hidden == null || child.hidden == false) {
if (child.hidden == undefined || child.hidden == false) {
menuChildren.push(child);
}
});

View File

@ -0,0 +1,151 @@
<template>
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('setting.advancedMenuShow')" :back="handleClose" />
</template>
<ComplexTable
:data="treeData.hideMenu"
:show-header="false"
style="width: 100%; margin-bottom: 20px"
row-key="id"
default-expand-all
>
<el-table-column prop="title" :label="$t('setting.menu')">
<template #default="{ row }">
{{ i18n.global.t(row.title) }}
</template>
</el-table-column>
<el-table-column prop="isCheck" :label="$t('setting.ifShow')">
<template #default="{ row }">
<el-switch v-model="row.isCheck" @change="onSaveStatus(row)" />
</template>
</el-table-column>
</ComplexTable>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="saveHideMenus">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { DialogProps, ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { updateSetting } from '@/api/modules/setting';
import { MsgSuccess } from '@/utils/message';
const drawerVisible = ref();
const loading = ref();
const defaultCheck = ref([]);
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
menuList: string;
}
const menuList = ref();
const treeData = reactive({
hideMenu: [],
checkedData: [],
});
function loadCheck(data: any, checkList: any) {
if (data.children === null) {
if (data.isCheck) {
checkList.push(data.id);
}
return;
}
for (const item of data) {
if (item.isCheck) {
checkList.push(item.id);
continue;
}
if (item.children) {
loadCheck(item.children, checkList);
}
}
}
const onSaveStatus = async (row: any) => {
if (row.label === '/xpack') {
if (!row.isCheck) {
for (const item of treeData.hideMenu[0].children) {
item.isCheck = false;
}
} else {
let flag = false;
for (const item of treeData.hideMenu[0].children) {
if (item.isCheck) {
flag = true;
}
}
if (!flag && row.isCheck) {
for (const item of treeData.hideMenu[0].children) {
item.isCheck = true;
}
}
}
} else {
let flag = false;
if (row.isCheck) {
treeData.hideMenu[0].isCheck = true;
}
for (const item of treeData.hideMenu[0].children) {
if (item.isCheck) {
flag = true;
}
}
if (!flag) {
treeData.hideMenu[0].isCheck = false;
}
}
};
const acceptParams = (params: DialogProps): void => {
menuList.value = params.menuList;
drawerVisible.value = true;
treeData.hideMenu = [];
defaultCheck.value = [];
treeData.hideMenu.push(JSON.parse(menuList.value));
loadCheck(treeData.hideMenu, defaultCheck.value);
};
const handleClose = () => {
drawerVisible.value = false;
};
const saveHideMenus = async () => {
ElMessageBox.confirm(i18n.global.t('setting.confirmMessage'), i18n.global.t('setting.advancedMenuShow'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
const updateJson = JSON.stringify(treeData.hideMenu[0]);
await updateSetting({ key: 'XpackHideMenu', value: updateJson })
.then(async () => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loading.value = false;
drawerVisible.value = false;
emit('search');
window.location.reload();
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -101,6 +101,16 @@
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('setting.advancedMenuShow')">
<el-input disabled v-model="form.proHideMenus">
<template #append>
<el-button v-show="!show" @click="onChangeHideMenus" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
@ -113,6 +123,7 @@
<SystemIP ref="systemIPRef" @search="search()" />
<Timeout ref="timeoutRef" @search="search()" />
<Network ref="networkRef" @search="search()" />
<HideMenu ref="hideMenuRef" @search="search()" />
</div>
</template>
@ -130,6 +141,7 @@ import Timeout from '@/views/setting/panel/timeout/index.vue';
import PanelName from '@/views/setting/panel/name/index.vue';
import SystemIP from '@/views/setting/panel/systemip/index.vue';
import Network from '@/views/setting/panel/default-network/index.vue';
import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
const loading = ref(false);
const i18n = useI18n();
@ -152,6 +164,9 @@ const form = reactive({
complexityVerification: '',
defaultNetwork: '',
defaultNetworkVal: '',
proHideMenus: ref(i18n.t('setting.unSetting')),
hideMenuList: '',
});
const show = ref();
@ -162,8 +177,18 @@ const panelNameRef = ref();
const systemIPRef = ref();
const timeoutRef = ref();
const networkRef = ref();
const hideMenuRef = ref();
const unset = ref(i18n.t('setting.unSetting'));
interface Node {
id: string;
title: string;
path?: string;
label: string;
isCheck: boolean;
children?: Node[];
}
const search = async () => {
const res = await getSettingInfo();
form.userName = res.data.userName;
@ -179,8 +204,39 @@ const search = async () => {
form.complexityVerification = res.data.complexityVerification;
form.defaultNetwork = res.data.defaultNetwork;
form.defaultNetworkVal = res.data.defaultNetwork === 'all' ? i18n.t('commons.table.all') : res.data.defaultNetwork;
form.proHideMenus = res.data.xpackHideMenu;
form.hideMenuList = res.data.xpackHideMenu;
// title
const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedTitles = getCheckedTitles(json);
form.proHideMenus = checkedTitles.toString();
};
function extractTitles(node: Node, result: string[]): void {
if (node.isCheck && !node.children) {
result.push(i18n.t(node.title));
}
if (node.children) {
for (const childNode of node.children) {
extractTitles(childNode, result);
}
}
}
function getCheckedTitles(json: Node): string[] {
let result: string[] = [];
extractTitles(json, result);
if (result.length === 0) {
result.push(i18n.t('setting.unSetting'));
}
if (result.length === json.children.length) {
result = [];
result.push(i18n.t('setting.showAll'));
}
return result;
}
const onChangePassword = () => {
passwordRef.value.acceptParams({ complexityVerification: form.complexityVerification });
};
@ -200,6 +256,10 @@ const onChangeNetwork = () => {
networkRef.value.acceptParams({ defaultNetwork: form.defaultNetwork });
};
const onChangeHideMenus = () => {
hideMenuRef.value.acceptParams({ menuList: form.hideMenuList });
};
const onSave = async (key: string, val: any) => {
loading.value = true;
if (key === 'Language') {