uniapp imitates NetEase cloud music project (publishing applets, H5 and Android apps)

The project was made by watching the video of Teacher Ghost of Qianfeng Education. I processed the background on the basis of the teacher and made the pole moveable to make the whole interface more reasonable.

Transmission Gate: https://www.bilibili.com/video/BV1VM4y1g7eg?p=1

I have compiled it into WeChat applet, H5, and Android App, all of which are in my warehouse , and there are also teachers' information

Creation is not easy, please don't be stingy with your star

Let’s take a look at the H5 demo first (it’s almost the same as the Netease Cloud applet version): H5 demo address

demo

It can be seen that there are still some compatibility issues, and you may need to adjust it on the small program side, most of the functions are the same

Applet demo

App also has some compatibility issues, but the development efficiency is here, and it can be adjusted later

1. Environment preparation

The environment required for the project:

  • Netease cloud music interface document (unofficial): https://neteasecloudmusicapi.vercel.app/#/
  • Music interface backend address: https://github.com/Binaryify/NeteaseCloudMusicApi
  • Download HBuildX and WeChat Mini Program Development Assistant by yourself and register a mini program developer account

1.1 Install the backend locally

First we need to clone the backend code:

git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git

image-20220420170805104

Install the dependent modules required by the project (cmd enters the project)

cnpm i

After installation, you can start

npm start

Then you can access the backend, the default port is 3000

image-20220420172313957

1.2 docker installation backend

I installed it once on the server with docker

Pull the image first:

docker pull binaryify/netease_cloud_music_api

image-20220420172534475

start up:

docker run -d -p 3000:3000 --name netease_cloud_music_api    binaryify/netease_cloud_music_api

The interface docs here have some hints:

Note: When running in docker, since request is used to send requests, several proxy-related environment variables (listed below) will be checked. These environment variables will affect the proxy of request. For details, please refer to the documentation of request . If the agent pointed to by these environment variables is not available, it will cause an error, so you must pay attention to these environment variables when using docker. However, if you add the proxy parameter to the query, the environment variable will be overwritten, just will use the proxy you provided via the proxy parameter.

I don't care about it here, please refer to the official website for details (you can private message me to ask for my interface)

2. Home page

Select the project to create uniapp and configure your applet AppID

image-20220420180158647

2.1 Custom navigationStyle

We can set navigationStyle to empty and customize

"navigationStyle":"custom"

image-20220420183029154

2.2 Encapsulating the navigationStyle component

Since navigationStyle is used in many interfaces, we can encapsulate it into a component

image-20220420194337063

The settings here are the same as vue, now index.vue uses custom components, and then passes the value to the packaged components

<template>
	<view>
		<musichead title="Eureka-Music"></musichead>
	</view>
</template>

The component props:['title']receives with:

<template>
	<view>
		{
   
   {title}}
	</view>
</template>
<script>
	export default {
		name:"musichead",
		data() {
			return {
				
			};
		},
		props:['title']
	}
</script>

2.3 Basic layout of homepage

<template>
	<view class="index">
		<musichead title="Eureka-Music" :icon="false"></musichead>
		<view class="container">
			<scroll-view scroll-y="true">
				<view class="index-search">
					<text class="iconfont iconsearch"></text>
					<input type="text" placeholder="搜索歌曲" />
				</view>
				<view class="index-list">
					<view class="index-list-item">
						<view class="index-list-img">
							<image src="../../static/wangyiyunyinyue.png" mode=""></image>
							<text>每天更新</text>
						</view>
						<view class="index-list-text">
							<view>1. 音乐一</view>
							<view>1. 音乐一</view>
							<view>1. 音乐一</view>
						</view>
					</view>
				</view>
			</scroll-view>
		</view>
	</view>
</template>

<script>
	import '@/common/iconfont.css'
	export default {
		data() {
			return {
				title: 'Hello'
			}
		},
		onLoad() {

		},
		methods: {

		}
	}
</script>

<style>
	.index {}

	.index-search {
		display: flex;
		/* 上下居中 */
		align-items: center;
		height: 70rpx;
		margin: 70rpx 30rpx 30rpx 30rpx;
		background: #f7f7f7;
		border-radius: 50rpx;
	}

	.index-search text {
		font-size: 26rpx;
		margin-right: 26rpx;
		margin-left: 28rpx;
	}

	.index-search input {
		font-size: 28rpx;
		flex: 1;
	}

	.index-list {
		margin: 0 30rpx;
	}

	.index-list-item {
		display: flex;
		margin-bottom: 34rpx;
	}

	.index-list-img {
		width: 212rpx;
		height: 212rpx;
		position: relative;
		border-radius: 30rpx;
		overflow: hidden;
		margin-right: 22rpx;
	}

	.index-list-img image {
		width: 100%;
		height: 100%;
	}

	.index-list-img text {
		position: absolute;
		left: 12rpx;
		bottom: 16rpx;
		color: white;
		font-size: 20rpx;
	}

	.index-list-text {
		font-size: 24rpx;
		line-height: 66rpx;
	}
</style>

Write the style here first:

image-20220420210322785

2.4 Interface call & data rendering

Next we call our interface

Search the leaderboard in the official document to find the calling method corresponding to the interface

所有榜单内容摘要
说明 : 调用此接口,可获取所有榜单内容摘要

