小程序笔记(云音乐,coderwhy)

248 阅读5分钟

小程序笔记

登录逻辑以及实施

image-20230308202253669

后台播放

const audioContext = wx.getBackgroundAudioManager()
//需要设置后台播放的歌名
audioContext.title = res.songs[0].name

顺序播放、随机播放、单曲循环

changeNewMusicAction(ctx, isNext = true) {
      // 1.获取当前索引
      let index = ctx.playListIndex

      // 2.根据不同的播放模式, 获取下一首歌的索引
      switch(ctx.playModeIndex) {
        case 0: // 顺序播放
          index = isNext ? index + 1: index -1
          if (index === -1) index = ctx.playListSongs.length - 1
          if (index === ctx.playListSongs.length) index = 0
          break
        case 1: // 单曲循环
          break
        case 2: // 随机播放
          index = Math.floor(Math.random() * ctx.playListSongs.length)
          break
      }

      console.log(index)

      // 3.获取歌曲
      let currentSong = ctx.playListSongs[index]
      if (!currentSong) {
        currentSong = ctx.currentSong
      } else {
        // 记录最新的索引
        ctx.playListIndex = index
      }

      // 4.播放新的歌曲
      this.dispatch("playMusicWithSongIdAction", { id: currentSong.id, isRefresh: true })
    }

var,let,const三者的特点和区别

https://blog.csdn.net/xiewenhui111/article/details/113133330

歌词滚动

<swiper-item class="lyric">
		<scroll-view class="lyric-list" scroll-y scroll-top="{{lyricScrollTop}}" scroll-with-animation>
			<block wx:for="{{lyricInfos}}" wx:key="index">
				<view class="item {{currentLyricIndex === index ? 'active': ''}}" style="padding-top: {{index === 0 ? (contentHeight/2-80): 0}}px; padding-bottom: {{index === lyricInfos.length - 1 ? (contentHeight/2+80): 0}}px;">
					{{item.text}}
				</view>
			</block>
		</scroll-view>
</swiper-item>

目标歌词展示

//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						console.log(currentLyricInfo.text);
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex })
					}
					break
				}
			}

歌词转换格式化

法一:
// 正则(regular)表达式(expression): 字符串匹配利器

// [00:58.65]  \是对[]和.转义
const timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/

export function parseLyric(lyricString) {
  const lyricStrings = lyricString.split("\n")

  const lyricInfos = []
  for (const lineString of lyricStrings) {
    // [00:58.65]他们说 要缝好你的伤 没有人爱小丑
    const timeResult = timeRegExp.exec(lineString)
    if (!timeResult) continue
    // 1.获取时间
    const minute = timeResult[1] * 60 * 1000
    const second = timeResult[2] * 1000
    const millsecondTime = timeResult[3]
    const millsecond = millsecondTime.length === 2 ? millsecondTime * 10: millsecondTime * 1
    const time = minute + second + millsecond

    // 2.获取歌词文
    const text = lineString.replace(timeRegExp, "")
    lyricInfos.push({ time, text })
  }
  return lyricInfos
}

法二:
// 实现歌词滚动
    lyricScroll(currentLyric) {
      let placeholderHeight = 0;
      // 获取歌词item
      let lyricsArr = document.querySelectorAll(".lyricsItem");
      // 获取歌词框
      let lyrics = document.querySelector(".lyrics");
      // console.log(lyrics.offsetTop)//123
      // console.log(lyricsArr[0].offsetTop)//123
      // placeholder的高度
      if (placeholderHeight == 0) {
        placeholderHeight = lyricsArr[0].offsetTop - lyrics.offsetTop;//123-123
        // console.log(placeholderHeight)//0
      }
      //   歌词item在歌词框的高度 = 歌词框的offsetTop - 歌词item的offsetTop
        // console.log(currentLyric);//歌词索引
        // console.log(lyricsArr[currentLyric - 1])//歌词第一句打印的是全部歌词,后面打印的是上一句歌词的div
      if (lyricsArr[currentLyric - 1]) {
        let distance = lyricsArr[currentLyric - 1].offsetTop - lyrics.offsetTop;
        // console.log(lyricsArr[currentLyric - 1].offsetTop)
        // console.log(lyrics.offsetTop)//123
        // console.log(distance)
        //   lyricsArr[currentLyric].scrollIntoView();
        lyrics.scrollTo({
          behavior: "smooth",
          top: distance - placeholderHeight,
        });
      }
    }
