uni-app Vue3实现一个酷炫的多功能音乐播放器🎵支持微信小程序后台播放💃💃💃


theme: condensed-night-purple
highlight: atelier-cave-light

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

本文存在多张gif演示图,建议在wifi环境下阅读📖

最近在做网易云音乐微信小程序开源项目的时候,关于播放器功能参考了一些成熟的微信小程序,如网易云音乐小程序QQ音乐小程序,但是发现这些小程序端的播放器相对于APP端来说较简单,只支持一些基础功能,那么是否可以在小程序端实现一个功能相对完善的音乐播放器呢🤔

通过调研一些音乐类APP,一个功能相对完善的音乐播放器大概需要支持以下几个功能:

  • 歌曲切换
  • 进度条拖拽
  • 快进⏩快退⏪
  • 歌词同步
  • 歌词跳转
  • 歌曲后台播放(微信小程序)

Dec-01-2022 12-09-14.gif

对播放器按照功能进行拆分,大致结构如下图所示👇
主要分为控制区域歌词区域

截屏2022-12-01 下午12.48.56.png

下面来一起实现吧👇

初始化全局播放器

页面切换时也需要保持音乐持续播放,因此需要对播放器进行全局状态管理,由于VueX对ts并不友好,此处引入Pinia
状态管理 Pinia

全局初始化Audio实例:
uni.createInnerAudioContext() Audio实例
uni.getBackgroundAudioManager() 微信小程序后台播放

由于直接在Pinia中初始化Audio实例会出现切换歌曲创建多个实例的bug,因此通过在App.vue文件中初始化实例实现全局唯一Audio实例

initPlayer创建全局Audio实例

// initPlayer.ts
// 全局初始化audio实例,解决微信小程序无法正常使用pinia调用audio实例的bug
let innerAudioContext:any

export const createPlayer = () => {
    return innerAudioContext = uni.getBackgroundAudioManager ?
    uni.getBackgroundAudioManager() : uni.createInnerAudioContext()
}

export const getPlayer = () => innerAudioContext

usePlayerStore统一管理播放器状态和方法,useInitPlayer初始化播放器并进行实时监听

// Pinia
import { defineStore, storeToRefs } from 'pinia';
import { onUnmounted, watch } from 'vue';
import { getSongUrl, getSongDetail, getSongLyric } from '../config/api/song';
import type { Song, SongUrl } from '../config/models/song';
import { getPlayer } from '../config/utils/initPlayer';

