一、大致效果
二、使用步骤
1.歌词详情页代码块
javascript
<template>
<view class="play">
<view class="play_centent" :style="{ 'background-image': 'url(' + playInfo.siPic + ')' }">
<div class="cover-mask" style="opacity: 0.9"></div>
</view>
<view style="position: absolute;z-index: 999;width: 100%;height: 100%;">
<view class="headerPl">
<view class="" @click="goone">
<u-icon name="arrow-left" color="white" size="40" style="margin-top: 4px;"></u-icon>
</view>
<view class="header_cen">
{{playInfo.name}}
</view>
</view>
<view class="author">{{
playInfo.nickName
}}</view>
<view class="img-container" :style="{ transform: 'translate(-50%, -50%) rotate(' + rotate + 'deg)' }"
v-show="!lyricShow">
<image :src="playInfo.siPic" class="authorImg" @click="getLyric"></image>
</view>
<scroll-view
scroll-y
v-if="lyricShow"
@click="lyricShow = false"
:scroll-top="scrollTop"
class="lyric-container"
:style="{ top: CustomBar + 35 + 'px' }"
>
<view v-if="lyricList.length > 0">
<view
class="lyric-item"
:class="{ active: index == currentLyricIndex }"
v-for="(item, index) in lyricList"
:key="index"
style="text-align: center"
>
{{ item.content }}
</view>
</view>
<p v-else class="noLyric">暂无歌词</p>
</scroll-view>
<view class="bottom-control">
<view class="progress">
<view class="audio-number">{{ playDetailInfo.current }}</view>
<!-- {{playDetailInfo.current_value}}
{{playDetailInfo.duration_value}} -->
<slider class="audio-slider" activeColor="rgb(248, 78, 81)" block-size="8"
:value="playDetailInfo.current_value" :max="playDetailInfo.duration_value"
@change="handleChange"></slider>
<view class="audio-number">{{ playDetailInfo.duration }}</view>
</view>
<view class="iconList flex">
<view v-if="!playComm.xunhuan" @click="playComm.xunhuan = !playComm.xunhuan">
<svg t="1730535056260" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1872" width="30" height="30"><path d="M342.69 297.61c-22.96 0-41.3-18.63-41.3-41.32 0-22.67 18.34-41.03 41.3-41.03h457.64c61.35 0 117.2 25.32 157.94 65.49 40.76 40.71 65.73 96.88 65.73 158.23v146.07c0 61.65-24.97 117.52-65.73 158.23-40.73 40.17-96.58 65.46-157.94 65.46H342.69c-61.68 0-117.52-25.29-158.23-65.46-36.09-36.62-60.52-85.52-64.6-139.06h-95.4C11.09 604.22 0 592.88 0 579.52c0-6.71 2.93-13.1 7.57-17.16l67.21-67.82 67.8-67.48c9.29-9.59 24.73-9.59 34.32 0h0.56l67.5 67.48 67.21 67.82c9.58 9.29 9.58 25 0 34.91-5.24 4.65-12.22 7.57-19.23 6.95h-91.02c4.05 31.15 18.9 59.66 40.11 80.9h0.3c25.88 25.59 61.41 41.56 100.37 41.56h457.64c38.66 0 74.16-15.98 99.75-41.56C925.97 659.53 942 624.01 942 585.05V438.98c0-38.96-16.03-74.46-41.91-100.05-25.59-25.91-61.09-41.32-99.75-41.32H342.69zM187.63 555.35h47.74l-25.32-25.88v-0.27l-50.32-50.05-50.34 50.05v0.27L83.8 555.35h103.83z" fill="#ffffff" p-id="1873"></path></svg>
</view>
<text v-if="playComm.xunhuan" class="iconfont" :class="'icon-unlike'" @click="playComm.xunhuan = !playComm.xunhuan"></text>
<text class="iconfont icon-play-left" style="margin-left:35px" @click="handleChangePlay(-1)"></text>
<text class="iconfont" :class="!playComm.paused ? 'icon-play' : 'icon-pause'"
style="font-size: 90rpx; margin: 0 35px" @click="playMusic"></text>
<text class="iconfont icon-play-right" @click="handleChangePlay(1)"></text>
<text class="iconfont icon-liebiao" style="margin-left: 65rpx; font-size: 56rpx"
@click.stop="modelShow = true"></text>
</view>
</view>
</view>
<playListUtl v-if="modelShow" :modelShow="modelShow" @close="close"
@nextSong="(item,index)=>nextSong(item,index)" @del="(item,index)=>del(item,index)">
</playListUtl>
</view>
</template>
<script setup>
import {
reactive,
ref,
} from 'vue'
import {
useAppStore
} from '@/stores/plear'
import {
onLoad,
onPageScroll,
onShow
} from "@dcloudio/uni-app";
import playListUtl from "@/components/playList.vue";
import {
useRouter,
useRoute
} from 'uniapp-router-next'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const innerAudioContext = appStore.$state.context
const playInfo = appStore.$state.sole
const playComm = appStore.$state
const scrollTop = ref(0)
const lyricList = ref([])
const CustomBar = ref(100)
const lyricShow = ref(false)
const currentLyricIndex = ref(false)
const modelShow = ref(false)
const playDetailInfo = ref({
current: "00.00",
current_value: "0",
duration_value: "0",
duration: "00.00"
})
onShow(()=>{
initLyric()
})
// 追踪歌词
const initLyric = ()=> {
if (lyricList.length == 0) return;
let timeStamp = playDetailInfo.value.current;
currentLyricIndex.value = lyricList.value.findIndex((item, index) => {
return item.time < timeStamp && lyricList.value[index + 1]
? lyricList.value[index + 1].time > timeStamp
: true;
});
scrollTop.value = currentLyricIndex.value * 36;
}
// 歌词
const getLyric = (bool)=> {
console.log(1245)
lyricShow.value = true;
let data = [];
// const id = this.playInfo.id;
// const res = await this.$api.getLyric({ id });
data = (playInfo.lyrics || "").split("\n");
let timeReg = /^\[.*\]/;
let json = [];
data.forEach((item) => {
if (item.match(timeReg)) {
let time = item.match(timeReg)[0].substr(1, 8);
let minute = time.substr(0, 2);
let second = time.substr(3, 2);
let ms = time.substr(6, 2);
json.push({
time,
ms:
parseInt(minute) * 60 * 1000 +
parseInt(second) * 1000 +
parseInt(ms) * 10,
content: item.substr(11),
});
}
});
lyricList.value = json;
console.log(json)
}
// f返回
const goone = ()=>{
console.log(123)
// router.go(-1)
uni.navigateBack();
}
const handleChange = (val) => {
console.log(val)
innerAudioContext.seek(val.detail.value)
}
// 上一首下一首
const handleChangePlay = (val) => {
// if (val == 1) {
playComm.tabDate.forEach((el,index) => {
console.log(el.sid , playInfo.sid)
if (el.sid == playInfo.sid) {
if (playComm.tabDate[ val == 1 ? index + 1 : index -1]) {
// innerAudioContext.src = el.souce;
nextSong(playComm.tabDate[val ==1 ? index + 1 : index -1],index)
throw Error();
}else{
nextSong(playComm.tabDate[val ==1 ? 0 : playComm.tabDate.length - 1],index)
throw Error();
}
}
})
// }
}
// 播放暂停
const playMusic = () => {
console.log(123)
innerAudioContext.src = playInfo.souce;
// innerAudioContext.seek(this.currenttime)
innerAudioContext.volume = 0.5
playComm.paused = !playComm.paused
if (!playComm.paused) {
// seek
innerAudioContext.seek(playComm.Time)
innerAudioContext.play()
} else {
innerAudioContext.pause()
}
}
innerAudioContext.onTimeUpdate(() => {
// 获取当前播放的总时长,单位:秒
const currentTime = innerAudioContext.currentTime;
// console.log(Time.value > 0)
if(currentTime > 0){
playComm.Time = currentTime
playDetailInfo.value.current = convertSecondsToMinutesAndSeconds(playComm.Time)
playDetailInfo.value.current_value = playComm.Time
}
initLyric()
// console.log('当前播放时间:', currentTime);
});
// 防止为播放进来
onLoad(() => {
if (innerAudioContext.src == "") {
innerAudioContext.src = playInfo.souce;
}
setTimeout(() => {
if (innerAudioContext.duration > 0) {
playDetailInfo.value.duration = convertSecondsToMinutesAndSeconds(innerAudioContext
.duration)
playDetailInfo.value.duration_value = innerAudioContext.duration
}
if (playComm.Time > 0) {
playDetailInfo.value.current = convertSecondsToMinutesAndSeconds(playComm.Time)
playDetailInfo.value.current_value = playComm.Time
}
}, 500)
// playDetailInfo.duration = convertSecondsToMinutesAndSeconds(convertSecondsToMinutesAndSeconds)
})
// 转换秒数
const convertSecondsToMinutesAndSeconds = (seconds) => {
// 取整秒数,因为分钟和秒通常不包含小数
const intSeconds = Math.floor(seconds);
// 计算分钟数
const minutes = Math.floor(intSeconds / 60);
// 计算剩余的秒数
const secs = intSeconds % 60;
// 返回格式化后的时间字符串,确保分钟和秒都是两位数
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
innerAudioContext.onCanplay((el) => {
// 官方bug 解决方法
console.log(el)
});
// 播放完成
innerAudioContext.onEnded(() => {
console.log(123456)
// innerAudioContext.loop = true
console.log(playComm.xunhuan)
if (!playComm.xunhuan) {
handleChangePlay(1)
}else{
const X = getRandomItem(playComm.tabDate)
console.log(X)
nextSong(X)
}
})
// 随机
const getRandomItem = (array)=> {
// 生成一个从 0 到 array.length - 1 的随机索引
const randomIndex = Math.floor(Math.random() * array.length);
// 返回数组中该索引对应的元素
return array[randomIndex];
}
// 列表
const del = (item, index) => {
playComm.tabDate = playComm.tabDate.filter(el => el.sid != item.sid)
if (item.sid == playInfo.sid) {
if (playComm.tabDate.length > 0) {
nextSong(playComm.tabDate[0], index)
} else {
playComm.Time = 0
playComm.paused = true
innerAudioContext.stop()
}
}
}
const nextSong = (item, index) => {
console.log(123)
playComm.Time = 0
playComm.paused = true
playInfo.souce = item.souce
playInfo.lyrics = item.lyrics
playInfo.sid = item.sid
playInfo.nickName = item.nickName
playInfo.name = item.name
playInfo.siPic = item.siPic
setTimeout(()=>{
playDetailInfo.value.duration = convertSecondsToMinutesAndSeconds(innerAudioContext
.duration)
playDetailInfo.value.duration_value = innerAudioContext.duration
},500)
playMusic()
}
const close = ()=>{
console.log(12)
modelShow.value = false
}
</script>
<style lang="scss">
.bottom-control {
position: absolute;
bottom: 10%;
left: 10px;
right: 10px;
.progress {
width: 100%;
display: flex;
align-items: center;
.audio-number {
width: 120upx;
font-size: 24upx;
line-height: 1;
color: #fff;
text-align: center;
}
.audio-slider {
flex: 1;
margin: 0;
}
}
.iconList {
justify-content: center;
align-items: center;
margin-top: 26rpx;
.iconfont {
color: #fff;
font-size: 48rpx;
}
}
}
.lyric-container {
position: absolute;
bottom: calc(10% + 100px);
.lyric-item {
color: #e1d7f0;
height: 40px;
line-height: 40px;
&.active {
color: rgb(248, 78, 81);
}
}
.noLyric {
position: absolute;
top: 50%;
left: 50%;
color: #fff;
transform: translate(-50%, -50%);
}
}
.img-container {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
width: 450rpx;
height: 450rpx;
background: url(../../static/images/musicImg.png) no-repeat;
background-size: 100% 100%;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: all 1s linear;
.authorImg {
border-radius: 50%;
width: 315rpx;
height: 315rpx;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.author {
position: absolute;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.9);
font-size: 28rpx;
}
.header_cen {
font-size: 20px;
color: white;
position: absolute;
// width: 95%;
left: 50%;
transform: translateX(-50%);
top: 19rpx;
text-align: center;
}
.headerPl {
// display: flex;
padding: 10px;
width: 100%;
}
.play {
height: 100vh;
width: 100%;
border: 1px solid red;
}
.play_centent {
height: 100vh;
width: 100%;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 0;
background-size: cover;
background-position: center;
// filter: blur(8px);
&:after {
content: "";
position: absolute;
width: 130%;
height: 130%;
left: 0;
top: 0;
z-index: 1;
filter: blur(15px);
transform: translate(-3rem, -3rem);
background: inherit;
background-size: 100% 100%;
}
}
.cover-mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 3;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
}
</style>
2.@/stores/plear文件
组件使用的是pinia不是vuex
javascript
import { defineStore } from 'pinia'
interface AppSate {
// config: Record<string, any>
}
export const useAppStore = defineStore({
id: 'plear',
state: (): AppSate => (
{
tabDate: [
{
siPic: "http://p1.music.126.net/9bVOooAY6U6EJLzpv1Fikw==/109951169682871673.jpg?param=300y300",
sid: "197444381412",
name: "我记得456",
nickName: "赵雷",
lyrics:"",
souce: "http://m801.music.126.net/20241102095804/1d592017727f26c48e713c1308fa99a8/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/17718439149/44a7/5592/7271/b58b2979b640d886d829e1c6647f8d8a.mp3?vuutv=hJa0vveoJbaqH+QeXIhk6FsOkfY6HxkPT9oAeYVt3YYrrWwUNNeDA44/GAtQx1LPliwKogEkdEjUE6QoxQzp9vtIbDY4Seyvf54daEHmgjA="
},
{
siPic: "http://p1.music.126.net/9bVOooAY6U6EJLzpv1Fikw==/109951169682871673.jpg?param=300y300",
sid: "1974443814",
name: "心墙",
lyrics:"[00:00.000] 作词 : 姚若龙\n[00:00.177] 作曲 : 林俊杰\n[00:00.354] 编曲 : 陈炯顺\n[00:00.531] 制作人 : 吴剑泓/陈炯顺\n[00:00.708]我学着不去担心的太远\n[00:03.894]不计划太多反而能勇敢冒险\n[00:07.395]丰富地过每一天\n[00:09.395]快乐地看每一天\n[00:15.145]Wooh~ 第一次遇见阴天遮住你侧脸\n[00:18.644]有什么故事好想了解\n[00:22.395]我感觉 我懂你的特别\n[00:28.895]你的心有一道墙\n[00:32.645]但我发现一扇窗\n[00:37.395]偶尔透出一丝暖暖的微光\n[00:43.896]就算你有一道墙\n[00:47.646]我的爱会攀上窗台盛放\n[00:51.895]打开窗你会看到 悲伤融化\n[01:15.147]我学着不去担心的太远\n[01:18.897]不计划太多反而能勇敢冒险\n[01:22.397]丰富地过每一天\n[01:24.397]快乐地看每一天\n[01:29.898]Wooh~ 第一次遇见阴天遮住你侧脸\n[01:33.647]有什么故事好想了解\n[01:37.397]我感觉 我懂你的特别\n[01:43.898]你的心有一道墙\n[01:47.648]但我发现一扇窗\n[01:52.398]偶尔透出一丝暖暖的微光\n[01:58.898]就算你有一道墙\n[02:02.648]我的爱会攀上窗台盛放\n[02:06.898]打开窗你会看到 悲伤融化\n[02:14.149]你的心有一道墙\n[02:17.649]但我发现一扇窗\n[02:23.106]偶尔透出一丝暖暖的微光\n[02:29.106]就算你有一道墙\n[02:32.606]我的爱会攀上窗台盛放\n[02:36.856]打开窗你会看到 悲伤融化\n[02:44.108]你会闻到幸福晴朗的芬芳\n",
nickName: "MD李",
souce: "http://m801.music.126.net/20241102175101/34d7b7449b1a0799cc14d243db6c379e/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/8193344258/f791/6ce5/1393/89f955cf0eab540f91bed2765ce20ca9.mp3?vuutv=KxvXvGxO3vcVjhvxpRGtSLeAvFSS0UwqmyxqZ+ebfb5oQoZ9Yb5GUi3KAXNoSxdEEOrPELyZ3f+5rnNTUCWe03y0IsEmgb/hXvDq4wPTdMI="
}
],
paused: true,
Time: 0,
xunhuan: false,
context: uni.createInnerAudioContext(),
sole: {
siPic: "http://p1.music.126.net/9bVOooAY6U6EJLzpv1Fikw==/109951169682871673.jpg?param=300y300",
sid: "1974443814",
name: "心墙",
nickName: "MD李",
souce: "http://slm54lcbc.hn-bkt.clouddn.com/uploads/music/20241028/20241028225914096694612.mp3?e=1730137094&token=VffSr6dlaO_zmkIeZSULAGAgMk8FgUiatiMkF0C1:edRJLwaIuGkdf1tE22fhMYfI92I=",
lyrics: "[00:00.000] 作词 : 黄家驹\n[00:01.000] 作曲 : 黄家驹\n[00:02.000] 编曲 : Beyond\n[00:03.000] 制作人 : Beyond/Gordon O'Yang\n[00:06.000]\n[00:28.650]钟声响起归家的讯号\n[00:33.036]在他生命里\n[00:36.281]仿佛带点唏嘘\n[00:41.564]黑色肌肤给他的意义\n[00:46.038]是一生奉献 肤色斗争中\n[00:54.543]年月把拥有变做失去\n[01:01.056]疲倦的双眼带着期望\n[01:07.551]今天只有残留的躯壳\n[01:11.162]迎接光辉岁月\n[01:14.391]风雨中抱紧自由\n[01:20.515]一生经过彷徨的挣扎\n[01:24.167]自信可改变未来\n[01:27.343]问谁又能做到\n[01:31.091]\n[01:43.174]可否不分肤色的界限\n[01:47.643]愿这土地里\n[01:50.774]不分你我高低\n[01:56.103]缤纷色彩闪出的美丽\n[02:00.572]是因它没有\n[02:03.764]分开每种色彩\n[02:09.060]年月把拥有变做失去\n[02:15.509]疲倦的双眼带着期望\n[02:22.001]今天只有残留的躯壳\n[02:25.740]迎接光辉岁月\n[02:28.901]风雨中抱紧自由\n[02:34.992]一生经过彷徨的挣扎\n[02:38.655]自信可改变未来\n[02:41.900]问谁又能做到\n[02:45.698]\n[03:23.643]今天只有残留的躯壳\n[03:27.339]迎接光辉岁月\n[03:30.561]风雨中抱紧自由\n[03:36.626]一生经过彷徨的挣扎\n[03:40.315]自信可改变未来\n[03:43.493]问谁又能做到\n[03:48.026]Woo\n[03:50.494]\n[03:55.305]Ah\n[03:57.239]\n[03:59.359]今天只有残留的躯壳\n[04:02.934]迎接光辉岁月\n[04:06.174]风雨中抱紧自由\n[04:12.267]一生经过彷徨的挣扎\n[04:15.855]自信可改变未来\n[04:19.047]问谁又能做到\n[04:23.624]Woo\n[04:26.040]\n[04:30.932]Ah\n[04:32.610]\n[04:35.027]今天只有残留的躯壳\n[04:38.580]迎接光辉岁月\n[04:41.808]风雨中抱紧自由\n[04:47.928]一生经过彷徨的挣扎\n[04:51.555]自信可改变未来\n[04:53.560] Synth Programming : Gordon O'Yang / 叶世荣\n[04:54.560] Mixed by Philip Kwok"
}
}),
getters: {
},
actions: {
}
})
3.@/components/playList.vue
javascript
<template>
<u-popup v-model="props.modelShow" mode="bottom" length="55%" @close="close" border-radius="14">
<view style="padding: 20px 0;height: 100%;">
<view class="cu-dialog play-list-dialog" style="height: 10%;">
<view style="text-align: center;">
播放列表
<text class="light-text">(共{{playComm.tabDate.length}}首)</text>
</view>
</view>
<view style="height: 90%;overflow: auto;">
<view v-for="(item,index) in playComm.tabDate" :key="item.sid" :class="item.sid == playInfo.sid ? 'prolist' : 'fixed-container'">
<view @click="nextSong(item,index)" class="cu-avatar playImage round" :style="'background-image:url(' + item.siPic + ')'"></view>
<view @click="nextSong(item,index)" class="play-center" style="width: 70%;max-width: 70%;">
<view class="music-name" style="font-size: 14px;">{{ item.name }}</view>
<view class="music-author" style="font-size: 12px;">{{ item.nickName }}</view>
</view>
<view style="line-height: 60px;">
<u-icon name="trash-fill" color="red" size="40" style="margin-top: 20px;" @click.top="del(item,index)"></u-icon>
</view>
</view>
</view>
</view>
</u-popup>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useAppStore } from '@/stores/plear'
import { useRouter, useRoute } from 'uniapp-router-next'
import { onLoad, onPageScroll } from "@dcloudio/uni-app";
const emit = defineEmits(["'close'","del","nextSong"]);
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const innerAudioContext = appStore.$state.context
const close = ()=>{
emit("close",false)
}
const nextSong = (item,index)=>{
emit("nextSong",item,index)
}
const del = (item,index)=>{
emit("del",item,index)
}
const props = defineProps({
modelShow: {
type: Boolean,
default: false
}
})
onLoad(()=>{
console.log(1245)
})
// appStore.$state.nickName = 100
// console.log(appStore.$state)
// const cardStyle = {
// background: 'linear-gradient(yellow, pink)'
// }
const modelShow = ref(false)
const playInfo = appStore.$state.sole
const playComm = appStore.$state
</script>
<style scoped lang="scss">
.prolist{
display: flex;
background-image: linear-gradient(to right, rgba(247, 73, 79, 0.1), rgba(247, 73, 79, 0.05));
}
.prolistetde{
display: flex;
}
.light-text{
color: gray;
font-size: 12px;
}
.fixed-container {
display: flex;
}
.play-right {
width: 92px;
display: flex;
justify-content: space-between;
align-items: center;
.play-list {
font-size: 30px;
margin-right: 12px;
}
}
.play-center {
flex: auto;
display: flex;
flex-wrap: wrap;
align-content: center;
max-width: calc(100% - 165px);
.music-name {
color: #000;
font-size: 32rpx;
margin-bottom: 4px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.music-author {
font-size: 24rpx;
color: rgba(0, 0, 0, 0.5);
width: 100%;
}
}
.inbottom {
width: 100%;
position: fixed;
bottom: 90rpx;
height: 60px;
box-shadow: 0 1px 2px #001500;
// line-height: 50px;
padding-top: 10rpx;
background: white;
// border: 1px solid red;
}
.play-right {
display: flex;
}
.playImage {
width: 80rpx;
height: 80rpx;
margin: auto 15px auto 10px;
border-radius: 50%;
}
</style>