🌈 style: 优化歌词与桌面歌词样式问题

This commit is contained in:
imsyy
2025-11-10 00:36:18 +08:00
parent 1b2985892b
commit 6b653bc5e8
9 changed files with 155 additions and 105 deletions

5
components.d.ts vendored
View File

@@ -61,8 +61,11 @@ declare module 'vue' {
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NFloatButton: typeof import('naive-ui')['NFloatButton']
NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
@@ -70,6 +73,7 @@ declare module 'vue' {
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
@@ -100,6 +104,7 @@ declare module 'vue' {
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']

View File

@@ -40,7 +40,7 @@
"@electron-toolkit/utils": "^4.0.0",
"@imsyy/color-utils": "^1.0.2",
"@material/material-color-utilities": "^0.3.0",
"@neteasecloudmusicapienhanced/api": "^4.29.15",
"@neteasecloudmusicapienhanced/api": "^4.29.16",
"@pixi/app": "^7.4.3",
"@pixi/core": "^7.4.3",
"@pixi/display": "^7.4.3",

10
pnpm-lock.yaml generated
View File

@@ -34,8 +34,8 @@ importers:
specifier: ^0.3.0
version: 0.3.0
'@neteasecloudmusicapienhanced/api':
specifier: ^4.29.15
version: 4.29.15
specifier: ^4.29.16
version: 4.29.16
'@pixi/app':
specifier: ^7.4.3
version: 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))
@@ -967,8 +967,8 @@ packages:
'@material/material-color-utilities@0.3.0':
resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==}
'@neteasecloudmusicapienhanced/api@4.29.15':
resolution: {integrity: sha512-/dw0ON90NDXJnULEyKzGEvKjjkJyHYb9fJgKa1dxsN5AIN/tXNAu4AV2vMJcPGfA/NV7iRnLDEPS8ocJCbCU2w==}
'@neteasecloudmusicapienhanced/api@4.29.16':
resolution: {integrity: sha512-ml6cTErjJ/fn+E0wvlNGGoW74rDj3mfogO85xySuTCuX/Mdhuayv+pPtoSm5YDM6j26hHmS8+rV2ma4o16pUiw==}
engines: {node: '>=12'}
hasBin: true
@@ -5331,7 +5331,7 @@ snapshots:
'@material/material-color-utilities@0.3.0': {}
'@neteasecloudmusicapienhanced/api@4.29.15':
'@neteasecloudmusicapienhanced/api@4.29.16':
dependencies:
'@unblockneteasemusic/server': 0.28.0
axios: 1.13.2

View File

@@ -58,7 +58,10 @@
]"
:style="{
filter: settingStore.lyricsBlur
? `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
? (playSeek >= item.time && playSeek < item.endTime) ||
statusStore.lyricIndex === index
? 'blur(0)'
: `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
: 'blur(0)',
}"
@click="jumpSeek(item.time)"

View File

@@ -287,7 +287,10 @@
</n-collapse-transition>
</div>
<div class="set-list">
<n-h3 prefix="bar"> Apple Music-like Lyrics </n-h3>
<n-h3 prefix="bar">
Apple Music-like Lyrics
<n-tag type="warning" size="small" round>Beta</n-tag>
</n-h3>
<n-card class="set-item">
<div class="label">
<n-text class="name">使用 Apple Music-like Lyrics</n-text>
@@ -318,7 +321,10 @@
</n-card>
</div>
<div v-if="isElectron" class="set-list">
<n-h3 prefix="bar"> 桌面歌词 </n-h3>
<n-h3 prefix="bar">
桌面歌词
<n-tag type="warning" size="small" round>Beta</n-tag>
</n-h3>
<n-card class="set-item">
<div class="label">
<n-text class="name">开启桌面歌词</n-text>

View File

