歌单列表 重启微信自动消失 重新搜索点歌即可
设置好后 – 如果首页不显示 – 就重启微信√
![图片[1]-Lab主页点歌卡片教程-苹果签主题官网](https://www.ilko.cn/wp-content/uploads/2026/05/image-1024x462.jpeg)
教程如下 ↓ :
注意事项:
首页卡片Widget选项里 – 要按照我图片的修改
底部的内容位置
(要求默认 显示不全就是你改动过 恢复默认数值就行)
内容缩放: 100
内容X位移: 0
内容Y位移: 0
显示不全 就让AI修改一下宽度 高度200不动
卡片偏移 就自己慢慢调 内容X位移+内容Y位移+内容缩放
一般是不需要动的 实在解决不了的再调整
显示正常的什么都不用改 内容位置也不需要改 默认就行
![图片[2]-Lab主页点歌卡片教程-苹果签主题官网](https://www.ilko.cn/wp-content/uploads/2026/05/image-1-706x1024.jpeg)
每个手机比例不一样
显示不全 就发给AI修改下(插件打开都有显示)
![图片[3]-Lab主页点歌卡片教程-苹果签主题官网](https://www.ilko.cn/wp-content/uploads/2026/05/image-2-1024x742.jpeg)
↑ 只改宽度 – 高度200不变
长摁下方 – 文字内容
选择全选 – 进行复制
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>至简のiMessage</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
background: transparent;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.music-card {
width: 398px;
height: 200px;
border-radius: 20px;
padding: 12px 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
position: relative;
}
@media (prefers-color-scheme: light) {
.music-card { background: #fff; color: #000; }
#progressBar { background: #eee; }
.playlist-pop { background: #fff; border:1px solid #eee; }
.playlist-item:hover { background: #f5f5f5; }
.add-music-btn, .small-btn { background: #f0f0f0; color: #000; }
.delete-btn { color: #999; }
.delete-btn:hover { color: #e53e3e; }
.clear-all-btn { color:#999; border-top:1px solid #eee; }
.lyric-area { background: rgba(0,0,0,0.04); color: #444; }
.lyric-line.active { color: #007aff; }
.source-toggle { background: rgba(0,0,0,0.06); color:#333; }
.toggle-hint { background: rgba(0,0,0,0.7); color: #fff; }
}
@media (prefers-color-scheme: dark) {
.music-card { background: #202020; color: #fff; }
#progressBar { background: #333; }
.playlist-pop { background: #1a1a1a; border:1px solid #333; }
.playlist-item:hover { background: #2a2a2a; }
.add-music-btn, .small-btn { background: #2c2c2c; color: #fff; }
.delete-btn { color: #888; }
.delete-btn:hover { color: #f56565; }
.clear-all-btn { color:#aaa; border-top:1px solid #333; }
.lyric-area { background: rgba(255,255,255,0.05); color: #ccc; }
.lyric-line.active { color: #ff9f0a; }
.source-toggle { background: rgba(255,255,255,0.1); color:#ddd; }
.toggle-hint { background: rgba(0,0,0,0.75); color: #f0f0f0; }
#searchDialog { background: #202020; color: #fff; }
}
.top-row {
display: flex;
gap: 12px;
align-items: center;
}
.cover {
width: 56px;
height: 56px;
border-radius: 12px;
object-fit: cover;
cursor: pointer;
background: #666;
}
.info {
flex: 1;
min-width: 0;
}
.song-name {
font-size: 15px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-artist {
font-size: 11px;
opacity: 0.7;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.progress-row {
margin: 6px 0 4px;
}
.time-row {
display: flex;
justify-content: space-between;
font-size: 10px;
margin-bottom: 4px;
}
#progressBar {
width: 100%;
height: 5px;
-webkit-appearance: none;
border-radius: 3px;
outline: none;
cursor: pointer;
}
#progressBar::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: inherit;
border: 2px solid #888;
cursor: pointer;
}
.lyric-area {
height: 48px;
overflow-y: auto;
font-size: 11px;
line-height: 1.4;
text-align: center;
border-radius: 10px;
padding: 6px 4px;
margin: 6px 0 4px;
scroll-behavior: smooth;
}
.lyric-line {
transition: all 0.1s;
white-space: pre-wrap;
word-break: break-word;
padding: 2px 0;
}
.lyric-line.active {
font-weight: 600;
transform: scale(1.02);
}
.control-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
}
.ctrl-buttons {
display: flex;
gap: 16px;
align-items: center;
}
.ctrl-btn {
background: none;
border: none;
color: inherit;
cursor: pointer;
display: inline-flex;
}
.ctrl-btn svg {
width: 22px;
height: 22px;
fill: currentColor;
}
.play-btn svg {
width: 30px;
height: 30px;
}
.small-btn {
border: none;
border-radius: 999px;
padding: 4px 12px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
}
.small-btn:hover {
opacity: 0.8;
transform: scale(0.96);
}
.btn-group {
display: flex;
gap: 6px;
}
.playlist-pop {
position: absolute;
bottom: 48px;
left: 12px;
width: 220px;
max-height: 150px;
border-radius: 12px;
padding: 6px;
overflow-y: auto;
display: none;
z-index: 99;
background: inherit;
border: 1px solid;
}
.playlist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px;
font-size: 12px;
border-radius: 8px;
cursor: pointer;
}
.playlist-item.active { font-weight: 600; background: rgba(128,128,128,0.2); }
.delete-btn {
font-size: 18px;
background: none;
border: none;
cursor: pointer;
padding: 0 4px;
}
.clear-all-btn {
width: 100%;
text-align: center;
padding: 6px;
font-size: 11px;
background: none;
border: none;
border-top: 1px solid;
margin-top: 4px;
cursor: pointer;
}
input[type="file"] { display: none; }
#searchDialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 260px;
border-radius: 20px;
padding: 16px;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
@media (prefers-color-scheme: light) {
#searchDialog { background: #fff; color: #000; }
}
#searchKeyword {
width: 100%;
padding: 10px;
border-radius: 40px;
border: 1px solid #ccc;
margin: 10px 0;
background: inherit;
color: inherit;
}
.dialog-buttons { display: flex; justify-content: flex-end; gap: 12px; }
/* 右上角切换源新样式 (按钮+浮动提示) */
.source-wrapper {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
}
.source-toggle {
font-size: 11px;
font-weight: 500;
padding: 5px 10px;
border-radius: 30px;
cursor: pointer;
backdrop-filter: blur(2px);
transition: all 0.2s ease;
white-space: nowrap;
background: rgba(128,128,128,0.15);
margin-left: 0;
}
.source-toggle:hover {
opacity: 0.8;
transform: scale(0.96);
}
.toggle-hint {
font-size: 10px;
padding: 4px 8px;
margin-top: 6px;
border-radius: 40px;
white-space: nowrap;
background: rgba(0,0,0,0.7);
color: #fff;
backdrop-filter: blur(4px);
font-weight: 500;
letter-spacing: 0.5px;
animation: floatHint 0.8s infinite ease-in-out;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
@keyframes floatHint {
0% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
100% { transform: translateY(0px); }
}
</style>
</head>
<body>
<div class="music-card">
<div class="top-row">
<img id="cover" class="cover" src="">
<div class="info">
<div class="song-name" id="songName">未播放</div>
<div class="song-artist" id="songArtist">点击 🔍 搜歌</div>
<div class="progress-row">
<div class="time-row"><span id="curTime">00:00</span><span id="totalTime">00:00</span></div>
<input type="range" id="progressBar" min="0" max="100" value="0">
</div>
</div>
<!-- 音源切换按钮 + 动态提示(默认隐藏) -->
<div class="source-wrapper">
<div id="sourceToggle" class="source-toggle">🎵 QQ音乐</div>
<div id="toggleHint" class="toggle-hint" style="display: none;">↑点击切换音源</div>
</div>
</div>
<div id="lyricArea" class="lyric-area"><div class="lyric-line">✨ 暂无歌词,点击“📄 歌词”获取</div></div>
<div class="control-bar">
<div class="ctrl-buttons">
<button class="ctrl-btn" id="playlistBtn">☰</button>
<button class="ctrl-btn" id="prevBtn"><svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg></button>
<button class="ctrl-btn play-btn" id="playBtn"><svg id="playIcon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button>
<button class="ctrl-btn" id="nextBtn"><svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
</div>
<div class="btn-group">
<button class="small-btn" id="lyricBtn">📄 歌词</button>
<button class="small-btn" id="searchBtn">🔍 搜歌</button>
<button class="small-btn" id="addMusicBtn">📂 添加</button>
</div>
</div>
<div class="playlist-pop" id="playlistPop"></div>
<input type="file" id="musicPick" accept="audio/*" multiple>
<input type="file" id="lrcPick" accept=".lrc,.txt">
<input type="file" id="coverPick" accept="image/*" style="display:none">
</div>
<div id="searchDialog" style="display:none;">
<div style="font-weight:600;">🎵 搜索歌曲</div>
<input type="text" id="searchKeyword" placeholder="歌名,例如:晴天">
<div class="dialog-buttons"><button id="searchCancelBtn">取消</button><button id="searchConfirmBtn">搜索</button></div>
</div>
<audio id="audioPlayer">
<script>
// DOM 元素
const audio = document.getElementById('audioPlayer');
const cover = document.getElementById('cover');
const songNameSpan = document.getElementById('songName');
const artistSpan = document.getElementById('songArtist');
const curTimeSpan = document.getElementById('curTime');
const totalTimeSpan = document.getElementById('totalTime');
const progressBar = document.getElementById('progressBar');
const playBtn = document.getElementById('playBtn');
const playIcon = document.getElementById('playIcon');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const playlistBtn = document.getElementById('playlistBtn');
const playlistPop = document.getElementById('playlistPop');
const searchBtn = document.getElementById('searchBtn');
const searchDialog = document.getElementById('searchDialog');
const searchKeyword = document.getElementById('searchKeyword');
const searchCancel = document.getElementById('searchCancelBtn');
const searchConfirm = document.getElementById('searchConfirmBtn');
const addMusicBtn = document.getElementById('addMusicBtn');
const musicPick = document.getElementById('musicPick');
const lyricBtn = document.getElementById('lyricBtn');
const lrcPick = document.getElementById('lrcPick');
const lyricArea = document.getElementById('lyricArea');
const coverPick = document.getElementById('coverPick');
const sourceToggle = document.getElementById('sourceToggle');
const toggleHint = document.getElementById('toggleHint');
// 全局数据
let playList = [];
let nowIndex = 0;
let isPlaying = false;
let currentLyrics = [];
let artistRestoreTimer = null;
let hintTimeout = null; // 提示自动隐藏定时器
let globalListenerRemover = null; // 用于移除全局交互监听
// 音乐源切换 (qq / wyy)
let currentMusicSource = 'qq';
// ========== 性能优化核心:歌词缓存,避免频繁 innerHTML 重建 ==========
let cachedLyricLines = []; // 存储当前歌词行的DOM元素(仅当歌词非空时有效)
let cachedNoLyricDiv = null; // 无歌词时的占位div(用于复用)
let currentLyricVersion = 0; // 用于标记歌词是否变化
// 重建歌词DOM(仅在歌词数据变化时调用一次)
function rebuildLyricDOM() {
lyricArea.innerHTML = '';
cachedLyricLines = [];
if (!currentLyrics.length) {
// 无歌词:显示占位符(完全保留原文字样式)
if (!cachedNoLyricDiv) {
cachedNoLyricDiv = document.createElement('div');
cachedNoLyricDiv.className = 'lyric-line';
cachedNoLyricDiv.innerText = '🎤 暂无歌词,点击“📄 歌词”上传或获取';
}
lyricArea.appendChild(cachedNoLyricDiv);
return;
}
// 有歌词:批量构建行
const fragment = document.createDocumentFragment();
for (let i = 0; i < currentLyrics.length; i++) {
const lineDiv = document.createElement('div');
lineDiv.className = 'lyric-line';
lineDiv.innerText = currentLyrics[i].text;
fragment.appendChild(lineDiv);
cachedLyricLines.push(lineDiv);
}
lyricArea.appendChild(fragment);
}
// 仅更新高亮行和滚动(高频调用,极轻量)
function updateLyricHighlight(currentTime) {
if (!currentLyrics.length) return;
let activeIndex = -1;
for (let i = 0; i < currentLyrics.length; i++) {
const line = currentLyrics[i];
const nextTime = (i + 1 < currentLyrics.length) ? currentLyrics[i+1].time : Infinity;
if (currentTime >= line.time && currentTime < nextTime) {
activeIndex = i;
break;
}
}
// 处理最后一句之后的情况
if (activeIndex === -1 && currentLyrics.length && currentTime >= currentLyrics[currentLyrics.length-1].time) {
activeIndex = currentLyrics.length - 1;
}
// 更新 DOM 中的 active 类
for (let i = 0; i < cachedLyricLines.length; i++) {
const lineDiv = cachedLyricLines[i];
if (i === activeIndex) {
if (!lineDiv.classList.contains('active')) {
lineDiv.classList.add('active');
// 滚动到可视区
lineDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else {
if (lineDiv.classList.contains('active')) lineDiv.classList.remove('active');
}
}
}
// 原 renderLyrics 的替代:根据歌词状态做轻量渲染(保留原函数名但内部优化,完全兼容旧调用)
function renderLyrics(currentTime = 0) {
// 确保歌词区域与 currentLyrics 同步(若版本未变则只更新高亮,否则重建)
// 外部调用时如果是歌词数据变化了,会先修改 currentLyrics 然后调用本函数,因此需要检测是否需要重建
// 简便方式:对比 currentLyrics 与现有缓存的行数是否匹配(歌词内容变更时行数会变)
let needRebuild = false;
if (!currentLyrics.length) {
// 当前无歌词,但 lyricArea 内如果没有占位符或者有歌词行残留则重建
const hasNoLyricDiv = lyricArea.children.length === 1 && lyricArea.children[0] === cachedNoLyricDiv;
if (!hasNoLyricDiv || !cachedNoLyricDiv) needRebuild = true;
} else {
// 有歌词,但缓存的行数不匹配或 lyricArea 内容不是由 cachedLyricLines 构成
if (cachedLyricLines.length !== currentLyrics.length) needRebuild = true;
else {
// 可选:检查第一个歌词文本是否一致,简单防错
if (cachedLyricLines.length > 0 && cachedLyricLines[0].innerText !== currentLyrics[0].text) needRebuild = true;
}
}
if (needRebuild) {
rebuildLyricDOM();
}
// 更新高亮
if (currentLyrics.length) {
updateLyricHighlight(currentTime);
}
}
// 外部设置歌词时调用 (完全保留原逻辑,只是内部会触发重建)
function setLyricsForCurrent(song, lyricsArray) {
song.lyrics = lyricsArray;
if (playList[nowIndex] === song) {
currentLyrics = lyricsArray || [];
renderLyrics(audio.currentTime);
}
}
// ----- 其余所有原有函数完全保留,只有上面歌词渲染部分被优化 -----
// 以下所有代码均从原版复制,未改动任何文字描述、提示、按钮文案
function formatTime(sec) {
if (isNaN(sec)) return '00:00';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return `${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
}
function parseLRC(lrcStr) {
const lines = lrcStr.split(/\r?\n/);
const list = [];
for (const line of lines) {
const match = line.match(/\[(\d{2}):(\d{2}(?:\.\d+)?)\](.*)/);
if (match) {
const min = parseInt(match[1]);
const sec = parseFloat(match[2]);
const time = min * 60 + sec;
const text = match[3].trim();
if (text) list.push({ time, text });
}
}
list.sort((a,b) => a.time - b.time);
return list;
}
function escapeHtml(str) { return str.replace(/[&<>]/g, function(m){ return {'&':'&','<':'<','>':'>'}[m];}); }
function renderPlaylist() {
playlistPop.innerHTML = '';
playList.forEach((song, idx) => {
const div = document.createElement('div');
div.className = `playlist-item ${idx === nowIndex ? 'active' : ''}`;
const nameSpan = document.createElement('span');
nameSpan.innerText = `${song.name} - ${song.artist}`;
nameSpan.style.flex = '1';
nameSpan.style.cursor = 'pointer';
nameSpan.onclick = (e) => { e.stopPropagation(); loadSong(idx); autoPlay(); closePop(); };
const delBtn = document.createElement('button');
delBtn.innerText = '✕';
delBtn.className = 'delete-btn';
delBtn.onclick = (e) => { e.stopPropagation(); deleteSong(idx); };
div.appendChild(nameSpan);
div.appendChild(delBtn);
playlistPop.appendChild(div);
});
if (playList.length) {
const clearBtn = document.createElement('button');
clearBtn.innerText = '🗑️ 清空所有歌曲';
clearBtn.className = 'clear-all-btn';
clearBtn.onclick = () => clearAllSongs();
playlistPop.appendChild(clearBtn);
}
}
function closePop() { playlistPop.style.display = 'none'; }
function deleteSong(idx) {
if (idx < 0 || idx >= playList.length) return;
playList.splice(idx, 1);
if (playList.length === 0) {
nowIndex = 0;
isPlaying = false;
audio.src = '';
cover.src = '';
songNameSpan.innerText = '未播放';
artistSpan.innerText = '点击 🔍 搜歌';
currentLyrics = [];
renderLyrics();
renderPlaylist();
return;
}
if (idx < nowIndex) nowIndex--;
else if (idx === nowIndex) {
if (nowIndex >= playList.length) nowIndex = playList.length - 1;
loadSong(nowIndex);
if (isPlaying) autoPlay();
} else {
loadSong(nowIndex);
}
renderPlaylist();
}
function clearAllSongs() {
playList = [];
nowIndex = 0;
isPlaying = false;
audio.src = '';
cover.src = '';
songNameSpan.innerText = '未播放';
artistSpan.innerText = '点击 🔍 搜歌';
currentLyrics = [];
renderLyrics();
renderPlaylist();
if (playlistPop) playlistPop.style.display = 'none';
}
function loadSong(idx) {
if (!playList.length) return;
nowIndex = (idx + playList.length) % playList.length;
const song = playList[nowIndex];
audio.src = song.src;
songNameSpan.innerText = song.name;
if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
artistSpan.innerText = song.artist;
if (song.coverBase64) cover.src = song.coverBase64;
else if (song.coverUrl) cover.src = song.coverUrl;
else cover.src = '';
currentLyrics = song.lyrics || [];
renderLyrics(0);
renderPlaylist();
}
function autoPlay() {
if (!audio.src) return;
audio.play().catch(e=>console.log);
isPlaying = true;
playIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
const currentArtist = playList[nowIndex] ? playList[nowIndex].artist : '';
artistSpan.innerText = '🎵 正在播放';
artistRestoreTimer = setTimeout(() => {
artistSpan.innerText = currentArtist;
}, 1000);
}
function pausePlay() {
audio.pause();
isPlaying = false;
playIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
const currentArtist = playList[nowIndex] ? playList[nowIndex].artist : '';
artistSpan.innerText = '⏸ 已暂停';
artistRestoreTimer = setTimeout(() => {
artistSpan.innerText = currentArtist;
}, 1000);
}
audio.ontimeupdate = () => {
if(audio.duration) {
const percent = (audio.currentTime / audio.duration) * 100;
progressBar.value = percent;
curTimeSpan.innerText = formatTime(audio.currentTime);
totalTimeSpan.innerText = formatTime(audio.duration);
if (currentLyrics.length) renderLyrics(audio.currentTime);
}
};
progressBar.oninput = () => { if(audio.duration) audio.currentTime = (progressBar.value/100) * audio.duration; };
audio.onended = () => nextBtn.click();
prevBtn.onclick = () => { if(playList.length) { loadSong(nowIndex-1); autoPlay(); } };
nextBtn.onclick = () => { if(playList.length) { loadSong(nowIndex+1); autoPlay(); } };
playBtn.onclick = () => {
if(!playList.length) return;
if(isPlaying) pausePlay();
else autoPlay();
};
playlistBtn.onclick = (e) => { e.stopPropagation(); playlistPop.style.display = playlistPop.style.display === 'block' ? 'none' : 'block'; };
document.addEventListener('click', (e) => { if(!document.querySelector('.music-card').contains(e.target)) closePop(); });
cover.onclick = () => coverPick.click();
coverPick.onchange = async (e) => {
const file = e.target.files[0];
if (!file || !playList[nowIndex]) return;
const reader = new FileReader();
reader.onload = async (res) => {
const imgUrl = res.target.result;
cover.src = imgUrl;
playList[nowIndex].coverBase64 = imgUrl;
if (playList[nowIndex].fileName && window.TM && playList[nowIndex].isLocal) {
const b64 = await TM.fileToBase64(file);
await TM.saveFile(playList[nowIndex].coverFileName, b64, 'image');
}
};
reader.readAsDataURL(file);
};
addMusicBtn.onclick = () => musicPick.click();
musicPick.onchange = async (e) => {
if (!window.TM) { alert("当前环境不支持存储, 仅临时播放"); }
for (let f of e.target.files) {
const rawName = f.name.replace(/\.\w+$/, '');
const parts = rawName.split(' - ');
const songTitle = parts[0] || rawName;
const artist = parts[1] || '本地音乐';
let uri = URL.createObjectURL(f);
if (window.TM) {
const fileName = 'mus_'+Date.now()+'_'+f.name;
const b64 = await TM.fileToBase64(f);
await TM.saveFile(fileName, b64, 'audio');
uri = await TM.loadFile('audio', fileName);
playList.push({
name: songTitle, artist, src: uri, coverBase64: '', coverUrl: '',
isLocal: true, fileName, coverFileName: 'cover_'+fileName+'.jpg', lyrics: null
});
} else {
playList.push({
name: songTitle, artist, src: uri, coverBase64: '', coverUrl: '',
isLocal: false, fileName: null, lyrics: null
});
}
}
renderPlaylist();
if (playList.length === 1) { loadSong(0); autoPlay(); }
musicPick.value = '';
};
lyricBtn.onclick = () => {
if (!playList.length) { alert("请先播放歌曲"); return; }
if (confirm("自动获取网络歌词?\n(取消则上传本地.lrc文件)")) {
const song = playList[nowIndex];
fetchLyricsFromNet(song.name, song.artist).then(lyrics => {
if (lyrics && lyrics.length) {
setLyricsForCurrent(song, lyrics);
alert("歌词已加载");
} else alert("未找到歌词,可手动上传");
});
} else lrcPick.click();
};
lrcPick.onchange = async (e) => {
const file = e.target.files[0];
if (!file || !playList[nowIndex]) return;
const text = await file.text();
const lyrics = parseLRC(text);
if (lyrics.length) {
setLyricsForCurrent(playList[nowIndex], lyrics);
if (playList[nowIndex].fileName && window.TM && playList[nowIndex].isLocal) {
const lrcName = playList[nowIndex].fileName.replace(/\.\w+$/, '.lrc');
if (lrcName) await TM.saveFile(lrcName, btoa(unescape(encodeURIComponent(text))), 'audio');
}
alert("歌词已加载");
} else alert("无效LRC格式");
lrcPick.value = '';
};
async function fetchLyricsFromNet(title, artist) {
try {
const api = `https://lrclib.net/api/get?track_name=${encodeURIComponent(title)}&artist_name=${encodeURIComponent(artist)}`;
const resp = await fetch(api);
const data = await resp.json();
if (data.syncedLyrics) return parseLRC(data.syncedLyrics);
else return null;
} catch(e) { return null; }
}
// 搜索对话框
searchDialog.style.display = 'none';
searchBtn.onclick = () => {
hideToggleHintAndCleanup(); // 立即隐藏浮窗提示并清理监听
searchDialog.style.display = 'block';
searchKeyword.value = '';
searchKeyword.focus();
};
searchCancel.onclick = () => { searchDialog.style.display = 'none'; };
searchConfirm.onclick = async () => {
const kw = searchKeyword.value.trim();
if (!kw) return alert("输入歌名");
searchDialog.style.display = 'none';
await searchSongBySource(kw);
};
searchKeyword.addEventListener('keypress', e => { if(e.key === 'Enter') searchConfirm.click(); });
async function searchSongBySource(songName) {
let apiUrl = '';
if (currentMusicSource === 'qq') {
apiUrl = `https://api.iosnn.cn/apis.php?api=hbqq&name=${encodeURIComponent(songName)}`;
} else {
apiUrl = `https://api.iosnn.cn/apis.php?api=hbqq&name=${encodeURIComponent(songName)}`;
}
try {
const resp = await fetch(apiUrl);
const data = await resp.json();
if (data.code !== 200 || !data.music_url || !data.title) {
throw new Error(data.msg || data.message || "接口未返回有效歌曲");
}
const musicUrl = data.music_url;
const coverUrl = (data.cover && data.cover.startsWith('http')) ? data.cover : '';
let lyricsArray = null;
const rawLrc = data.lrc || data.lyric || null;
if (rawLrc && typeof rawLrc === 'string' && rawLrc.trim().length > 0) {
lyricsArray = parseLRC(rawLrc);
}
if (!lyricsArray || lyricsArray.length === 0) {
const netLyrics = await fetchLyricsFromNet(data.title, data.singer);
if (netLyrics && netLyrics.length) lyricsArray = netLyrics;
}
const newSong = {
name: data.title,
artist: data.singer || '未知歌手',
src: musicUrl,
coverUrl: coverUrl,
coverBase64: '',
isLocal: false,
lyrics: lyricsArray,
fileName: null,
coverFileName: null
};
playList.push(newSong);
renderPlaylist();
const idx = playList.length - 1;
loadSong(idx);
autoPlay();
} catch (err) {
alert(`搜歌失败 (${currentMusicSource === 'qq' ? 'QQ音乐' : '网易云'}): ` + err.message);
console.error(err);
}
}
async function initLocal() {
if (!window.TM) return;
const audioFiles = await TM.listFiles('audio');
const imgFiles = await TM.listFiles('image');
for (let f of audioFiles) {
if (f.startsWith('mus_')) {
const uri = await TM.loadFile('audio', f);
let rawName = f.replace(/^mus_\d+_/, '').replace(/\.\w+$/, '');
let title = rawName;
let artist = '本地音乐';
const dashIndex = rawName.lastIndexOf(' - ');
if (dashIndex !== -1) {
title = rawName.substring(0, dashIndex);
artist = rawName.substring(dashIndex + 3);
}
const coverFileName = `cover_${f}.jpg`;
let coverBase64 = imgFiles.includes(coverFileName) ? await TM.loadFile('image', coverFileName) : '';
let lyricsArr = null;
const lrcName = f.replace(/\.\w+$/, '.lrc');
if (audioFiles.includes(lrcName)) {
const lrcUri = await TM.loadFile('audio', lrcName);
if (lrcUri) {
const lrcResp = await fetch(lrcUri);
const lrcText = await lrcResp.text();
lyricsArr = parseLRC(lrcText);
}
}
playList.push({
name: title,
artist: artist,
src: uri,
coverBase64: coverBase64,
coverUrl: '',
fileName: f,
coverFileName: coverFileName,
lyrics: lyricsArr,
isLocal: true
});
}
}
renderPlaylist();
if (playList.length) loadSong(0);
}
function showToggleHint(keepGlobalListener = false) {
if (hintTimeout) clearTimeout(hintTimeout);
toggleHint.style.display = 'block';
hintTimeout = setTimeout(() => {
hideToggleHintAndCleanup();
}, 2000);
if (!keepGlobalListener || !globalListenerRemover) {
if (globalListenerRemover) globalListenerRemover();
const handleUserInteraction = (e) => {
let target = e.target;
let isSourceWrapper = false;
while (target && target !== document.body) {
if (target.classList && (target.classList.contains('source-wrapper') ||
target.id === 'sourceToggle' || target.id === 'toggleHint')) {
isSourceWrapper = true;
break;
}
target = target.parentElement;
}
if (!isSourceWrapper) hideToggleHintAndCleanup();
};
document.addEventListener('click', handleUserInteraction, true);
document.addEventListener('touchstart', handleUserInteraction, true);
globalListenerRemover = () => {
document.removeEventListener('click', handleUserInteraction, true);
document.removeEventListener('touchstart', handleUserInteraction, true);
};
}
}
function hideToggleHintAndCleanup() {
if (hintTimeout) { clearTimeout(hintTimeout); hintTimeout = null; }
toggleHint.style.display = 'none';
if (globalListenerRemover) { globalListenerRemover(); globalListenerRemover = null; }
}
function showInitialHint() {
hideToggleHintAndCleanup();
showToggleHint(false);
}
function updateSourceToggleUI() {
if (currentMusicSource === 'qq') {
sourceToggle.innerHTML = '🎵 QQ音乐';
sourceToggle.style.background = 'rgba(0,122,255,0.15)';
} else {
sourceToggle.innerHTML = '☁️ 网易云';
sourceToggle.style.background = 'rgba(236, 72, 54, 0.18)';
}
}
function toggleMusicSource() {
if (currentMusicSource === 'qq') {
currentMusicSource = 'wyy';
} else {
currentMusicSource = 'qq';
}
updateSourceToggleUI();
if (globalListenerRemover) {
if (hintTimeout) clearTimeout(hintTimeout);
toggleHint.style.display = 'block';
hintTimeout = setTimeout(() => {
hideToggleHintAndCleanup();
}, 2000);
} else {
showToggleHint(false);
}
const tip = currentMusicSource === 'qq' ? '已切换至 QQ音乐 搜歌' : '已切换至 网易云音乐 搜歌';
const oldHint = artistSpan.innerText;
artistSpan.innerText = tip;
if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
artistRestoreTimer = setTimeout(() => {
if (playList[nowIndex]) artistSpan.innerText = playList[nowIndex].artist;
else artistSpan.innerText = oldHint;
artistRestoreTimer = null;
}, 2000);
}
sourceToggle.addEventListener('click', (e) => {
e.stopPropagation();
toggleMusicSource();
});
window.addEventListener('DOMContentLoaded', () => {
updateSourceToggleUI();
initLocal();
setTimeout(() => {
showInitialHint();
}, 100);
});
</script>
</body>
</html>
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END











暂无评论内容