法三:
/**
 * 解析歌词字符串
 * 得到一个歌词对象的数组
 * 每个歌词对象:
 * {time:开始时间, words: 歌词内容}
 */
function parseLrc() {
  var lines = lrc.split('\n');
  var result = []; // 歌词对象数组
  for (var i = 0; i < lines.length; i++) {
    var str = lines[i];
    var parts = str.split(']');
    var timeStr = parts[0].substring(1);
    var obj = {
      time: parseTime(timeStr),
      words: parts[1],
    };
    result.push(obj);
  }
  return result;
}

/**
 * 将一个时间字符串解析为数字(秒)
 * @param {String} timeStr 时间字符串
 * @returns
 */
function parseTime(timeStr) {
  var parts = timeStr.split(':');
  return +parts[0] * 60 + +parts[1];
}

var lrcData = parseLrc();

// 获取需要的 dom
var doms = {
  audio: document.querySelector('audio'),
  ul: document.querySelector('.container ul'),
  container: document.querySelector('.container'),
};

/**
 * 计算出,在当前播放器播放到第几秒的情况下
 * lrcData数组中,应该高亮显示的歌词下标
 * 如果没有任何一句歌词需要显示,则得到-1
 */
function findIndex() {
  // 播放器当前时间
  var curTime = doms.audio.currentTime;
  for (var i = 0; i < lrcData.length; i++) {
    if (curTime < lrcData[i].time) {
      return i - 1;
    }
  }
  // 找遍了都没找到(说明播放到最后一句)
  return lrcData.length - 1;
}

// 界面

/**
 * 创建歌词元素 li
 */
function createLrcElements() {
  var frag = document.createDocumentFragment(); // 文档片段,这是在进行优化,直接操作dom树比parseHTML效率高
  for (var i = 0; i < lrcData.length; i++) {
    var li = document.createElement('li');
    li.textContent = lrcData[i].words;
    frag.appendChild(li); // 改动了 dom 树
  }
  doms.ul.appendChild(frag);
}

createLrcElements();

// 容器高度
var containerHeight = doms.container.clientHeight;
// 每个 li 的高度
var liHeight = doms.ul.children[0].clientHeight;
// 最大偏移量
var maxOffset = doms.ul.clientHeight - containerHeight;
/**
 * 设置 ul 元素的偏移量
 */
function setOffset() {
  var index = findIndex();
  var offset = liHeight * index + liHeight / 2 - containerHeight / 2;
  if (offset < 0) {
    offset = 0;
  }
  if (offset > maxOffset) {
    offset = maxOffset;
  }
  doms.ul.style.transform = `translateY(-${offset}px)`;
  // 去掉之前的 active 样式
  var li = doms.ul.querySelector('.active');
  if (li) {
    li.classList.remove('active');
  }

  li = doms.ul.children[index];
  if (li) {
    li.classList.add('active');//classList是元素的类样式列表,如果元素类样式过多,就不适合用li.className('active')
  }
}
//时间变化的函数
doms.audio.addEventListener('timeupdate', setOffset);

音乐播放器

