From 57a441312f2b3655b44c511c5add76ad3b4be1b0 Mon Sep 17 00:00:00 2001 From: alger Date: Sun, 15 Mar 2026 14:11:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=87=8D=E6=9E=84=20SearchBar?= =?UTF-8?q?=E3=80=81=E9=9B=86=E6=88=90=20useScrollTitle=20=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E6=BB=9A=E5=8A=A8=E6=98=BE=E7=A4=BA=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=93=E8=BE=91=E6=90=9C=E7=B4=A2=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重新设计 SearchBar:左侧 Tab(播放列表/MV/排行榜)+ 滑动指示器 + 搜索框自动展开收缩 - 新增 navTitle store 和 useScrollTitle hook,支持页面滚动后在 SearchBar 显示标题 - 集成 useScrollTitle 到 MusicListPage、歌手详情、关注/粉丝列表、搜索结果页 - 修复搜索结果页专辑点击跳转失败(缺失 type 字段) - 新增 5 种语言 searchBar tab i18n 键值 --- src/i18n/lang/en-US/comp.ts | 7 +- src/i18n/lang/ja-JP/comp.ts | 7 +- src/i18n/lang/ko-KR/comp.ts | 7 +- src/i18n/lang/zh-CN/comp.ts | 7 +- src/i18n/lang/zh-Hant/comp.ts | 7 +- src/renderer/components/common/SearchItem.vue | 5 +- src/renderer/hooks/useScrollTitle.ts | 58 + src/renderer/layout/components/SearchBar.vue | 1191 +++++++++++------ src/renderer/store/index.ts | 1 + src/renderer/store/modules/navTitle.ts | 27 + src/renderer/views/artist/detail.vue | 6 + src/renderer/views/music/MusicListPage.vue | 252 +++- src/renderer/views/search/SearchResult.vue | 6 + src/renderer/views/user/followers.vue | 10 + src/renderer/views/user/follows.vue | 10 + 15 files changed, 1103 insertions(+), 498 deletions(-) create mode 100644 src/renderer/hooks/useScrollTitle.ts create mode 100644 src/renderer/store/modules/navTitle.ts diff --git a/src/i18n/lang/en-US/comp.ts b/src/i18n/lang/en-US/comp.ts index 8a7ade0..b04d139 100644 --- a/src/i18n/lang/en-US/comp.ts +++ b/src/i18n/lang/en-US/comp.ts @@ -173,7 +173,11 @@ export default { zoom: 'Zoom', zoom100: 'Zoom 100%', resetZoom: 'Reset Zoom', - zoomDefault: 'Default Zoom' + zoomDefault: 'Default Zoom', + tabPlaylist: 'Playlist', + tabMv: 'MV', + tabCharts: 'Charts', + cancelSearch: 'Cancel' }, titleBar: { closeTitle: 'Choose how to close', @@ -199,6 +203,7 @@ export default { addToPlaylistSuccess: 'Add to Playlist Success', operationFailed: 'Operation Failed', songsAlreadyInPlaylist: 'Songs already in playlist', + locateCurrent: 'Locate current song', historyRecommend: 'Daily History', fetchDatesFailed: 'Failed to fetch dates', fetchSongsFailed: 'Failed to fetch songs', diff --git a/src/i18n/lang/ja-JP/comp.ts b/src/i18n/lang/ja-JP/comp.ts index e6cbd8d..619593f 100644 --- a/src/i18n/lang/ja-JP/comp.ts +++ b/src/i18n/lang/ja-JP/comp.ts @@ -173,7 +173,11 @@ export default { zoom: 'ページズーム', zoom100: '標準ズーム100%', resetZoom: 'クリックしてズームをリセット', - zoomDefault: '標準ズーム' + zoomDefault: '標準ズーム', + tabPlaylist: 'プレイリスト', + tabMv: 'MV', + tabCharts: 'チャート', + cancelSearch: 'キャンセル' }, titleBar: { closeTitle: '閉じる方法を選択してください', @@ -199,6 +203,7 @@ export default { addToPlaylist: 'プレイリストに追加', addToPlaylistSuccess: 'プレイリストに追加しました', songsAlreadyInPlaylist: '楽曲は既にプレイリストに存在します', + locateCurrent: '再生中の曲を表示', historyRecommend: '履歴の日次推薦', fetchDatesFailed: '日付リストの取得に失敗しました', fetchSongsFailed: '楽曲リストの取得に失敗しました', diff --git a/src/i18n/lang/ko-KR/comp.ts b/src/i18n/lang/ko-KR/comp.ts index 87f0f3c..563ee3e 100644 --- a/src/i18n/lang/ko-KR/comp.ts +++ b/src/i18n/lang/ko-KR/comp.ts @@ -172,7 +172,11 @@ export default { zoom: '페이지 확대/축소', zoom100: '표준 확대/축소 100%', resetZoom: '클릭하여 확대/축소 재설정', - zoomDefault: '표준 확대/축소' + zoomDefault: '표준 확대/축소', + tabPlaylist: '플레이리스트', + tabMv: 'MV', + tabCharts: '차트', + cancelSearch: '취소' }, titleBar: { closeTitle: '닫기 방법을 선택해주세요', @@ -198,6 +202,7 @@ export default { addToPlaylist: '재생 목록에 추가', addToPlaylistSuccess: '재생 목록에 추가 성공', songsAlreadyInPlaylist: '곡이 이미 재생 목록에 있습니다', + locateCurrent: '현재 재생 곡 찾기', historyRecommend: '일일 기록 권장', fetchDatesFailed: '날짜를 가져오지 못했습니다', fetchSongsFailed: '곡을 가져오지 못했습니다', diff --git a/src/i18n/lang/zh-CN/comp.ts b/src/i18n/lang/zh-CN/comp.ts index 2f35fe6..80b61ca 100644 --- a/src/i18n/lang/zh-CN/comp.ts +++ b/src/i18n/lang/zh-CN/comp.ts @@ -166,7 +166,11 @@ export default { zoom: '页面缩放', zoom100: '标准缩放100%', resetZoom: '点击重置缩放', - zoomDefault: '标准缩放' + zoomDefault: '标准缩放', + tabPlaylist: '播放列表', + tabMv: 'MV', + tabCharts: '排行榜', + cancelSearch: '取消' }, titleBar: { closeTitle: '请选择关闭方式', @@ -192,6 +196,7 @@ export default { addToPlaylist: '添加到播放列表', addToPlaylistSuccess: '添加到播放列表成功', songsAlreadyInPlaylist: '歌曲已存在于播放列表中', + locateCurrent: '定位当前播放', historyRecommend: '历史日推', fetchDatesFailed: '获取日期列表失败', fetchSongsFailed: '获取歌曲列表失败', diff --git a/src/i18n/lang/zh-Hant/comp.ts b/src/i18n/lang/zh-Hant/comp.ts index e7cca5d..0b66453 100644 --- a/src/i18n/lang/zh-Hant/comp.ts +++ b/src/i18n/lang/zh-Hant/comp.ts @@ -166,7 +166,11 @@ export default { zoom: '頁面縮放', zoom100: '標準縮放100%', resetZoom: '點擊重設縮放', - zoomDefault: '標準縮放' + zoomDefault: '標準縮放', + tabPlaylist: '播放清單', + tabMv: 'MV', + tabCharts: '排行榜', + cancelSearch: '取消' }, titleBar: { closeTitle: '請選擇關閉方式', @@ -192,6 +196,7 @@ export default { addToPlaylist: '新增至播放清單', addToPlaylistSuccess: '新增至播放清單成功', songsAlreadyInPlaylist: '歌曲已存在於播放清單中', + locateCurrent: '定位當前播放', historyRecommend: '歷史日推', fetchDatesFailed: '獲取日期列表失敗', fetchSongsFailed: '獲取歌曲列表失敗', diff --git a/src/renderer/components/common/SearchItem.vue b/src/renderer/components/common/SearchItem.vue index e0bb344..a218144 100644 --- a/src/renderer/components/common/SearchItem.vue +++ b/src/renderer/components/common/SearchItem.vue @@ -103,7 +103,10 @@ const handleClick = async () => { id: props.item.id, type: 'album', name: props.item.name, - listInfo: { picUrl: props.item.picUrl }, + listInfo: { + ...props.item, + coverImgUrl: props.item.picUrl + }, canRemove: false }); } else if (props.item.type === 'playlist') { diff --git a/src/renderer/hooks/useScrollTitle.ts b/src/renderer/hooks/useScrollTitle.ts new file mode 100644 index 0000000..f918eed --- /dev/null +++ b/src/renderer/hooks/useScrollTitle.ts @@ -0,0 +1,58 @@ +import { isRef, onMounted, onUnmounted, Ref, watch } from 'vue'; + +import { useNavTitleStore } from '@/store/modules/navTitle'; + +/** + * 页面标题滚动监听 hook + * + * 当 titleEl 元素滚出视口时,在 SearchBar 中显示标题。 + * 滚回视口后自动隐藏 SearchBar 中的标题。 + * + * @param title 标题文本(字符串或 Ref) + * @param titleEl 需要被监听的标题 DOM 元素(通常是页面 h1/h2) + * + * @example + * // 在页面组件中: + * const titleRef = ref(null); + * useScrollTitle('歌单名称', titleRef); + * // 或响应式标题: + * useScrollTitle(computed(() => playlist.value?.name ?? ''), titleRef); + */ +export function useScrollTitle(title: string | Ref, titleEl: Ref) { + const store = useNavTitleStore(); + let observer: IntersectionObserver | null = null; + + const setupObserver = (el: HTMLElement) => { + observer?.disconnect(); + observer = new IntersectionObserver(([entry]) => store.setVisible(!entry.isIntersecting), { + threshold: 0, + rootMargin: '-56px 0px 0px 0px' + }); + observer.observe(el); + }; + + onMounted(() => { + // 设置初始标题 + store.setTitle(isRef(title) ? title.value : title); + + // 等待 DOM 就绪后启动 observer + if (titleEl.value) { + setupObserver(titleEl.value); + } + }); + + // 响应式标题:当 Ref 变化时同步更新 store + if (isRef(title)) { + watch(title, (v) => store.setTitle(v)); + } + + // titleEl 延迟挂载时补充 observer + watch(titleEl, (el) => { + if (el) setupObserver(el); + }); + + onUnmounted(() => { + observer?.disconnect(); + store.clear(); + }); +} diff --git a/src/renderer/layout/components/SearchBar.vue b/src/renderer/layout/components/SearchBar.vue index a382d6d..f0c60b6 100644 --- a/src/renderer/layout/components/SearchBar.vue +++ b/src/renderer/layout/components/SearchBar.vue @@ -1,168 +1,190 @@ @@ -171,7 +193,7 @@ import { useDebounceFn } from '@vueuse/core'; import { computed, onMounted, ref, watch, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { getSearchKeyword } from '@/api/home'; import { getUserDetail } from '@/api/login'; @@ -182,6 +204,7 @@ import Coffee from '@/components/Coffee.vue'; import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const'; import { useDownloadStatus } from '@/hooks/useDownloadStatus'; import { useZoom } from '@/hooks/useZoom'; +import { useNavTitleStore } from '@/store/modules/navTitle'; import { useSearchStore } from '@/store/modules/search'; import { useSettingsStore } from '@/store/modules/settings'; import { useUserStore } from '@/store/modules/user'; @@ -191,148 +214,115 @@ import { checkUpdate, UpdateResult } from '@/utils/update'; import config from '../../../../package.json'; const router = useRouter(); +const route = useRoute(); +const navTitleStore = useNavTitleStore(); const searchStore = useSearchStore(); const settingsStore = useSettingsStore(); const userStore = useUserStore(); const userSetOptions = ref(USER_SET_OPTIONS); const { t, locale } = useI18n(); -// 下载状态 const { downloadingCount, navigateToDownloads } = useDownloadStatus(); - -// 显示下载按钮逻辑 -const showDownloadButton = computed(() => { - return ( +const showDownloadButton = computed( + () => isElectron && (settingsStore.setData?.alwaysShowDownloadButton || downloadingCount.value > 0) - ); -}); - -// 使用缩放hook +); const { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, isZoom100 } = useZoom(); -// 显示返回按钮 +// ── Back button ─────────────────────────────────────── const showBackButton = computed(() => { const meta = router.currentRoute.value.meta; - if (!settingsStore.isMobile && meta.isMobile === false) { - return false; - } + if (!settingsStore.isMobile && meta.isMobile === false) return false; return meta.back === true; }); +const goBack = () => router.back(); -// 返回上一页 -const goBack = () => { - router.back(); +// ── Tabs ────────────────────────────────────────────── +const tabs = computed(() => [ + { + key: 'playlist', + label: t('comp.searchBar.tabPlaylist'), + path: '/', + icon: 'ri-play-list-2-fill' + }, + { key: 'mv', label: t('comp.searchBar.tabMv'), path: '/mv', icon: 'ri-movie-2-fill' }, + { + key: 'charts', + label: t('comp.searchBar.tabCharts'), + path: '/toplist', + icon: 'ri-bar-chart-grouped-fill' + } +]); +const isTabActive = (path: string) => route.path === path; + +// Sliding pill +const tabsTrackRef = ref(null); +const tabElsRef = ref([]); +const setTabRef = (el: HTMLElement, i: number) => { + if (el) tabElsRef.value[i] = el; +}; +const activeTabIndex = computed(() => tabs.value.findIndex((t) => isTabActive(t.path))); +const sliderStyle = computed(() => { + const el = tabElsRef.value[activeTabIndex.value]; + if (!el) return { opacity: '0' }; + return { + transform: `translateX(${el.offsetLeft}px)`, + width: `${el.offsetWidth}px`, + opacity: '1' + }; +}); + +// ── Search expand / collapse ────────────────────────── +const isSearchExpanded = ref(false); +const inputFocused = ref(false); +const inputRef = ref(null); + +const handleFocus = () => { + inputFocused.value = true; + isSearchExpanded.value = true; + if (searchValue.value && suggestions.value.length) showSuggestions.value = true; +}; +const handleBlur = () => { + inputFocused.value = false; + setTimeout(() => { + showSuggestions.value = false; + isSearchExpanded.value = false; + }, 150); }; -// 推荐热搜词 +// ── Search logic ────────────────────────────────────── const hotSearchKeyword = ref(t('comp.searchBar.searchPlaceholder')); const hotSearchValue = ref(''); -const loadHotSearchKeyword = async () => { - const { data } = await getSearchKeyword(); - hotSearchKeyword.value = data.data.showKeyword; - hotSearchValue.value = data.data.realkeyword; -}; - -const loadPage = async () => { - const token = localStorage.getItem('token'); - if (!token) return; - const { data } = await getUserDetail(); - userStore.user = - data.profile || userStore.user || JSON.parse(localStorage.getItem('user') || '{}'); - localStorage.setItem('user', JSON.stringify(userStore.user)); -}; - -loadPage(); - -watchEffect(() => { - if (userStore.user) { - userSetOptions.value = USER_SET_OPTIONS; - } else { - userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout'); - } -}); - -const restartApp = () => { - window.electron.ipcRenderer.send('restart'); -}; - -const toLogin = () => { - router.push('/user'); -}; - -// 页面初始化 -onMounted(() => { - loadHotSearchKeyword(); - loadPage(); - checkForUpdates(); - isElectron && initZoomFactor(); -}); - -const isDark = computed({ - get: () => settingsStore.theme === 'dark', - set: () => settingsStore.toggleTheme() -}); - -// 搜索词 const searchValue = ref(''); -// 使用 watch 代替 watchEffect 监听搜索值变化,确保深度监听 watch( () => searchStore.searchValue, - (newValue) => { - if (newValue) { - searchValue.value = newValue; - } + (v) => { + if (v) searchValue.value = v; }, { immediate: true } ); const search = () => { - const { value } = searchValue; - if (value === '') { + const val = searchValue.value; + if (!val) { searchValue.value = hotSearchValue.value; return; } - + const q = { keyword: val, type: searchStore.searchType }; if (router.currentRoute.value.path === '/search-result') { - searchStore.searchValue = value; - router.replace({ - path: '/search-result', - query: { - keyword: value, - type: searchStore.searchType - } - }); - return; + searchStore.searchValue = val; + router.replace({ path: '/search-result', query: q }); + } else { + router.push({ path: '/search-result', query: q }); } - - router.push({ - path: '/search-result', - query: { - keyword: value, - type: searchStore.searchType - } - }); - - console.log(`[UI] 执行搜索,关键词: "${searchValue.value}"`); // <--- 日志 K - showSuggestions.value = false; // 搜索后强制隐藏 + showSuggestions.value = false; }; const selectSearchType = (key: number) => { searchStore.searchType = key; - if (searchValue.value) { - if (router.currentRoute.value.path === '/search-result') { - search(); - } else { - router.push({ - path: '/search-result', - query: { - keyword: searchValue.value, - type: key - } - }); - } - } + if (searchValue.value) + router.push({ path: '/search-result', query: { keyword: searchValue.value, type: key } }); }; const rawSearchTypes = ref(SEARCH_TYPES); @@ -340,21 +330,97 @@ const searchTypeOptions = computed(() => { locale.value; return rawSearchTypes.value .filter(() => isElectron) - .map((type) => ({ - label: t(type.label), - key: type.key - })); + .map((type) => ({ label: t(type.label), key: type.key })); }); -const selectItem = async (key: string) => { - // switch 判断 +const suggestions = ref([]); +const showSuggestions = ref(false); +const suggestionsLoading = ref(false); +const highlightedIndex = ref(-1); + +const debouncedSuggest = useDebounceFn(async (kw: string) => { + if (!kw.trim()) { + suggestions.value = []; + showSuggestions.value = false; + return; + } + suggestionsLoading.value = true; + suggestions.value = await getSearchSuggestions(kw); + suggestionsLoading.value = false; + showSuggestions.value = suggestions.value.length > 0; + highlightedIndex.value = -1; +}, 300); + +const handleInput = (v: string) => debouncedSuggest(v); + +const selectSuggestion = (s: string) => { + searchValue.value = s; + showSuggestions.value = false; + search(); +}; + +const handleKeydown = (e: KeyboardEvent) => { + const len = suggestions.value.length; + if (!showSuggestions.value || !len) { + if (e.key === 'Enter') search(); + return; + } + if (e.key === 'ArrowDown') { + e.preventDefault(); + highlightedIndex.value = (highlightedIndex.value + 1) % len; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + highlightedIndex.value = (highlightedIndex.value - 1 + len) % len; + } + if (e.key === 'Enter') { + e.preventDefault(); + highlightedIndex.value >= 0 + ? selectSuggestion(suggestions.value[highlightedIndex.value]) + : search(); + } + if (e.key === 'Escape') { + showSuggestions.value = false; + } +}; + +// ── User / misc ─────────────────────────────────────── +const loadHotSearch = async () => { + const { data } = await getSearchKeyword(); + hotSearchKeyword.value = data.data.showKeyword; + hotSearchValue.value = data.data.realkeyword; +}; +const loadPage = async () => { + if (!localStorage.getItem('token')) return; + const { data } = await getUserDetail(); + userStore.user = + data.profile || userStore.user || JSON.parse(localStorage.getItem('user') || '{}'); + localStorage.setItem('user', JSON.stringify(userStore.user)); +}; +loadPage(); +watchEffect(() => { + userSetOptions.value = userStore.user + ? USER_SET_OPTIONS + : USER_SET_OPTIONS.filter((i) => i.key !== 'logout'); +}); + +const restartApp = () => window.electron.ipcRenderer.send('restart'); +const toLogin = () => router.push('/user'); +const toGithub = () => window.open('http://donate.alger.fun/download', '_blank'); +const toGithubRelease = () => { + window.location.href = 'https://donate.alger.fun/download'; +}; + +const isDark = computed({ + get: () => settingsStore.theme === 'dark', + set: () => settingsStore.toggleTheme() +}); + +const selectItem = (key: string) => { switch (key) { case 'logout': userStore.handleLogout(); break; - case 'login': - router.push('/login'); - break; case 'set': router.push('/set'); break; @@ -364,242 +430,503 @@ const selectItem = async (key: string) => { case 'refresh': window.location.reload(); break; - default: } }; -const toGithub = () => { - window.open('http://donate.alger.fun/download', '_blank'); -}; - const updateInfo = ref({ hasUpdate: false, latestVersion: '', currentVersion: config.version, releaseInfo: null }); - const checkForUpdates = async () => { try { - const result = await checkUpdate(config.version); - if (result) { - updateInfo.value = result; - } - } catch (error) { - console.error('检查更新失败:', error); + const r = await checkUpdate(config.version); + if (r) updateInfo.value = r; + } catch (e) { + void e; // 更新检查失败时静默处理 } }; -const toGithubRelease = () => { - window.location.href = 'https://donate.alger.fun/download'; -}; - -const suggestions = ref([]); -const showSuggestions = ref(false); -const suggestionsLoading = ref(false); -const highlightedIndex = ref(-1); // -1 表示没有高亮项 -// 使用防抖函数来避免频繁请求API -const debouncedGetSuggestions = useDebounceFn(async (keyword: string) => { - if (!keyword.trim()) { - suggestions.value = []; - showSuggestions.value = false; - return; - } - suggestionsLoading.value = true; - suggestions.value = await getSearchSuggestions(keyword); - suggestionsLoading.value = false; - // 只有当有建议时才显示面板 - showSuggestions.value = suggestions.value.length > 0; - highlightedIndex.value = -1; -}, 300); // 300ms延迟 - -const handleInput = (value: string) => { - debouncedGetSuggestions(value); -}; -const handleFocus = () => { - if (searchValue.value && suggestions.value.length > 0) { - showSuggestions.value = true; - } -}; - -const handleBlur = () => { - setTimeout(() => { - showSuggestions.value = false; - }, 150); -}; - -const selectSuggestion = (suggestion: string) => { - searchValue.value = suggestion; - showSuggestions.value = false; - search(); -}; -const handleKeydown = (event: KeyboardEvent) => { - // 如果建议列表不显示,则不处理上下键 - if (!showSuggestions.value || suggestions.value.length === 0) { - // 如果是回车键,则正常执行搜索 - if (event.key === 'Enter') { - search(); - } - return; - } - - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); // 阻止光标移动到末尾 - highlightedIndex.value = (highlightedIndex.value + 1) % suggestions.value.length; - break; - case 'ArrowUp': - event.preventDefault(); // 阻止光标移动到开头 - highlightedIndex.value = - (highlightedIndex.value - 1 + suggestions.value.length) % suggestions.value.length; - break; - case 'Enter': - event.preventDefault(); // 阻止表单默认提交行为 - if (highlightedIndex.value !== -1) { - // 如果有高亮项,就选择它 - selectSuggestion(suggestions.value[highlightedIndex.value]); - } else { - // 否则,执行默认搜索 - search(); - } - break; - case 'Escape': - showSuggestions.value = false; // 按 Esc 隐藏建议 - break; - } -}; +onMounted(() => { + loadHotSearch(); + loadPage(); + checkForUpdates(); + isElectron && initZoomFactor(); +}); - diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index a41cc8c..333abbf 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -21,6 +21,7 @@ export * from './modules/localMusic'; export * from './modules/lyric'; export * from './modules/menu'; export * from './modules/music'; +export * from './modules/navTitle'; export * from './modules/player'; export * from './modules/playerCore'; export * from './modules/playHistory'; diff --git a/src/renderer/store/modules/navTitle.ts b/src/renderer/store/modules/navTitle.ts new file mode 100644 index 0000000..5c22817 --- /dev/null +++ b/src/renderer/store/modules/navTitle.ts @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +/** + * 导航栏标题 store + * 用于页面滚动后在 SearchBar 中显示当前页面标题 + * 无持久化,页面卸载时自动清除 + */ +export const useNavTitleStore = defineStore('navTitle', () => { + const title = ref(''); + const isVisible = ref(false); + + const setTitle = (t: string) => { + title.value = t; + }; + + const setVisible = (v: boolean) => { + isVisible.value = v; + }; + + const clear = () => { + title.value = ''; + isVisible.value = false; + }; + + return { title, isVisible, setTitle, setVisible, clear }; +}); diff --git a/src/renderer/views/artist/detail.vue b/src/renderer/views/artist/detail.vue index 793090a..5528bc9 100644 --- a/src/renderer/views/artist/detail.vue +++ b/src/renderer/views/artist/detail.vue @@ -100,6 +100,7 @@

{{ artistInfo.name }} @@ -434,6 +435,7 @@ import { getMusicDetail } from '@/api/music'; import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import PlayBottom from '@/components/common/PlayBottom.vue'; import SongItem from '@/components/common/SongItem.vue'; +import { useScrollTitle } from '@/hooks/useScrollTitle'; import router from '@/router'; import { usePlayerStore } from '@/store'; import { IArtist } from '@/types/artist'; @@ -465,6 +467,10 @@ const artistInfo = ref(); const songs = ref([]); const albums = ref([]); +const titleElRef = ref(null); +const artistTitle = computed(() => artistInfo.value?.name ?? ''); +useScrollTitle(artistTitle, titleElRef); + // 加载状态 const loading = ref(false); const songLoading = ref(false); diff --git a/src/renderer/views/music/MusicListPage.vue b/src/renderer/views/music/MusicListPage.vue index b785fae..1a7e003 100644 --- a/src/renderer/views/music/MusicListPage.vue +++ b/src/renderer/views/music/MusicListPage.vue @@ -1,6 +1,6 @@