修改圆形图,修改对话框标题内容。

This commit is contained in:
陈乾 2026-01-31 21:20:43 +08:00
parent 62de130228
commit 146a96c6a6
2 changed files with 277 additions and 63 deletions

View File

@ -29,7 +29,6 @@ export class AudioPlayerComponent {
this.createPlayerHTML(); this.createPlayerHTML();
this.initializeElements(); this.initializeElements();
this.setupEventListeners(); this.setupEventListeners();
this.setupCanvas();
this.initializeAudioContext(); this.initializeAudioContext();
} }
@ -69,7 +68,7 @@ export class AudioPlayerComponent {
<!-- 可视化效果 --> <!-- 可视化效果 -->
<div class="visualization-section" style="display: ${this.options.showVisualization ? 'block' : 'none'};"> <div class="visualization-section" style="display: ${this.options.showVisualization ? 'block' : 'none'};">
<canvas id="visualizer" width="100%" height="100%"></canvas> <canvas id="visualizer"></canvas>
<div class="visualization-controls"> <div class="visualization-controls">
<button class="viz-btn active" data-mode="bars">条形图</button> <button class="viz-btn active" data-mode="bars">条形图</button>
<button class="viz-btn" data-mode="wave">波形图</button> <button class="viz-btn" data-mode="wave">波形图</button>
@ -171,20 +170,6 @@ export class AudioPlayerComponent {
}); });
} }
setupCanvas() {
if (!this.canvas) return;
const resizeCanvas = () => {
const container = this.canvas.parentElement;
const rect = container.getBoundingClientRect();
this.canvas.width = Math.min(600, rect.width - 40);
this.canvas.height = 300;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
initializeAudioContext() { initializeAudioContext() {
try { try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
@ -655,11 +640,38 @@ export class AudioPlayerComponent {
}, 500); }, 500);
} }
// 更新歌曲标题的方法 // 更新歌曲标题的方法 - 从媒体信息获取
updateSongTitle() { updateSongTitle() {
if (!this.currentAudioUrl) return; if (!this.currentAudioUrl) return;
// 从URL中提取歌曲信息 // 尝试从音频文件的媒体信息中获取歌曲信息
this.extractAudioMetadata(this.currentAudioUrl).then(metadata => {
// 触发自定义事件,通知外部更新标题
const event = new CustomEvent('songLoaded', {
detail: {
title: metadata.title || '未知歌曲',
artist: metadata.artist || '未知艺术家',
album: metadata.album || '',
url: this.currentAudioUrl
}
});
// 从播放器容器向上冒泡事件
if (this.container && this.container.parentNode) {
this.container.parentNode.dispatchEvent(event);
}
}).catch(error => {
console.error('Failed to extract audio metadata:', error);
// 失败时回退到从URL提取
this.extractTitleFromUrl();
});
}
// 从URL提取歌曲信息的回退方法
extractTitleFromUrl() {
if (!this.currentAudioUrl) return;
// 从URL中提取歌曲信息作为回退
let title = '未知歌曲'; let title = '未知歌曲';
try { try {
const urlParts = this.currentAudioUrl.split('/'); const urlParts = this.currentAudioUrl.split('/');
@ -668,13 +680,15 @@ export class AudioPlayerComponent {
const nameWithoutQuery = decodedFilename.split('?')[0]; const nameWithoutQuery = decodedFilename.split('?')[0];
title = nameWithoutQuery.split('.')[0].replace(/[-_]/g, ' ').trim(); title = nameWithoutQuery.split('.')[0].replace(/[-_]/g, ' ').trim();
} catch (e) { } catch (e) {
console.error('Failed to extract song title:', e); console.error('Failed to extract song title from URL:', e);
} }
// 触发自定义事件,通知外部更新标题 // 触发自定义事件,通知外部更新标题
const event = new CustomEvent('songLoaded', { const event = new CustomEvent('songLoaded', {
detail: { detail: {
title: title, title: title,
artist: '',
album: '',
url: this.currentAudioUrl url: this.currentAudioUrl
} }
}); });
@ -685,6 +699,133 @@ export class AudioPlayerComponent {
} }
} }
// 从音频文件提取元数据
async extractAudioMetadata(url) {
try {
// 创建AudioContext
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 获取音频文件
const response = await fetch(url, { mode: 'cors' });
const arrayBuffer = await response.arrayBuffer();
// 解码音频数据
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// 尝试提取ID3标签信息
const metadata = this.parseID3Tags(arrayBuffer);
// 关闭AudioContext
audioContext.close();
return metadata;
} catch (error) {
console.error('Error extracting audio metadata:', error);
return { title: '', artist: '', album: '' };
}
}
// 解析ID3标签
parseID3Tags(arrayBuffer) {
const metadata = { title: '', artist: '', album: '' };
try {
const dv = new DataView(arrayBuffer);
let offset = 0;
let header = '';
// 检查是否有ID3标签
if (dv.getUint8(0) === 0x49 && dv.getUint8(1) === 0x44 && dv.getUint8(2) === 0x33) {
// 跳过ID3头
offset += 10;
// 读取帧
while (offset < arrayBuffer.byteLength) {
// 读取帧头
const frameId = String.fromCharCode(
dv.getUint8(offset),
dv.getUint8(offset + 1),
dv.getUint8(offset + 2),
dv.getUint8(offset + 3)
);
// 读取帧大小
const frameSize =
(dv.getUint8(offset + 4) << 21) +
(dv.getUint8(offset + 5) << 14) +
(dv.getUint8(offset + 6) << 7) +
dv.getUint8(offset + 7);
// 读取帧标志
offset += 10;
// 读取帧内容
if (frameId === 'TIT2') {
metadata.title = this.decodeID3Text(dv, offset, frameSize);
} else if (frameId === 'TPE1') {
metadata.artist = this.decodeID3Text(dv, offset, frameSize);
} else if (frameId === 'TALB') {
metadata.album = this.decodeID3Text(dv, offset, frameSize);
}
offset += frameSize;
}
}
} catch (error) {
console.error('Error parsing ID3 tags:', error);
}
return metadata;
}
// 解码ID3文本
decodeID3Text(dv, offset, length) {
try {
// 检查编码方式 (0: ISO-8859-1, 1: UTF-16 with BOM, 2: UTF-16BE, 3: UTF-8)
const encoding = dv.getUint8(offset);
offset += 1;
length -= 1;
let text = '';
if (encoding === 0) {
// ISO-8859-1
for (let i = 0; i < length; i++) {
const charCode = dv.getUint8(offset + i);
if (charCode === 0) break; // 结束符
text += String.fromCharCode(charCode);
}
} else if (encoding === 1 || encoding === 2) {
// UTF-16
const littleEndian = encoding === 1;
if (littleEndian) {
// 跳过BOM
offset += 2;
length -= 2;
}
for (let i = 0; i < length; i += 2) {
const charCode = littleEndian ?
(dv.getUint8(offset + i + 1) << 8) + dv.getUint8(offset + i) :
(dv.getUint8(offset + i) << 8) + dv.getUint8(offset + i + 1);
if (charCode === 0) break; // 结束符
text += String.fromCharCode(charCode);
}
} else if (encoding === 3) {
// UTF-8
const uint8Array = new Uint8Array(dv.buffer, offset, length);
text = new TextDecoder('utf-8').decode(uint8Array);
// 移除可能的结束符
text = text.replace(/\u0000.*$/, '');
}
return text.trim();
} catch (error) {
console.error('Error decoding ID3 text:', error);
return '';
}
}
handleAudioError(error) { handleAudioError(error) {
console.error('音频加载错误详情:', { console.error('音频加载错误详情:', {
error: error, error: error,
@ -872,33 +1013,63 @@ export class AudioPlayerComponent {
drawCircle() { drawCircle() {
if (!this.ctx || !this.canvas) return; if (!this.ctx || !this.canvas) return;
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const centerX = this.canvas.width / 2; // 获取画布的实际显示尺寸(考虑设备像素比)
const centerY = this.canvas.height / 2; const displayWidth = this.canvas.width / window.devicePixelRatio;
const radius = Math.min(centerX, centerY) - 50; const displayHeight = this.canvas.height / window.devicePixelRatio;
for (let i = 0; i < this.bufferLength; i++) { // 圆心位置在画布中央
const centerX = displayWidth / 2 + Math.min(displayWidth,displayHeight) / 4; //增加一点偏移量
const centerY = displayHeight / 2;
// 缩小圆形图大小,使用较小的半径
const baseRadius = Math.min(centerX, centerY) * 0.5; // 从0.8改为0.5,缩小一半
const maxAmplitude = Math.min(centerX, centerY) * 0.5; // 最大振幅也相应缩小
// 绘制内部基础圆形(可选,增加层次感)
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, baseRadius, 0, 2 * Math.PI);
this.ctx.stroke();
// 绘制频谱线条
const step = Math.max(1, Math.floor(this.bufferLength / 128)); // 减少线条数量,提高性能
for (let i = 0; i < this.bufferLength; i += step) {
const angle = (i / this.bufferLength) * Math.PI * 2; const angle = (i / this.bufferLength) * Math.PI * 2;
const barHeight = (this.dataArray[i] / 255) * radius; const amplitude = (this.dataArray[i] / 255) * maxAmplitude;
const x1 = centerX + Math.cos(angle) * radius; // 起始点(基础圆上)
const y1 = centerY + Math.sin(angle) * radius; const x1 = centerX + Math.cos(angle) * baseRadius;
const x2 = centerX + Math.cos(angle) * (radius + barHeight); const y1 = centerY + Math.sin(angle) * baseRadius;
const y2 = centerY + Math.sin(angle) * (radius + barHeight);
const gradient = this.ctx.createLinearGradient(x1, y1, x2, y2); // 结束点(根据频率数据延伸)
gradient.addColorStop(0, '#667eea'); const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude);
gradient.addColorStop(1, '#764ba2'); const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude);
this.ctx.strokeStyle = gradient; // 根据频率数据计算颜色
this.ctx.lineWidth = 3; const intensity = this.dataArray[i] / 255;
const hue = (i / this.bufferLength) * 360; // 彩虹色调
const saturation = 80 + (intensity * 20); // 饱和度随强度变化
const lightness = 40 + (intensity * 40); // 亮度随强度变化
this.ctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
this.ctx.lineWidth = 1.5 + (intensity * 1.5); // 线条宽度随强度变化
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.moveTo(x1, y1); this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2); this.ctx.lineTo(x2, y2);
this.ctx.stroke(); this.ctx.stroke();
} }
// 绘制中心点(可选,增加视觉焦点)
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, 2, 0, 2 * Math.PI);
this.ctx.fill();
} }
// 工具方法 // 工具方法
@ -1000,7 +1171,13 @@ export function createAudioPlayerDialog(options = {}) {
// 创建歌曲信息区域 // 创建歌曲信息区域
const songInfo = document.createElement('div'); const songInfo = document.createElement('div');
songInfo.className = 'song-info'; songInfo.className = 'song-info';
songInfo.innerHTML = '<div class="song-title">正在播放</div><div class="song-url"></div>'; songInfo.innerHTML = `
<div class="song-main-info">
<span class="song-title">正在播放</span>
<span class="song-artist"></span>
<span class="song-album"></span>
</div>
`;
titleBar.appendChild(songInfo); titleBar.appendChild(songInfo);
// 创建关闭按钮 // 创建关闭按钮
@ -1013,6 +1190,7 @@ export function createAudioPlayerDialog(options = {}) {
// 创建播放器容器 // 创建播放器容器
const playerContainer = document.createElement('div'); const playerContainer = document.createElement('div');
playerContainer.className="player-dialog-component";
dialog.appendChild(playerContainer); dialog.appendChild(playerContainer);
document.body.appendChild(dialog); document.body.appendChild(dialog);
@ -1021,9 +1199,10 @@ export function createAudioPlayerDialog(options = {}) {
const player = new AudioPlayerComponent(playerContainer, mergedOptions); const player = new AudioPlayerComponent(playerContainer, mergedOptions);
// 更新歌曲信息的方法 // 更新歌曲信息的方法
const updateSongInfo = (url, title = null) => { const updateSongInfo = (url, title = null, artist = null, album = null) => {
const songTitle = songInfo.querySelector('.song-title'); const songTitle = songInfo.querySelector('.song-title');
const songUrl = songInfo.querySelector('.song-url'); const songArtist = songInfo.querySelector('.song-artist');
const songAlbum = songInfo.querySelector('.song-album');
// 从URL中提取歌曲名称如果可能 // 从URL中提取歌曲名称如果可能
let finalTitle = title || '未知歌曲'; let finalTitle = title || '未知歌曲';
@ -1039,8 +1218,10 @@ export function createAudioPlayerDialog(options = {}) {
} }
} }
// 更新标题和艺术家信息
songTitle.textContent = finalTitle; songTitle.textContent = finalTitle;
songUrl.textContent = url || ''; songArtist.textContent = artist ? ` - ${artist}` : '';
songAlbum.textContent = album ? ` | ${album}` : '';
}; };
// 监听加载事件更新歌曲信息 // 监听加载事件更新歌曲信息
@ -1052,8 +1233,8 @@ export function createAudioPlayerDialog(options = {}) {
// 监听歌曲加载完成事件,更新更准确的标题信息 // 监听歌曲加载完成事件,更新更准确的标题信息
dialog.addEventListener('songLoaded', (e) => { dialog.addEventListener('songLoaded', (e) => {
const { title, url } = e.detail; const { title, artist, album, url } = e.detail;
updateSongInfo(url, title); updateSongInfo(url, title, artist, album);
}); });
closeBtn.addEventListener('click', () => { closeBtn.addEventListener('click', () => {

View File

@ -84,20 +84,6 @@ body {
transform: translateY(-2px); transform: translateY(-2px);
} }
/* 可视化区域 */
.visualization-section {
margin-bottom: 30px;
text-align: center;
}
#visualizer {
border: 2px solid #e0e0e0;
border-radius: 15px;
background: #000;
width: 100%;
height: 320px;
}
.visualization-controls { .visualization-controls {
margin-top: 15px; margin-top: 15px;
display: flex; display: flex;
@ -1123,6 +1109,11 @@ body.loading-active {
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
.player-dialog-component{
width: 100%;
overflow: hidden;
}
/* 音频播放器组件样式 */ /* 音频播放器组件样式 */
.audio-player-component { .audio-player-component {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
@ -1136,9 +1127,15 @@ body.loading-active {
/* 可视化区域 */ /* 可视化区域 */
.visualization-section { .visualization-section {
margin-bottom: 30px; margin-bottom: 30px;
margin-left: auto;
margin-right: auto;
overflow: hidden;
text-align: center;
position: relative;
} }
#visualizer { #visualizer {
margin: 0 0;
width: 100%; width: 100%;
height: 300px; height: 300px;
border-radius: 15px; border-radius: 15px;
@ -1552,6 +1549,24 @@ button:focus-visible {
min-width: 320px; min-width: 320px;
} }
/* 优化滚动条样式 */
.audio-player-dialog > div::-webkit-scrollbar {
width: 6px;
}
.audio-player-dialog > div::-webkit-scrollbar-track {
background: #f1f1f1;
}
.audio-player-dialog > div::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.audio-player-dialog > div::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.audio-player-dialog::backdrop { .audio-player-dialog::backdrop {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
@ -1575,21 +1590,39 @@ button:focus-visible {
overflow: hidden; overflow: hidden;
} }
.song-main-info {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.song-title { .song-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
margin-bottom: 3px; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-artist {
font-size: 14px;
opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-album {
font-size: 12px;
opacity: 0.8;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.song-url { .song-url {
font-size: 12px; display: none; /* 隐藏URL改为显示媒体信息 */
opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
/* 关闭按钮 */ /* 关闭按钮 */