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:
7
go.sum
7
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -32,9 +32,10 @@ export const usePermissionStore = defineStore('permission', {
|
||||
},
|
||||
resetPermission() {
|
||||
this.$reset()
|
||||
},
|
||||
/** 设置隐藏的菜单 */
|
||||
setHiddenRoutes(hiddenRoutes: string[]) {
|
||||
this.hiddenRoutes = hiddenRoutes
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
pick: ['hiddenRoutes']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user