接口地址 : /toplist/detail

调用例子 : /toplist/detail

We encapsulate all the requests and create two files in the common directory config.jsandapi.js

config.js

export const baseUrl = 'http://localhost:3000/';

api.js

import {
    
     baseUrl } from './config.js';
/**
 * 歌曲榜单
 */
export function topList(){
    
    
	// 只需要前四个榜单
	var listIds = ['3' , '0' , '2' , '1' ];
	return new Promise(function(resolve,reject){
    
    
		uni.request({
    
    
			url: `${
      
      baseUrl}/toplist/detail`,
			method: 'GET',
			data: {
    
    },
			success: res => {
    
    
				console.log(res);
				let result = res.data.list;
				result.length = 4;
				for(let i=0;i<result.length;i++){
    
    
					result[i].listId = listIds[i];
				}
				resolve(result);
			},
			fail: (err) => {
    
    
				console.log(err);
			},
			complete: () => {
    
    }
		});
	});
}

Render data on the front page

<view class="index-list">
    <view class="index-list-item" v-for="(item,index) in topList" :key="id">
        <view class="index-list-img">
            <image :src="item.coverImgUrl" mode=""></image>
            <text>{
   
   {item.updateFrequency}}</text>
        </view>
        <view class="index-list-text">
            <view v-for="(musicItem,index) in item.tracks" :key="index">
                {
   
   {index+1}}.{
   
   {musicItem.first}}.{
   
   {musicItem.second}}
            </view>
        </view>
    </view>
</view>

After the rendering is complete, there is an interface on the front end

image-20220420213638633

3. List details page

Next, we will make the details page in the home page

First we create the corresponding file

image-20220420213902742

The route will be automatically generated in pages.json, remember to change the head to our custom

"navigationStyle":"custom",

Next, let's set the compilation mode so that we don't need to click into this interface every time

3.1 Details page layout

Make a simple layout first, add a background image

<template>
	<view>
		<musichead title="歌单" :icon="true" color="white"></musichead>
		<!-- 背景图 -->
		<view class="flexbg">

		</view>
	</view>
</template>

<script>
	import musichead from '../../components/musichead/musichead.vue'
	export default {
		data() {
			return {

			}
		},
		methods: {

		},
		onLoad(options) {
			console.log(options)
		}
	}
</script>

<style>

</style>

The style is written to the global configuration file App.vue

.flexbg {
    
    
    z-index: -1;
    width: 100%;
    height: 100vh;
    position: fixed;
    left: 0;
    top: 0;
    background-image: url(./static/wangyiyunyinyue.png);
    background-size: cover;
    background-position: center 0;
    /* 背景模糊以及毛边框特效 */
    filter: blur(10px);
    transform: scale(1.2);
}

We can get a basic look:

image-20220420225517763

3.2 Rendering data

Next we write the part of the scroll bar in this interface

Let's write the above layout first

<scroll-view scroll-y="true">
    <view class="list-head">
        <view class="list-head-img">
            <image src="../../static/logo.jpg" mode=""></image>
            <text class="iconfont iconyousanjiao">30亿</text>
        </view>
        <view class="list-head-text">
            <view>测试文字</view>
            <view>
                <image src="../../static/logo.jpg" mode=""></image>测试文字2
            </view>
            <view>
                段落测试文字段落测试文字段落测试文字段落测试文字段落测试文字
            </view>
        </view>
    </view>
</scroll-view>
<style scoped>
	.list-head {
		display: flex;
	}

	.list-head-img {
		width: 264rpx;
		height: 264rpx;
		border-radius: 30rpx;
		overflow: hidden;
		position: relative;
		margin-left: 26rpx;
		margin-right: 42rpx;
	}

	.list-head-img image {
		width: 100%;
		height: 100%;
	}

	.list-head-img text {
		position: absolute;
		right: 8rpx;
		top: 8rpx;
		color: white;
		font-size: 26rpx;
	}

	.list-head-text {
		flex: 1;
		color: #f0f2f7;
	}

	.list-head-text view:nth-child(1) {
		color: white;
		font-size: 34rpx;
	}

	.list-head-text view:nth-child(2) {
		display: flex;
		margin: 20rpx 0;
	}

	.list-head-text view:nth-child(2) image {
		width: 54rpx;
		height: 54rpx;
		border-radius: 50%;
		margin-right: 34rpx;
		font-size: 24rpx;
		align-items: center;
	}

	.list-head-text view:nth-child(3) {
		line-height: 34rpx;
		font-size: 22rpx;
	}
</style>

After writing, the basic look will be there

image-20220420234736566

There is a problem here. Netease Cloud has restricted the interface of the songs on the list. You cannot get the information without logging in.

image-20220421151457448

Here we can only save the country with curves, and obtain these data through the song details interface. The specific method is as follows:

image-20220421151854379

First get the id of the list and pass it directly to the list component

image-20220421152344997

The list component calls the song list interface with the id:

/**
 * 根据首页歌曲模块获取具体歌单
 * @param {列表id} listId
 */
