搜索
热搜: 活动 交友 discuz
查看: 1|回复: 0

马黑黑老师《audioplayer修正版(存档)》

[复制链接]
  • TA的每日心情
    开心
    昨天 17:07
  • 签到天数: 428 天

    连续签到: 9 天

    [LV.9]以坛为家II

    484

    主题

    1296

    回帖

    1万

    积分

    社区管理员

    积分
    13383

    最佳新人活跃会员热心会员推广达人宣传达人灌水之王突出贡献优秀版主荣誉管理

    发表于 昨天 22:10 | 显示全部楼层 |阅读模式
    1. /** audioplayer.js(2026年6月4日更新)

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

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

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

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

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

    38.         // 初始化
    39.         this.generateUI();
    40.         this.initRandomQueue();
    41.         this.placeMList();
    42.         this.displayPlayer();
    43.         this.bindAudEvents();
    44.         this.playFirst();
    45.     }

    46.     // 加载+播放曲目
    47.     loadTrack(index) {
    48.         if (index < 0 || index >= this.playList.length) return;
    49.         this.currentIndex = index;
    50.         const [url, title] = this.playList[index];

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

    53.         // 歌单高亮+翻页
    54.         if (!this.isSingle) {
    55.             this.mlist.dataset.currentsong = '正在播放 :' + title;
    56.             const lists = this.mlist.querySelectorAll('li');
    57.             const curList = this.mlist.querySelector(`li[data-idx="${index}"]`);
    58.             lists.forEach(li => li.classList.remove('list-highlight'));
    59.             curList.classList.add('list-highlight');
    60.             const ol = curList.parentNode;
    61.             const distance = curList.offsetTop - curList.offsetHeight - 10;
    62.             ol.scrollTo({left: 0, top: distance, behavior: 'smooth'});
    63.         }
    64.         this.mState();
    65.     }

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

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

    75.     // 上一首
    76.     playPrev() {
    77.         if (this.isSingle) return;
    78.         let prevIndex = this.currentIndex - 1;
    79.         if (prevIndex < 0) prevIndex = this.playList.length - 1;
    80.         this.loadTrack(prevIndex);
    81.     }

    82.     // 下一首
    83.     playNext() {
    84.         if (this.isSingle) return;
    85.         let nextIndex = this.currentIndex + 1;
    86.         if (nextIndex >= this.playList.length) nextIndex = 0;
    87.         this.loadTrack(nextIndex);
    88.     }

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

    97.     // 按钮、视频等状态维护
    98.     mState() {
    99.         const vids = this.pa.querySelectorAll('video');
    100.         if (this.aud.paused) {
    101.             this.playbtn.classList.remove('clip-pause');
    102.             this.playbtn.classList.add('clip-play');
    103.             this.pa.style.setProperty('--state', 'paused');
    104.             if (vids) vids.forEach(vid => vid.pause());
    105.         } else {
    106.             this.playbtn.classList.remove('clip-play');
    107.               this.playbtn.classList.add('clip-pause');
    108.               this.pa.style.setProperty('--state', 'running');
    109.               if (vids) vids.forEach(vid => vid.play());
    110.         }
    111.     }

    112.     // 播放结束处理
    113.     handlePlayEnd() {
    114.         if (this.isSingle) {
    115.             // 单曲循环
    116.             this.aud.currentTime = 0;
    117.             this.aud.play();
    118.         } else {
    119.             // 多曲:从随机队列取歌
    120.             if (this.randomQueue.length === 0) {
    121.                 this.resetRandomQueue(); // 周期结束,重置随机队列
    122.             }
    123.             const nextTrack = this.randomQueue.shift();
    124.             const nextIndex = this.playList.findIndex(item => item[0] === nextTrack[0]);
    125.             this.loadTrack(nextIndex);
    126.         }
    127.     }

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

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

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

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

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

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

    159.         // 出错
    160.         this.aud.addEventListener('error', (e) => {
    161.             this.showError(`播放失败:${this.playList[this.currentIndex][1]}`);
    162.             this.handlePlayEnd();
    163.         });
    164.     }

    165.     // 创建UI
    166.     generateUI() {
    167.         if (document.querySelector('#audio-player-style')) return;
    168.         const style = document.createElement('style');
    169.         style.id = 'audio-player-style';
    170.         style.textContent = [
    171.             `.player { position: absolute; padding: 6px; width: 460px; height: 40px; line-height: 40px; display: flex; align-items: center; gap: 10px; transition: .75s; opacity: var(--opacity); }`,
    172.             `.player * { box-sizing: border-box; }`,
    173.             `.aud-btn { width: 35px; height: 35px; border: 1px solid currentColor; border-radius: 50%; cursor: pointer; position: relative; display: grid; place-items: center; }`,
    174.             `.aud-btn:hover { background: rgba(0,0,0,.25); }`,
    175.             `.aud-btn::before { content: ''; position: absolute; width: 50%; height: 50%; background: currentColor; clip-path: var(--clip-path); }`,
    176.             `.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%; }`,
    177.             `.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; }`,
    178.             `.common-btn:hover { background: rgba(0,0,0,.25); }`,
    179.             `.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; }`,
    180.             `.music-list::before { position: sticky; content: attr(data-currentsong); font-weight: bold; padding: 5px 15px;}`,
    181.             `.music-list ol { height: 160px; overflow: auto; scrollbar-width: thin; scrollbar-color: currentColor transparent; }`,
    182.             `.music-list ol li span { cursor: pointer; }`,
    183.             `.music-list ol li span:hover { opacity: .75; }`,
    184.             `.aud-tmsg { user-select: none; cursor: default; }`,
    185.             `.clip-play { --clip-path: polygon(0 0, 0 100%, 100% 50%);}`,
    186.             `.clip-pause { --clip-path: polygon(45% 0, 45% 100%, 10% 100%, 10% 0, 90% 0, 90% 100%, 55% 100%, 55% 0); }`,
    187.             `.list-highlight { color: red; }`,
    188.             `.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; }`,
    189.             `.btnFs:hover { font-weight: bold; }`,
    190.         ].join('');
    191.         document.head.appendChild(style);

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

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

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

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

    219.         // 进度条
    220.         this.prog = document.createElement('div');
    221.         this.prog.classList.add('aud-prog');
    222.         this.prog.addEventListener('click', (e) => {
    223.             const duration = this.aud.duration;
    224.             if (isNaN(duration)) return;
    225.             this.aud.currentTime = duration * e.offsetX / this.prog.offsetWidth;
    226.         });
    227.         this.prog.addEventListener('mousemove', (e) => {
    228.             const duration = this.aud.duration;
    229.             this.prog.title = this.s2m(duration * e.offsetX / this.prog.offsetWidth);
    230.         });
    231.         this.player.appendChild(this.prog);

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

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

    243.         // 列表弹出/收起+按钮箭头变换
    244.         this.listControl.addEventListener('click', () => {
    245.             let hide = this.mlist.style.display === 'block';
    246.             this.mlist.style.display = hide ? 'none' : 'block';
    247.             this.listControl.textContent = this.listControl.textContent === '▲' ? '▼' : '▲';
    248.             if (!hide) {
    249.                 const li = this.mlist.querySelector(`li[data-idx="${this.currentIndex}"]`);
    250.                 const ol = li.parentNode;
    251.                 const distance = li.offsetTop - li.offsetHeight - 10;
    252.                 ol.scrollTo({left: 0, top: distance, behavior: 'smooth'});
    253.             }
    254.         });
    255.         this.player.appendChild(this.listControl);
    256.         // 音乐列表
    257.         this.mlist = document.createElement('div');
    258.         this.mlist.classList.add('music-list');
    259.         this.generateMusicList();
    260.         this.player.appendChild(this.mlist);
    261.         }

    262.         // 全屏按钮
    263.         if (this.config.fs) {
    264.             this.fs_btn = document.createElement('div');
    265.             this.fs_btn.classList.add('btnFs');
    266.             this.fullScreen(this.fs_btn);
    267.             this.pa.appendChild(this.fs_btn);
    268.         }

    269.         this.pa.appendChild(this.player);

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

    279.         // 热键
    280.         document.addEventListener('keydown', (e) => {
    281.             if(e.altKey) {
    282.                 if (e.key === 'x') this.togglePlay();
    283.                 if (e.key === 'p') this.playPrev();
    284.                 if (e.key === 'n') this.playNext();
    285.                 if (e.key === 'l') this.listControl.click();
    286.             }
    287.         });
    288.     }

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

    298.     // 生成列表
    299.     generateMusicList() {
    300.         const ol = document.createElement('ol');
    301.         this.playList.forEach((list, idx) => {
    302.             const li = document.createElement('li');
    303.             li.innerHTML = `<span>${list[1]}</span>`;
    304.             li.dataset.idx = idx;
    305.             li.onclick = () => {
    306.                 this.selectTrack(idx);
    307.             }
    308.             ol.appendChild(li);
    309.         });
    310.         this.mlist.appendChild(ol);
    311.     }

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

    320.         document.addEventListener('fullscreenchange', () => {
    321.             if (document.fullscreenElement !== null) {
    322.                 isFullscreen = true;
    323.                 btn.textContent = '退出全屏';
    324.             } else {
    325.                 isFullscreen = false;
    326.                 btn.textContent = '进入全屏';
    327.             }
    328.         });

    329.         document.addEventListener('keydown', (e) => {
    330.             if (e.key === 'F11') {
    331.                 e.preventDefault();
    332.                 isFullscreen ? document.exitFullscreen() : this.pa.requestFullscreen();
    333.             }
    334.         });
    335.     };

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

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

    351.     // 错误处理
    352.       showError(msg) {
    353.           if (this.mlist) {
    354.               this.mlist.dataset.currentsong = msg;
    355.           } else console.log(msg);
    356.       }

    357.     // 时间格式化
    358.     s2m(seconds) {
    359.         const min = Math.floor(seconds / 60).toString().padStart(2, '0');
    360.         const sec = Math.floor(seconds % 60).toString().padStart(2, '0');
    361.         return `${min}:${sec}`;
    362.     }
    363. }
    复制代码


    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|小黑屋|纳兰音画网 ( 蜀ICP备2021011087号 )

    GMT+8, 2026-6-5 01:57 , Processed in 0.123347 second(s), 22 queries .

    Powered by Discuz! X3.5

    © 2001-2020 Comsenz Inc.

    快速回复 返回顶部 返回列表