马黑黑 发表于 2026-6-4 17:47

audioplayer修正版(存档)

/** audioplayer.js(2026年6月4日更新)

    在指定父元素生成播放器+全屏按钮,
    支持添加多个自定义音频控制按钮,
    支持热键操作(F11、Alt+X/N/P/L)

    1. 前台配置:
    let option = {
      pa: '.pa'; // 或者 '#pa' | pa
      urls: [
            ['歌曲地址1', '曲名1'],
            ['歌曲地址2', '曲名2'],
      ],
      fs: false, // 禁用全屏按钮,缺省值 true(启用)
      btns: , // 自定义播放控制器(若需要)
    }

    2. 实例化举例:const aud = new AudPlayer(option);

    3. 前台CSS:
    ① 播 放 器: .player { width: 420px; bottom: 10px; right: 20px; color: gold; }
    ② 全屏按钮: .btnFs { top: 20px; right: 20px; color: gold; }
*/
class AudPlayer {
    constructor(config = {}) {
      // 基础配置
      this.config = {
            pa: config.pa || document.body,
            urls: config.urls || [],
            fs: true,
            btns: config.btns,
      };

      // 关键DOM+核心状态
      this.pa = this.getParentElement();
      this.aud = new Audio();
      this.fs_btn = null;
      this.playList = [...this.config.urls]; // 原始歌单
      this.randomQueue = []; // 随机播放队列
      this.currentIndex = 0; // 当前播放索引
      this.isPlaying = false;
      this.isSingle = this.playList.length === 1;

      // 初始化
      this.generateUI();
      this.initRandomQueue();
      this.placeMList();
      this.displayPlayer();
      this.bindAudEvents();
      this.playFirst();
    }

    // 加载+播放曲目
    loadTrack(index) {
      if (index < 0 || index >= this.playList.length) return;
      this.currentIndex = index;
      const = this.playList;

      this.aud.src = url;
      this.aud.play().catch(err => this.showError('自动播放受限,请点击播放按钮'));

      // 歌单高亮+翻页
      if (!this.isSingle) {
            this.mlist.dataset.currentsong = '正在播放 :' + title;
            const lists = this.mlist.querySelectorAll('li');
            const curList = this.mlist.querySelector(`li`);
            lists.forEach(li => li.classList.remove('list-highlight'));
            curList.classList.add('list-highlight');
            const ol = curList.parentNode;
            const distance = curList.offsetTop - curList.offsetHeight - 10;
            ol.scrollTo({left: 0, top: distance, behavior: 'smooth'});
      }
      this.mState();
    }

    // 首次播放
    playFirst() {
      const idx = Math.floor(Math.random() * this.playList.length);
      this.loadTrack(idx);
    }

    // 手动选曲(不影响随机队列)
    selectTrack(index) {
      this.loadTrack(index);
    }

    // 上一首
    playPrev() {
      if (this.isSingle) return;
      let prevIndex = this.currentIndex - 1;
      if (prevIndex < 0) prevIndex = this.playList.length - 1;
      this.loadTrack(prevIndex);
    }

    // 下一首
    playNext() {
      if (this.isSingle) return;
      let nextIndex = this.currentIndex + 1;
      if (nextIndex >= this.playList.length) nextIndex = 0;
      this.loadTrack(nextIndex);
    }

    // 切换播放/暂停
    togglePlay() {
      if (this.isPlaying) {
            this.aud.pause();
      } else {
            this.aud.play().catch(err => this.showError('播放失败,请检查音频链接'));
      }
    }

    // 按钮、视频等状态维护
    mState() {
      const vids = this.pa.querySelectorAll('video');
      if (this.aud.paused) {
            this.playbtn.classList.remove('clip-pause');
            this.playbtn.classList.add('clip-play');
            this.pa.style.setProperty('--state', 'paused');
            if (vids) vids.forEach(vid => vid.pause());
      } else {
            this.playbtn.classList.remove('clip-play');
            this.playbtn.classList.add('clip-pause');
            this.pa.style.setProperty('--state', 'running');
            if (vids) vids.forEach(vid => vid.play());
      }
    }