export function list(listId){
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/playlist/detail?id=${
      
      listId}`,
		method: 'GET'
	});
}

In this way, you can get the list data you want without logging in

image-20220421152756592

Now we render the data up:

There may be SQ (HD) and 独家logos below the list, and these two logos can be obtained from the fields below

image-20220421163506291

Here we add a filter to format the numbers on the list. This filter is also needed for later likes, so we write it in the global file

Vue.filter('formatCount',function(value){
	
	if( value >= 10000 && value < 100000000 ){
		value /= 10000; 
		return value.toFixed(1) + '万';
	}
	else if(value >= 100000000){
		value /= 100000000;
		return value.toFixed(1) + '亿';
	}
	else{
		return value;
	}
	
});

image-20220421164905768

3.3 Complete code

Paste the complete code here:

The effect after completion is like this, in fact, it is exactly the same as Netease Cloud

image-20220421165618124

<template>
	<view class="list">
		<view class="flexbg" :style="{'background-image':'url('+ playlist.coverImgUrl +')'}"></view>
		<musichead title="歌单" :icon="true" color="white"></musichead>
		<view class="container">
			<scroll-view scroll-y="true">
				<view class="list-head">
					<view class="list-head-img">
						<image :src="playlist.coverImgUrl" mode=""></image>
						<text class="iconfont iconyousanjiao">{
   
   { playlist.playCount | formatCount }}</text>
					</view>
					<view class="list-head-text">
						<view>{
   
   { playlist.name }}</view>
						<view>
							<image :src="playlist.creator.avatarUrl" mode=""></image>
							<text>{
   
   { playlist.creator.nickname }}</text>
						</view>
						<view>{
   
   { playlist.description }}</view>
					</view>
				</view>
				<!-- #ifdef MP-WEIXIN -->
				<button v-show="isShow" class="list-share" open-type="share">
					<text class="iconfont iconicon-"></text>分享给微信好友
				</button>
				<!-- #endif -->
				<view class="list-music">
					<view v-show="isShow" class="list-music-title">
						<text class="iconfont iconbofang1"></text>
						<text>播放全部</text>
						<text>(共{
   
   { playlist.trackCount }}首)</text>
					</view>
					<!-- <view class="list-music-item">
						<view class="list-music-top">1</view>
						<view class="list-music-song">
							<view>与我无关</view>
							<view>
								<image src="../../static/dujia.png" mode=""></image>
								<image src="../../static/sq.png" mode=""></image>
								阿冗 - 与我无关
							</view>
						</view>
						<text class="iconfont iconbofang"></text>
					</view> -->
					<view class="list-music-item" v-for="(item,index) in playlist.tracks" :key="item.id"
						@tap="handleToDetail(item.id)">
						<view class="list-music-top">{
   
   { index + 1 }}</view>
						<view class="list-music-song">
							<view>{
   
   { item.name }}</view>
							<view>
								<image v-if=" privileges[index].flag > 60 && privileges[index].flag < 70"
									src="../../static/dujia.png" mode=""></image>
								<image v-if="privileges[index].maxbr == 999000" src="../../static/sq.png" mode="">
								</image>
								{
   
   { item.ar[0].name }} - {
   
   { item.name }}
							</view>
						</view>
						<text class="iconfont iconbofang"></text>
					</view>
				</view>
			</scroll-view>
		</view>
	</view>
</template>

<script>
	import musichead from '../../components/musichead/musichead.vue'
	import {
		list
	} from '../../common/api.js'
	import '../../common/iconfont.css'
	export default {
		data() {
			return {
				playlist: {
					coverImgUrl: '',
					trackCount: '',
					creator: ''
				},
				privileges: [],
				isShow: false
			}
		},
		components: {
			musichead
		},
		onLoad(options) {
			uni.showLoading({
				title:'加载中...'
			})
			let listId = options.listId;
			list(listId).then((res) => {
				if (res[1].data.code == '200') {
					this.playlist = res[1].data.playlist;
					this.privileges = res[1].data.privileges;
					this.isShow = true;
					uni.hideLoading();
					this.$store.commit('INIT_CHANGE', this.playlist.trackIds);
				}
			});
		},
		methods: {
			handleToDetail(id) {
				uni.navigateTo({
					url: '/pages/detail/detail?songId=' + id
				});
			}
		}
	}
</script>

<style scoped>
	.list-head {
		display: flex;
		margin: 30rpx;
	}

	.list-head-img {
		width: 265rpx;
		height: 265rpx;
		border-radius: 15rpx;
		margin-right: 40rpx;
		overflow: hidden;
		position: relative;
	}

	.list-head-img image {
		width: 100%;
		height: 100%;
	}

	.list-head-img text {
		position: absolute;
		font-size: 26rpx;
		color: white;
		right: 8rpx;
		top: 8rpx;
	}

	.list-head-text {
		flex: 1;
		font-size: 24rpx;
		color: #e1e2e3;
	}

	.list-head-text image {
		width: 52rpx;
		height: 52rpx;
		border-radius: 50%;
	}

	.list-head-text view:nth-child(1) {
		font-size: 34rpx;
		color: #ffffff;
	}

	.list-head-text view:nth-child(2) {
		display: flex;
		align-items: center;
		margin: 30rpx 0;
	}

	.list-head-text view:nth-child(2) text {
		margin-left: 15rpx;
	}

	.list-head-text view:nth-child(3) {
		line-height: 38rpx;
	}

	.list-share {
		width: 330rpx;
		height: 72rpx;
		margin: 0 auto;
		background: rgba(0, 0, 0, 0.4);
		text-align: center;
		line-height: 72rpx;
		font-size: 26rpx;
		color: white;
		border-radius: 36rpx;
	}

	.list-share text {
		margin-right: 15rpx;
	}

	.list-music {
		background: white;
		border-radius: 50rpx;
		overflow: hidden;
		margin-top: 45rpx;
	}

	.list-music-title {
		height: 58rpx;
		line-height: 58rpx;
		margin: 30rpx 30rpx 70rpx 30rpx;
	}

	.list-music-title text:nth-child(1) {
		font-size: 58rpx;
	}

	.list-music-title text:nth-child(2) {
		font-size: 34rpx;
		margin: 0 10rpx 0 25rpx;
	}

	.list-music-title text:nth-child(3) {
		font-size: 28rpx;
		color: #b2b2b2;
	}

	.list-music-item {
		display: flex;
		margin: 0 30rpx 70rpx 44rpx;
		align-items: center;
	}

	.list-music-top {
		width: 56rpx;
		font-size: 28rpx;
		color: #979797;
	}

	.list-music-song {
		flex: 1;
		line-height: 40rpx;
	}

	.list-music-song view:nth-child(1) {
		font-size: 30rpx;
		width: 70vw;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.list-music-song view:nth-child(2) {
		font-size: 22rpx;
		color: #a2a2a2;
		width: 70vw;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.list-music-song image {
		width: 34rpx;
		height: 22rpx;
		margin-right: 10rpx;
	}

	.list-music-item text {
		font-size: 50rpx;
		color: #c8c8c8;
	}
</style>

4. Song details page

Similarly, we have to write in the form of a custom header

First, we write the jump code and add events to the item of each song

@tap="handleToDetail(item.id)
methods: {
    
    
    handleToDetail(id) {
    
    
        uni.navigateTo({
    
    
            url: '/pages/detail/detail?songId=' + id
        });
    }
}

4.1 Song detail page layout

The layout elements here are slightly richer.

First, layout the playing disc and the stick

image-20220421172859629

Write a little style to make its position more reasonable

.detail-play{
    
     width:580rpx; height:580rpx; background:url(~@/static/disc.png); background-size:cover; margin:210rpx auto 44rpx auto; position: relative;}
.detail-play image{
    
     width:380rpx; height:380rpx; border-radius: 50%; position: absolute; left:0; top:0; right:0; bottom:0; margin:auto; animation:10s linear infinite move; animation-play-state: paused;}
@keyframes move{
    
    
from{
    
     transform : rotate(0deg);}
to{
    
     transform : rotate(360deg);}
}
.detail-play .detail-play-run{
    
     animation-play-state: running;}
.detail-play text{
    
     width:100rpx; height:100rpx; font-size:100rpx; position: absolute; left:0; top:0; right:0; bottom:0; margin:auto; color:white;}
.detail-play view{
    
     position: absolute; width:170rpx; height:266rpx; position: absolute; left:60rpx; right:0;  margin:auto; top:-170rpx; background:url(~@/static/needle.png); background-size:cover;}

After adjusting the style, the layout is more reasonable

image-20220421173029807

Next is some other layouts, some components here will not be done

What is written here is intermittent, so let’s paste the last one together. It contains the part of rendering data, and the code of layout is commented out.

Effect:

image-20220421174327773

<template>
	<view class="detail">
		<view class="flexbg" :style="{backgroundImage:'url('+ songDetail.al.picUrl +')'}"></view>
		<musichead :title="songDetail.name" :icon="true" color="white"></musichead>
		<view class="container">
			<scroll-view scroll-y="true">
				<view class="detail-play" @tap="handleToPlay">
					<image :src="songDetail.al.picUrl" :class="{ 'detail-play-run' : isplayrotate }" mode=""></image>
					<text class="iconfont" :class="playicon"></text>
					<view></view>
				</view>
				<view class="detail-lyric">
					<view class="detail-lyric-wrap" :style="{ transform : 'translateY(' +  - (lyricIndex - 1) * 82  + 'rpx)' }">
						<!-- <view class="detail-lyric-item">测试文字阿斯顿撒所</view>
						<view class="detail-lyric-item active">测试文字阿斯</view>
						<view class="detail-lyric-item">测试顿撒所洒水大所大按时</view> -->
						<view class="detail-lyric-item" :class="{ active : lyricIndex == index}" v-for="(item,index) in songLyric" :key="index">{
   
   { item.lyric }}</view>
					</view>
				</view>
				<view class="detail-like">
					<view class="detail-like-title">喜欢这首歌的人也听</view>
					<view class="detail-like-list">
						<!-- <view class="detail-like-item">
							<view class="detail-like-img"><image src="../../static/wangyiyunyinyue.png" mode=""></image></view>
							<view class="detail-like-song">
								<view>蓝</view>
								<view>
									<image src="../../static/dujia.png" mode=""></image>
									<image src="../../static/sq.png" mode=""></image>
									石白其 - 蓝
								</view>
							</view>
							<text class="iconfont iconbofang"></text>
						</view> -->
						<view class="detail-like-item" v-for="(item,index) in songSimi" :key="index" @tap="handleToSimi(item.id)">
							<view class="detail-like-img"><image :src="item.album.picUrl" mode=""></image></view>
							<view class="detail-like-song">
								<view>{
   
   {item.name}}</view>
								<view>
									<image v-if="item.privilege.flag > 60 && item.privilege.flag < 70" src="../../static/dujia.png" mode=""></image>
									<image v-if="item.privilege.maxbr == 999000" src="../../static/sq.png" mode=""></image>
									{
   
   {item.artists[0].name}} - {
   
   {item.name}}
								</view>
							</view>
							<text class="iconfont iconbofang"></text>
						</view>
					</view>
				</view>
				<view class="detail-comment">
					<view class="detail-comment-title">精彩评论</view>
					<!-- <view class="detail-comment-item">
						<view class="detail-comment-img"><image src="../../static/wangyiyunyinyue.png" mode=""></image></view>
						<view class="detail-comment-content">
							<view class="detail-comment-head">
								<view class="detail-comment-name">
									<view>是啊冗的冗</view>
									<view>2020年1月2日</view>
								</view>
								<view class="detail-comment-like">
									56026 <text class="iconfont iconlike"></text>
								</view>
							</view>
							<view class="detail-comment-text">
								测试文字测试文字测试文字测试文字测试文字测试文字测试文字测试文字
							</view>
						</view>
					</view> -->
					<view class="detail-comment-item" v-for="(item,index) in songComment" :key="index">
						<view class="detail-comment-img"><image :src="item.user.avatarUrl" mode=""></image></view>
						<view class="detail-comment-content">
							<view class="detail-comment-head">
								<view class="detail-comment-name">
									<view>{
   
   { item.user.nickname }}</view>
									<view>{
   
   { item.time | formatTime }}</view>
								</view>
								<view class="detail-comment-like">
									{
   
   { item.likedCount | formatCount }} <text class="iconfont iconlike"></text>
								</view>
							</view>
							<view class="detail-comment-text">
								{
   
   { item.content }}
							</view>
						</view>
					</view>
				</view>
			</scroll-view>
		</view>
	</view>
</template>

<script>
	import { songDetail , songUrl , songLyric , songSimi , songComment } from '../../common/api.js';
	import '../../common/iconfont.css'
	export default {
		data() {
			return {
				songDetail : {
					al : { picUrl : '' }
				},
				songSimi : [],
				songComment : [],
				songLyric : [],
				lyricIndex : 0,
				playicon : 'iconpause',
				isplayrotate : true
			}
		},
		onLoad(options){
			this.playMusic(options.songId);
		},
		onUnload(){
			this.cancelLyricIndex();
			// #ifdef H5
			this.bgAudioMannager.destroy();
			// #endif
		},
		onHide(){
			this.cancelLyricIndex();
			// #ifdef H5
			this.bgAudioMannager.destroy();
			// #endif
		},
		methods: {
			playMusic(songId){
				this.$store.commit('NEXT_ID',songId);
				Promise.all([songDetail(songId),songSimi(songId),songComment(songId),songLyric(songId),songUrl(songId)]).then((res)=>{
					if(res[0][1].data.code == '200'){
						this.songDetail = res[0][1].data.songs[0];
					}
					if( res[1][1].data.code == '200' ){
						this.songSimi = res[1][1].data.songs;
					}
					if( res[2][1].data.code == '200' ){
						this.songComment = res[2][1].data.hotComments;
					}
					if(res[3][1].data.code == '200'){
						let lyric = res[3][1].data.lrc.lyric;
						let result = [];
						let re = /\[([^\]]+)\]([^[]+)/g;
						lyric.replace(re,($0,$1,$2)=>{
							result.push({ time : this.formatTimeToSec($1) , lyric : $2 });
						});
						this.songLyric = result;
					}
					if(res[4][1].data.code == '200'){
						// #ifdef MP-WEIXIN
						this.bgAudioMannager = uni.getBackgroundAudioManager();
						this.bgAudioMannager.title = this.songDetail.name;
						// #endif
						// #ifdef H5
						if(!this.bgAudioMannager){
							this.bgAudioMannager = uni.createInnerAudioContext();
						}
						this.playicon = 'iconbofang1';
						this.isplayrotate = false;
						// #endif
						this.bgAudioMannager.src = res[4][1].data.data[0].url;
						this.listenLyricIndex();
						this.bgAudioMannager.onPlay(()=>{
							this.playicon = 'iconpause';
							this.isplayrotate = true;
							this.listenLyricIndex();
						});
						this.bgAudioMannager.onPause(()=>{
							this.playicon = 'iconbofang1';
							this.isplayrotate = false;
							this.cancelLyricIndex();
						});
						this.bgAudioMannager.onEnded(()=>{
							this.playMusic(this.$store.state.nextId);
						});
					}
				});
			},
			formatTimeToSec(time){
				var arr = time.split(':');
				return (parseFloat(arr[0]) * 60 + parseFloat(arr[1])).toFixed(2);
			},
			handleToPlay(){
				if(this.bgAudioMannager.paused){
					this.bgAudioMannager.play();
				}
				else{
					this.bgAudioMannager.pause();
				}
			},
			listenLyricIndex(){
				clearInterval(this.timer);
				this.timer = setInterval(()=>{
					for(var i=0;i<this.songLyric.length;i++){
						if( this.songLyric[this.songLyric.length-1].time < this.bgAudioMannager.currentTime ){
							this.lyricIndex = this.songLyric.length-1;
							break;
						}
						if( this.songLyric[i].time < this.bgAudioMannager.currentTime && this.songLyric[i+1].time > this.bgAudioMannager.currentTime ){
							this.lyricIndex = i;
						}
					}
				});
			},
			cancelLyricIndex(){
				clearInterval(this.timer);
			},
			handleToSimi(songId){
				this.playMusic(songId);
			}
		}
	}
</script>

<style scoped>
	.detail-play{ width:580rpx; height:580rpx; background:url(~@/static/disc.png); background-size:cover; margin:210rpx auto 44rpx auto; position: relative;}
	.detail-play image{ width:380rpx; height:380rpx; border-radius: 50%; position: absolute; left:0; top:0; right:0; bottom:0; margin:auto; animation:10s linear infinite move; animation-play-state: paused;}
	@keyframes move{
		from{ transform : rotate(0deg);}
		to{ transform : rotate(360deg);}
	}
	.detail-play .detail-play-run{ animation-play-state: running;}
	.detail-play text{ width:100rpx; height:100rpx; font-size:100rpx; position: absolute; left:0; top:0; right:0; bottom:0; margin:auto; color:white;}
	.detail-play view{ position: absolute; width:170rpx; height:266rpx; position: absolute; left:60rpx; right:0;  margin:auto; top:-170rpx; background:url(~@/static/needle.png); background-size:cover;}
	
	.detail-lyric{ height:246rpx; line-height: 82rpx; font-size:32rpx; text-align: center; color:#949495; overflow: hidden;}
	.active{ color:white;}
	.detail-lyric-wrap{ transition: .5s;}
	.detail-lyric-item{ height:82rpx;}
	
	.detail-like{ margin:0 32rpx;}
	.detail-like-title{ font-size:36rpx; color:white; margin:50rpx 0;}
	.detail-like-list{}
	.detail-like-item{ display: flex; margin-bottom:38rpx; align-items: center;}
	.detail-like-img{ width:82rpx; height:82rpx; border-radius: 15rpx; overflow: hidden; margin-right:20rpx;}
	.detail-like-img image{ width:100%; height:100%;}
	.detail-like-song{ flex:1;}
	.detail-like-song view:nth-child(1){ color:white; font-size:30rpx; width:70vw; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 10rpx;}
	.detail-like-song view:nth-child(2){ font-size:22rpx; color:#a2a2a2; width:70vw; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
	.detail-like-song image{ width:34rpx; height:22rpx; margin-right:10rpx;}
	.detail-like-item text{ font-size:50rpx; color:#877764;}
	
	.detail-comment{ margin:0 32rpx;}
	.detail-comment-title{ font-size:36rpx; color:white; margin:50rpx 0;}
	.detail-comment-item{ display: flex; margin-bottom:28rpx;}
	.detail-comment-img{ width:66rpx; height:66rpx; border-radius: 50%; overflow: hidden; margin-right:18rpx;}
	.detail-comment-img image{ width:100%; height:100%}
	.detail-comment-content{ flex:1; color:#cac9cd;}
	.detail-comment-head{ display: flex; justify-content: space-between;}
	.detail-comment-name view:nth-child(1){ font-size:24rpx;}
	.detail-comment-name view:nth-child(2){ font-size:20rpx;}
	.detail-comment-like{ font-size:30rpx;}
	.detail-comment-text{ line-height: 40rpx; color:white; font-size:28rpx; margin-top:16rpx; border-bottom:1px #595860 solid; padding-bottom: 40rpx;}
	
</style>

4.2 Song interface

http://localhost:3000/song/detail?ids=483937795  ##歌曲详情接口
http://localhost:3000/simi/song?id=483937795	##和当前歌曲相似的歌曲接口
http://localhost:3000/comment/music?id=483937795  ##歌曲评论接口
http://localhost:3000/lyric?id=483937795 		##歌词接口
http://localhost:3000/song/url?id=483937795     ##播放音乐接口

Let's encapsulate these interfaces in api.js

/**
 * 歌曲详情接口
 * @param {歌曲id} id
 */
export function songDetail(id) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/song/detail?ids=${
      
      id}`,
		method: 'GET'
	})
}
/**
 * 播放歌曲接口
 * @param {Object} id
 */