export const usePlayerStore = defineStore({
    id: 'Player',
    state: () => ({
        // audio: uni.createInnerAudioContext(),   // Audio实例
        loopType: 0, // 循环模式 0 列表循环 1 单曲循环 2随机播放
        playList: [] as Song[], // 播放列表
        showPlayList: false, // 播放列表显隐
        id: 0,  // 当前歌曲id
        url: '',    // 歌曲url
        songUrl: {} as SongUrl,
        song: {} as Song,
        isPlaying: false, // 是否播放中
        isPause: false, // 是否暂停
        sliderInput: false, // 是否正在拖动进度条
        ended: false, // 是否播放结束
        muted: false, // 是否静音
        currentTime: 0, // 当前播放时间
        duration: 0, // 总播放时长
        currentLyric: null, // 解析后歌词数据
        playerShow: false, // 控制播放器显隐
    }),
    getters: {
        playListCount: (state) => { // 播放列表歌曲总数
            return state.playList.length;
        },
        thisIndex: (state) => { // 当前播放歌曲索引
            return state.playList.findIndex((song) => song.id === state.id);
        },
        nextSong(state): Song { // 切换下一首
            const { thisIndex, playListCount } = this;
            if (thisIndex === playListCount - 1) {  // 最后一首
                return state.playList[0];
            } else {    // 切换下一首
                const nextIndex: number = thisIndex + 1;
                return state.playList[nextIndex];
            }
        },
        prevSong(state): Song { // 返回上一首
            const { thisIndex } = this;
            if (thisIndex === 0) {  // 第一首
                return state.playList[state.playList.length - 1];
            } else {    // 返回上一首
                const prevIndex: number = thisIndex - 1;
                return state.playList[prevIndex];
            }
        }
    },
    actions: {
        // 播放列表里面添加音乐
        pushPlayList(replace: boolean, ...list: Song[]) {
            if (replace) {
                this.playList = list;
                return;
            }
            list.forEach((song) => {    // 筛除重复歌曲
                if (this.playList.filter((s) => s.id == song.id).length <= 0) {
                    this.playList.push(song);
                }
            })
        },
        // 删除播放列表中某歌曲
        deleteSong(id: number) {
            this.playList.splice(
                this.playList.findIndex((s) => s.id == id),
                1
            )
        },
        // 清空播放列表
        clearPlayList() {
            this.songUrl = {} as SongUrl;
            this.url = '';
            this.id = 0;
            this.song = {} as Song;
            this.isPlaying = false;
            this.isPause = false;
            this.sliderInput = false;
            this.ended = false;
            this.muted = false;
            this.currentTime = 0;
            this.playList = [] as Song[];
            this.showPlayList = false;
            const audio = getPlayer();
            audio.stop();
            setTimeout(() => {
                this.duration = 0;
            }, 500);
        },
        // 播放
        async play(id: number) {
            console.log('play')
            if (id == this.id) return;
            this.ended = false;
            this.isPause = false;
            this.isPlaying = false;
            const data = await getSongUrl(id);
            console.log(data)
            // 筛掉会员歌曲和无版权歌曲 freeTrialInfo字段为试听时间
            if(data.url && !data.freeTrialInfo) {
                const audio = getPlayer();
                this.id = id;
                this.songDetail();
                setTimeout(() => {
                    if(!!uni.getBackgroundAudioManager) {
                        // 微信小程序后台播放 数据显示
                        audio.title = this.song.name;   // 歌名
                        audio.singer = this.song.ar[0].name;    // 歌手名
                        audio.epname = this.song.al.name;   // 专辑名
                        audio.coverImgUrl = this.song.al.picUrl;    // 封面
                    }
                    audio.src = 'https://music.163.com/song/media/outer/url?id=' + data.id + '.mp3';
                    // console.log(audio.title)
                    audio.play();
                    this.isPlaying = true;
                    this.songUrl = data;
                    this.url = data.url;
                    audio.onError((err:any) => {
                        this.id = id;
                        uni.showToast({
                            icon: "error",
                            title: "该歌曲无法播放"
                        })
                        this.isPause = true;
                        // this.deleteSong(id);
                        // this.next();
                    })
                }, 500)
            }else{
                uni.showToast({
                    icon: "error",
                    title: "该歌曲无法播放"
                })
                this.deleteSong(id);
                this.next();
            }
        },
        // 获取歌词
        async getLyric(id: number) {
            const lyricData = await getSongLyric(id);
            const lyric = JSON.parse(JSON.stringify(lyricData)).lyric;
            return lyric
        },
        // 缓存歌词
        saveLyric(currentLyric: any) {
            this.currentLyric = currentLyric;
        },
        // 播放结束
        playEnd() {
            this.isPause = true;
            console.log('播放结束');
            switch (this.loopType) {
                case 0:
                    this.next();
                    break;
                case 1:
                    this.rePlay();
                    break;
                case 2:
                    this.randomPlay();
                    break;
            }
        },
        // 获取歌曲详情
        async songDetail() {
            this.song = await getSongDetail(this.id);
            this.pushPlayList(false, this.song);
        },
        // 重新播放
        rePlay() {
            setTimeout(() => {
                console.log('replay');
                this.currentTime = 0;
                this.ended = false;
                this.isPause = false;
                this.isPlaying = true;
                const audio = getPlayer();
                audio.seek(0);
                audio.play();
            }, 1500)
        },
        // 下一曲
        next() {
            if (this.loopType === 2) {
                this.randomPlay();
            } else {
                if(this.id === this.nextSong.id) {
                    uni.showToast({
                        icon: "none",
                        title: "没有下一首"
                    })
                }else{
                    this.play(this.nextSong.id);
                }
            }
        },
        // 上一曲
        prev() {
            if(this.id === this.prevSong.id) {
                uni.showToast({
                    icon: "none",
                    title: "没有上一首"
                })
            }else{
                this.play(this.prevSong.id);
            }
        },
        // 随机播放
        randomPlay() {
            console.log('randomPlay')
            this.play(
        this.playList[Math.ceil(Math.random() * this.playList.length - 1)].id,
            )
        },
        // 播放、暂停
        togglePlay() {
            if (!this.song.id) return;
            this.isPlaying = !this.isPlaying;
            const audio = getPlayer();
            if (!this.isPlaying) {
                audio.pause();
                this.isPause = true;
            } else {
                audio.play();
                this.isPause = false;
            }
        },
        setPlay() {
            if (!this.song.id) return;
            const audio = getPlayer();
            this.isPlaying = true;
            audio.play();
            this.isPause = false;
        },
        setPause() {
            if (!this.song.id) return;
            const audio = getPlayer();
            this.isPlaying = false;
            audio.pause();
            this.isPause = true;
        },
        // 切换循环类型
        toggleLoop() {
            if (this.loopType == 2) {
                this.loopType = 0;
            } else {
                this.loopType++;
            }
        },
        // 快进
        forward(val: number) {
            const audio = getPlayer();
            audio.seek(this.currentTime + val);
        },
        // 后退
        backup(val: number) {
            const audio = getPlayer();
            if(this.currentTime < 5) {
                audio.seek(0)
            }else{
                audio.seek(this.currentTime - val);
            }
        },
        // 修改播放时间
        onSliderChange(val: number) {
            const audio = getPlayer();
            audio.seek(val);
        },
        // 定时器
        interval() {
            if (this.isPlaying && !this.sliderInput) {
                const audio = getPlayer();
                this.currentTime = parseInt(audio.currentTime.toString());
                this.duration = parseInt(audio.duration.toString());
                audio.onEnded(() => {
                    // console.log('end')
                    this.ended = true
                })
            }
        },
        // 控制播放器显隐
        setPlayerShow(val: number) {
            // val 0:显示 1:隐藏
            if (val === 0) {
                this.playerShow = true;
            } else {
                this.playerShow = false;
            }
        }
    }
})