    // 播放结束处理
    handlePlayEnd() {
      if (this.isSingle) {
            // 单曲循环
            this.aud.currentTime = 0;
            this.aud.play();
      } else {
            // 多曲:从随机队列取歌
            if (this.randomQueue.length === 0) {
                this.resetRandomQueue(); // 周期结束,重置随机队列
            }
            const nextTrack = this.randomQueue.shift();
            const nextIndex = this.playList.findIndex(item => item === nextTrack);
            this.loadTrack(nextIndex);
      }
    }

    // 初始化随机播放队列
    initRandomQueue() {
      if (this.isSingle) return;
      this.randomQueue = [...this.playList].sort(() => Math.random() - 0.5);
    }

    // 重置随机队列(一个周期结束后)
    resetRandomQueue() {
      this.initRandomQueue();
    }

    // 音频事件绑定
    bindAudEvents() {
      // 时间更新
      this.aud.addEventListener('timeupdate', () => {
            const { currentTime, duration } = this.aud;
            this.prog.style.setProperty('--prog',`${currentTime / duration * 100}%`);
            this.tmsg.textContent = `${this.s2m(currentTime)} / ${this.s2m(duration)}`;
      });

      // 播放结束
      this.aud.addEventListener('ended', () => {
            this.handlePlayEnd();
      });

      // 播放/暂停状态同步
      this.aud.addEventListener('play', () => {
            this.isPlaying = true;
            this.mState();
      });

      // 暂停
      this.aud.addEventListener('pause', () => {
            this.isPlaying = false;
            this.mState();
      });

      // 出错
      this.aud.addEventListener('error', (e) => {
            this.showError(`播放失败:${this.playList}`);
            this.handlePlayEnd();
      });
    }