@@ -195,6 +195,12 @@ const toGithub = () => {
margin-bottom: 0;
}
}
.n-h {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.n-collapse-transition {
margin-bottom: 12px;
&:last-child {

View File

@@ -26,61 +26,78 @@ export const getLyricData = async (id: number) => {
try {
// 检测本地歌词覆盖
const getLyric = getLyricFun(settingStore.localLyricPath, id);
// 先加载 LRC不阻塞到 TTML 完成
// 并发请求:如果 TTML 先到并且有效,则直接采用 TTML不再等待或覆盖为 LRC
const lrcPromise = getLyric("lrc", songLyric);
const ttmlPromise = settingStore.enableTTMLLyric ? getLyric("ttml", songLyricTTML) : null;
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
parsedLyricsData(lyricRes, lyricLocal && !settingStore.enableExcludeLocalLyrics);
// LRC 到达后即可认为加载完成
statusStore.lyricLoading = false;
let settled = false; // 是否已采用某一种歌词并结束加载状态
let ttmlAdopted = false; // 是否已采用 TTML
// TTML 并行加载,完成后增量更新,不阻塞整体流程
if (ttmlPromise) {
statusStore.usingTTMLLyric = false;
void ttmlPromise
.then(({ lyric: ttmlContent, isLocal: ttmlLocal }) => {
if (!ttmlContent) {
statusStore.usingTTMLLyric = false;
return;
}
const parsedResult = parseTTML(ttmlContent);
if (!parsedResult?.lines?.length) {
statusStore.usingTTMLLyric = false;
return;
}
const skipExcludeLocal = ttmlLocal && !settingStore.enableExcludeLocalLyrics;
const skipExcludeTTML = !settingStore.enableExcludeTTML;
const skipExclude = skipExcludeLocal || skipExcludeTTML;
const ttmlLyric = parseTTMLToAMLL(parsedResult, skipExclude);
const ttmlYrcLyric = parseTTMLToYrc(parsedResult, skipExclude);
console.log("TTML lyrics:", ttmlLyric, ttmlYrcLyric);
// 合并数据
const updates: Partial<{ yrcAMData: LyricLine[]; yrcData: LyricType[] }> = {};
if (ttmlLyric?.length) {
updates.yrcAMData = ttmlLyric;
console.log("✅ TTML AMLL lyrics success");
}
if (ttmlYrcLyric?.length) {
updates.yrcData = ttmlYrcLyric;
console.log("✅ TTML Yrc lyrics success");
}
if (Object.keys(updates).length) {
musicStore.setSongLyric(updates);
statusStore.usingTTMLLyric = true;
} else {
statusStore.usingTTMLLyric = false;
}
})
.catch((err) => {
console.error("❌ Error loading TTML lyrics:", err);
const adoptTTML = async () => {
if (!ttmlPromise) {
statusStore.usingTTMLLyric = false;
return;
}
try {
const { lyric: ttmlContent, isLocal: ttmlLocal } = await ttmlPromise;
if (!ttmlContent) {
statusStore.usingTTMLLyric = false;
});
} else {
statusStore.usingTTMLLyric = false;
}
return;
}
const parsedResult = parseTTML(ttmlContent);
if (!parsedResult?.lines?.length) {
statusStore.usingTTMLLyric = false;
return;
}
const skipExcludeLocal = ttmlLocal && !settingStore.enableExcludeLocalLyrics;
const skipExcludeTTML = !settingStore.enableExcludeTTML;
const skipExclude = skipExcludeLocal || skipExcludeTTML;
const ttmlLyric = parseTTMLToAMLL(parsedResult, skipExclude);
const ttmlYrcLyric = parseTTMLToYrc(parsedResult, skipExclude);
console.log("Lyrics: ", musicStore.songLyric);
const updates: Partial<{ yrcAMData: LyricLine[]; yrcData: LyricType[] }> = {};
if (ttmlLyric?.length) updates.yrcAMData = ttmlLyric;
if (ttmlYrcLyric?.length) updates.yrcData = ttmlYrcLyric;
if (Object.keys(updates).length) {
musicStore.setSongLyric(updates);
statusStore.usingTTMLLyric = true;
ttmlAdopted = true;
if (!settled) {
statusStore.lyricLoading = false;
settled = true;
}
console.log("✅ TTML lyrics adopted (prefer TTML)");
} else {
statusStore.usingTTMLLyric = false;
}
} catch (err) {
console.error("❌ Error loading TTML lyrics:", err);
statusStore.usingTTMLLyric = false;
}
};
const adoptLRC = async () => {
try {
const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
// 如果 TTML 已采用,则忽略 LRC
if (ttmlAdopted) return;
parsedLyricsData(lyricRes, lyricLocal && !settingStore.enableExcludeLocalLyrics);
statusStore.usingTTMLLyric = false;
if (!settled) {
statusStore.lyricLoading = false;
settled = true;
}
console.log("✅ LRC lyrics adopted");
} catch (err) {
console.error("❌ Error loading LRC lyrics:", err);
if (!settled) statusStore.lyricLoading = false;
}
};
// 启动并发任务TTML 与 LRC 同时进行,哪个先成功就先用
void adoptLRC();
void adoptTTML();
} catch (error) {
console.error("❌ Error loading lyrics:", error);
statusStore.usingTTMLLyric = false;

View File

@@ -3,7 +3,7 @@ import { isDev, isElectron } from "./env";
import { useSettingStore } from "@/stores";
import { getCookie } from "./cookie";
import { isLogin } from "./auth";
import axiosRetry from "axios-retry";
// import axiosRetry from "axios-retry";
// 全局地址
const baseURL: string = String(isDev ? "/api/netease" : import.meta.env["VITE_API_URL"]);
@@ -18,10 +18,10 @@ const server: AxiosInstance = axios.create({
});
// 请求重试
axiosRetry(server, {
// 重试次数
retries: 3,
});
// axiosRetry(server, {
// // 重试次数
// retries: 3,
// });
// 请求拦截器
server.interceptors.request.use(

View File

@@ -118,7 +118,7 @@
</template>
<script setup lang="ts">
import { Position, useRafFn } from "@vueuse/core";
import { useRafFn } from "@vueuse/core";
import { LyricContentType, LyricType } from "@/types/main";
import { LyricConfig, LyricData, RenderLine } from "@/types/desktop-lyric";
import defaultDesktopLyricConfig from "@/assets/data/lyricConfig";
@@ -332,40 +332,50 @@ const dragState = reactive({
/**
* 桌面歌词拖动开始
* @param _position 拖动位置
* @param event 指针事件
*/
const lyricDragStart = async (_position: Position, event: PointerEvent) => {
const onDocPointerDown = async (event: PointerEvent) => {
if (lyricConfig.isLock) return;
// 仅左键触发
if (event.button !== 0) return;
const target = event?.target as HTMLElement | null;
if (!target) return;
// 过滤 header 中的按钮:不触发拖拽
if (target.closest(".menu-btn")) return;
startDrag(event);
};
/**
* 桌面歌词拖动开始
* @param event 指针事件
*/
const startDrag = async (event: PointerEvent) => {
dragState.isDragging = true;
const { x, y } = await window.electron.ipcRenderer.invoke("get-window-bounds");
const { width, height } = await window.api.store.get("lyric");
// 直接限制最大宽高
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
width,
height,
fixed: true,
});
window.electron.ipcRenderer.send("toggle-fixed-max-size", { width, height, fixed: true });
dragState.startX = event?.screenX ?? 0;
dragState.startY = event?.screenY ?? 0;
dragState.startWinX = x;
dragState.startWinY = y;
dragState.winWidth = width ?? 0;
dragState.winHeight = height ?? 0;
document.addEventListener("pointermove", onDocPointerMove, { capture: true });
document.addEventListener("pointerup", onDocPointerUp, { capture: true });
event.preventDefault();
};
/**
* 桌面歌词拖动移动
* @param _position 拖动位置
* @param event 指针事件
*/
const lyricDragMove = async (_position: Position, event: PointerEvent) => {
const onDocPointerMove = async (event: PointerEvent) => {
if (!dragState.isDragging || lyricConfig.isLock) return;
const screenX = event?.screenX ?? 0;
const screenY = event?.screenY ?? 0;
let newWinX = Math.round(dragState.startWinX + (screenX - dragState.startX));
let newWinY = Math.round(dragState.startWinY + (screenY - dragState.startY));
// 可选:限制在屏幕边界(支持多屏)
// 是否限制在屏幕边界(支持多屏)
if (lyricConfig.limitBounds) {
const { minX, minY, maxX, maxY } = await window.electron.ipcRenderer.invoke(
"get-virtual-screen-bounds",
@@ -382,36 +392,30 @@ const lyricDragMove = async (_position: Position, event: PointerEvent) => {
);
};
// 监听桌面歌词拖动
useDraggable(desktopLyricRef, {
onStart: (position, event) => {
lyricDragStart(position, event);
},
onMove: (position, event) => {
lyricDragMove(position, event);
},
onEnd: () => {
// 关闭拖拽状态
dragState.isDragging = false;
requestAnimationFrame(() => {
// 恢复拖拽前宽高
window.electron.ipcRenderer.send(
"update-lyric-size",
dragState.winWidth,
dragState.winHeight,
);
// 根据字体大小恢复一次高度
const height = fontSizeToHeight(lyricConfig.fontSize);
if (height) pushWindowHeight(height);
// 恢复最大宽高
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
width: dragState.winWidth,
height: dragState.winHeight,
fixed: false,
});
/**
* 桌面歌词拖动结束
*/
const onDocPointerUp = () => {
if (!dragState.isDragging) return;
// 关闭拖拽状态
dragState.isDragging = false;
// 移除全局监听
document.removeEventListener("pointermove", onDocPointerMove, { capture: true });
document.removeEventListener("pointerup", onDocPointerUp, { capture: true });
requestAnimationFrame(() => {
// 恢复拖拽前宽高
window.electron.ipcRenderer.send("update-lyric-size", dragState.winWidth, dragState.winHeight);
// 根据字体大小恢复一次高度
const height = fontSizeToHeight(lyricConfig.fontSize);
if (height) pushWindowHeight(height);
// 恢复最大宽高
window.electron.ipcRenderer.send("toggle-fixed-max-size", {
width: dragState.winWidth,
height: dragState.winHeight,
fixed: false,
});
},
});
});
};
// 监听窗口大小变化
const { height: winHeight } = useWindowSize();
@@ -547,11 +551,16 @@ onMounted(() => {
} else {
pauseSeek();
}
// 拖拽入口
document.addEventListener("pointerdown", onDocPointerDown, { capture: true });
});
onBeforeUnmount(() => {
// 关闭 RAF
pauseSeek();
// 解绑事件
document.removeEventListener("pointerdown", onDocPointerDown, { capture: true });
if (dragState.isDragging) onDocPointerUp();
});
</script>
@@ -570,9 +579,10 @@ onBeforeUnmount(() => {
border-radius: 12px;
overflow: hidden;
transition: background-color 0.3s;
cursor: move;
cursor: default;
.header {
margin-bottom: 12px;
cursor: default;
// 子内容三等分grid
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -752,6 +762,9 @@ onBeforeUnmount(() => {
}
}
}
.lyric-container {
cursor: move;
}
}
</style>