🌈 style: 优化部分样式

This commit is contained in:
imsyy
2025-10-29 18:29:58 +08:00
parent 829dc3b591
commit 9145eb034b
15 changed files with 384 additions and 89 deletions

View File

@@ -27,7 +27,7 @@ const initFileIpc = (): void => {
const filePath = resolve(dirPath).replace(/\\/g, "/");
console.info(`📂 Fetching music files from: ${filePath}`);
// 查找指定目录下的所有音乐文件
const musicFiles = await FastGlob("**/*.{mp3,wav,flac}", { cwd: filePath });
const musicFiles = await FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
// 解析元信息
const metadataPromises = musicFiles.map(async (file) => {
const filePath = join(dirPath, file);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor"
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M6 20h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17m-6 3V10zm0 2q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h7V6q0-2.075 1.463-3.537T18 1q1.775 0 3.1 1.075t1.75 2.7q.125.425-.162.825T22 6q-.425 0-.7-.175t-.4-.575q-.275-.95-1.062-1.6T18 3q-1.25 0-2.125.875T15 6v2h3q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22z"/></svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor"
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z" />
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor" d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -1,7 +1,11 @@
<!-- 全局图标 -->
<template>
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
<div ref="svgContainer" class="svg-container" />
<div
ref="svgContainer"
:style="{ transform: offset ? `translateY(${offset}px)` : undefined }"
class="svg-container"
/>
</n-icon>
</template>
@@ -10,6 +14,7 @@ const props = defineProps<{
name: string;
size?: string | number;
color?: string;
offset?: number;
depth?: 1 | 2 | 3 | 4 | 5;
}>();

View File

@@ -14,6 +14,7 @@
<s-image
v-else-if="settingStore.playerBackgroundType === 'blur'"
:src="musicStore.songCover"
:observe-visibility="false"
class="bg-img"
alt="cover"
/>

View File

@@ -11,6 +11,7 @@
<s-image
:key="musicStore.getSongCover()"
:src="musicStore.getSongCover('l')"
:observe-visibility="false"
class="cover-img"
/>
<!-- 动态封面 -->

View File

@@ -14,6 +14,8 @@
:key="imgSrc"
:alt="alt || 'image'"
:class="['cover', { loaded: isLoaded }]"
:decoding="decodeAsync ? 'async' : 'auto'"
:loading="nativeLazy ? 'lazy' : 'eager'"
@load="imageLoaded"
@error="imageError"
/>
@@ -27,9 +29,21 @@ const props = withDefaults(
src: string | undefined;
defaultSrc?: string;
alt?: string;
// 是否进行可视状态变化
observeVisibility?: boolean;
// 在不可视时是否释放图片以回收内存
releaseOnHide?: boolean;
// 是否使用浏览器异步解码
decodeAsync?: boolean;
// 是否使用原生懒加载
nativeLazy?: boolean;
}>(),
{
defaultSrc: "/images/song.jpg?assest",
observeVisibility: true,
releaseOnHide: false,
decodeAsync: true,
nativeLazy: true,
},
);
@@ -49,31 +63,57 @@ const imgContainer = ref<HTMLImageElement>();
// 是否加载完成
const isLoaded = ref<boolean>(false);
// 可视状态上一次值,避免重复 emit
const lastShowState = ref<boolean | null>(null);
// 加载竞态 token防止旧图片回调覆盖新状态
const loadToken = ref<number>(0);
const currentToken = ref<number>(0);
// 是否可视
const isCanLook = useElementVisibility(imgContainer);
// 图片加载完成
const imageLoaded = (e: Event) => {
// 竞态保护:仅响应最新一次设置的图片
if (currentToken.value !== loadToken.value) return;
if (isLoaded.value) return;
isLoaded.value = true;
// 加载完成
emit("load", e);
};
// 图片加载失败
const imageError = (e: Event) => {
// 竞态保护
if (currentToken.value !== loadToken.value) return;
isLoaded.value = false;
// 避免默认图也反复触发导致死循环
if (imgSrc.value !== props.defaultSrc) {
imgSrc.value = props.defaultSrc;
// 加载失败
}
emit("error", e);
};
// 可视状态变化
// 可视状态变化(可控)
watch(
isCanLook,
(show) => {
if (!props.observeVisibility) return;
// 去重:仅在状态变化时触发
if (lastShowState.value !== show) {
lastShowState.value = show;
emit("update:show", show);
if (show) imgSrc.value = props.src;
}
if (show) {
// 进入可视区再加载,避免重复赋值
if (imgSrc.value !== props.src) {
loadToken.value += 1;
currentToken.value = loadToken.value;
imgSrc.value = props.src;
}
} else if (props.releaseOnHide) {
// 释放图片以回收内存
if (imgSrc.value !== undefined) imgSrc.value = undefined;
}
},
{ immediate: true },
);
@@ -83,13 +123,40 @@ watch(
() => props.src,
(val) => {
isLoaded.value = false;
// 不同值时才进行赋值,减少重绘
if (props.observeVisibility) {
if (isCanLook.value) {
if (imgSrc.value !== val) {
loadToken.value += 1;
currentToken.value = loadToken.value;
imgSrc.value = val;
}
} else {
imgSrc.value = undefined;
if (props.releaseOnHide) {
if (imgSrc.value !== undefined) imgSrc.value = undefined;
}
}
} else {
if (imgSrc.value !== val) {
loadToken.value += 1;
currentToken.value = loadToken.value;
imgSrc.value = val;
}
}
},
{ immediate: true },
);
onUnmounted(() => {
try {
if (imgRef.value) imgRef.value.src = "";
} catch {
/* empty */
}
imgSrc.value = undefined;
imgRef.value = undefined;
imgContainer.value = undefined;
});
</script>
<style lang="scss" scoped>

View File

@@ -393,3 +393,38 @@ export const shuffleArray = <T>(arr: T[]): T[] => {
}
return copy;
};
/**
* 在浏览器空闲时执行任务
* @param task 要执行的任务
*/
export const runIdle = (task: () => void) => {
try {
const ric = window?.requestIdleCallback as ((cb: () => void) => number) | undefined;
if (typeof ric === "function") {
ric(() => {
try {
task();
} catch {
/* empty */
}
});
} else {
setTimeout(() => {
try {
task();
} catch {
/* empty */
}
}, 0);
}
} catch {
setTimeout(() => {
try {
task();
} catch {
/* empty */
}
}, 0);
}
};

View File

@@ -5,7 +5,7 @@ import { cloneDeep } from "lodash-es";
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
import { calculateProgress } from "./time";
import { isElectron, isDev, shuffleArray } from "./helper";
import { isElectron, isDev, shuffleArray, runIdle } from "./helper";
import { heartRateList } from "@/api/playlist";
import { formatSongsList } from "./format";
import { isLogin } from "./auth";
@@ -40,7 +40,6 @@ class Player {
private analyser: AnalyserNode | null = null;
private dataArray: Uint8Array<ArrayBuffer> | null = null;
/** 其他数据 */
private testNumber: number = 0;
private message: MessageReactive | null = null;
/** 预载下一首歌曲播放地址缓存(仅存 URL不创建 Howl */
private nextPrefetch: { id: number; url: string | null; ublock: boolean } | null = null;
@@ -48,6 +47,8 @@ class Player {
private playSessionId: number = 0;
/** 是否正在切换歌曲 */
private switching: boolean = false;
/** 当前曲目重试信息(按歌曲维度计数) */
private retryInfo: { songId: number; count: number } = { songId: 0, count: 0 };
constructor() {
// 创建播放器实例
this.player = new Howl({ src: [""], format: allowPlayFormat, autoplay: false });
@@ -56,14 +57,51 @@ class Player {
// 挂载全局
window.$player = this;
}
/**
* 新建会话并返回会话 id
*/
private newSession(): number {
this.playSessionId += 1;
return this.playSessionId;
}
/**
* 检查传入会话是否过期
*/
private isStale(sessionId: number): boolean {
return sessionId !== this.playSessionId;
}
/**
* 保护执行:会话过期则早退
*/
private guard<T>(sessionId: number, fn: () => T): T | undefined {
if (this.isStale(sessionId)) return;
return fn();
}
/**
* 重置底层播放器与定时器(幂等)
*/
private resetPlayerCore() {
try {
this.player?.off();
} catch {
/* empty */
}
try {
Howler.stop();
Howler.unload();
} catch {
/* empty */
}
this.cleanupAllTimers();
}
/**
* 处理播放状态
*/
private handlePlayStatus(sessionId?: number) {
private handlePlayStatus() {
// const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
const currentSessionId = sessionId ?? this.playSessionId;
const currentSessionId = this.playSessionId;
// 清理定时器
clearInterval(this.playerInterval);
// 更新播放状态
@@ -171,12 +209,7 @@ class Player {
* @param autoPlay 是否自动播放
* @param seek 播放位置
*/
private async createPlayer(
src: string,
autoPlay: boolean = true,
seek: number = 0,
sessionId?: number,
) {
private async createPlayer(src: string, autoPlay: boolean = true, seek: number = 0) {
// 获取数据
const dataStore = useDataStore();
const musicStore = useMusicStore();
@@ -184,23 +217,15 @@ class Player {
const settingStore = useSettingStore();
// 播放信息
const { id, path, type } = musicStore.playSong;
const currentSessionId = sessionId ?? this.playSessionId;
const currentSessionId = this.playSessionId;
// 检查会话是否过期
if (currentSessionId !== this.playSessionId) {
console.log("🚫 Session expired, skipping player creation");
return;
}
// 清理播放器(移除事件,停止并卸载)
try {
this.player.off();
} catch {
/* empty */
}
Howler.stop();
Howler.unload();
// 清理所有定时器
this.cleanupAllTimers();
// 再次检查会话是否过期(异步操作后)
// 统一重置底层播放器
this.resetPlayerCore();
// 二次检查会话
if (currentSessionId !== this.playSessionId) {
console.log("🚫 Session expired after cleanup, aborting");
return;
@@ -217,27 +242,29 @@ class Player {
rate: statusStore.playRate,
});
// 播放器事件(绑定当前会话)
this.playerEvent({ seek, sessionId: currentSessionId });
this.playerEvent({ seek });
// 播放设备
if (!settingStore.showSpectrums) this.toggleOutputDevice();
// 自动播放(仅一次性触发)
if (autoPlay) await this.play();
// 获取歌曲附加信息 - 非电台和本地
if (type !== "radio" && !path) {
getLyricData(id);
runIdle(() => getLyricData(id));
} else resetSongLyric();
// 定时获取状态
if (!this.playerInterval) this.handlePlayStatus(currentSessionId);
if (!this.playerInterval) this.handlePlayStatus();
// 新增播放历史
if (type !== "radio") dataStore.setHistory(musicStore.playSong);
// 获取歌曲封面主色
if (!path) getCoverColor(musicStore.songCover);
if (!path) runIdle(() => getCoverColor(musicStore.songCover));
// 更新 MediaSession
if (!path) this.updateMediaSession();
if (!path) runIdle(() => this.updateMediaSession());
// 开发模式
if (isDev) window.player = this.player;
// 异步预载下一首播放地址(不阻塞当前播放)
// 预载下一首播放地址
runIdle(() => {
void this.prefetchNextSongUrl();
});
}
/**
* 播放器事件
@@ -246,8 +273,6 @@ class Player {
options: {
// 恢复进度
seek?: number;
// 当前会话 id用于忽略过期事件
sessionId?: number;
} = { seek: 0 },
) {
// 获取数据
@@ -257,7 +282,7 @@ class Player {
const playSongData = getPlaySongData();
// 获取配置
const { seek } = options;
const currentSessionId = options.sessionId ?? this.playSessionId;
const currentSessionId = this.playSessionId;
// 初次加载
this.player.once("load", () => {
if (currentSessionId !== this.playSessionId) return;
@@ -284,6 +309,14 @@ class Player {
}
// 更新状态
statusStore.playLoading = false;
// 重置当前曲目重试计数
try {
const current = getPlaySongData();
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
this.retryInfo = { songId: Number(sid || 0), count: 0 };
} catch {
/* empty */
}
// ipc
if (isElectron) {
window.electron.ipcRenderer.send("play-song-change", getPlayerInfo());
@@ -297,6 +330,14 @@ class Player {
this.player.on("play", () => {
if (currentSessionId !== this.playSessionId) return;
window.document.title = getPlayerInfo() || "SPlayer";
// 重置重试计数
try {
const current = getPlaySongData();
const sid = current?.type === "radio" ? current?.dj?.id : current?.id;
this.retryInfo = { songId: Number(sid || 0), count: 0 };
} catch {
/* empty */
}
// ipc
if (isElectron) {
window.electron.ipcRenderer.send("play-status-change", true);
@@ -338,9 +379,15 @@ class Player {
this.player.on("loaderror", (sourceid, err: unknown) => {
if (currentSessionId !== this.playSessionId) return;
const code = typeof err === "number" ? err : undefined;
this.errorNext(code);
this.handlePlaybackError(code);
console.error("❌ song error:", sourceid, playSongData, err);
});
this.player.on("playerror", (sourceid, err: unknown) => {
if (currentSessionId !== this.playSessionId) return;
const code = typeof err === "number" ? err : undefined;
this.handlePlaybackError(code);
console.error("❌ song play error:", sourceid, playSongData, err);
});
}
/**
* 初始化 MediaSession
@@ -440,27 +487,31 @@ class Player {
updateSpectrumData();
}
/**
* 播放错误
* 在播放错误时,播放下一首
* 集中处理播放错误与重试策略
*/
private async errorNext(errCode?: number) {
private async handlePlaybackError(errCode?: number) {
const dataStore = useDataStore();
// 次数加一
this.testNumber++;
if (this.testNumber > 5) {
this.testNumber = 0;
this.resetStatus();
window.$message.error("当前重试次数过多,请稍后再试");
return;
const playSongData = getPlaySongData();
const currentSongId = playSongData?.type === "radio" ? playSongData.dj?.id : playSongData?.id;
// 初始化/切换曲目时重置计数
if (!this.retryInfo.songId || this.retryInfo.songId !== Number(currentSongId || 0)) {
this.retryInfo = { songId: Number(currentSongId || 0), count: 0 };
}
// 错误 2 通常为网络地址过期
if (errCode === 2) {
// 重载播放器
this.retryInfo.count += 1;
// 错误码 2资源过期或临时网络错误允许较少次数的刷新
if (errCode === 2 && this.retryInfo.count <= 2) {
await this.initPlayer(true, this.getSeek());
return;
}
// 播放下一曲
// 其它错误:最多 3 次
if (this.retryInfo.count <= 3) {
await this.initPlayer(true, 0);
return;
}
// 超过次数:切到下一首或清空
this.retryInfo.count = 0;
if (dataStore.playList.length > 1) {
window.$message.error("当前歌曲播放失败,已跳至下一首");
await this.nextOrPrev("next");
} else {
window.$message.error("当前列表暂无可播放歌曲");
@@ -498,12 +549,12 @@ class Player {
musicStore.playSong.cover = "/images/song.jpg?assest";
}
// 获取主色
getCoverColor(musicStore.playSong.cover);
runIdle(() => getCoverColor(musicStore.playSong.cover));
// 获取歌词数据
const { lyric, format } = await window.electron.ipcRenderer.invoke("get-music-lyric", path);
parseLocalLyric(lyric, format);
// 更新媒体会话
this.updateMediaSession();
runIdle(() => this.updateMediaSession());
} catch (error) {
window.$message.error("获取本地歌曲元信息失败");
console.error("Failed to parse local music info:", error);
@@ -540,7 +591,7 @@ class Player {
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
const sessionId = ++this.playSessionId;
const sessionId = this.newSession();
try {
// 获取播放数据
const playSongData = getPlaySongData();
@@ -550,9 +601,12 @@ class Player {
musicStore.playSong = playSongData;
// 更改状态
statusStore.playLoading = true;
// 清理旧播放器与计时器
this.resetPlayerCore();
// 本地歌曲
if (path) {
await this.createPlayer(`file://${path}`, autoPlay, seek, sessionId);
if (this.isStale(sessionId)) return;
await this.createPlayer(`file://${path}`, autoPlay, seek);
// 获取歌曲元信息
await this.parseLocalMusicInfo(path);
}
@@ -564,7 +618,8 @@ class Player {
const cached = this.nextPrefetch;
if (cached && cached.id === songId && cached.url) {
statusStore.playUblock = cached.ublock;
await this.createPlayer(cached.url, autoPlay, seek, sessionId);
if (this.isStale(sessionId)) return;
await this.createPlayer(cached.url, autoPlay, seek);
} else {
// 官方地址失败或仅为试听时再尝试解锁Electron 且非电台且开启解灰)
const canUnlock = isElectron && type !== "radio" && settingStore.useSongUnlock;
@@ -572,20 +627,23 @@ class Player {
if (officialUrl && !isTrial) {
// 官方可播放且非试听
statusStore.playUblock = false;
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
if (this.isStale(sessionId)) return;
await this.createPlayer(officialUrl, autoPlay, seek);
} else if (canUnlock) {
// 官方失败或为试听时尝试解锁
const unlockUrl = await getUnlockSongUrl(playSongData);
if (unlockUrl) {
statusStore.playUblock = true;
console.log("🎼 Song unlock successfully:", unlockUrl);
await this.createPlayer(unlockUrl, autoPlay, seek, sessionId);
if (this.isStale(sessionId)) return;
await this.createPlayer(unlockUrl, autoPlay, seek);
} else if (officialUrl) {
// 解锁失败,若允许试听则播放试听
if (isTrial && settingStore.playSongDemo) {
window.$message.warning("当前歌曲仅可试听,请开通会员后重试");
statusStore.playUblock = false;
await this.createPlayer(officialUrl, autoPlay, seek, sessionId);
if (this.isStale(sessionId)) return;
await this.createPlayer(officialUrl, autoPlay, seek);
} else {
// 不允许试听
statusStore.playUblock = false;
@@ -663,6 +721,7 @@ class Player {
async pause(changeStatus: boolean = true) {
const statusStore = useStatusStore();
const settingStore = useSettingStore();
const localSession = this.playSessionId;
// 播放器未加载完成或不存在
if (!this.player || this.player.state() !== "loaded") {
@@ -675,7 +734,7 @@ class Player {
await new Promise<void>((resolve) => {
this.player.fade(statusStore.playVolume, 0, settingStore.getFadeTime);
this.player.once("fade", () => {
this.player.pause();
this.guard(localSession, () => this.player.pause());
resolve();
});
});

38
src/views/DesktopLyrics/index.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
import { LyricType } from "@/types/main";
/** 桌面歌词数据 */
export interface LyricData {
/** 播放歌曲名称 */
playName: string;
/** 播放状态 */
playStatus: boolean;
/** 播放进度 */
progress: number;
/** 歌词数据 */
lrcData: LyricType[];
yrcData: LyricType[];
}
/** 桌面歌词配置 */
export interface LyricConfig {
/** 是否锁定歌词 */
isLock: boolean;
/** 已播放颜色 */
playedColor: string;
/** 未播放颜色 */
unplayedColor: string;
/** 描边 */
stroke: string;
/** 描边宽度 */
strokeWidth: number;
/** 字体 */
fontFamily: string;
/** 字体大小 */
fontSize: number;
/** 行高 */
lineHeight: number;
/** 是否双行 */
isDoubleLine: boolean;
/** 文本排版位置 */
position: "left" | "center" | "right" | "both";
}

View File

@@ -4,45 +4,71 @@
{{ lyricData.playName }}
{{ lyricConfig }}
456456
<n-flex class="menu" align="center" justify="space-between">
<n-flex class="name">
<div class="menu-btn">
<SvgIcon name="Logo" />
<div class="header" align="center" justify="space-between">
<n-flex :wrap="false" align="center" justify="flex-start" size="small">
<div class="menu-btn" title="返回应用">
<SvgIcon name="Music" />
</div>
<div class="menu-btn" title="增加字体大小">
<SvgIcon :offset="-1" name="TextSizeAdd" />
</div>
<div class="menu-btn" title="减少字体大小">
<SvgIcon :offset="-1" name="TextSizeReduce" />
</div>
<span class="song-name">{{ lyricData.playName }}</span>
</n-flex>
<n-flex :wrap="false" align="center" justify="center" size="small">
<div class="menu-btn" title="上一曲">
<SvgIcon name="SkipPrev" />
</div>
<div class="menu-btn" :title="lyricData.playStatus ? '暂停' : '播放'">
<SvgIcon :name="lyricData.playStatus ? 'Pause' : 'Play'" />
</div>
<div class="menu-btn" title="下一曲">
<SvgIcon name="SkipNext" />
</div>
</n-flex>
<n-flex :wrap="false" align="center" justify="flex-end" size="small">
<div class="menu-btn" title="锁定">
<SvgIcon name="Lock" />
</div>
<div class="menu-btn" title="解锁">
<SvgIcon name="LockOpen" />
</div>
<div class="menu-btn" title="关闭">
<SvgIcon name="Close" />
</div>
</n-flex>
</div>
</div>
</n-config-provider>
</template>
<script setup lang="ts">
import type { LyricType } from "@/types/main";
import { Position } from "@vueuse/core";
import { LyricConfig, LyricData } from ".";
// 桌面歌词数据
const lyricData = reactive<{
playName: string;
playStatus: boolean;
progress: number;
lrcData: LyricType[];
yrcData: LyricType[];
lyricIndex: number;
}>({
const lyricData = reactive<LyricData>({
playName: "未知歌曲",
playStatus: false,
progress: 0,
lrcData: [],
yrcData: [],
lyricIndex: -1,
});
// 桌面歌词配置
const lyricConfig = reactive<{
fontSize: number;
lineHeight: number;
}>({
const lyricConfig = reactive<LyricConfig>({
isLock: false,
playedColor: "#fff",
unplayedColor: "#ccc",
stroke: "#000",
strokeWidth: 2,
fontFamily: "system-ui",
fontSize: 24,
lineHeight: 48,
isDoubleLine: false,
position: "center",
});
// 桌面歌词元素
@@ -57,6 +83,10 @@ const lyricDragMove = (position: Position) => {
useDraggable(desktopLyricsRef, {
onMove: lyricDragMove,
});
onMounted(() => {
// 接收歌词配置
});
</script>
<style scoped lang="scss">
@@ -68,16 +98,62 @@ useDraggable(desktopLyricsRef, {
overflow: hidden;
transition: background-color 0.3s;
cursor: move;
.header {
opacity: 0;
margin-bottom: 12px;
transition: opacity 0.3s;
// 子内容三等分grid
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 12px;
> * {
min-width: 0;
}
.song-name {
font-size: 1em;
text-align: left;
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-btn {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 6px;
border-radius: 8px;
will-change: transform;
transition:
background-color 0.3s,
transform 0.3s;
cursor: pointer;
.n-icon {
font-size: 24px;
}
&:hover {
background-color: rgba(255, 255, 255, 0.3);
}
&:active {
transform: scale(0.98);
}
}
}
&:hover {
&:not(.lock) {
background-color: rgba(0, 0, 0, 0.3);
.header {
opacity: 1;
}
}
}
}
</style>
<!-- <style>
<style>
body {
background-image: url("https://picsum.photos/1920/1080");
}
</style> -->
</style>

View File

@@ -17,7 +17,7 @@
"paths": {
"@/*": ["src/*"]
},
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts"],
"types": ["node", "electron", "electron-vite/node", "./auto-imports.d.ts", "./components.d.ts"],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"target": "es2022"
}