    // 创建UI
    generateUI() {
      if (document.querySelector('#audio-player-style')) return;
      const style = document.createElement('style');
      style.id = 'audio-player-style';
      style.textContent = [
            `.player { position: absolute; padding: 6px; width: 460px; height: 40px; line-height: 40px; display: flex; align-items: center; gap: 10px; transition: .75s; opacity: var(--opacity); }`,
            `.player * { box-sizing: border-box; }`,
            `.aud-btn { width: 35px; height: 35px; border: 1px solid currentColor; border-radius: 50%; cursor: pointer; position: relative; display: grid; place-items: center; }`,
            `.aud-btn:hover { background: rgba(0,0,0,.25); }`,
            `.aud-btn::before { content: ''; position: absolute; width: 50%; height: 50%; background: currentColor; clip-path: var(--clip-path); }`,
            `.aud-prog { flex-grow: 1; height: 12px; background: linear-gradient(to right, currentColor var(--prog), transparent var(--prog), transparent 0); border: 1px solid currentColor; border-radius: 12px; cursor: pointer; --prog: 0%; }`,
            `.common-btn { width: 26px; height: 26px; border: 1px solid currentColor; border-radius: 6px; padding: 0; font: normal 16px/26px sans-serif; text-align: center; user-select: none; cursor: pointer; }`,
            `.common-btn:hover { background: rgba(0,0,0,.25); }`,
            `.music-list { position: absolute; left: 50%; transform: translateX(-50%); width: 100%; max-width: 460px; min-height: 100%; height: 232px; border-radius: 6px; background: rgba(0,0,0,.25); box-shadow: 3px 3px 6px gray; display: none; }`,
            `.music-list::before { position: sticky; content: attr(data-currentsong); font-weight: bold; padding: 5px 15px;}`,
            `.music-list ol { height: 160px; overflow: auto; scrollbar-width: thin; scrollbar-color: currentColor transparent; }`,
            `.music-list ol li span { cursor: pointer; }`,
            `.music-list ol li span:hover { opacity: .75; }`,
            `.aud-tmsg { user-select: none; cursor: default; }`,
            `.clip-play { --clip-path: polygon(0 0, 0 100%, 100% 50%);}`,
            `.clip-pause { --clip-path: polygon(45% 0, 45% 100%, 10% 100%, 10% 0, 90% 0, 90% 100%, 55% 100%, 55% 0); }`,
            `.list-highlight { color: red; }`,
            `.btnFs { position: absolute; padding: 6px 12px; border: 3px solid currentColor; border-radius: 12px; font-size: 1.2em; color: currentColor; background: rgba(0,0,0,.25); transition: .75s; opacity: var(--opacity); user-select: none; cursor: pointer; }`,
            `.btnFs:hover { font-weight: bold; }`,
      ].join('');
      document.head.appendChild(style);

      // 播放器容器
      this.player = document.createElement('div');
      this.player.classList.add('player');

      // 前一首按钮
      if (!this.isSingle) {
            const btnPrev = document.createElement('div');
            btnPrev.classList.add('common-btn');
            btnPrev.textContent = '←';
            btnPrev.title = '前一首(Alt+P)';
            btnPrev.addEventListener('click', () => this.playPrev());
            this.player.appendChild(btnPrev);
      }

      // 播放|暂停按钮
      this.playbtn = document.createElement('div');
      this.playbtn.classList.add('aud-btn', 'clip-pause');
      this.playbtn.title = '播放/暂停(Alt+X)';
      this.playbtn.addEventListener('click', () => this.togglePlay());
      this.player.appendChild(this.playbtn);

      // 下一首按钮
      if (!this.isSingle) {
            const btnNext = document.createElement('div');
            btnNext.classList.add('common-btn');
            btnNext.textContent = '→';
            btnNext.title = '下一首(Alt+N)';
            btnNext.addEventListener('click', () => this.playNext());
            this.player.appendChild(btnNext);
      }

      // 进度条
      this.prog = document.createElement('div');
      this.prog.classList.add('aud-prog');
      this.prog.addEventListener('click', (e) => {
            const duration = this.aud.duration;
            if (isNaN(duration)) return;
            this.aud.currentTime = duration * e.offsetX / this.prog.offsetWidth;
      });
      this.prog.addEventListener('mousemove', (e) => {
            const duration = this.aud.duration;
            this.prog.title = this.s2m(duration * e.offsetX / this.prog.offsetWidth);
      });
      this.player.appendChild(this.prog);

      // 数字时间
      this.tmsg = document.createElement('div');
      this.tmsg.classList.add('aud-tmsg');
      this.tmsg.textContent = '00:00 / 00:00';
      this.player.appendChild(this.tmsg);

      // 列表控制按钮
      if (!this.isSingle) {
      this.listControl = document.createElement('div');
      this.listControl.classList.add('common-btn');
      this.listControl.textContent = '▼';
      this.listControl.title = '音乐列表(Alt+L)';

      // 列表弹出/收起+按钮箭头变换
      this.listControl.addEventListener('click', () => {
            let hide = this.mlist.style.display === 'block';
            this.mlist.style.display = hide ? 'none' : 'block';
            this.listControl.textContent = this.listControl.textContent === '▲' ? '▼' : '▲';
            if (!hide) {
                const li = this.mlist.querySelector(`li`);
                const ol = li.parentNode;
                const distance = li.offsetTop - li.offsetHeight - 10;
                ol.scrollTo({left: 0, top: distance, behavior: 'smooth'});
            }
      });
      this.player.appendChild(this.listControl);
      // 音乐列表
      this.mlist = document.createElement('div');
      this.mlist.classList.add('music-list');
      this.generateMusicList();
      this.player.appendChild(this.mlist);
      }

      // 全屏按钮
      if (this.config.fs) {
            this.fs_btn = document.createElement('div');
            this.fs_btn.classList.add('btnFs');
            this.fullScreen(this.fs_btn);
            this.pa.appendChild(this.fs_btn);
      }

      this.pa.appendChild(this.player);

      // 自定义添加的播放按钮(数组doms传参)
      if (this.config.btns) {
            this.config.btns.forEach(btn => {
                btn.title = '播放/暂停(Alt+X)';
                btn.addEventListener('click', () => {
                  this.togglePlay();
                });
            });
      }

      // 热键
      document.addEventListener('keydown', (e) => {
            if(e.altKey) {
                if (e.key === 'x') this.togglePlay();
                if (e.key === 'p') this.playPrev();
                if (e.key === 'n') this.playNext();
                if (e.key === 'l') this.listControl.click();
            }
      });
    }