export function songUrl(id) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/song/url?id=${
      
      id}`,
		method: 'GET'
	})
}
/**
 * 歌词接口
 * @param {Object} id
 */
export function songLyric(id) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/lyric?id=${
      
      id}`,
		method: 'GET'
	})
}
/**
 * 和当前歌曲类似歌曲接口
 * @param {Object} id
 */
export function songSimi(id) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/simi/song?id=${
      
      id}`,
		method: 'GET'
	})
}
/**
 * 评论接口
 * @param {Object} id
 */
export function songComment(id) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/comment/music?id=${
      
      id}`,
		method: 'GET'
	})
}

4.3 Data Rendering

There is already rendering code in 4.1

4.4 Set up background music manager

It needs to be configured here, otherwise there will be a phenomenon that music cannot be played on the WeChat applet

"requireBackgroudModes":["audio"]

image-20220421182735241

4.5 VueX shared data

Now we have a problem. When a song is played, we need it to automatically switch to the next song. Here we need to use VueX to pass the information

VueX is built into Uniapp, so it can be imported directly without installing dependencies

image-20220421185153448

this.$store.commit('INIT_CHANGE', this.playlist.trackIds);

Let's see if there is a deposit

image-20220421200809935

Let it play the next song

image-20220421201544447

5. Search page