export const useInitPlayer = () => {
    let timer: any;
    const { interval, playEnd, setPlayerShow } = usePlayerStore();
    const { ended, song } = storeToRefs(usePlayerStore());

    // 监听播放结束
    watch(ended, (ended) => {
        console.log('start')
        if (!ended) return
        console.log('end')
        playEnd()
    }),

    // 监听当前歌曲控制播放器显隐
    watch(song, (song) => {
        if (song) {
            setPlayerShow(0);
        } else {
            setPlayerShow(1);
        }
    }),

    // 启动定时器
    console.log('启动定时器');
    timer = setInterval(interval, 1000);

    // 清除定时器
    onUnmounted(() => {
        console.log('清除定时器');
        clearInterval(timer);
    })
}

App.vue中创建Audio实例初始化播放器

// App.vue
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { createPlayer } from '@/config/utils/initPlayer'
import { useInitPlayer } from '@/store/player'
onLaunch(() => {
  createPlayer()
  useInitPlayer() // 初始化播放器控件
})

现在全局播放器已经初始化完成✅

播放/暂停/切换/快进快退

由于已经通过Pinia封装了播放器的基本功能,现在直接调用即可👍

// song.vue
<!-- 播放器控制区域 -->
<view class="flex w-full justify-around items-center my-4">
    <!-- 上一首 -->
    <i class="iconfont icon-shangyishoushangyige text-2xl" @click="prev"></i>
    <!-- 快退 -->
    <i class="iconfont icon-kuaitui text-3xl" @click="backup(5)"></i>
    <!-- 播放/暂停 -->
    <view>
        <i v-if="!isPause" class="iconfont icon-zanting text-4xl" @click="togglePlay"></i>
        <i v-else class="iconfont icon-bofang text-4xl" @click="togglePlay"></i>
    </view>
    <!-- 快进 -->
    <i class="iconfont icon-kuaijin text-3xl" @click="forward(5)"></i>
    <!-- 下一首 -->
    <i class="iconfont icon-xiayigexiayishou text-2xl" @click="next"></i>
</view>
import { toRefs } from 'vue';
import { usePlayerStore } from '@/store/player';
const { song, id, isPause, togglePlay, forward, backup, next, prev } = toRefs(usePlayerStore());

实现声波进度条🔊

