From 6017491d6d7c6b4eeeb2166d7fe470ed12b641a9 Mon Sep 17 00:00:00 2001 From: RhoPaper Date: Sat, 18 Jul 2026 00:54:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E4=BE=A7=E8=BE=B9=E6=A0=8F=E5=88=86=E7=BB=84?= =?UTF-8?q?=E3=80=81=E6=8E=92=E5=BA=8F=E4=B8=8E=E6=98=BE=E9=9A=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 2 +- src/assets/icons/type-off.svg | 6 + src/components/layout/SideBar.vue | 299 +++++---- src/components/layout/sidebarNav.ts | 70 ++ .../custom/SidebarCustomizeConfig.vue | 631 ++++++++++++++++++ src/components/ui/SMenu.vue | 1 + src/i18n/locales/en-US.json | 21 + src/i18n/locales/zh-CN.json | 21 + src/settings/categories/appearance.ts | 6 + src/stores/settings.ts | 69 +- src/types/settings.ts | 50 ++ 11 files changed, 1056 insertions(+), 120 deletions(-) create mode 100644 src/assets/icons/type-off.svg create mode 100644 src/components/layout/sidebarNav.ts create mode 100644 src/components/settings/custom/SidebarCustomizeConfig.vue diff --git a/components.d.ts b/components.d.ts index 381335e0..0f343c01 100644 --- a/components.d.ts +++ b/components.d.ts @@ -87,7 +87,6 @@ declare module 'vue' { IconLucideCloudUpload: typeof import('~icons/lucide/cloud-upload')['default'] IconLucideCopy: typeof import('~icons/lucide/copy')['default'] IconLucideDatabase: typeof import('~icons/lucide/database')['default'] - IconLucideDisc: typeof import('~icons/lucide/disc')['default'] IconLucideDisc3: typeof import('~icons/lucide/disc3')['default'] IconLucideDownload: typeof import('~icons/lucide/download')['default'] IconLucideEllipsis: typeof import('~icons/lucide/ellipsis')['default'] @@ -229,6 +228,7 @@ declare module 'vue' { SFormItem: typeof import('./src/components/ui/SFormItem.vue')['default'] SIconSwap: typeof import('./src/components/ui/SIconSwap.vue')['default'] SideBar: typeof import('./src/components/layout/SideBar.vue')['default'] + SidebarCustomizeConfig: typeof import('./src/components/settings/custom/SidebarCustomizeConfig.vue')['default'] SideBarLogo: typeof import('./src/components/layout/SideBarLogo.vue')['default'] SImg: typeof import('./src/components/ui/SImg.vue')['default'] SInput: typeof import('./src/components/ui/SInput.vue')['default'] diff --git a/src/assets/icons/type-off.svg b/src/assets/icons/type-off.svg new file mode 100644 index 00000000..8cc6a935 --- /dev/null +++ b/src/assets/icons/type-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/layout/SideBar.vue b/src/components/layout/SideBar.vue index 6c5c68ee..5e15afad 100644 --- a/src/components/layout/SideBar.vue +++ b/src/components/layout/SideBar.vue @@ -1,28 +1,23 @@ + + diff --git a/src/components/ui/SMenu.vue b/src/components/ui/SMenu.vue index 1a46b2c0..7fc41099 100644 --- a/src/components/ui/SMenu.vue +++ b/src/components/ui/SMenu.vue @@ -110,6 +110,7 @@ const handleSelect = (item: SMenuItem) => { >
(stored: T[], all: readonly T[]): T[] => { return [...known, ...missing]; }; +/** + * 对账侧边栏导航分组:仅保留已知导航项并去重(空组保留), + * 新增的导航项补到末组,存档无效时回退默认分组 + * @param stored - 存档分组 + * @returns 对账后的分组 + */ +const reconcileNavGroups = (stored: unknown): SidebarNavGroup[] => { + const all = DEFAULT_SIDEBAR_NAV_GROUPS.flatMap((group) => group.keys); + const seen = new Set(); + const groups: SidebarNavGroup[] = []; + for (const raw of Array.isArray(stored) ? stored : []) { + const record = raw as Partial | null; + if (!Array.isArray(record?.keys)) continue; + const keys: string[] = []; + for (const key of record.keys) { + if (!all.includes(key) || seen.has(key)) continue; + seen.add(key); + keys.push(key); + } + groups.push({ + name: typeof record.name === "string" ? record.name : "", + showName: record.showName === true, + keys, + }); + } + if (groups.length === 0) + return DEFAULT_SIDEBAR_NAV_GROUPS.map((group) => ({ ...group, keys: [...group.keys] })); + const missing = all.filter((key) => !seen.has(key)); + if (missing.length > 0) groups[groups.length - 1].keys.push(...missing); + return groups; +}; + export const useSettingsStore = defineStore( "settings", () => { @@ -41,6 +78,14 @@ export const useSettingsStore = defineStore( routeTransition: "fade", sidebarCollapsed: false, sidebarPlaylistCover: false, + sidebarNavGroups: DEFAULT_SIDEBAR_NAV_GROUPS.map((group) => ({ + ...group, + keys: [...group.keys], + })), + sidebarHiddenKeys: [], + sidebarKeepEmptyDivider: false, + sidebarNameWithDivider: false, + sidebarPlaylistOrder: { myLocal: [], myOnline: [], subscribed: [] }, showQualitySwitch: false, closeAction: "hide", rememberCloseChoice: false, @@ -265,12 +310,34 @@ export const useSettingsStore = defineStore( storage: localStorage, omit: ["system"], afterHydrate: ({ store }) => { - const { lyric } = store as unknown as { lyric: LyricSettings }; + const { lyric, appearance } = store as unknown as { + lyric: LyricSettings; + appearance: AppearanceSettings; + }; if (typeof lyric.detectBackgroundLyrics !== "boolean") { lyric.detectBackgroundLyrics = true; } lyric.lyricSourceOrder = reconcileOrder(lyric.lyricSourceOrder, ALL_PLATFORMS); lyric.lyricFormatOrder = reconcileOrder(lyric.lyricFormatOrder, DEFAULT_LYRIC_FORMAT_ORDER); + appearance.sidebarNavGroups = reconcileNavGroups(appearance.sidebarNavGroups ?? []); + const validHiddenKeys = new Set([ + ...DEFAULT_SIDEBAR_NAV_GROUPS.flatMap((group) => group.keys), + SIDEBAR_GROUP_MY_PLAYLISTS, + SIDEBAR_GROUP_SUBSCRIBED, + ]); + appearance.sidebarHiddenKeys = (appearance.sidebarHiddenKeys ?? []).filter( + (key) => key !== "/" && (validHiddenKeys.has(key) || key.startsWith("/collection/")), + ); + const storedOrder = (appearance.sidebarPlaylistOrder ?? {}) as Partial< + Record + >; + const orderOf = (value: unknown): string[] => + Array.isArray(value) ? value.filter((key) => typeof key === "string") : []; + appearance.sidebarPlaylistOrder = { + myLocal: orderOf(storedOrder.myLocal), + myOnline: orderOf(storedOrder.myOnline), + subscribed: orderOf(storedOrder.subscribed), + }; }, }, }, diff --git a/src/types/settings.ts b/src/types/settings.ts index 6a207a85..85fcde31 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -68,6 +68,46 @@ export const DEFAULT_LYRIC_SOURCE_ORDER: LyricSourceOrder = [...ALL_PLATFORMS]; /** 默认格式优先级 */ export const DEFAULT_LYRIC_FORMAT_ORDER: LyricFormatOrder = [...DEFAULT_LYRIC_FORMAT_ORDER_SHARED]; +/** 侧边栏「我的歌单」分组 key(仅显隐,不参与排序) */ +export const SIDEBAR_GROUP_MY_PLAYLISTS = "group-my-playlists"; + +/** 侧边栏「收藏的歌单」分组 key(仅显隐,不参与排序) */ +export const SIDEBAR_GROUP_SUBSCRIBED = "group-subscribed"; + +/** 侧边栏导航分组(匿名分组,可命名;组间以分隔线或分组名区分) */ +export interface SidebarNavGroup { + /** 分组名,空字符串为未命名 */ + name: string; + /** 是否在侧栏显示分组名 */ + showName: boolean; + /** 组内导航项 key(路由路径) */ + keys: string[]; +} + +/** 侧边栏歌单显示顺序(key 为歌单路由路径;空数组为自然顺序) */ +export interface SidebarPlaylistOrder { + /** 我的歌单 - 本地 */ + myLocal: string[]; + /** 我的歌单 - 在线 */ + myOnline: string[]; + /** 收藏的歌单 */ + subscribed: string[]; +} + +/** 侧边栏导航项默认分组 */ +export const DEFAULT_SIDEBAR_NAV_GROUPS: SidebarNavGroup[] = [ + { + name: "", + showName: false, + keys: ["/", "/library", "/artists/local", "/albums/local", "/folders"], + }, + { + name: "", + showName: false, + keys: ["/liked", "/favorites", "/cloud", "/download", "/streaming", "/history"], + }, +]; + /** 歌词设置 */ export interface LyricSettings { /** 歌词来源偏好 */ @@ -200,6 +240,16 @@ export interface AppearanceSettings { sidebarCollapsed: boolean; /** 侧边栏歌单项显示封面 */ sidebarPlaylistCover: boolean; + /** 侧边栏导航分组(匿名分组,组间以分隔线区分) */ + sidebarNavGroups: SidebarNavGroup[]; + /** 侧边栏隐藏的导航项与歌单分组 */ + sidebarHiddenKeys: string[]; + /** 无可见项的分组是否保留分隔线(留白) */ + sidebarKeepEmptyDivider: boolean; + /** 显示分组名时是否叠加分隔线 */ + sidebarNameWithDivider: boolean; + /** 侧边栏歌单显示顺序 */ + sidebarPlaylistOrder: SidebarPlaylistOrder; /** 播放栏显示快捷音质切换 */ showQualitySwitch: boolean; /** 点击关闭按钮的行为 */ From 0074eaa390d94292177a5c0377b868ee013b5827 Mon Sep 17 00:00:00 2001 From: RhoPaper Date: Sat, 18 Jul 2026 01:36:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E5=B0=81=E8=A3=85=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=AD=98=E6=A1=A3=E5=AF=B9=E8=B4=A6=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E4=BF=AE=E6=AD=A3=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/settings.ts | 50 +++++++++++++++++++++++++++--------------- src/types/settings.ts | 2 +- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 76b602b1..b405459f 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -66,6 +66,36 @@ const reconcileNavGroups = (stored: unknown): SidebarNavGroup[] => { return groups; }; +/** + * 对账侧边栏隐藏键:仅保留有效的导航项、歌单分组与歌单路由键,首页不可隐藏 + * @param stored - 存档隐藏键 + * @returns 对账后的隐藏键 + */ +const reconcileHiddenKeys = (stored: string[]): string[] => { + const valid = new Set([ + ...DEFAULT_SIDEBAR_NAV_GROUPS.flatMap((group) => group.keys), + SIDEBAR_GROUP_MY_PLAYLISTS, + SIDEBAR_GROUP_SUBSCRIBED, + ]); + return stored.filter((key) => key !== "/" && (valid.has(key) || key.startsWith("/collection/"))); +}; + +/** + * 对账侧边栏歌单顺序:剔除非法存档,保证各字段均为字符串数组 + * @param stored - 存档顺序 + * @returns 对账后的顺序 + */ +const reconcilePlaylistOrder = (stored: unknown): SidebarPlaylistOrder => { + const record = (stored ?? {}) as Partial>; + const orderOf = (value: unknown): string[] => + Array.isArray(value) ? value.filter((key) => typeof key === "string") : []; + return { + myLocal: orderOf(record.myLocal), + myOnline: orderOf(record.myOnline), + subscribed: orderOf(record.subscribed), + }; +}; + export const useSettingsStore = defineStore( "settings", () => { @@ -320,24 +350,8 @@ export const useSettingsStore = defineStore( lyric.lyricSourceOrder = reconcileOrder(lyric.lyricSourceOrder, ALL_PLATFORMS); lyric.lyricFormatOrder = reconcileOrder(lyric.lyricFormatOrder, DEFAULT_LYRIC_FORMAT_ORDER); appearance.sidebarNavGroups = reconcileNavGroups(appearance.sidebarNavGroups ?? []); - const validHiddenKeys = new Set([ - ...DEFAULT_SIDEBAR_NAV_GROUPS.flatMap((group) => group.keys), - SIDEBAR_GROUP_MY_PLAYLISTS, - SIDEBAR_GROUP_SUBSCRIBED, - ]); - appearance.sidebarHiddenKeys = (appearance.sidebarHiddenKeys ?? []).filter( - (key) => key !== "/" && (validHiddenKeys.has(key) || key.startsWith("/collection/")), - ); - const storedOrder = (appearance.sidebarPlaylistOrder ?? {}) as Partial< - Record - >; - const orderOf = (value: unknown): string[] => - Array.isArray(value) ? value.filter((key) => typeof key === "string") : []; - appearance.sidebarPlaylistOrder = { - myLocal: orderOf(storedOrder.myLocal), - myOnline: orderOf(storedOrder.myOnline), - subscribed: orderOf(storedOrder.subscribed), - }; + appearance.sidebarHiddenKeys = reconcileHiddenKeys(appearance.sidebarHiddenKeys ?? []); + appearance.sidebarPlaylistOrder = reconcilePlaylistOrder(appearance.sidebarPlaylistOrder); }, }, }, diff --git a/src/types/settings.ts b/src/types/settings.ts index 85fcde31..9ab364b3 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -240,7 +240,7 @@ export interface AppearanceSettings { sidebarCollapsed: boolean; /** 侧边栏歌单项显示封面 */ sidebarPlaylistCover: boolean; - /** 侧边栏导航分组(匿名分组,组间以分隔线区分) */ + /** 侧边栏导航分组(匿名分组,可命名;组间以分隔线或分组名区分) */ sidebarNavGroups: SidebarNavGroup[]; /** 侧边栏隐藏的导航项与歌单分组 */ sidebarHiddenKeys: string[];