5.1 Search page layout

The layout here is similar to before, first copy the components and some of the same styles, and paste the styles directly here

image-20220421210240151

<template>
	<view class="search">
		<musichead title="搜索" :icon="true" :iconBlack="true"></musichead>
		<view class="container">
			<scroll-view scroll-y="true">
				<view class="search-search">
					<text class="iconfont iconsearch"></text>
					<input type="text" placeholder="搜索歌曲" v-model="searchWord" @confirm="handleToSearch" @input="handleToSuggest" />
					<text v-show="searchType == 2" @tap="handleToClose" class="iconfont iconguanbi"></text>
				</view>
				<block v-if="searchType == 1">
					<view class="search-history">
						<view class="search-history-head">
							<text>历史记录</text>
							<text class="iconfont iconlajitong" @tap="handleToClear"></text>
						</view>
						<view class="search-history-list">
							<view v-for="(item,index) in historyList" :key="index" @tap="handleToWord(item)">{
   
   { item }}</view>
						</view>
					</view>
					<view class="search-hot">
						<view class="search-hot-title">热搜榜</view>
						<!-- <view class="search-hot-item">
							<view class="search-hot-top">1</view>
							<view class="search-hot-word">
								<view>
									少年 <image src="../../static/dujia.png" mode="aspectFit"></image>
								</view>
								<view>"少年"这个词实在是太美好了</view>
							</view>
							<text class="search-hot-count">2968644</text>
						</view> -->
						<view class="search-hot-item" v-for="(item,index) in searchHot" :key="index" @tap="handleToWord(item.searchWord)">
							<view class="search-hot-top">{
   
   { index + 1 }}</view>
							<view class="search-hot-word">
								<view>
									{
   
   { item.searchWord }} <image :src="item.iconType ? item.iconUrl : ''" mode="aspectFit"></image>
								</view>
								<view>{
   
   { item.content }}</view>
							</view>
							<text class="search-hot-count">{
   
   { item.score | formatCount }}</text>
						</view>
					</view>
				</block>
				<block v-else-if="searchType == 2">
					<view class="search-result">
						<!-- <view class="search-result-item">
							<view class="search-result-word">
								<view>少年</view>
								<view>
									<image src="../../static/dujia.png" mode=""></image>
									<image src="../../static/dujia.png" mode=""></image>
									许巍 - 爱如少年
								</view>
							</view>
							<text class="iconfont iconbofang"></text>
						</view> -->
						<view class="search-result-item" v-for="(item,index) in searchList" :key="index" @tap="handleToDetail(item.id)">
							<view class="search-result-word">
								<view>{
   
   { item.name }}</view>
								<view>{
   
   { item.artists[0].name }} - {
   
   { item.album.name }}</view>
							</view>
							<text class="iconfont iconbofang"></text>
						</view>
					</view>
				</block>
				<block v-else-if="searchType == 3">
					<view class="search-suggest">
						<view class="search-suggest-title">搜索"{
   
   { this.searchWord }}"</view>
						<!-- <view class="search-suggest-item">
							<text class="iconfont iconsearch"></text>
							少年抖音
						</view> -->
						<view class="search-suggest-item" v-for="(item,index) in suggestList" :key="index" @tap="handleToWord(item.keyword)">
							<text class="iconfont iconsearch"></text>
							{
   
   { item.keyword }}
						</view>
					</view>
				</block>
			</scroll-view>
		</view>
	</view>