    // 列表定位+控制按钮状态
    placeMList() {
      if (this.isSingle) return;
      const style = window.getComputedStyle(this.player);
      const up = parseInt(style.getPropertyValue('bottom')) >= parseInt(style.getPropertyValue('top'));
      const ar = ['▲','▼' ];
      this.listControl.textContent = ar[+up];
      this.mlist.style.setProperty(`${up ? 'top' : 'bottom'}`, '100%');
    }

    // 生成列表
    generateMusicList() {
      const ol = document.createElement('ol');
      this.playList.forEach((list, idx) => {
            const li = document.createElement('li');
            li.innerHTML = `<span>${list}</span>`;
            li.dataset.idx = idx;
            li.onclick = () => {
                this.selectTrack(idx);
            }
            ol.appendChild(li);
      });
      this.mlist.appendChild(ol);
    }

    // 全屏
    fullScreen = (btn) => {
      let isFullscreen = false;
      btn.textContent = '进入全屏';
      btn.title = 'F11';
      btn.addEventListener('click', () => {
            isFullscreen ? document.exitFullscreen() : this.pa.requestFullscreen();
      });

      document.addEventListener('fullscreenchange', () => {
            if (document.fullscreenElement !== null) {
                isFullscreen = true;
                btn.textContent = '退出全屏';
            } else {
                isFullscreen = false;
                btn.textContent = '进入全屏';
            }
      });

      document.addEventListener('keydown', (e) => {
            if (e.key === 'F11') {
                e.preventDefault();
                isFullscreen ? document.exitFullscreen() : this.pa.requestFullscreen();
            }
      });
    };

    // 播放器+全屏隐身现身
    displayPlayer() {
      let timerId;
      this.pa.addEventListener('mousemove', () => {
      clearTimeout(timerId);
            this.pa.style.setProperty('--opacity', '1');
            timerId = setTimeout(() => this.pa.style.setProperty('--opacity', '0'), 3000);
      });
    }

      // 获取元素(支持 id/class/元素实体)
      getParentElement() {
          const pa = this.config.pa;
          if (pa instanceof HTMLElement) return pa;
          return document.querySelector(pa) || document.body;
      }

    // 错误处理
      showError(msg) {
          if (this.mlist) {
            this.mlist.dataset.currentsong = msg;
          } else console.log(msg);
      }

    // 时间格式化
    s2m(seconds) {
      const min = Math.floor(seconds / 60).toString().padStart(2, '0');
      const sec = Math.floor(seconds % 60).toString().padStart(2, '0');
      return `${min}:${sec}`;
    }
}

马黑黑 发表于 2026-6-4 17:50

2026年6月4日修正版:

一、修复业已发现的 bugs;

二、更改曲目列表翻滚机制(改用 scroll API 替换 scrollIntoView API)。

红影 发表于 2026-6-4 21:46

越调整越完美。黑黑辛苦了{:4_187:}

杨帆 发表于 2026-6-4 22:42

马黑黑 发表于 2026-6-4 17:50
2026年6月4日修正版:

一、修复业已发现的 bugs;


匠心之作,经典奉献,谢谢分享{:4_180:}

