mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-07-13 22:07:29 +08:00
fix(ui): 修复内存泄漏、搜索竞态、播客时长及颜色/指令边界问题
- MusicFullMobile: 卸载清理睡眠定时器 interval;getTextColors 补上调用括号 - loading 指令: 改为每元素独立实例、卸载真正销毁、class 用 classList,避免多个 v-loading 争用单例 - lyric/index: 歌词窗口 receive-lyric IPC 监听卸载时解绑 - SearchBar/SearchResult/list: 三处异步搜索加竞态守卫,丢弃过期返回避免旧结果覆盖新结果 - linearColor: 0 尺寸图片 NaN 兜底、渐变色标越界兜底 - utils/secondToMinute: 时长 >=1 小时进位为 h:mm:ss(修复播客时长显示 78:34)
This commit is contained in:
@@ -962,6 +962,12 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(autoScrollTimer.value);
|
clearTimeout(autoScrollTimer.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理睡眠定时器倒计时刷新 interval,避免组件卸载后残留、闭包持有 store 无法回收
|
||||||
|
if (sleepTimerInterval) {
|
||||||
|
clearInterval(sleepTimerInterval);
|
||||||
|
sleepTimerInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 清理鼠标事件监听
|
// 清理鼠标事件监听
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
@@ -1084,7 +1090,7 @@ const { getLrcStyle: originalLrcStyle } = useLyricProgress();
|
|||||||
|
|
||||||
// 修改 getLrcStyle 函数
|
// 修改 getLrcStyle 函数
|
||||||
const getLrcStyle = (index: number) => {
|
const getLrcStyle = (index: number) => {
|
||||||
const colors = textColors.value || getTextColors;
|
const colors = textColors.value || getTextColors();
|
||||||
const originalStyle = originalLrcStyle(index);
|
const originalStyle = originalLrcStyle(index);
|
||||||
|
|
||||||
if (index === nowIndex.value) {
|
if (index === nowIndex.value) {
|
||||||
|
|||||||
@@ -2,39 +2,46 @@ import { createVNode, render, VNode } from 'vue';
|
|||||||
|
|
||||||
import Loading from './index.vue';
|
import Loading from './index.vue';
|
||||||
|
|
||||||
const vnode: VNode = createVNode(Loading) as VNode;
|
// 每个使用 v-loading 的元素独立持有一个 Loading 实例,
|
||||||
|
// 避免此前"模块级单例 vnode"导致的多个 v-loading 争用同一实例、
|
||||||
|
// spinner 只出现在最后挂载元素上的问题。
|
||||||
|
const instanceMap = new WeakMap<HTMLElement, VNode>();
|
||||||
|
|
||||||
export const vLoading = {
|
const setLoading = (el: HTMLElement, visible: boolean) => {
|
||||||
// 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用
|
const vnode = instanceMap.get(el);
|
||||||
mounted: (el: HTMLElement) => {
|
if (visible) {
|
||||||
render(vnode, el);
|
vnode?.component?.exposed?.show();
|
||||||
},
|
} else {
|
||||||
// 在绑定元素的父组件 及他自己的所有子节点都更新后调用
|
|
||||||
updated: (el: HTMLElement, binding: any) => {
|
|
||||||
if (binding.value) {
|
|
||||||
vnode?.component?.exposed?.show();
|
|
||||||
} else {
|
|
||||||
vnode?.component?.exposed?.hide();
|
|
||||||
}
|
|
||||||
// 动态添加删除自定义class: loading-parent
|
|
||||||
formatterClass(el, binding);
|
|
||||||
},
|
|
||||||
// 绑定元素的父组件卸载后调用
|
|
||||||
unmounted: () => {
|
|
||||||
vnode?.component?.exposed?.hide();
|
vnode?.component?.exposed?.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const vLoading = {
|
||||||
|
// 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
|
||||||
|
mounted: (el: HTMLElement, binding: any) => {
|
||||||
|
const vnode = createVNode(Loading);
|
||||||
|
render(vnode, el);
|
||||||
|
instanceMap.set(el, vnode);
|
||||||
|
setLoading(el, !!binding.value);
|
||||||
|
formatterClass(el, binding);
|
||||||
|
},
|
||||||
|
// 在绑定元素的父组件及他自己的所有子节点都更新后调用
|
||||||
|
updated: (el: HTMLElement, binding: any) => {
|
||||||
|
setLoading(el, !!binding.value);
|
||||||
|
// 动态添加删除自定义class: loading-parent
|
||||||
|
formatterClass(el, binding);
|
||||||
|
},
|
||||||
|
// 绑定元素的父组件卸载后调用:真正卸载组件实例,释放资源
|
||||||
|
unmounted: (el: HTMLElement) => {
|
||||||
|
render(null, el);
|
||||||
|
instanceMap.delete(el);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function formatterClass(el: HTMLElement, binding: any) {
|
function formatterClass(el: HTMLElement, binding: any) {
|
||||||
const classStr = el.getAttribute('class');
|
|
||||||
const tagetClass: number = classStr?.indexOf('loading-parent') as number;
|
|
||||||
if (binding.value) {
|
if (binding.value) {
|
||||||
if (tagetClass === -1) {
|
el.classList.add('loading-parent');
|
||||||
el.setAttribute('class', `${classStr} loading-parent`);
|
} else {
|
||||||
}
|
el.classList.remove('loading-parent');
|
||||||
} else if (tagetClass > -1) {
|
|
||||||
const classArray: Array<string> = classStr?.split('') as string[];
|
|
||||||
classArray.splice(tagetClass - 1, tagetClass + 15);
|
|
||||||
el.setAttribute('class', classArray?.join(''));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,14 +382,22 @@ const showSuggestions = ref(false);
|
|||||||
const suggestionsLoading = ref(false);
|
const suggestionsLoading = ref(false);
|
||||||
const highlightedIndex = ref(-1);
|
const highlightedIndex = ref(-1);
|
||||||
|
|
||||||
|
// 竞态守卫:记录最后一次请求的关键词,响应乱序返回时丢弃过期结果
|
||||||
|
let lastSuggestKeyword = '';
|
||||||
const debouncedSuggest = useDebounceFn(async (kw: string) => {
|
const debouncedSuggest = useDebounceFn(async (kw: string) => {
|
||||||
if (!kw.trim()) {
|
if (!kw.trim()) {
|
||||||
suggestions.value = [];
|
suggestions.value = [];
|
||||||
showSuggestions.value = false;
|
showSuggestions.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
lastSuggestKeyword = kw;
|
||||||
suggestionsLoading.value = true;
|
suggestionsLoading.value = true;
|
||||||
suggestions.value = await getSearchSuggestions(kw);
|
const result = await getSearchSuggestions(kw);
|
||||||
|
// 若期间输入已变化,丢弃这次结果,避免旧关键词建议覆盖新关键词
|
||||||
|
if (kw !== lastSuggestKeyword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
suggestions.value = result;
|
||||||
suggestionsLoading.value = false;
|
suggestionsLoading.value = false;
|
||||||
showSuggestions.value = suggestions.value.length > 0;
|
showSuggestions.value = suggestions.value.length > 0;
|
||||||
highlightedIndex.value = -1;
|
highlightedIndex.value = -1;
|
||||||
|
|||||||
@@ -45,16 +45,19 @@ export const calculateAnimationDelay = (index: any, baseDelay: number = 0.03): s
|
|||||||
return `${delay.toFixed(3)}s`;
|
return `${delay.toFixed(3)}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将秒转换为分钟和秒
|
// 将秒转换为时长字符串:<1 小时为 mm:ss,>=1 小时进位为 h:mm:ss
|
||||||
export const secondToMinute = (s: number) => {
|
export const secondToMinute = (s: number) => {
|
||||||
if (!s) {
|
if (!s || s < 0) {
|
||||||
return '00:00';
|
return '00:00';
|
||||||
}
|
}
|
||||||
const minute: number = Math.floor(s / 60);
|
const hour: number = Math.floor(s / 3600);
|
||||||
|
const minute: number = Math.floor((s % 3600) / 60);
|
||||||
const second: number = Math.floor(s % 60);
|
const second: number = Math.floor(s % 60);
|
||||||
const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`;
|
const pad = (n: number): string => (n > 9 ? `${n}` : `0${n}`);
|
||||||
const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`;
|
if (hour > 0) {
|
||||||
return `${minuteStr}:${secondStr}`;
|
return `${hour}:${pad(minute)}:${pad(second)}`;
|
||||||
|
}
|
||||||
|
return `${pad(minute)}:${pad(second)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化数字 千,万, 百万, 千万,亿
|
// 格式化数字 千,万, 百万, 千万,亿
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ const getAverageColor = (data: Uint8ClampedArray): number[] => {
|
|||||||
b += data[i + 2];
|
b += data[i + 2];
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
// 0 尺寸图片(count 为 0)会得到 NaN,回退为中性灰避免生成 rgb(NaN,...)
|
||||||
|
if (count === 0) {
|
||||||
|
return [128, 128, 128];
|
||||||
|
}
|
||||||
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
|
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -306,8 +310,13 @@ export const createGradientString = (
|
|||||||
colors: { r: number; g: number; b: number }[],
|
colors: { r: number; g: number; b: number }[],
|
||||||
percentages = [0, 50, 100]
|
percentages = [0, 50, 100]
|
||||||
) => {
|
) => {
|
||||||
|
const count = colors.length;
|
||||||
return `linear-gradient(to bottom, ${colors
|
return `linear-gradient(to bottom, ${colors
|
||||||
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
|
.map((color, i) => {
|
||||||
|
// 当颜色数量与传入的 percentages 不一致时,按索引均匀分布,避免出现 undefined%
|
||||||
|
const percent = percentages[i] ?? (count > 1 ? Math.round((i / (count - 1)) * 100) : 0);
|
||||||
|
return `rgb(${color.r}, ${color.g}, ${color.b}) ${percent}%`;
|
||||||
|
})
|
||||||
.join(', ')})`;
|
.join(', ')})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ const loadList = async (type: string, isLoadMore = false) => {
|
|||||||
offset: page.value * TOTAL_ITEMS
|
offset: page.value * TOTAL_ITEMS
|
||||||
};
|
};
|
||||||
const { data } = await getListByCat(params);
|
const { data } = await getListByCat(params);
|
||||||
|
// 竞态守卫:返回时若分类已切换,丢弃过期结果,避免旧分类覆盖新分类
|
||||||
|
if (type !== currentType.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isLoadMore) {
|
if (isLoadMore) {
|
||||||
recommendList.value.push(...data.playlists);
|
recommendList.value.push(...data.playlists);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ const showTranslation = computed(() => lyricSetting.value.showTranslation);
|
|||||||
|
|
||||||
let hideControlsTimer: number | null = null;
|
let hideControlsTimer: number | null = null;
|
||||||
let removeMousePresenceListener: (() => void) | null = null;
|
let removeMousePresenceListener: (() => void) | null = null;
|
||||||
|
let removeReceiveLyricListener: (() => void) | null = null;
|
||||||
|
|
||||||
const isHovering = ref(false);
|
const isHovering = ref(false);
|
||||||
|
|
||||||
@@ -806,8 +807,8 @@ onMounted(() => {
|
|||||||
updateContainerHeight();
|
updateContainerHeight();
|
||||||
window.addEventListener('resize', updateContainerHeight);
|
window.addEventListener('resize', updateContainerHeight);
|
||||||
|
|
||||||
// 监听歌词数据
|
// 监听歌词数据(保存移除函数,卸载时解绑,避免窗口复用/HMR 时监听器叠加)
|
||||||
windowData.electron.ipcRenderer.on('receive-lyric', (_, data) => {
|
const disposeReceiveLyric = windowData.electron.ipcRenderer.on('receive-lyric', (_, data) => {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
handleDataUpdate(parsedData);
|
handleDataUpdate(parsedData);
|
||||||
@@ -815,6 +816,9 @@ onMounted(() => {
|
|||||||
console.error('Error parsing lyric data:', error);
|
console.error('Error parsing lyric data:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (typeof disposeReceiveLyric === 'function') {
|
||||||
|
removeReceiveLyricListener = disposeReceiveLyric;
|
||||||
|
}
|
||||||
|
|
||||||
// 通知主窗口歌词窗口已就绪,请求发送完整歌词数据
|
// 通知主窗口歌词窗口已就绪,请求发送完整歌词数据
|
||||||
windowData.electron.ipcRenderer.send('lyric-ready');
|
windowData.electron.ipcRenderer.send('lyric-ready');
|
||||||
@@ -847,6 +851,10 @@ onUnmounted(() => {
|
|||||||
removeMousePresenceListener();
|
removeMousePresenceListener();
|
||||||
removeMousePresenceListener = null;
|
removeMousePresenceListener = null;
|
||||||
}
|
}
|
||||||
|
if (removeReceiveLyricListener) {
|
||||||
|
removeReceiveLyricListener();
|
||||||
|
removeReceiveLyricListener = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkTheme = () => {
|
const checkTheme = () => {
|
||||||
|
|||||||
@@ -381,6 +381,11 @@ const loadSearch = async (isLoadMore = false) => {
|
|||||||
offset: page.value * ITEMS_PER_PAGE
|
offset: page.value * ITEMS_PER_PAGE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 竞态守卫:请求返回时若关键词/类型已切换,丢弃这次过期结果
|
||||||
|
if (keywords !== currentKeyword.value || type !== searchType.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const songs = data.result.songs || [];
|
const songs = data.result.songs || [];
|
||||||
const albums = data.result.albums || [];
|
const albums = data.result.albums || [];
|
||||||
const mvs = (data.result.mvs || []).map((item: any) => ({
|
const mvs = (data.result.mvs || []).map((item: any) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user