一些音乐APP有时会使用声波形状的进度条,但在前端项目中很少看到有人使用😅
截屏2022-12-01 下午1.53.59.png
那么就自己实现一个简易的声波进度条组件吧👇

此处进度条组件为了美观方便采用固定声波条数+固定样式,可以根据实际需求对该组件进行改进

// MusicProgressBar.vue
<view class="w-full mt-4">
    <!-- skeleton -->
    <view v-if="!duration" style="height: 62rpx" class="animate-pulse bg-gray-200"></view>
    <!-- progressBar -->
    <view v-else class="flex justify-center items-center">
        <view class="mr-4">{{ moment(currentTime * 1000).format('mm:ss') }}</view>
        <view class="flex flex-shrink-0 justify-center items-center">
            <view v-for="(item) in 34" :key="item" class="progress-item"
                :class="[currentLine < item ? 'line-' + (item) + '' : 'line-' + (item) + ' line_active' ]"
                @click="moveProgress(item)"></view>
        </view>
        <view class="ml-4">{{ moment(duration * 1000).format('mm:ss') }}</view>
    </view>
</view>
import { toRefs, computed, getCurrentInstance } from 'vue';
import { usePlayerStore } from '@/store/player';

const { currentTime, duration } = toRefs(usePlayerStore());
const { onSliderChange } = usePlayerStore();
const moment = getCurrentInstance()?.appContext.config.globalProperties.$moment;

const currentLine = computed(() => {    // 实时监听当前进度条位置
    const val = duration.value / 34;    // 获取进度条单位长度
    const nowLine = (currentTime.value / val)
    return nowLine
})

const moveProgress = (index: number) => {   // 拖动进度条改变歌曲播放进度
    // 小程序端拖拽时存在一定延迟
    const val = duration.value / 34;
    const newTime = Number((val * index).toFixed(0))
    onSliderChange(newTime)
}
.progress-item {
    width: 8rpx;
    margin: 2rpx;
    @apply bg-slate-300;
}

.line_active {
    @apply bg-blue-400
}

.line-1 {
    height: 12rpx;
}

.line-2 {
    height: 16rpx;
}

...

.line-34 {
    height: 12rpx;
}

实现效果✍️
截屏2022-12-01 下午2.00.32.png

那么到此为止,播放器的控制区域就已经全部实现了🎉🎉🎉

歌词解析

下面开始实现歌词部分,首先处理接口返回的歌词数据:
网易云音乐接口文档

[00:00.000] 作词 : 太一\n[00:01.000] 作曲 : 太一\n[00:02.000] 编曲 : 太一\n[00:03.000] 制作人 : 太一\n[00:04.858]我决定撇弃我的回忆\n[00:08.126]摸了摸人类盛典的样子\n[00:11.156]在此之前\n[00:12.857]我曾经\n[00:14.377]善\n[00:15.076]意\n[00:15.932]过\n[00:16.760]\n[00:29.596]怎么懂\n[00:31.833]我该怎么能让人懂\n[00:35.007]我只会跟耳朵相拥\n[00:38.226]天胡也救不了人的凡\n[00:41.512]涌远流动\n[00:44.321]神的眼睛该有擦镜布\n[00:47.587]残疾的心灵也很辛苦\n[00:50.755]真正摔过的流星乃真无数\n[00:56.002]\n[01:02.545]这一路磕磕绊绊走的仓促\n[01:05.135]爬上逆鳞摘下龙嘴里面含的珠\n[01:08.403]\n[01:08.781]“❀.”\n[01:34.409]\n[01:37.920]只会用这样的回答洗礼我内心的不甘\n[01:40.571]因为皎洁的事情需要挖肺掏心的呼喊\n[01:43.367]人们总是仰头看\n[01:44.510]总会莫名的忌惮\n[01:45.811]生怕触动自己的不堪\n[01:47.402]压低帽檐送世界一句\n[01:48.705]生\n[01:49.179]而\n[01:49.611]烂\n[01:50.012]漫\n[01:50.787]年轻人凭什么出头谁啊谁啊非起竿\n[01:53.971]贴上个标签接受才比较比较简单\n[01:57.210]没有人会这样描绘描绘音乐的图案\n[02:00.168]他要是不死难道胸口画了剜\n[02:03.218]\n[02:17.550]怎样算漂亮\n[02:20.746]不想再仰望\n[02:23.943]这个梨不让\n[02:28.126]这才是\n[02:28.791]我模样\n[02:30.399]月泛光\n[02:31.976]赤裸胸膛还有跌宕\n[02:35.174]现实催促激素般的生长\n[02:38.363]心跳在消亡\n[02:39.960]脉搏在癫狂\n[02:41.588]我模样\n[02:43.177]月泛光\n[02:44.752]踉跄也要大旗飘扬\n[02:47.951]诗意都变的似笑非笑的堂皇\n[02:49.818]谱写的变始料未料的苍茫\n[02:51.368]人生没下一场\n[02:52.583]可我不活那下一趟\n[03:01.452]\n[03:05.568]“❀.”\n[03:31.195]\n[03:31.516] 和声 : 太一\n[03:31.837] 器乐 : 太一\n[03:32.158] 录音 : 太一\n[03:32.479] 混音 : 太一\n[03:32.800] 母带 : 太一\n