马黑黑 发表于 2026-6-4 22:45

杨帆 发表于 2026-6-4 22:42
匠心之作,经典奉献,谢谢分享

言重了,玩玩而已

马黑黑 发表于 2026-6-4 22:46

红影 发表于 2026-6-4 21:46
越调整越完美。黑黑辛苦了

不会有完美。通常都是发现了问题就去解决问题,然后在解决问题的同时又制造了更多的问题{:4_170:}

杨帆 发表于 2026-6-4 22:54

马黑黑 发表于 2026-6-4 22:46
不会有完美。通常都是发现了问题就去解决问题,然后在解决问题的同时又制造了更多的问题

是啊,问题 + 解决 = 进步,循环 = 创新

马黑黑 发表于 2026-6-4 22:56

杨帆 发表于 2026-6-4 22:54
是啊,问题 + 解决 = 进步,循环 = 创新

写一个完全没有问题的东东不容易

杨帆 发表于 2026-6-4 23:02

马黑黑 发表于 2026-6-4 22:56
写一个完全没有问题的东东不容易

是,太不容易了~不过,其重点不在于完全没问题,而在于发现问题并不断解决问题

霜染枫丹 发表于 2026-6-4 23:21

这个看着头晕,说明我看看就可以了{:4_173:}{:4_204:}

马黑黑 发表于 2026-6-5 12:22

霜染枫丹 发表于 2026-6-4 23:21
这个看着头晕,说明我看看就可以了

{:4_181:}

马黑黑 发表于 2026-6-5 12:23

杨帆 发表于 2026-6-4 23:02
是,太不容易了~不过,其重点不在于完全没问题,而在于发现问题并不断解决问题

只能如此吧

红影 发表于 2026-6-6 20:01

马黑黑 发表于 2026-6-4 22:46
不会有完美。通常都是发现了问题就去解决问题,然后在解决问题的同时又制造了更多的问题

解决问题的通知,发现更多问题,不是制造的问题呢,然后继续解决,不断靠近完美。
就像趋于无限大是一样的道理。

马黑黑 发表于 2026-6-6 20:18

红影 发表于 2026-6-6 20:01
解决问题的通知,发现更多问题,不是制造的问题呢,然后继续解决,不断靠近完美。
就像趋于无限大是一样 ...

现在我终于能够理解很多维护了很久的项目,突然就放弃了。原因当然是多方面的、综合的,其中之一是越维护越没有底气,因为问题可能会更多

红影 发表于 2026-6-6 21:29

马黑黑 发表于 2026-6-6 20:18
现在我终于能够理解很多维护了很久的项目,突然就放弃了。原因当然是多方面的、综合的,其中之一是越维护 ...

能够发现那么多问题,本身也是进步,前进本来就是螺旋式的,虽然有更多的问题的,但后来的问题比之前的问题层次更高了。

马黑黑 发表于 2026-6-7 09:15

红影 发表于 2026-6-6 21:29
能够发现那么多问题,本身也是进步,前进本来就是螺旋式的,虽然有更多的问题的,但后来的问题比之前的问 ...

问题是靠大家发现的

红影 发表于 2026-6-8 13:52

马黑黑 发表于 2026-6-7 09:15
问题是靠大家发现的

嗯,人多力量大。

马黑黑 发表于 2026-6-8 20:48

红影 发表于 2026-6-8 13:52
嗯,人多力量大。

所言极是

红影 发表于 2026-6-12 23:30

马黑黑 发表于 2026-6-8 20:48
所言极是

那也需要懂的人多点,不懂的多了也没用啊{:4_173:}

马黑黑 发表于 2026-6-13 07:59

红影 发表于 2026-6-12 23:30
那也需要懂的人多点,不懂的多了也没用啊

想懂就会懂,人人都可以做懂王
页: [1] 2 3 4 5
查看完整版本: audioplayer修正版(存档)