// pages/music-player/index.js
import {getSongDetail,getSongLyric} from '../../services/api_player'
import {audioContext} from '../../store/player-store'
import {parseLyric} from '../../utils/parse-lyric'
Page({
	data: {
		id:0,
		currentSong:{},
		currentPage:0,
		contentHeight:0,
		//显示歌词
		isMusicLyric:true,
		//总时长
		durationTime:0,
		//当前时间
		currentTime:0,
		//滑动到的时间(百分比)
		sliderValue:0,
		//是否在滑动
		isSliderChanging:false,
		//歌词
		lyricInfos:[],
		//当前播放歌词
		currentLyricText:"",
		//当前播放歌词索引
		currentLyricIndex:0,
		//要滚动的距离
		lyricScrollTop:0
	},

	onLoad(options) {
		const id = options.id
		this.setData({id})
		this.getPageData(id)
		//动态计算高度,宽度
		const screenHeight = getApp().globalData.screenHeight
		const statusBarHeight = getApp().globalData.statusBarHeight
		const navBarHeight = getApp().globalData.navBarHeight
		const contentHeight = screenHeight - statusBarHeight - navBarHeight
		const deviceRadio = getApp().globalData.deviceRadio
		this.setData({contentHeight,isMusicLyric:deviceRadio >= 2})
		//创建播放器
		audioContext.stop()
		audioContext.src = `https://music.163.com/song/media/outer/url?id=${id}.mp3`
		audioContext.autoplay = true
		//audioContext事件监听
		this.setupAudioContextListener()
	},
// ========================   网络请求   ======================== 
	getPageData(id){
		getSongDetail(id).then(res => {
			this.setData({currentSong:res.songs[0],durationTime: res.songs[0].dt})
		})
		getSongLyric(id).then(res => {
			const lyricString = res.lrc.lyric
			const lyric = parseLyric(lyricString)
			this.setData({lyricInfos:lyric})
		})
	},
// ========================   事件处理   ======================== 
	handleSwiperChange(event){
		const current = event.detail.current
		this.setData({currentPage:current})
	},
	handleSliderChange(event){
		// 1.获取slider变化值(百分比)
		const value = event.detail.value
		// 2.计算需要播放的currentTime
		const currentTime = this.data.durationTime * value / 100
		// 3.设置context播放currentTime位置的音乐
		audioContext.pause()
		audioContext.seek(currentTime / 1000)
		// 4.记录最新的sliderValue, 并且需要讲isSliderChaning设置回false
		this.setData({ sliderValue: value, isSliderChanging: false })
	},
	handleSliderChanging(event){
		const value = event.detail.value
    	const currentTime = this.data.durationTime * value / 100
    	this.setData({ isSliderChanging: true, currentTime})
	},
	//事件监听
	setupAudioContextListener(){
		audioContext.onCanplay(() => {
			audioContext.play()
		})
		audioContext.onTimeUpdate(() => {
			//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex ,lyricScrollTop:currentIndex * 35})
					}
					break
				}
			}
		})
	}
})

image mode的属性

mode 有效值:

mode 有 13 种模式,其中 4 种是缩放模式,9 种是裁剪模式。

模式 值 说明
缩放 scaleToFill 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
缩放 aspectFit 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
缩放 aspectFill 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
缩放 widthFix 宽度不变,高度自动变化,保持原图宽高比不变
裁剪 top 不缩放图片,只显示图片的顶部区域
裁剪 bottom 不缩放图片,只显示图片的底部区域
裁剪 center 不缩放图片,只显示图片的中间区域
裁剪 left 不缩放图片,只显示图片的左边区域
裁剪 right 不缩放图片,只显示图片的右边区域
裁剪 top left 不缩放图片,只显示图片的左上边区域
裁剪 top right 不缩放图片,只显示图片的右上边区域
裁剪 bottom left 不缩放图片,只显示图片的左下边区域
裁剪 bottom right 不缩放图片,只显示图片的右下边区域

使用多个插槽,要设置

//想使用多个插槽,要设置
Component({
	 options: {
  		multipleSlots: true
 	},
})

搜索关键字的高亮以及rich-text的使用

****string2nodes.js

export default function stringToNodes(keyword, value) {
	const nodes = []
	if (keyword.toUpperCase().startsWith(value.toUpperCase())) {
	  const key1 = keyword.slice(0, value.length)
	  const node1 = {
		name: "span",
		attrs: { style: "color: #26ce8a; font-size: 14px;" },
		children: [ { type: "text", text: key1 } ]
	  }
	  nodes.push(node1)
  
	  const key2 = keyword.slice(value.length)
	  const node2 = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: key2 } ]
	  }
	  nodes.push(node2)
	} else {
	  const node = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: keyword } ]
	  } 
	  nodes.push(node)
	}
	return nodes
  }
  
  ****组件内
  import stringToNodes from '../../utils/string2nodes'
  
  handleSearch(event){
		//获取输入的关键字
		const searchValue = event.detail
		this.setData({searchValue})
		if (!searchValue.length){
			this.setData({suggestSongs:[]})
			return
		} 
		debounceGetSearchSuggest(searchValue).then(res => {
			const suggestSongs = res.result.allMatch || []
			this.setData({suggestSongs})
			// 转成nodes节点
			const suggestKeywords = suggestSongs.map(item => item.keyword)
			const suggestSongsNodes = []
			for( const item of suggestKeywords){
				const nodes = stringToNodes(item,searchValue)
				suggestSongsNodes.push(nodes)
			}
			this.setData({suggestSongsNodes})
		})
	}

防抖使用(搜索框)

**组件内
import debounce from '../../utils/debounce'
const debounceGetSearchSuggest = debounce(getSearchSuggest,600)