</template>

<script>
	import { searchHot , searchWord , searchSuggest } from '../../common/api.js'
	import '../../common/iconfont.css'
	export default {
		data() {
			return {
				searchHot : [],
				searchWord : '',
				historyList : [],
				searchType : 1,
				searchList : [],
				suggestList : []
			}
		},
		onLoad(){
			searchHot().then((res)=>{
				if(res[1].data.code == '200'){
					this.searchHot = res[1].data.data;
				}
			});
			uni.getStorage({
			    key: 'searchHistory',
			    success:(res)=>{
			        this.historyList = res.data;
			    }
			});
		},
		methods: {
			handleToSearch(){
				this.historyList.unshift(this.searchWord);
				this.historyList = [...new Set(this.historyList)];
				if(this.historyList.length > 10){
					this.historyList.length = 10;
				}
				uni.setStorage({
				    key: 'searchHistory',
				    data: this.historyList
				});
				this.getSearchList(this.searchWord);
			},
			handleToClear(){
				uni.removeStorage({
					key:'searchHistory',
					success:()=>{
						this.historyList = [];
					}
				});
			},
			getSearchList(word){
				searchWord(word).then((res)=>{
					if(res[1].data.code == '200'){
						this.searchList = res[1].data.result.songs;
						this.searchType = 2;
					}
				});
			},
			handleToClose(){
				this.searchWord = '';
				this.searchType = 1;
			},
			handleToSuggest(ev){
				let value = ev.detail.value;
				if(!value){
					this.searchType = 1;
					return;
				}
				searchSuggest(value).then((res)=>{
					if(res[1].data.code == '200'){
						this.suggestList = res[1].data.result.allMatch;
						this.searchType = 3;
					}
				});
			},
			handleToWord(word){
				this.searchWord = word;
				this.handleToSearch();
			},
			handleToDetail(songId){
				uni.navigateTo({
					url: '/pages/detail/detail?songId='+songId
				});
			}
		}
	}
