花潮论坛

搜索
热搜: 活动 交友 discuz
查看: 25|回复: 5

Audio Player类(初稿)

[复制链接]
  • TA的每日心情
    奋斗
    2026-5-4 17:03
  • 签到天数: 1807 天

    [LV.Master]伴坛终老

    3206

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

    发表于 2026-5-4 14:48 | 显示全部楼层 |阅读模式

    请马上登录,朋友们都在花潮里等着你哦:)

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    1. /** audioplayer.js

    2.     1. AudPlayer 类配置:
    3.     let option = {
    4.             pa: '.pa'; // 或者 '#pa' | pa
    5.             urls: [
    6.                     ['歌曲地址1', '曲名1'],
    7.                     ['歌曲地址2', '曲名2'],
    8.             ],
    9.             fs: false, // 启用全屏按钮,缺省值 true
    10.     }

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

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

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

    33.         // 初始化
    34.         this.generateUI();
    35.         this.initRandomQueue();
    36.         this.bindAudEvents();
    37.         this.placeMList();
    38.         this.displayPlayer();
    39.         this.playFirst();
    40.     }

    41.     // 加载+播放曲目
    42.     loadTrack(index) {
    43.         if (index < 0 || index >= this.playList.length) return;
    44.         this.currentIndex = index;
    45.         const [url, title] = this.playList[index];
    46.         
    47.         this.aud.src = url;
    48.         this.aud.play().catch(err => this.showError('自动播放被浏览器限制,请手动点击播放'));

    49.         // 更新歌单高亮/翻页
    50.         if (!this.isSingle) {
    51.             this.mlist.dataset.currentsong = '正在播放 :' + title;
    52.             const lists = this.mlist.querySelectorAll('li');
    53.             const curList = this.mlist.querySelector(`li[data-idx="${index}"]`);
    54.             lists.forEach(li => li.classList.remove('list-highlight'));
    55.             curList.classList.add('list-highlight');
    56.             curList.scrollIntoView({ behavior: 'smooth', block: 'center' });
    57.         }        
    58.     }

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

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

    68.     // 上一首
    69.     playPrev() {
    70.         if (this.isSingle) return;
    71.         let prevIndex = this.currentIndex - 1;
    72.         if (prevIndex < 0) prevIndex = this.playList.length - 1;
    73.         this.loadTrack(prevIndex);
    74.     }

    75.     // 下一首
    76.     playNext() {
    77.         if (this.isSingle) return;
    78.         let nextIndex = this.currentIndex + 1;
    79.         if (nextIndex >= this.playList.length) nextIndex = 0;
    80.         this.loadTrack(nextIndex);
    81.     }

    82.     // 切换播放/暂停+按钮状态
    83.     togglePlay() {
    84.         const vids = this.pa.querySelectorAll('video');
    85.         if (this.isPlaying) {
    86.           this.aud.pause();
    87.           this.playbtn.classList.remove('clip-pause');
    88.           this.playbtn.classList.add('clip-play');
    89.           this.pa.style.setProperty('--state', 'paused');
    90.           if (vids) vids.forEach(vid => vid.pause());
    91.         } else {
    92.           this.aud.play().catch(err => this.showError('播放失败,请检查音频链接'));
    93.           this.playbtn.classList.remove('clip-play');
    94.           this.playbtn.classList.add('clip-pause');
    95.           this.pa.style.setProperty('--state', 'running');
    96.           if (vids) vids.forEach(vid => vid.play());
    97.         }
    98.     }

    99.     // 播放结束处理
    100.     handlePlayEnd() {
    101.         if (this.isSingle) {
    102.             // 单曲循环
    103.             this.aud.currentTime = 0;
    104.             this.aud.play();
    105.         } else {
    106.             // 多曲:从随机队列取歌
    107.             if (this.randomQueue.length === 0) {
    108.                 this.resetRandomQueue(); // 周期结束,重置随机队列
    109.             }
    110.             const nextTrack = this.randomQueue.shift();
    111.             const nextIndex = this.playList.findIndex(item => item[0] === nextTrack[0]);
    112.             this.loadTrack(nextIndex);
    113.         }
    114.     }

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

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

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

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

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

    138.         // 暂停
    139.         this.aud.addEventListener('pause', () => this.isPlaying = false);

    140.         // 错误处理
    141.         this.aud.addEventListener('error', (e) => {
    142.             this.showError(`播放失败:${this.playList[this.currentIndex][1]}`);
    143.             this.handlePlayEnd();
    144.         });
    145.     }

    146.     // 创建UI
    147.     generateUI() {
    148.         if (document.querySelector('#audio-player-style')) return;
    149.         const style = document.createElement('style');
    150.         style.id = 'audio-player-style';
    151.         style.textContent = [
    152.             `.player { position: absolute; padding: 6px; width: 460px; height: 40px; line-height: 40px; display: flex; align-items: center; gap: 10px; transition: .75s; opacity: var(--opacity); }`,
    153.             `.player * { box-sizing: border-box; }`,
    154.             `.aud-btn { width: 35px; height: 35px; border: 1px solid currentColor; border-radius: 50%; cursor: pointer; position: relative; display: grid; place-items: center; }`,
    155.             `.aud-btn:hover { background: rgba(0,0,0,.25); }`,
    156.             `.aud-btn::before { content: ''; position: absolute; width: 50%; height: 50%; background: currentColor; clip-path: var(--clip-path); }`,
    157.             `.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%; }`,
    158.             `.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; }`,
    159.             `.common-btn:hover { background: rgba(0,0,0,.25); }`,
    160.             `.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; }`,
    161.             `.music-list::before { position: sticky; content: attr(data-currentsong); font-weight: bold; padding: 0 10px;}`,
    162.             `.music-list ol { height: 160px; overflow: auto; scrollbar-width: thin; scrollbar-color: currentColor transparent; }`,
    163.             `.music-list ol li span { cursor: pointer; }`,
    164.             `.music-list ol li span:hover { opacity: .75; }`,
    165.             `.aud-tmsg { user-select: none; cursor: default; }`,
    166.             `.clip-play { --clip-path: polygon(0 0, 0 100%, 100% 50%);}`,
    167.             `.clip-pause { --clip-path: polygon(45% 0, 45% 100%, 10% 100%, 10% 0, 90% 0, 90% 100%, 55% 100%, 55% 0); }`,
    168.             `.list-highlight { color: red; }`,
    169.             `.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; }`,
    170.             `.btnFs:hover { font-weight: bold; }`,
    171.         ].join('');
    172.         document.head.appendChild(style);
    173.         
    174.         // 播放器容器
    175.         this.player = document.createElement('div');
    176.         this.player.classList.add('player');

    177.         // 前一首按钮
    178.         if (!this.isSingle) {
    179.             const btnPrev = document.createElement('div');
    180.             btnPrev.classList.add('common-btn');
    181.             btnPrev.textContent = '←';
    182.             btnPrev.title = '前一首';
    183.             btnPrev.addEventListener('click', () => this.playPrev());
    184.             this.player.appendChild(btnPrev);
    185.         }

    186.         // 播放|暂停按钮
    187.         this.playbtn = document.createElement('div');
    188.         this.playbtn.classList.add('aud-btn', 'clip-pause');
    189.         this.playbtn.title = '播放/暂停';
    190.         this.playbtn.addEventListener('click', () => this.togglePlay());
    191.         this.player.appendChild(this.playbtn);

    192.         // 下一首按钮
    193.         if (!this.isSingle) {
    194.             const btnNext = document.createElement('div');
    195.             btnNext.classList.add('common-btn');
    196.             btnNext.textContent = '→';
    197.             btnNext.title = '下一首';
    198.             btnNext.addEventListener('click', () => this.playNext());
    199.             this.player.appendChild(btnNext);
    200.         }

    201.         // 进度条
    202.         this.prog = document.createElement('div');
    203.         this.prog.classList.add('aud-prog');
    204.         this.prog.addEventListener('click', (e) => {
    205.             const duration = this.aud.duration;
    206.             if (isNaN(duration)) return;
    207.             this.aud.currentTime = duration * e.offsetX / this.prog.offsetWidth;
    208.         });
    209.         this.prog.addEventListener('mousemove', (e) => {
    210.             const duration = this.aud.duration;
    211.             this.prog.title = this.s2m(duration * e.offsetX / this.prog.offsetWidth);
    212.         });
    213.         this.player.appendChild(this.prog);

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

    219.         // 列表控制按钮
    220.         if (!this.isSingle) {
    221.         this.listControl = document.createElement('div');
    222.         this.listControl.classList.add('common-btn');
    223.         this.listControl.textContent = '▼';
    224.         this.listControl.title = '音乐列表';

    225.         // 列表弹出/收起+按钮箭头变换
    226.         this.listControl.addEventListener('click', () => {
    227.             let hide = this.mlist.style.display === 'block';
    228.             this.mlist.style.display = hide ? 'none' : 'block';
    229.             this.listControl.textContent = this.listControl.textContent === '▲' ? '▼' : '▲';
    230.             if (!hide) {
    231.                 this.mlist.querySelector(`li[data-idx="${this.currentIndex}"]`).scrollIntoView({behavior: 'smooth', block: 'center'});
    232.             }
    233.         });
    234.         this.player.appendChild(this.listControl);
    235.         // 音乐列表
    236.         this.mlist = document.createElement('div');
    237.         this.mlist.classList.add('music-list');
    238.         this.generateMusicList();
    239.         this.player.appendChild(this.mlist);
    240.         }

    241.         // 全屏按钮
    242.         if (this.config.fs) {
    243.             this.fs_btn = document.createElement('div');
    244.             this.fs_btn.classList.add('btnFs');
    245.             this.fullScreen(this.fs_btn);
    246.             this.pa.appendChild(this.fs_btn);
    247.         }

    248.         this.pa.appendChild(this.player);
    249.     }

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

    259.     // 生成列表
    260.     generateMusicList() {
    261.         const ol = document.createElement('ol');
    262.         this.playList.forEach((list, idx) => {
    263.             const li = document.createElement('li');
    264.             li.innerHTML = `<span>${list[1]}</span>`;
    265.             li.dataset.idx = idx;
    266.             li.onclick = () => this.selectTrack(idx);
    267.             ol.appendChild(li);
    268.         });
    269.         this.mlist.appendChild(ol);
    270.     }

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

    279.         document.addEventListener('fullscreenchange', () => {
    280.             if (document.fullscreenElement !== null) {
    281.                 isFullscreen = true;
    282.                 btn.textContent = '退出全屏';
    283.             } else {
    284.                 isFullscreen = false;
    285.                 btn.textContent = '进入全屏';
    286.             }
    287.         });

    288.         document.addEventListener('keydown', (e) => {
    289.             if (e.key === 'F11') {
    290.                 e.preventDefault();
    291.                 isFullscreen ? document.exitFullscreen() : this.pa.requestFullscreen();
    292.             }
    293.         });
    294.     };

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

    304.       // 获取父元素(支持 id/class/元素实体)
    305.       getParentElement() {
    306.           const pa = this.config.pa;
    307.           if (pa instanceof HTMLElement) return pa;
    308.           return document.querySelector(pa) || document.body;
    309.       }
    310.    
    311.     // 错误处理
    312.       showError(msg) {
    313.           console.log(msg);
    314.       }

    315.     // 时间格式化
    316.     s2m(seconds) {
    317.         const min = Math.floor(seconds / 60).toString().padStart(2, '0');
    318.         const sec = Math.floor(seconds % 60).toString().padStart(2, '0');
    319.         return `${min}:${sec}`;
    320.     }
    321. }
    复制代码


    评分

    参与人数 2威望 +80 金钱 +160 经验 +80 收起 理由
    红影 + 50 + 100 + 50 匠心独运,细节精致入微!
    梦江南 + 30 + 60 + 30 匠心独运,细节精致入微!

    查看全部评分

  • TA的每日心情
    开心
    2026-5-4 09:02
  • 签到天数: 709 天

    [LV.9]以坛为家II

    493

    主题

    2万

    回帖

    5万

    积分

    贵宾

    Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7Rank: 7

    花潮美女鼠牛虎兔龙蛇马羊猴鸡狗猪缤纷心情心曲飞扬花好月圆飞龙在天音画大师天籁妙音共看流星风雨同行我心永远喜乐安康花潮贵宾

    发表于 2026-5-4 14:59 | 显示全部楼层
    黑黑老师,这么多的代码是不是又要封装啊?
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2026-5-4 17:03
  • 签到天数: 1807 天

    [LV.Master]伴坛终老

    3206

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-5-4 14:59 | 显示全部楼层
    功能:在指定的父元素中创建播放器+全屏按钮

    支持单曲、多曲,曲目配置方法请查看代码开头的注释。自动识别单曲、多曲,只提供一个音频文件信息为单曲,单曲时不封装前一首、下一首和列表显示、隐藏控制按钮;

    多曲时音频列表智能安排位置,依据播放器距离父元素上下边缘的距离而定;

    附带管控视频的播放、暂停以及维护 CSS 变量 --state;

    全屏支持快捷键(F11),播放器初稿版本未加入快捷键控制。

    应用举例:

    1. <style>
    2.     /* 帖子CSS代码 */
    3. </style>

    4. <div class="pa"></div>

    5. <script charset="utf-8" src="../api/audioplayer/audioplayer.js"></script>

    6. <script>
    7.     const options = {
    8.         pa: '.pa',
    9.         urls: [
    10.             ['./mp3/ztiu.mp3', 'ztiu'],
    11.             ['./mp3/珂拉琪 Collage - 3月桃花.mp3', '3月桃花'],
    12.             ['./mp3/黄龄/叹.mp3', '叹'],
    13.             ['./mp3/黄龄/小雨.mp3', '小雨'],
    14.             ['./mp3/黄龄/芯世纪.mp3', '芯世纪'],
    15.             ['./mp3/黄龄/时光吟.mp3', '时光吟'],
    16.         ],
    17.     };

    18.     const aud = new AudPlayer(options);

    19. </script>
    复制代码


    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2025-12-1 20:32
  • 签到天数: 1052 天

    [LV.10]以坛为家III

    1897

    主题

    33万

    回帖

    39万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮美女虎龙狗猪多彩人生星月交辉海样胸怀火热情怀优雅迷人神秘浪漫缤纷心情草莓情怀蝴蝶情怀心曲飞扬星星情怀七彩绚丽活泼开朗女儿情怀相遇之美一往情深花好月圆心香一瓣紫色情节飞龙在天金剪刀天籁妙音妙笔生花风雨同行我心永远天长地久幸福快乐绚丽缤纷喜乐安康中秋征文周年庆指尖上的流年舞会之星分析(喊冤)章总结章杀人王小强章最佳杀刺临屏写诗七夕诗钟活动第五届风云第六届风云情人节花潮管理

    发表于 2026-5-4 16:40 | 显示全部楼层
    这个播放器的功能很齐全,还能智能判定单曲、多曲,多曲时的界面很丰富。
    黑黑辛苦了
     
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2026-5-4 17:03
  • 签到天数: 1807 天

    [LV.Master]伴坛终老

    3206

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-5-4 17:07 | 显示全部楼层
    红影 发表于 2026-5-4 16:40
    这个播放器的功能很齐全,还能智能判定单曲、多曲,多曲时的界面很丰富。
    黑黑辛苦了

    本来还有静音、音量调节,斟酌之后感觉没啥必要,去掉了
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2026-5-4 17:03
  • 签到天数: 1807 天

    [LV.Master]伴坛终老

    3206

    主题

    13万

    回帖

    28万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

    花潮帅哥鼠牛虎兔龙蛇马羊猴鸡狗猪多彩人生星月交辉奔放热烈海样胸怀春风拂面火热情怀优雅迷人神秘浪漫相遇之美鹰傲苍穹花好月圆紫色情节飞龙在天王者至尊大将风范音画大师天籁妙音共看流星风雨同行我心永远幸福快乐喜乐安康侠骨柔肠心想事成开朗大方花潮管理

     楼主| 发表于 2026-5-4 17:07 | 显示全部楼层
    梦江南 发表于 2026-5-4 14:59
    黑黑老师,这么多的代码是不是又要封装啊?

    它本身就是已经封装好了,独立的文件
    回复 支持 反对

    使用道具 举报

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

    本版积分规则

    小黑屋|手机版|Archiver|服务支持:DZ动力|huachaowang.com Inc. ( 蜀ICP备17032287号-1 )

    GMT+8, 2026-5-4 19:55 , Processed in 0.064099 second(s), 25 queries .

    Powered by Discuz! X3.4

    © 2001-2013 Comsenz Inc.

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