debounceGetSearchSuggest(searchValue).then(res => {
			this.setData({suggestSongs:res.result.allMatch})
})

**debounce.js
export default function debounce(fn, delay = 500, immediate = false, resultCallback) {
	// 1.定义一个定时器, 保存上一次的定时器
	let timer = null
	let isInvoke = false
  
	// 2.真正执行的函数
	//...args是searchValue,输入的文字
	const _debounce = function(...args) {
	  return new Promise((resolve, reject) => {
		// 取消上一次的定时器
		if (timer) clearTimeout(timer)
  
		// 判断是否需要立即执行
		if (immediate && !isInvoke) {
		  const result = fn.apply(this, args)
		  if (resultCallback) resultCallback(result)
		  resolve(result)
		  isInvoke = true
		} else {
		  // 延迟执行
		  timer = setTimeout(() => {
			// 外部传入的真正要执行的函数
			const result = fn.apply(this, args)
			if (resultCallback) resultCallback(result)
			resolve(result)
			isInvoke = false
			timer = null
		  }, delay)
		}
	  })
	}
  
	// 封装取消功能
	_debounce.cancel = function() {
	  console.log(timer)
	  if (timer) clearTimeout(timer)
	  timer = null
	  isInvoke = false
	}
  
	return _debounce
  }

export和export default的区别

export和export default的区别 - 知乎 (zhihu.com)

data-使用以及动态key数组

	<block>
      <ranking-area-item item="{{originalRankingsongs}}" bindtap="handleMoreClickBtn" data-id="1"></ranking-area-item>
      <ranking-area-item item="{{newRankingsongs}}" bindtap="handleMoreClickBtn" data-id="2"></ranking-area-item>
      <ranking-area-item item="{{soarRankingsongs}}" bindtap="handleMoreClickBtn" data-id="3"></ranking-area-item>
    </block>
    
    
    **data-id  id为自定义名称,为下面事件event参数中添加一个id属性,rankingMap[id]动态id获取rankingMap数组中的value
    
    //排行榜点击事件
	handleMoreClickBtn(event){
		const rankingMap = {1:"originalRanking",2:"newRanking",3:"soarRanking"}
		const id = event.currentTarget.dataset.id
		const rankingName = rankingMap[id]
		this.navigateToDeatail(rankingName)
	}

引入hy-event-store,达到vuex效果

cnpm i hy-event-store

**index.js
import {rankingStore} from './ranking-store'
export {rankingStore}

**ranking-store.js
import {
	HYEventStore
} from 'hy-event-store'
import {
	getRankings
} from '../services/api_music'
const rankingStore = new HYEventStore({
	state: {
		hotRanking: {}
	},
	actions: {
		getRankingDataAction(ctx) {
			getRankings(3778678).then((res) => {
				console.log(res);
				ctx.hotRanking = res.playlist
			})
		}
	}
})
export {
	rankingStore
}

**组件
import {rankingStore} from '../../store/index'

onLoad(options) {
		this.getPageData()
		// 获取推荐音乐数据
		rankingStore.dispatch('getRankingDataAction')
		// 从store中获取数据
		rankingStore.onState("hotRanking",(res) =>{
			//刚开始的hotRanking为空对象
			if(!res.tracks) return
			const recommendSongs = res.tracks.slice(0,7)
			this.setData({recommendSongs})
		})
	},

小程序解决插槽动态显示方案

image-20230227104038541

  **header.wxss
  
  .header .slot:empty + .default {
	display: flex;
  }
  .header .default {
	display: none;
	align-items: center;
	font-size: 28rpx;
	color: #777;
  }
  **header.wxml
  
  <!--components/area-header/index.wxml-->
<view class="header">
  <view class="title">{{title}}</view>
  <view class="right" wx:if="{{showRight}}" bindtap="handleRightClick">
    <view class="slot"><slot></slot></view>
    <view class="default">
      <text>{{rightText}}</text>
      <image class="icon" src="/assets/images/icons/arrow-right.png"></image>
    </view>
  </view>
</view>
**home.wxml

<!-- 推荐歌曲 -->
<view class="recommend-song">
	<header title="推荐歌曲"></header>
</view>

setdata(同步是不那么好的)

image-20230227094106634

节流(规定时间内只能调用一次,普攻)

**throttle.js

