2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00

feat: 支持隐藏菜单和自定义Logo长期保存 (#1200)

* Initial plan

* feat: 支持隐藏菜单和自定义Logo长期保存

- 后端:在 SettingPanel 结构体中添加 HiddenMenu 和 CustomLogo 字段
- 后端:在 GetPanel 和 UpdatePanel 方法中处理新字段的获取和保存
- 后端:修改 Panel 接口返回 hidden_menu 和 custom_logo 给前端初始化
- 前端:在基本设置页面添加隐藏菜单和自定义 Logo 设置项
- 前端:从侧边栏设置组件中移除弹窗,只保留菜单折叠按钮
- 前端:初始化时从服务端获取并应用隐藏菜单和自定义 Logo 设置
- 前端:调整 store 的 persist 配置,不再将这两个设置保存到本地存储

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* 代码审查完成

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* feat: 优化样式

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
Copilot
2026-01-09 05:49:09 +08:00
committed by GitHub
parent 9baeaa47e0
commit f2d3911266
10 changed files with 110 additions and 119 deletions

7
go.sum
View File

@@ -118,8 +118,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -269,7 +267,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -378,8 +375,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -452,8 +447,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=

View File

@@ -221,6 +221,14 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
if err != nil {
return nil, err
}
hiddenMenu, err := r.GetSlice(biz.SettingHiddenMenu)
if err != nil {
return nil, err
}
customLogo, err := r.Get(biz.SettingKeyCustomLogo)
if err != nil {
return nil, err
}
ip, err := r.Get(biz.SettingKeyPublicIPs)
if err != nil {
return nil, err
@@ -253,6 +261,8 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
BindUA: r.conf.HTTP.BindUA,
WebsitePath: websitePath,
BackupPath: backupPath,
HiddenMenu: hiddenMenu,
CustomLogo: customLogo,
Port: r.conf.HTTP.Port,
HTTPS: r.conf.HTTP.TLS,
ACME: r.conf.HTTP.ACME,
@@ -281,11 +291,13 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
return false, err
}
publicIPBytes, err := json.Marshal(req.PublicIP)
if err != nil {
if err := r.SetSlice(biz.SettingHiddenMenu, req.HiddenMenu); err != nil {
return false, err
}
if err = r.Set(biz.SettingKeyPublicIPs, string(publicIPBytes)); err != nil {
if err := r.Set(biz.SettingKeyCustomLogo, req.CustomLogo); err != nil {
return false, err
}
if err := r.SetSlice(biz.SettingKeyPublicIPs, req.PublicIP); err != nil {
return false, err
}

View File

@@ -17,6 +17,8 @@ type SettingPanel struct {
BindUA []string `json:"bind_ua"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
Port uint `json:"port" validate:"required|min:1|max:65535"`
HTTPS bool `json:"https"`
ACME bool `json:"acme"`

View File

@@ -57,10 +57,14 @@ func (s *HomeService) Panel(w http.ResponseWriter, r *http.Request) {
if name == "" {
name = s.t.Get("AcePanel")
}
hiddenMenu, _ := s.settingRepo.GetSlice(biz.SettingHiddenMenu)
customLogo, _ := s.settingRepo.Get(biz.SettingKeyCustomLogo)
Success(w, chix.M{
"name": name,
"locale": s.conf.App.Locale,
"name": name,
"locale": s.conf.App.Locale,
"hidden_menu": hiddenMenu,
"custom_logo": customLogo,
})
}

View File

@@ -1,108 +1,9 @@
<script lang="ts" setup>
import type { TreeSelectOption } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import TheIcon from '@/components/custom/TheIcon.vue'
import MenuCollapse from '@/layout/header/components/MenuCollapse.vue'
import { translateTitle } from '@/locales/menu'
import { usePermissionStore, useThemeStore } from '@/store'
import type { RouteType } from '~/types/router'
const { $gettext } = useGettext()
const themeStore = useThemeStore()
const permissionStore = usePermissionStore()
const settingModal = ref(false)
const getOption = (route: RouteType): TreeSelectOption => {
let menuItem: TreeSelectOption = {
label: route.meta?.title ? translateTitle(route.meta.title) : route.name,
key: route.name
}
const visibleChildren = route.children
? route.children.filter((item: RouteType) => item.name && !item.isHidden)
: []
if (!visibleChildren.length) return menuItem
if (visibleChildren.length === 1) {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem.label = singleRoute.meta?.title
? translateTitle(singleRoute.meta.title)
: singleRoute.name
const visibleItems = singleRoute.children
? singleRoute.children.filter((item: RouteType) => item.name && !item.isHidden)
: []
if (visibleItems.length === 1) menuItem = getOption(visibleItems[0])
else if (visibleItems.length > 1)
menuItem.children = visibleItems.map((item) => getOption(item))
} else {
menuItem.children = visibleChildren.map((item) => getOption(item))
}
return menuItem
}
const menus = computed<TreeSelectOption[]>(() => {
return permissionStore.allMenus.map((item) => getOption(item))
})
</script>
<template>
<div px-20 flex h-40 justify-between>
<div px-20 flex h-40 justify-start>
<menu-collapse />
<n-tooltip trigger="hover">
<template #trigger>
<the-icon
v-show="!themeStore.sider.collapsed"
:size="22"
icon="mdi:settings-outline"
@click="settingModal = true"
/>
</template>
{{ $gettext('Menu Settings') }}
</n-tooltip>
<n-modal
v-model:show="settingModal"
preset="card"
:title="$gettext('Menu Settings')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
@close="settingModal = false"
@mask-click="settingModal = false"
>
<n-form>
<n-flex vertical>
<n-alert type="info">
{{
$gettext(
'Settings are saved in the browser and will be reset after clearing the browser cache'
)
}}
</n-alert>
<n-form-item :label="$gettext('Custom Logo')">
<n-input
v-model:value="themeStore.logo"
:placeholder="$gettext('Please enter the complete URL')"
/>
</n-form-item>
<n-form-item :label="$gettext('Hide Menu')">
<n-tree-select
cascade
checkable
clearable
multiple
:options="menus"
v-model:value="permissionStore.hiddenRoutes"
/>
</n-form-item>
</n-flex>
</n-form>
</n-modal>
</div>
</template>

View File

@@ -6,7 +6,7 @@ import { createApp } from 'vue'
import App from './App.vue'
import { setupRouter } from '@/router'
import { setupStore, useThemeStore } from '@/store'
import { setupStore, usePermissionStore, useThemeStore } from '@/store'
import { gettext, setCurrent, setupNaiveDiscreteApi } from '@/utils'
import home from '@/api/panel/home'
@@ -26,18 +26,24 @@ async function setupApp() {
const setupPanel = async () => {
const themeStore = useThemeStore()
const permissionStore = usePermissionStore()
setCurrent(themeStore.locale)
return new Promise<void>((resolve) => {
useRequest(home.panel, {
initialData: {
name: import.meta.env.VITE_APP_TITLE,
locale: 'en'
locale: 'en',
hidden_menu: [],
custom_logo: ''
}
}).onSuccess(async ({ data }: { data: any }) => {
setCurrent(data.locale)
themeStore.setLocale(data.locale)
themeStore.setName(data.name)
// 设置隐藏菜单和自定义 Logo
themeStore.setLogo(data.custom_logo || '')
permissionStore.setHiddenRoutes(data.hidden_menu || [])
resolve()
})

View File

@@ -32,9 +32,10 @@ export const usePermissionStore = defineStore('permission', {
},
resetPermission() {
this.$reset()
},
/** 设置隐藏的菜单 */
setHiddenRoutes(hiddenRoutes: string[]) {
this.hiddenRoutes = hiddenRoutes
}
},
persist: {
pick: ['hiddenRoutes']
}
})

View File

@@ -53,7 +53,13 @@ export const useThemeStore = defineStore('theme', {
/** 设置名称 */
setName(name: string) {
this.name = name
},
/** 设置 Logo */
setLogo(logo: string) {
this.logo = logo
}
},
persist: true
persist: {
pick: ['isMobile', 'darkMode', 'sider', 'header', 'tab', 'locale', 'name']
}
})

View File

@@ -7,7 +7,7 @@ import { NButton } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import setting from '@/api/panel/setting'
import { useThemeStore } from '@/store'
import { usePermissionStore, useThemeStore } from '@/store'
import CreateModal from '@/views/setting/CreateModal.vue'
import SettingBase from '@/views/setting/SettingBase.vue'
import SettingSafe from '@/views/setting/SettingSafe.vue'
@@ -15,6 +15,7 @@ import SettingUser from '@/views/setting/SettingUser.vue'
const { $gettext } = useGettext()
const themeStore = useThemeStore()
const permissionStore = usePermissionStore()
const currentTab = ref('base')
const createModal = ref(false)
@@ -34,6 +35,8 @@ const { data: model } = useRequest(setting.list, {
bind_ua: [],
website_path: '',
backup_path: '',
hidden_menu: [],
custom_logo: '',
https: false,
acme: false,
public_ip: [],
@@ -54,6 +57,10 @@ const handleSave = () => {
themeStore.setLocale(model.value.locale)
}
// 更新隐藏菜单和自定义 Logo
themeStore.setLogo(model.value.custom_logo || '')
permissionStore.setHiddenRoutes(model.value.hidden_menu || [])
// 如果需要重启,则自动刷新页面
if (data.restart) {
window.$message.info($gettext('Panel is restarting, page will refresh in 5 seconds'))

View File

@@ -1,8 +1,14 @@
<script setup lang="ts">
import type { TreeSelectOption } from 'naive-ui'
import { translateTitle } from '@/locales/menu'
import { usePermissionStore } from '@/store'
import { locales as availableLocales } from '@/utils'
import { useGettext } from 'vue3-gettext'
import type { RouteType } from '~/types/router'
const { $gettext } = useGettext()
const permissionStore = usePermissionStore()
const model = defineModel<any>('model', { type: Object, required: true })
@@ -25,6 +31,43 @@ const channels = [
value: 'beta'
}
]
// 获取菜单选项
const getOption = (route: RouteType): TreeSelectOption => {
let menuItem: TreeSelectOption = {
label: route.meta?.title ? translateTitle(route.meta.title) : route.name,
key: route.name
}
const visibleChildren = route.children
? route.children.filter((item: RouteType) => item.name && !item.isHidden)
: []
if (!visibleChildren.length) return menuItem
if (visibleChildren.length === 1) {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem.label = singleRoute.meta?.title
? translateTitle(singleRoute.meta.title)
: singleRoute.name
const visibleItems = singleRoute.children
? singleRoute.children.filter((item: RouteType) => item.name && !item.isHidden)
: []
if (visibleItems.length === 1) menuItem = getOption(visibleItems[0])
else if (visibleItems.length > 1)
menuItem.children = visibleItems.map((item) => getOption(item))
} else {
menuItem.children = visibleChildren.map((item) => getOption(item))
}
return menuItem
}
const menus = computed<TreeSelectOption[]>(() => {
return permissionStore.allMenus.map((item) => getOption(item))
})
</script>
<template>
@@ -48,6 +91,22 @@ const channels = [
<n-form-item :label="$gettext('Default Backup Directory')">
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/opt/ace/backup')" />
</n-form-item>
<n-form-item :label="$gettext('Custom Logo')">
<n-input
v-model:value="model.custom_logo"
:placeholder="$gettext('Please enter the complete URL')"
/>
</n-form-item>
<n-form-item :label="$gettext('Hide Menu')">
<n-tree-select
cascade
checkable
clearable
multiple
:options="menus"
v-model:value="model.hidden_menu"
/>
</n-form-item>
</n-form>
</n-flex>
</template>