截屏2022-12-01 下午2.12.04.png

接口返回的lyric数据为string类型,显然是不能直接使用的,需要我们手动转化成数组形式

/**
 *  lyric2Array.ts
 *  将接口返回的lyric数据转化成数组格式
 */ 

export interface ILyric {
    time: number,
    lyric: string,
    uid: number
}

interface IReturnLyric {
    lyric: ILyric[],    // 歌词
    tlyric?: ILyric[]   // 翻译歌词
}

export const formatMusicLyrics = (lyric?: string, tlyric?: string):IReturnLyric => {
    if (lyric === '') {
      return { lyric: [{ time: 0, lyric: '暂无歌词', uid: 520520 }] }
    }
    const lyricObjArr: ILyric[] = [] // 最终返回的歌词数组
  
    // 将歌曲字符串变成数组,数组每一项就是当前歌词信息
    const lineLyric:any = lyric?.split(/\n/)
  
    // 匹配中括号里正则的
    const regTime = /\d{2}:\d{2}.\d{2,3}/
  
    // 循环遍历歌曲数组
    for (let i = 0; i < lineLyric?.length; i++) {
      if (lineLyric[i] === '') continue
      const time:number = formatLyricTime(lineLyric[i].match(regTime)[0])
  
      if (lineLyric[i].split(']')[1] !== '') {
        lyricObjArr.push({
          time: time,
          lyric: lineLyric[i].split(']')[1],
          uid: parseInt(Math.random().toString().slice(-6)) // 生成随机uid
        })
      }
    }
    console.log(lyricObjArr)
  
    return {
      lyric: lyricObjArr
    }
  }
  
  const formatLyricTime = (time: string) => {   // 格式化时间
    const regMin = /.*:/
    const regSec = /:.*\./
    const regMs = /\./
  
    const min = parseInt((time.match(regMin) as any)[0].slice(0, 2))
    let sec = parseInt((time.match(regSec) as any)[0].slice(1, 3))
    const ms = time.slice((time.match(regMs) as any).index + 1, (time.match(regMs) as any).index + 3)
    if (min !== 0) {
      sec += min * 60
    }
    return Number(sec + '.' + ms)
}
// song.vue
import { formatMusicLyrics } from '@/config/utils/lyric2Array';
const lyricData = ref<any>([]);

watch(() => id.value, (newVal, oldVal) => { // 歌曲歌词同步切换
    console.log(newVal, oldVal)
    if(newVal !== oldVal) {
        nextTick(() => {
            getLyric(newVal).then((res) => {
                lyricData.value = formatMusicLyrics(res)
            })
        })
    }
})

getLyric(id.value).then((res) => {  // 获取歌词
    lyricData.value = formatMusicLyrics(res)
})

转换完成后的Lyric数据:
截屏2022-12-01 下午2.21.44.png

歌词滚动

有了数据后就可以对数据进行处理了😃
新建一个Lyric组件处理歌词滚动歌词跳转的相关代码,动态获取组件高度

