目录
前言
一、学习总结
二、uni-app项目的创建
三、创建相关子页
pages.json页面路由的配置
使用循环骨架m-for-skeleton
对于实现播放器的启动和停止动画的方法
编辑
四、api接口的调用
前言
uni-app 是一个使用 Vue.js 开发所有前端应用的框架,用nui-app仿网易云的学习与总结
一、学习总结
- 让跨平台开发变得更简单。Uni-app使用Vue.js语法,可以使用同一份代码编译出iOS、Android、Web等多个平台的应用,减少了开发者的工作量。
- 减少开发难度。Uni-app提供了一套完整的开发生态圈,包括开发工具、UI组件库、API插件等,减少了开发者的投入。
- 性能表现良好。Uni-app使用了优化的渲染引擎,可以让应用在不同平台上都有流畅的性能表现。
- 社区活跃度高。Uni-app拥有一个庞大的开发社区,开发者可以在社区中获取资源、交流经验、解决问题。
- 可扩展性强。Uni-app提供了许多插件和组件,同时也支持第三方插件和组件,开发者可以根据需要进行扩展。
现将用nui-app仿网易云的相关步骤和代码分享和大家共同学习,若有不足之处,请见谅!
二、uni-app项目的创建
1. 创建uni-app空项目并分别运行到浏览器、Android模拟器和微信开发者工具
2. 创建uni-app的hello项目并分别运行到浏览器、Android模拟器和微信开发者工具,以查看官方的组件、接口、模板等示例
3. 项目结构
· pages文件夹存放页面
· static内的文件不会进行编译,不要放js文件,可放到common中(注意体积限制)
· unpackage文件夹存放打包的文件
· components文件夹存放各种组件
· App.vue代表应用,包括应用层的生命周期方法,全局样式等
· pages.json整个应用的页面集合,第一项为启动页,可配置页面路由及样式和标题
· manifest.json应用配置,包括图标配置、启动界面配置、权限配置及其他开发配置
· main.js应用入口文件
三、创建相关子页
在文件pages中分别创建index、list、player、search父文件和子页面index.vue、list.vue、player.vue、search.vue.
pages.json页面路由的配置
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
}
},
{
"path" : "pages/list/list",
"style" :
{
"enablePullDownRefresh": false
}
}
,{
"path" : "pages/search/search",
"style" :
{
"enablePullDownRefresh": false
}
}
,{
"path" : "pages/player/player",
"style" :
{
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "UAMusic-guo",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
使用循环骨架m-for-skeleton
循环骨架m-for-skeleton是一个第三方组件,用于在Vue.js应用程序中展示 loading 骨架屏。
安装循环骨架m-for-skeleton使用案例,可以通过以下步骤实现:
- 在终端中进入Vue.js项目的根目录。
- 运行以下命令安装m-for-skeleton组件:
npm install m-for-skeleton --save
- 在Vue.js项目的入口文件中引入组件:
import MforSkeleton from 'm-for-skeleton'
- 将组件注册为全局组件:
Vue.component('m-for-skeleton', MforSkeleton)
- 在需要展示 loading 骨架屏的组件中使用m-for-skeleton组件,例如:
<template>
<div>
<m-for-skeleton :count="5">
<div class="card">
<h2>Card Title</h2>
<p>Card description goes here</p>
</div>
</m-for-skeleton>
</div>
</template>
现在你可以在应用程序中使用循环骨架m-for-skeleton组件展示 loading 骨架屏。
1.使用了骨架屏框架的index.vu页面和效果图
<template>
<view class="content">
<uamhead :title="title"></uamhead>
<!-- <image class="logo" src="/static/long2.npg"></image>
<view class="text-area">
<text class="title">{{title}}</text>
</view> -->
<scroll-view scroll-y="true" >
<view>
<m-for-skeleton
:avatarSize="200"
:row="3"
:title="false"
:loading="loading"
isarc="square"
:titleStyle="{}"
v-for="(item,key) in 4"
:key="key">
</m-for-skeleton>
</view>
<view class="index-list" v-for="(item,index) in playlist" :key="index" @click="handleToList(item.id)">
<view class="index-list-item">
<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>
</scroll-view>
</view>
</template>
<script>
import { topList } from '../../common/api.js'
// const toplistdata=require('@/static/toplist.json')
import mForSkeleton from "@/components/m-for-skeleton/m-for-skeleton";
import uamhead from "../../components/uamhead/uamhead.vue"
export default {
components: {
mForSkeleton
},
data() {
return {
playlist:[],
title: 'UAMusic',
loading: true
}
},
onLoad() {
// this.playlist= toplistdata;
topList().then((res)=>{
if(res.length){
setTimeout(()=>{
this.playlist = res;
this.loading=false
},2000);
console.log(res)
}
});
},
methods: {
handleToList(id){
uni.navigateTo({
url:'/pages/list/list?listid='+id
})
}
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
.index-list{ margin:0 30rpx;;width: 95%}
.index-list-item{ display: flex; margin-bottom: 35rpx;}
.index-list-img{ width:212rpx; height:212rpx; margin-right:20rpx; border-radius: 15rpx; overflow: hidden; position: relative;}
.index-list-img image{ width:100%; height:100%;}
.index-list-img text{ position: absolute; font-size:22rpx; color:white; bottom: 15rpx; left:15rpx;}
.index-list-text{ flex:1; font-size:24rpx; line-height: 68rpx;}
</style>
2.list.vue页面和效果图
<template>
<view>
<uamhead :title="title"></uamhead>
<view class="list-head">
<view class="list-head-img">
<image :src="playlist.coverImgUrl" mode=""></image>
<text class="iconfont iconyousanjiao">{{ playlist.playCount }}</text>
</view>
<view class="list-head-text">
<view>{{ playlist.name }}</view>
<view>{{ playlist.description }}</view>
</view>
</view>
<scroll-view scroll-y="true" class="scroll-Y">
<view v-show="isShow" class="list-music-title">
<text class="iconfont iconbofang1"></text>
<text>播放全部</text>
<text>(共{{ playlist.trackCount }}首)</text>
</view>
<view class="list-music">
<view class="list-music-item" v-for="(item,index) in playlist.tracks" :key="item.id"
@tap="navPlayer(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/独家.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>
</template>
<script>
import {uamhead} from '../../components/uamhead/uamhead.vue'
import {
list
} from '../../common/api.js'
export default {
data() {
return {
title: "榜单列表",
playlist: {
coverImgUrl: '',
trackCount: '',
creator: ''
},
privileges: [],
isShow: false
}
},
onLoad(options) {
let listid = options.listid;
list(listid).then((res) => {
if (res.data.code == '200') {
this.title = res.data.playlist.name
this.playlist = res.data.playlist
this.privileges = res.data.privileges
this.isShow = true
}
})
},
methods: {
navPlayer(id) {
console.log(id)
uni.navigateTo({
url:'/pages/player/player?songId='+id
})
}
}
}
</script>
<style>
.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: #c3d1e3;
}
.list-head-text image {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
}
.list-music {
background: white;
border-radius: 50rpx;
overflow: hidden;
margin-top: 45rpx;
}
.list-music-item {
display: flex;
margin: 0 30rpx 60rpx 44rpx;
align-items: center;
}
.list-music-top {
width: 56rpx;
font-size: 28rpx;
color: #979797;
}
.list-music-song {
flex: 1;
line-height: 40rpx;
}
.scroll-Y {
height: 700rpx;
}
.list-music-title {
height: 58rpx;
line-height: 58rpx;
margin: 30rpx 30rpx 70rpx 30rpx;
}
.list-music-song image {
width: 34rpx;
height: 22rpx;
margin-right: 10rpx;
}
</style>
3.player.vue页面和效果图
<template>
<view>
<uamhead :title="title"></uamhead>
<view class="player">
<image :src="song.picUrl" :class="{'run' : isplayrotate}" mode=""></image>
<text class="iconfont iconpause" v-if="isplayrotate" @tap="noplaying"></text>
<text class="iconfont iconbofang" v-else="" @tap="playing"></text>
<view></view>
</view>
<scroll-view class="lyric" scroll-y="true">
<view class= "wrap" :style="{ transform : ' translateY(' + -(lyricIndex - 1)*82 + 'rpx)' }">
<view class="item" :class="{ active : lyricIndex == index}" v-for="(item, index) in song.lyric" :key="index" >
{{item. lyric}}
</view>
</view>
</scroll-view>
<view>{{song.artist}}:{{song.name}}</view>
</view>
</template>
<script>
const innerAudioContext = uni.createInnerAudioContext();
import uamhead from '../../components/uamhead/uamhead.vue'
import {
songDetail,
songLyric,
songUrl
} from '../../common/api.js';
export default {
data() {
return {
title: "黑胶唱片",
song: {
id: '',
name: '',
artist: '',
picUrl: '',
songUrl: '',
lyric: ''
},
isplayrotate: false,
lyricIndex: 0
}
},
onLoad(options) {
let sId =options.songId;
songDetail(sId).then(res => {
let s = res.data.songs[0]
this.song.name = s.name
this.song.id = s.id
this.song.artist = s.ar[0].name
this.song.picUrl = s.al.picUrl
//获取音乐的地址
songUrl(this.song.id).then(res => {
this.song.songUrl = res.data.data[0].url;
console.log(this.song.songUrl)
})
songLyric(sId).then(res=>{
var lyric=res.data.lrc.lyric;
var result=[];
let re = /\[([^\]]+)\]([^[]+)/g;
// console.log('this.song.lyric')
lyric.replace(re,($0,$1,$2)=>{
result.push({time : this.formatTimeToSec($1),lyric : $2});
});
this.song.lyric=result;
})
})
},
methods: {
playing() {
innerAudioContext.autoplay = true;
innerAudioContext.src = this.song.songUrl;
innerAudioContext.onPlay(() => {
this.isplayrotate = true;
this.listenLyricIndex();
this.innerAudioContext = innerAudioContext
});
innerAudioContext.onEnded(()=>{
this.isplayrotate=false;
})
},
noplaying() {
innerAudioContext.pause();
this.isplayrotate = false;
innerAudioContext.onPause(() => { //暂停时调用的方法
innerAudioContext.startTime = innerAudioContext.currentTime //startTime 设置开始时间 currentTime 暂停时的秒数
});
},
formatTimeToSec(time){
var arr=time.split(':');
return (parseFloat(arr[0]) *60 + parseFloat(arr[1])).toFixed(2);
},
listenLyricIndex(){
clearInterval(this.timer);
this.timer= setInterval(()=>{
for(var i=0;i<this.song.lyric.length;i++){
if(this.song.lyric[this.song.lyric.length-1].time < this.innerAudioContext.currentTime ){
this.lyricIndex=this.song.lyric.length-1;
break;
}
if(this.song.lyric[i].time<this.innerAudioContext.currentTime && this.song.lyric[i+1].time > this.innerAudioContext.currentTime ){
this.lyricIndex=i;
}
}
});
}
}
}
</script>
<style>
.player {
width: 480rpx;
height: 480rpx;
background: url(~@/static/disc.png);
background-size: cover;
margin: 210rpx auto 44rpx auto;
position: relative;
z-index: 2;
}
.player image {
width: 480rpx;
height: 480rpx;
border-radius: 50%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 3;
}
.player text {
width: 100rpx;
height: 100rpx;
font-size: 100rpx;
position: absolute;
left: 0;
top: 0;
right: 0rpx;
bottom: 0;
margin: 180rpx auto;
color: white;
z-index: 4;
}
.player 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;
}
.player image {
width: 295rpx;
height: 295rpx;
border-radius: 50%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 3;
animation: 10s linear infinite move;
animation-play-state: paused;
}
@keyframes move {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.player .run {
animation-play-state: running;
}
.lyric{height: 260rpx;line-height: 82rpx;font-size: 32rpx;text-align: center;color: #949495;overflow: hidden;}
.active{ color:blue;}
.lyric .wrap{ transition:.5s;}
.lyric .item{ /*
*/
/* overflow: hidden */;
}
</style>
对于实现播放器的启动和停止动画的方法
需要使用JavaScript来启动和停止动画。你可以使用JavaScript来动态地应用CSS类名来触发或停止动画。你可以使用以下代码来启动动画:
document.getElementById('yourElement').classList.add('yourAnimationClass');
这将向具有id“yourElement”的元素添加一个类名为“yourAnimationClass”的CSS类,该类定义了你的CSS动画。
停止动画时,你可以使用以下代码:
document.getElementById('yourElement').classList.remove('yourAnimationClass');
这将从具有id“yourElement”的元素中删除所添加的类名,从而停止动画。
可以使用任何JavaScript事件来触发这些方法,例如单击按钮或滚动到页面底部。
4.search.vue页面和效果页面
<template>
<view class="search">
<uamhead :title="title"></uamhead>
<view title="搜索" :icon="true" :iconBlack="true"></view>
<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" 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>
<!-- <text class="search-hot-count">{{ item.score}}</text>2222222222222222 -->
</view>
</view>
</view>
</block>
<block v-else-if="searchType == 2">
<view class="search-result">
<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" 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 {
title: '',
searchHot: [],
searchWord: '',
historyList: [],
searchType: 1,
searchList: [],
suggestList: []
}
},
onLoad() {
searchHot().then((res) => {
if (res.data.code == '200') {
this.searchHot = res.data.data;
// console.log(res)
}
});
uni.getStorage({
key: 'searchHistory',
success: (res) => {
this.historyList = res.data;
console.log(res)
}
});
},
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.data.code == '200') {
this.searchList = res.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.data.code == '200') {
this.suggestList = res.data.result.allMatch;
this.searchType = 3;
}
});
},
handleToWord(word) {
this.searchWord = word;
this.handleToSearch();
},
handleToDetail(id) {
uni.navigateTo({
url: '/pages/player/player?songId=' + id
});
}
}
}
</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) {
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>
四、api接口的调用
在common中创建api.js和config.js
api.js页面
import { baseUrl } from './config.js';
export function topList(){
return new Promise(function(resolve,reject){
uni.request({
url: `${baseUrl}/toplist/detail`,
method: 'GET',
data: {},
success: res => {
let result = res.data.list;
resolve(result.splice(0,4));
},
fail: (err) => {
console.log(err);
},
complete: () => {}
});
});
}
export function list(listId){
return uni.request({
url: `${baseUrl}/playlist/detail?id=${listId}`,
method: 'GET'
});
}
export function songDetail(id){
return uni.request({
url : `${baseUrl}/song/detail?ids=${id}`,
method : 'GET'
})
}
export function songUrl(id){
return uni.request({
url : `${baseUrl}/song/url?id=${id}`,
method : 'GET'
})
}
export function songLyric(id){
return uni.request({
url : `${baseUrl}/lyric?id=${id}`,
method : 'GET'
})
}
export function songSimi(id){
return uni.request({
url : `${baseUrl}/simi/song?id=${id}`,
method : 'GET'
})
}
export function songComment(id){
return uni.request({
url : `${baseUrl}/comment/music?id=${id}`,
method : 'GET'
})
}
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'
})
}
config.js页面
export const baseUrl = 'https://flask-web-frak-shishn-kvmjsphrif.cn-shenzhen.fcapp.run';
以上也只是对网易云播放器的模仿与学习