export default function throttle(fn, interval = 1000, options = { leading: true, trailing: false }) {

  // 1.记录上一次的开始时间

  const { leading, trailing, resultCallback } = options

  let lastTime = 0

  let timer = null

 

  // 2.事件触发时, 真正执行的函数

  const _throttle = function(...args) {

   return new Promise((resolve, reject) => {

​    // 2.1.获取当前事件触发时的时间const nowTime = new Date().getTime()

​    if (!lastTime && !leading) lastTime = nowTime

 

​    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数const remainTime = interval - (nowTime - lastTime)

​    if (remainTime <= 0) {

​     if (timer) {

​      clearTimeout(timer)

​      timer = null

​     }

 

​     // 2.3.真正触发函数const result = fn.apply(this, args)

​     if (resultCallback) resultCallback(result)

​     resolve(result)

​     // 2.4.保留上次触发的时间

​     lastTime = nowTime

​     return

​    }

 

​    if (trailing && !timer) {

​     timer = setTimeout(() => {

​      timer = null

​      lastTime = !leading ? 0: new Date().getTime()

​      const result = fn.apply(this, args)

​      if (resultCallback) resultCallback(result)

​      resolve(result)

​     }, remainTime)

​    }

   })

  }

 

  _throttle.cancel = function() {

   if(timer) clearTimeout(timer)

   timer = null

   lastTime = 0

  }
  return _throttle
 }
 
 
组件引用:

import throttle from '../../utils/throttle'
const throttleQueryRect = throttle(queryRect)

//动态计算swiper高度,防止手机不同样式不同
	handleSwiperHeight() {
		throttleQueryRect(".swiper-image").then(res =>{
			const rect = res[0]
			this.setData({swiperHeight:rect.height})
		})
}

导入vant weapp

npm init -y
cnpm i @vant/weapp@1.3.3 -S--production
将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。
{
	"usingComponents": {
		"van-search": "@vant/weapp/search/index"
	}
}

打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。

动态计算swiper高度,防止手机不同样式不同

<swiper-item class="swiper-item">
			<image src="{{item.pic}}" mode="widthFix" class="swiper-image" bindload="handleSwiperHeight"/>
</swiper-item>
  
  
  swiperHeight:0
  
  handleSwiperHeight() {

​    //获取图片的高度const query = wx.createSelectorQuery()

​    query.select('.swiper-image').boundingClientRect()

​    query.exec((res) =>{

​      const rect = res[0]

​      this.setData({swiperHeight:rect.height})

​    })

  }

二次封装接口

image-20230225223116952

image-20230225223018460

image-20230225224553962

(.wxs)日期和数量格式化

function formatCount(count) {
	var counter = parseInt(count)
	if (counter>100000000) {
		return (counter/100000000).toFixed(1) + '亿'
	}else if (counter>10000) {
		return (counter/10000).toFixed(1) + '万'
	}else{
		return counter + ''
	}
}
function addZero(time) {
	time = time + ''
	return ('00' + time).slice(time.length)
}
function formatDuration(time) {
	time = time/1000
	var minut = Math.floor(time/60)
	var second = Math.floor(time) % 60
	return addZero(minut) + ':' + addZero(second)
}
// commonjs
module .exports={
	formatCount:formatCount,
	formatDuration:formatDuration
}
<wxs src="../../utils/format.wxs" module="format"/>

<view class="count">
​     {{format.formatCount(item.playCount)}}
</view>

组件抽离

image-20230226104344554

image-20230226104407306

image-20230226104439901

data-xxx的使用

data-xx 的作用是在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理

比如 写一个list列表 想知道点击的list列表的那一个item ,比如获取点击的图片,等等

使用data-xx 需要注意的 xx 是自己取的名字, 后面跟着的渲染一定要是使用的值,否则无效

比如点击的是list 后面跟的是list的值,如果是图片后面就要是图片url的地址,
————————————————
版权声明:本文为CSDN博主「胡小牧」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33210042/article/details/91983464

<view wx:for="{{topMvs}}" wx:key="id" class="item">
		<video-item-v1 item='{{item}}' bindtap="VideoItemBtn" data-item="{{item}}"></video-item-v1>
</view>


VideoItemBtn(event){
		const id = event.currentTarget.dataset.item.id
		//页面跳转
		wx.navigateTo({
		  url: `/pages/detail-video/index?id=${id}`,
		})
	}

安装tailwindcss

image-20230225153341363

image-20230225153815117

image-20230225160644483

报错解决方案

image-20230225160435943