<Lyric :scrollHeight="scrollH" :lyricData="lyricData" />
<scroll-view id="lyric" scroll-y :scroll-top="scrollH" :style="{ 'height': scrollHeight + 'px'}">
    <view v-for="(item, index) in lyricData.lyric" :key="index"
    class="flex justify-center mx-8 text-center py-2 lyric-item"
    :class="lyricIndex === index ? 'text-blue-300 opacity-100 scale-110' : 'opacity-20'"
    @click="lyricJump(index)">
        {{item.lyric}}
    </view>
</scroll-view>
import { ref, toRefs, watch, nextTick, getCurrentInstance } from 'vue';
import { usePlayerStore } from '@/store/player'
const { currentTime, id } = toRefs(usePlayerStore())
const { onSliderChange } = usePlayerStore();
...
const loading = ref<boolean>(true)
const lyricIndex = ref<number>(0)   // 当前高亮歌词索引
let scrollH = ref<number>(0) // 歌词居中显示需要滚动的高度
let lyricH: number = 0 // 歌词当前的滚动高度
let flag: boolean = true // 判断当前高亮索引是否已经超过了歌词数组的长度
const currentInstance = getCurrentInstance();   // vue3绑定this

uni-app 微信小程序 通过uni.createSelectorQuery()获取节点
H5端小程序端歌词滚动存在速度差,暂时没找到原因,需要对当前歌词高亮索引进行条件编译

// 核心方法 handleLyricTransform 计算当前歌词滚动高度实现高亮歌词居中显示
const handleLyricTransform = (currentTime: number) => { // 实现歌词同步滚动
    nextTick(() => {    // 获取所有lyric-item的节点数组
        loading.value = false
        const curIdx = props.lyricData.lyric.findIndex((item:any) => {  // 获取当前索引
            return (currentTime <= item.time)
        })
        // const item = props.lyricData.lyric[curIdx - 1] // 获取当前歌词信息
        // 获取lyric节点
        const LyricRef = uni.createSelectorQuery().in(currentInstance).select("#lyric");
        LyricRef.boundingClientRect((res) => {
            if(res) {
                // 获取lyric高度的1/2,用于实现自动居中定位
                const midLyricViewH = ((res as any).height / 2)
                if(flag) {
                    // 实时获取最新Dom
                    const lyricRef = uni.createSelectorQuery().in(currentInstance).selectAll(".lyric-item");
                    lyricRef.boundingClientRect((res) => {
                        if(res) {
                            // console.log(res)
                            // 获取当前播放歌词对应索引 H5端 curIdx - 1 | 微信小程序端 curIdx
                            // #ifdef MP-WEIXIN
                            lyricIndex.value = curIdx;  // 获得高亮索引
                            // #endif
                            // #ifndef MP-WEIXIN
                            lyricIndex.value = curIdx - 1;  // 获得高亮索引
                            // #endif
                            if (lyricIndex.value >= (res as Array<any>).length) {
                                flag = false
                                return
                            }
                            lyricH = ((res as Array<any>)[curIdx].top - (res as Array<any>)[0].top)
                            if(midLyricViewH > 0 && lyricH > midLyricViewH) {
                                scrollH.value = lyricH - midLyricViewH
                            }
                        }
                    }).exec()
                }
            }
        }).exec()
    })
}

这里的重点主要是handleLyricTransform的调用时机,由于需要根据歌曲播放进度实时改变,因此需要监听currentTime变化

// 监听歌曲播放进程
watch(() => currentTime.value, (val) => {
    // console.log(val)
    handleLyricTransform(val)
})

Dec-01-2022 14-38-53.gif

歌词跳转

最后实现歌词跳转,通过lyricJump方法调用usePlayerStore的onSliderChange

const lyricJump = (index: number) => {
    onSliderChange(Number(props.lyricData.lyric[index].time.toFixed(0)))
    lyricIndex.value = index
}

Dec-01-2022 14-41-03.gif

最后进行一些边界处理,刷新页面防止播放数据异常直接返回首页切换歌曲重置歌词状态添加切换动画等等…具体代码就不在这里演示了,可以参考下方项目源码👇

完整代码

这样就完整的实现了一个功能完善的微信小程序音乐播放器,快来一起试试吧💃💃💃

写在最后

  • 文章内容是基于个人开源项目的原创内容,如需要转载请备注原文链接~
  • 👍 如果对您有帮助,您的点赞是我创作的动力
© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容