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:
parent
79ca1b4d6b
commit
08da7802c6
@ -54,6 +54,7 @@ type SettingInfo struct {
|
||||
FileRecycleBin string `json:"fileRecycleBin"`
|
||||
|
||||
SnapshotIgnore string `json:"snapshotIgnore"`
|
||||
XpackHideMenu string `json:"xpackHideMenu"`
|
||||
}
|
||||
|
||||
type SettingUpdate struct {
|
||||
|
@ -75,6 +75,7 @@ func Init() {
|
||||
|
||||
migrations.AddSnapshotIgnore,
|
||||
migrations.AddDatabaseIsDelete,
|
||||
migrations.AddXpackHideMenu,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ export namespace Setting {
|
||||
weChatVars: string;
|
||||
dingVars: string;
|
||||
snapshotIgnore: string;
|
||||
xpackHideMenu: string;
|
||||
}
|
||||
export interface SettingUpdate {
|
||||
key: string;
|
||||
|
@ -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',
|
||||
|
@ -1357,6 +1357,12 @@ const message = {
|
||||
currentVersion: '當前運行版本:',
|
||||
|
||||
license: '許可證',
|
||||
advancedMenuShow: '高級功能選單顯示',
|
||||
showMainAdvancedMenu: '如果只保留 1 個選單,則側邊欄只會顯示高級功能主選單',
|
||||
showAll: '全部顯示',
|
||||
ifShow: '是否顯示',
|
||||
menu: '選單',
|
||||
confirmMessage: '即將刷新頁面更新高級功能菜單列表,是否繼續?',
|
||||
},
|
||||
license: {
|
||||
community: '社區版',
|
||||
|
@ -1359,6 +1359,12 @@ const message = {
|
||||
currentVersion: '当前运行版本:',
|
||||
|
||||
license: '许可证',
|
||||
advancedMenuShow: '高级功能菜单显示',
|
||||
showMainAdvancedMenu: '如果只保留 1 个菜单,则侧边栏只会显示高级功能主菜单',
|
||||
showAll: '全部显示',
|
||||
ifShow: '是否显示',
|
||||
menu: '菜单',
|
||||
confirmMessage: '即将刷新页面更新高级功能菜单列表,是否继续?',
|
||||
},
|
||||
license: {
|
||||
community: '社区版',
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
151
frontend/src/views/setting/panel/hidemenu/index.vue
Normal file
151
frontend/src/views/setting/panel/hidemenu/index.vue
Normal 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>
|
@ -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') {
|
||||
|
Loading…
x
Reference in New Issue
Block a user