</script>

<style scoped>
	.search-search{ display: flex; background:#f7f7f7; height:73rpx; margin:28rpx 30rpx 30rpx 30rpx; border-radius: 50rpx; align-items: center;}
	.search-search text{ margin:0 27rpx;} 
	.search-search input{ font-size:26rpx; flex:1;}
	
	.search-history{ margin:0 30rpx; font-size:26rpx;}
	.search-history-head{ display: flex; justify-content: space-between;}
	.search-history-list{ display: flex; margin-top:36rpx; flex-wrap: wrap;}
	.search-history-list view{ padding:20rpx 40rpx; background:#f7f7f7; border-radius: 50rpx; margin-right:30rpx; margin-bottom: 20rpx;}
	
	.search-hot{ margin:30rpx 30rpx; font-size:26rpx; color:#bebebe;}
	.search-hot-title{}
	.search-hot-item{ display: flex; align-items: center; margin-top: 40rpx;}
	.search-hot-top{ width:60rpx; color:#fb2221; font-size:34rpx;}
	.search-hot-word{ flex:1;}
	.search-hot-word view:nth-child(1){ font-size::;rpx; color:black;}
	.search-hot-word image{ width:48rpx; height:22rpx;}
	.search-hot-count{}
	
	.search-result{ border-top: 2rpx #e5e5e5 solid; padding:30rpx;}
	.search-result-item{ display: flex; align-items: center; border-bottom: 2rpx #e5e5e5 solid; padding-bottom:30rpx; margin-bottom: 30rpx;}
	.search-result-item text{ font-size:50rpx;}
	.search-result-word{ flex:1;}
	.search-result-word view:nth-child(1){ font-size:28rpx; color:#3e6694;}
	.search-result-word view:nth-child(2){ font-size:26rpx;}
	
	.search-suggest{ border-top: 2rpx #e5e5e5 solid; padding:30rpx; font-size:26rpx; }
	.search-suggest-title{ color:#537caa; margin-bottom: 40rpx;}
	.search-suggest-item{ color:#666666; margin-bottom: 70rpx;}
	.search-suggest-item text{ color:#c2c2c2; font-size:26rpx; margin-right:26rpx;}
</style>

5.2 Search page interface

http://localhost:3000/search/hot/detail  ##热词的接口
http://localhost:3000/search?keywords=少年	##歌曲搜索结果接口
http://localhost:3000/search/suggest?keywords=少年&type=mobile  ##提示,你如输入"少"会提示可能出现的歌曲

Encapsulate the interface:

export function searchHot() {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/search/hot/detail`,
		method: 'GET'
	})
}

export function searchWord(word) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/search?keywords=${
      
      word}`,
		method: 'GET'
	})
}

export function searchSuggest(word) {
    
    
	return uni.request({
    
    
		url: `${
      
      baseUrl}/search/suggest?keywords=${
      
      word}&type=mobile`,
		method: 'GET'
	})
}

5.3 Data Rendering

In section 5.1 the data has been rendered

5.4 Search Tips

Some students may not know what the search prompt is. In fact, it is a very common function in our life. The following animation demonstrates it

search tips

Let's write the logic of the search prompt

<block v-else-if="searchType == 3">
    <view class="search-suggest">
        <view class="search-suggest-title">搜索"{
   
   { this.searchWord }}"</view>
        <!-- <view class="search-suggest-item">
<text class="iconfont iconsearch"></text>
少年抖音
</view> -->
        <view class="search-suggest-item" v-for="(item,index) in suggestList" :key="index" @tap="handleToWord(item.keyword)">
            <text class="iconfont iconsearch"></text>
            {
   
   { item.keyword }}
        </view>
    </view>
</block>

There may be a little problem with the small program here, just modify it a little

image-20220421222342359

<input type="text" placeholder="搜索歌曲" v-model="searchWord" @confirm="handleToSearch" @input="handleToSuggest"/>
handleToSuggest(ev){
    
    
    let value = ev.detail.value;
    //为空返回第一页
    if(!value){
    
    
        this.searchType = 1;
        return;
    }
    searchSuggest(value).then((res)=>{
    
    
        if(res[1].data.code == '200'){
    
    
            this.suggestList = res[1].data.result.allMatch;
            this.searchType = 3;
        }
    });
},

6. Home skeleton screen display

The frame Loadingis still not elegant enough, here we use the more common skeleton screen for processing

Let's go to uniapp's plug-in market to search for the skeleton screen plug-in, click on the right to add it to your own HBuilderX

image-20220421223936123

image-20220421224154195

You can see that it is installed automatically, but the style in the plug-in is written in scss, we need to install the plug-in to compile

image-20220421225052624

Usually installed by default

image-20220421225128280

use

Import the component first

// 导入组件
import mForSkeleton from "@/components/m-for-skeleton/m-for-skeleton";

Guess you like

Origin blog.csdn.net/fengxiandada/article/details/124355765