uniapp微信小程序&支付宝小程序的初体验,记录一些初次遇到的大坑小坑~

1、打开hbuildx,新建一个uniapp项目,选默认模板就可以,然后运行项目就可以选择

2、 下载安装微信/支付宝开发者工具,并且记住安装的路径!

安装好以后在hbuildx的工具-->设置-->运行配置-->开发工具路径里,将路径填上(选上),这样就可以运行项目了,这时运行微信小程序项目会在开发者工具上报错,是因为没有AppID

3、注册微信小程序

百度:微信公众平台-->账号分类-->小程序,滑到最底,注册,这里有个大坑,就是一个邮箱只能注册一个小程序,一旦注册就不能注销啦。注册好微信小程序前期就要一个AppID就好。微信公众平台扫码-->微信选择登录的小程序-->开发-->开发管理-->开发设置下就可以找到AppID了,把他复制下来到项目-->manifest.json-->微信小程序配置

4、注册支付宝小程序

百度:支付宝开放平台-->创建应用,然后支付宝小程序的AppID跟微信小程序的不一样,位置也不一样,看图,然后复制下来,到hbuildx的项目-->manifest.json-->支付宝小程序配置

 配置好APPID并不是万事大吉啦。要运行到支付宝开发工具必须选对文件夹~~这块也算一个小坑啦。点击运行-->选择支付宝小程,然后等待编译,编译完成以后,你的支付宝开发工具就会自动打开,但是你的项目不会。现在需要点击右上角-->选择项目-->选择你的项目文件夹-->打开-->选择unpackage-->选择dist-->选择dev-->选择mp-alipay。这样就可以在支付宝开发工具打开项目了,不用担心,这边写了代码,支付宝开发工具上也是自动更新的

上面基本步骤就跑通了,下面就可以写代码了。

5、写代码

不分前后,想起一个更新一个

1、uni.request()在支付宝不能解析responseType(文档上说了),换成my.request,还好我把请求封装了一下,不然真是大麻烦了,下面是uniapp网络请求的封装

(1)根目录下新建utils文件夹,建index.js内容如下

const baseUrl = 'https://xxxx.com' // 全局地址

import store from '../store/index.js'// 用来记录登录状态,统一封装token

module.exports = (params) => {
	let url = params.url; // 请求地址 例如:/api/order/payRevertDevice
	let method = params.method; // 请求方法
	let header = params.header || {}; // 请求头
	let data = params.data || {}; // 参数
	let isjson = params.isjson || false // 请求头的content-type,默认不是'application/json',如果需要就设置为TRUE
	
	if (method) {
		method = method.toUpperCase(); //	小写转大写
	}
	// 请求类型
	if (!isjson) {
		header = {
			"content-type": "application/x-www-form-urlencoded"
		}
	}else {
		header = {
			"content-type": "application/json"
		}
	}
	// 获取登录状态
	let token = store.state.token
	if (token) {
		header = {
			...header,
			token
		}
	}

	//	发起请求 加载动画
	if (!params.hideLoading) {
		uni.showLoading({
			title: "加载中"
		})
	}
	
	
	// 支付宝小程序不支持res,用my.request
	// #ifdef MP-ALIPAY
	my.request({
		url: baseUrl + url,
		method: method || "GET",
		data: data,
		headers: header,
		dataType: 'json',
		success: res => {
			console.log(res,'http request res')
			if (res.statusCode && res.statusCode != 200) {
				//	api错误
				uni.showModal({
					// content: res.msg
					content: res
				})
				return;
			}
			typeof params.success == "function" && params.success(res.data);
		},
		fail: err => {
			uni.showModal({
				content: err
			})
			typeof params.fail == "function" && params.fail(err.data);
		},
		complete: (e) => {
			// console.log("请求完成");
			uni.hideLoading()
			typeof params.complete == "function" && params.complete(e.data);
			return;
		}
	})
	// #endif

	// 微信小程序的请求 
	// #ifdef MP-WEIXIN
	//	发起网络请求
	uni.request({
		url: baseUrl + url,
		method: method || "GET",
		header: header,
		data: data,
		dataType: "json",
		sslVerify: false, //	是否验证ssl证书
		success: res => {
			if (res.statusCode && res.statusCode != 200) {
				//	api错误
				uni.showModal({
					content: res.msg
				})
				return;
			}
			typeof params.success == "function" && params.success(res.data);
		},
		fail: err => {
			uni.showModal({
				content: err.msg
			})
			typeof params.fail == "function" && params.fail(err.data);
		},
		complete: (e) => {
			// console.log("请求完成");
			uni.hideLoading()
			typeof params.complete == "function" && params.complete(e.data);
			return;
		}
	})
	//  #endif	
}

(2)在main.js里挂载到vue的原型链上,这样组件就可以使用this.http()啦

import http from './utils/http.js'
Vue.prototype.http = http



import store from './store/index.js'
//把vuex定义成全局组件
Vue.prototype.$store = store


store:这里主要是记录token

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
	state: {
		hasLogin: false,
		token: '',
	},
	mutations: {
		login(state, res) {
			state.hasLogin = true
			state.token = res
		},
	},
	actions: {}
})
export default store

使用:以登录为例

this.http({
		url: '/api/user/aliLogin',
		method: 'POST',
		isjson: true,
		data: { // 根据接口文档填写参数,我这里目前需要一下四个
			code,
			avatar,
			nickname,
			userType: 1
		},
        success: res => {
			// console.log(res, 'MP-ALIPAY login res')
			if (res.code === 200) {
			// 存token 这样以后每次请求就自动带token了
			this.$store.commit('login', res.data)
            // 再进行其他操作

        },
        fail: err =>{}
})

2、支付宝小程序的大坑

以前没有接触过支付宝小程序,坑实在是太多了

(1)在支付宝开放平台注册支付宝小程序之后,它的环境有两种。一种是沙箱环境,一种是线上环境,这两个是独立的,千万不能混了,而且沙箱环境可调式的能力很少,我都是在线上环境调试的

但是,线上环境会有各种问题,首先要去支付宝公众平开,配置ip白名单,然后再在这个平台上开通各种能力,或许这样就好了,但是绝对不止这样,具体的流程忘记了,我弄了好几天才坑坑巴巴的调通了。 包括报什么ISV权限不足,然后官方文档说的什么auth_code什么什么的都是垃圾,就好好看看目前的环境,请求的地址,有没有白名单,能力有没有开通,或许就可以了

3、保存图片到本地

uni.getFileSystemManager().writeFile()、uni.saveImageToPhotosAlbum方法只能在微信小程序上使用,首先获取授权

			toSave() {
				uni.showLoading({
					title: "正在生成图片",
					mask: true,
				});

				uni.getSetting({
					success: (res) => {
						if (!res.authSetting["scope.writePhotosAlbum"]) {
							uni.authorize({
								scope: "scope.writePhotosAlbum",
								success: () => { // 授权成功
									// console.log('授权成功')
									uni.hideLoading();
									this.saveImg();
								},

								fail: (res) => {
									uni.hideLoading();
									// console.log('无法保存图片,请先授权')
									uni.showToast({
										title: '取消授权',
										icon: 'error'
									})
								},
							});
						} else { // 已经授权!
							// console.log('已经授权!')
							uni.hideLoading();
							this.saveImg();
						}
					},
				});
			},
			

 this.saveImg(),图片地址使用base64格式

saveImg() {
				let _this = this
				const base64Str = 'base64格式的图片地址'.slice(22), // 注意这里,截掉data:image/png;base64,
					buffer = uni.base64ToArrayBuffer(base64Str),
					filePath = wx.env.USER_DATA_PATH + "/wx.png"; // base64src.png 为保存的图片名称

				uni.getFileSystemManager().writeFile({
					filePath, // 先把文件写到临时目录里
					// 方式一:
					data: buffer,
					encoding: "binary",
					// 方式二:
					// data: this.imgBase64.slice(22),
					// encoding: "base64",
					success: (res) => {
						uni.saveImageToPhotosAlbum({
							filePath, // 将临时文件 保存到相册
							success: (res) => {
								uni.showToast({
									title: '图片保存成功',
									icon: 'success'
								})
								_this.$refs.popup.close()
							},
							fail: (error) => {
								uni.showToast({
									title: '图片保存失败',
									icon: 'error'
								})
								_this.$refs.popup.close()
							},
						});
					},
					fail: (error) => {
						uni.showToast({
							title: '图片保存失败',
							icon: 'error'
						})
					}
				});
			}

支付宝小程序上用my.saveImage()

toSave() {
				uni.showLoading({
					title: "正在生成图片",
					mask: true,
				});
				let _this = this
				my.saveImage({
					url: '图片的网络地址',
					success: res => {
						// console.log('saveImage', res)
						uni.showToast({
							title: '图片保存成功',
							icon: 'success'
						})
						uni.hideLoading()
						_this.$refs.popup.close()
					},
					fail: err => {
						uni.hideLoading()
						uni.showToast({
							title: '授权失败',
							icon: 'error'
						})
						// console.error('saveImage err', err)
						_this.$refs.popup.close()
					}
				})
			}

4、uniapp获取用户信息并且--->微信登录微信小程序、支付宝登录支付宝小程序

这块的坑不是很多,但是登录功能是必须有的,记录一下

微信小程序

login() {
				let _this = this
				uni.getSetting({
					success(res) {
						// console.log("授权:", res);
						uni.showModal({
							title: '授权提醒',
							content: '请您授权头像、昵称等信息,以便使用全部功能',
							cancelText: "随便逛逛",
							confirmText: '确认授权',
							success: function(showres) {
								if (showres.confirm) {
									uni.getUserProfile({
										desc: '获取您的昵称、头像、地区及性别', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
										lang: "zh_CN",
										success: (infoRes) => {
											// console.log('授权信息', infoRes);
											uni.login({
												provider: 'weixin',
												success: (res1) => {
													_this.weixinlogin(用户信息等参数)
												},
												fail: () => {
													uni.showToast({
														title: "微信登录授权失败",
														icon: "none"
													});
												}
											})
										},
										fail: err => {
											// console.log(err, 'getUserProfile err')
										}
									})
								} else if (showres.cancel) {
									// console.log('用户点击取消');
								}
							}
						})
					}
				})
			},



weixinlogin(参数) {
				this.http({
					url: '接口地址',
					method: 'POST',
					isjson: true,
					data: {
						登录接口需要的参数
					},
					header: {
						'content-type': 'application/json'
					},
					success: res => {
						// console.log(res, 'weixin login res')
						if (res.code === 200) {
							// store里存token,在上面的代码里写过,封装请求的时候将token封装进去
							this.$store.commit('login', res.data)
							uni.showToast({
								title: '登录成功!',
								icon: 'success'
							})
						} else {
							// console.log('登录失败!')
							uni.showToast({
								title: '登录失败!',
								icon: 'error'
							})
						}
					},

支付宝小程序获取用户信息登录

这里有点坑,调试的时候总是调不通,可以从一下几点找问题

1)确保代码没有问题,我是照着文档写的

2)支付宝小程序开放平台的各种配置,包括ip白名单、域名白名单、是不是这个小程序的开发者等

3)后端的秘钥,证书(都是小程序后台生成的)没有问题

4)APPID

<view class="my-beijing" @click="alilogin">


// 获取用户authCode
			alilogin() {
				let _this = this
				my.getAuthCode({
					scopes: 'auth_user',
					success: (res2) => {
						console.log(res2, 'res.authCode')
						// return
						_this.alipaylogin(res2.authCode)
						// console.log(res2,'res2') , userInfo.avatar, userInfo.nickName || '支付宝用户'
					},
					fail: (resfail) => {
						// 用户取消授权
					},
				});
			},


// 支付宝登录
			alipaylogin(code) {
				this.http({
					url: 'login接口地址',
					method: 'POST',
					data: {
						code
					},
					success: res => {
						if (res.code === 200) {
							// 存token
							this.$store.commit('login', res.data.token)
						} else {
							// console.log('MP-ALIPAY 登录失败!')
							uni.showToast({
								title: '登录失败!',
								icon: 'error'
							})
						}
					},
					fail: err => {
						// console.log(err, 'MP-ALIPAY login err')
						// console.log('登录失败!')
						uni.showToast({
							title: '登录失败!',
							icon: 'error'
						})
					}
				})
			},

5、获取180秒倒计时

<text v-if="!isgetcode" @click="sendMs">发送验证码</text>
<text v-else>{{content}}</text>





data() {
			return {
                isgetcode: false,
				count: 180,
				canClick: false,
				content: ''
			}
		},


methods: {
			// 获取倒计时
			daojishi() {
				if (!this.canClick) return
				this.canClick = false
				this.content = this.count + '秒' //这里解决60秒不见了的问题
				let clock = setInterval(() => {
					this.count--
					this.content = this.count + '秒'
					// console.log(this.content,'this.content')
					if (this.count <= 0) { //当倒计时小于0时清除定时器
						clearInterval(clock)
						this.content = '验证码'
						this.count = 180
						this.canClick = true
						this.isgetcode = false
					}
				}, 1000)
			},
}

6、支付

1)uniapp的微信支付

流程就是处理页面一些列数据,然后第一步调用后端给你的创建订单接口,第二步就调用uni.requestPayment()拉起支付就好了

// 创建订单
commitorder() {
				// 是否同意协议
				if (this.agreement_radio) {
					let query = {
						创建订单时的参数
					}
                    //调用创建订单接口
					this.http({
						url: '接口地址',
						method: 'POST',
						isjson: true,
						data: query,
						success: res => {
							// console.log(res, 'create order res')
							if (res.code == 200) {
                                //调用支付
								this.wxpay(res.data)
							}
						},
						fail: err => {
							// console.log(err, 'create order error')
						}
					})
				} else {
					// console.log('未同意协议')
					this.$nextTick(() => {})
				}
			},

this.wxpay()

// 拉起微信支付
			wxpay(data) {
				let _this = this
				// console.log(data, 'wxpay data'),uni.requestPayment()需要的参数在创建订单的接口返回,要不然就自己对照文档计算
				uni.requestPayment({
					provider: 'wxpay',
					timeStamp: data.timeStamp,
					nonceStr: data.nonceStr,
					package: data.package,
					signType: data.signType,
					paySign: data.paySign,
					success: function(res) {
						// console.log('success:' + JSON.stringify(res));
						// 跳转到支付成功页面
						uni.reLaunch({
							url: '../Play_Ok/Play_Ok'
						})
					},
					fail: function(err) {
						// console.log('fail:' + JSON.stringify(err));
						// 支付失败记得删除订单
					}
				});
			},

2)uniapp的支付宝支付

支付宝支付用uni的方法没有成功,所以去看了支付宝支付的文档,最后选择用支付宝预授权来完成支付。步骤还是一样的先创建订单获取支付时的参数(支付宝是tradeNo),然后拉起支付

            // 支付宝支付
			commitorder_alipay() {
				// 是否同意协议
				if (this.agreement_radio) {

					// 是否选择地址
					if (!this.haseaddr) {
						uni.showToast({
							title:'请选择地址',
							icon:'error'
						})
						return
					}


					let query = {
						// 创建订单参数
					}
					let _this = this
					this.http({
						url: '创建订单接口',
						method: 'POST',
						isjson: true,
						data: query,
						success: res => {
                            // 创建订单成功的回调,拉起支付
							console.log(res, 'create order res')
							if (res.code == 200) {
                                //创建订单成功,接口会返回tradeNo,调支付
								my.tradePay({
								  tradeNO:res.data.aliPay.tradeNo,
								  success: (res1) => {
									  // console.log(res1,'支付成功 res1')
									  if(res1.resultCode == 9000){
										  // 文档上说9000是成功,到这里就成功了
									  }else {
										 // 支付不成功,具体原因看返回,去对照文档
									  }
								  },
								  fail: (err1) => {
									 // 支付不成功,具体原因看返回,去对照文档
								  }
								});
							}
						},
						fail: err => {
							// 创建订单失败的回调
						}
					})
				} else {
					// console.log('未同意协议')
					this.$nextTick(() => {})

					my.showToast({
						type: 'none',
						content: '请阅读并勾选协议',
						duration: 3000,
						success: () => {}
					})
				}
			},

7、支付宝小程序图片处理 

图片的处理看文档就好了支付宝小程序图片的官方文档

但是,这里有种情况,就是图片的宽度固定,但是高度不固定,一般来说设置image标签的mode属性为scaleToFill就好了,有的时候会出现图片被压缩的情况,这时候就直接设置mode为widthFix直接解决(文档里有)

2022-7-29更新

换了一家公司也有uniapp的项目,主要是做企微跟飞书小程序

1、企微小程序的调试

微信开发者工具上下一个企微模拟器,然后选择就好了,跟微信小程序一样的

2、视频标签video

可以先看官方文档,包括uniapp、微信、飞书的,每个平台对video的支持程度不一样。我接到的需求是接口返回视频列表,而我要让一个个视频看起来像是一个视频在播放,包括自定义进度条、倍速、开始、暂停等控件

首先这个需求在PC端已经有了实现,我的选择是copy,但是由于我太菜了,没有完成转化,就只能自己写了,当然也有借鉴

来看文件结构,在pages里新建一个页面咯,没有写成组件

 全部的代码,需要注意的是,因为是CV的,所以有很多无关的代码我没时间也没能力去处理,这里只说关键点

1、看文档,找到不同平台对于video标签的支持程度。这样就可以很好的实现播放、暂停、timeupdate等

2、获取video实例,小程序里不支持操作DOM,获取video实例是通过API获取哦。这块有个大坑,一定要看好,是xx.createVideoContext方法而不是xx.createAudioContext

  onReady: function (res) {
    this.videoContext = wx.createVideoContext('myVideo', this)
  },

3、控件的实现,就是去看文档,各种开放的API都有,按照业务逻辑去调用就好了

4、拖动的进度条,这块是借鉴了之前大佬的写法,具体的代码的movePoint方法

5、终极大坑,video的层级,高,z-index不好用,那么我下面的写法在微信\企微小程序上是可以盖住video的,但是在飞书小程序就不行了,所以想了一个方法,就是将video的100% - 底下控件的高度,也算是变相解决问题了

<template>
  <div class="live_detail" @tap="clickTouchend">
    <div class="video-stop" v-show="video.isPause"></div>
    <!-- #ifdef MP-LARK -->
    <video
      :show-play-btn="false"
      x5-playsinline="true"
      x5-video-player-type="h5-page"
      x5-video-player-fullscreen="true"
	:show-loading="true"
      :controls="false"
      :autoplay="true"
      style="
        object-fit: fill;
        position: absolute;
        width: 100%;
        height: calc(100% - 120px);
        top: 0;
        left: 0;
      "
      id="myVideo"
      :src="videoUrl"
      @pause="bindpause"
      @play="bindplay"
      @timeupdate="timeupdate"
      @error="videoErrorCallback"
      @ended="videoEnd"
    ></video>
    <view class="video-con flex align-center just-center">
      <view class="flex top">
		  <view></view>
		  <view class="video-time flex align-center">
		    <span class="current-time w54">{{ getVideoTime() }}</span>
		    <span class="line">/</span>
		    <span class="w54">{{ videoTotalTime() }}</span>
		  </view>
        <view class="flex just-center progress" @touchmove.native.stop="movePoint">
          <view class="line"></view>
          <view
            class="point"
            :style="{ left: finLeft }"
            @touchstart.native.stop="drag = true"
            unselectable="on"
            onselectstart="return false"
            style="-moz-user-select: none; -webkit-user-select: none; -ms-user-select: none"
          ></view>
        </view>
	  </view>
      <view class="footer flex">
        <img v-if="roomAbout.avatar" :src="roomAbout.avatar" alt class="head" />
        <img v-else :src="require('@/static/images/head.png')" alt class="head" />
        <view class="f_other flex just-center">
          <view class="title">{{ roomAbout.nickName || '暂无昵称' }}</view>
          <view class="count">
            <span>粉丝数{{ roomAbout.dyfansNum || ' 暂无' }}</span>
            <span>场观{{ roomAbout.watchTimes || ' 暂无' }}</span>
            <span>峰值{{ roomAbout.maxOnlineNum || ' 暂无' }}</span>
          </view>
        </view>
        <view></view>
      </view>
    </view>
    <!-- #endif -->
    <!-- #ifdef MP-WEIXIN -->
    <video
      x5-video-player-type="h5-page"
      :show-loading="true"
      :controls="false"
      :autoplay="true"
      object-fit="fill"
      style="object-fit: fill; position: absolute; width: 100%; height: 100%; top: 0; left: 0"
      id="myVideo"
      :src="videoUrl"
      preload
      @pause="bindpause"
      @play="bindplay"
      @timeupdate="timeupdate"
      @error="videoErrorCallback"
      @ended="videoEnd"
      @canplay="canplay"
      @waiting="videoWaiting"
      @playing="videoPlaying"
      @videoOver="videoOver"
    ></video>
    <!-- 控件 -->
    <div class="video-con flex align-center just-center">
      <div class="flex top">
        <!-- 进度条 -->
        <div class="flex just-center progress" @touchmove.native.stop="movePoint">
          <div class="line"></div>
          <div
            class="point"
            :style="{ left: finLeft }"
            @touchstart.native.stop="drag = true"
            unselectable="on"
            onselectstart="return false"
            style="-moz-user-select: none; -webkit-user-select: none; -ms-user-select: none"
          ></div>
        </div>
        <!-- 时间 -->
        <div class="video-time flex align-center">
          <span class="current-time w54">{{ getVideoTime() }}</span>
          <span class="line">/</span>
          <span class="w54">{{ videoTotalTime() || '00:00:00' }}</span>
        </div>
        <!-- 倍速 -->
        <div class="ratio-play">
          <div class="play-text" @touchend.native.stop="showChooseRatio">
            {{ ratioText() }}
          </div>
          <div
            class="choose-ratio-list flex col"
            @touchend.native.stop="showChooseRatio"
            :class="{ active: isShowChooseRatio }"
          >
            <div class="choose-item" @tap="changeRatio(1.5)">1.5X</div>
            <div class="choose-item" @tap="changeRatio(1.25)">1.25X</div>
            <div class="choose-item" @tap="changeRatio(1)">1.0X</div>
            <div class="choose-item" @tap="changeRatio(0.8)">0.8X</div>
            <div class="choose-item" @tap="changeRatio(0.5)">0.5X</div>
          </div>
        </div>
      </div>
      <div class="footer flex">
        <img v-if="roomAbout.avatar" :src="roomAbout.avatar" alt class="head" />
        <img v-else :src="require('@/static/images/head.png')" alt class="head" />
        <div class="f_other flex just-center">
          <div class="title">{{ roomAbout.nickName || '暂无昵称' }}</div>
          <div class="count">
            <span>粉丝数{{ roomAbout.dyfansNum || ' 暂无' }}</span>
            <span>场观{{ roomAbout.watchTimes || ' 暂无' }}</span>
            <span>峰值{{ roomAbout.maxOnlineNum || ' 暂无' }}</span>
          </div>
        </div>
        <div></div>
      </div>
    </div>
    <!-- #endif -->
  </div>
</template>
<script>
import { getLiveRoom } from '@/api/play-record/play-record.js'

export default {
  name: 'liveDetail',
  data() {
    return {
      roomAbout: {}, // 所有返回
      videoList: [], // 视频列表
      videoListTotal: 0, // 视频列表长度
      totalSeconds: 0, // 视频总长度
      videoUrl: '', // 播放地址
      videoCurrentIndex: 0, // 这个应该是当前播放的视频索引
      ratio: 1, // 视频倍速
      video: {
        //视频有关内容
        currentTime: 0, // 当前时间
        isPause: true, // 是否显示暂停按钮
        timeArr: [], // 存储每个视频长度,目前不知道功能
        isClick: false, // unknow
        copyTime: 0, //unknow
        volume: 1 // unknow
      },
      videoTime: 0, // 不知道作用
      isShowChooseRatio: false, //显示倍速
      left: 0, // 滑动点的位置
      finLeft: 0, // 滑动点的位置
    }
  },
  // 监听视频时间变化
  watch: {
    'video.currentTime': {
      handler(val) {
        this.computeLeft()
        // this.timeupdate()
      }
    }
  },
  onReady: function (res) {
    this.videoContext = wx.createVideoContext('myVideo', this)
  },
  onLoad(options) {
    let query = {
      getRecordUrl: 1,
      getRoomDetail: 1,
      brandId: options.brandId,
      roomId: options.roomId,
      userId: ''
    }
    this.getLive(query)
  },
  methods: {
    videoErrorCallback: function (e) {
      this.$toast(e.detail.errMsg)
    },

    bindplay() {
      // // #ifdef MP-LARK
      // this.videoContext.exitFullScreen()
      // //  #endif
      this.video.isPause = false
    },
    bindpause() {
      this.video.isPause = true
    },
    computeLeft() {
      const total = this.videoTime + this.video.currentTime
      const left = (total / this.totalSeconds) * 360
      this.left = left + 'rpx'
      this.finLeft = (total / this.totalSeconds) * 360 + 'rpx'
    },
    movePoint(e) {
      let _this = this
      if (this.drag) {
        const line = uni.createSelectorQuery().in(this).select('.progress')
        line
          .boundingClientRect(function (data) {
            const x = ((e.touches[0].clientX - data.left) / data.width) * _this.totalSeconds
            const max = Math.max(..._this.video.timeArr)
            const item = _this.video.timeArr.find(i => i < x)
            if (item) {
              // 控制point如果超过最后一个视频,就不能滑动了
              if (x - item > max) {
                return
              }
              _this.calcVideo(x - item)
            }
          })
          .exec(function (res) {
            // 注意:exec方法必须执行,即便什么也不做,否则不会获取到任何数据
          })
      }
    },

    showChooseRatio() {
      if (this.ratioTimer) {
        clearTimeout(this.ratioTimer)
      }
      this.isShowChooseRatio = !this.isShowChooseRatio
    },
    // 获取视频
    getLive(query) {
      getLiveRoom(query).then(res => {
        if (res.code !== 0) return
        // console.log(res, 'res.data.liveRoomVideo')
        this.roomAbout = res.data
        this.videoList = res.data.liveRoomVideo
        this.videoListTotal = res.data.liveRoomVideo.length
        this.videoList.forEach(item => {
          item.videoStartTime = item.videoStartTime / 1000
          item.videoEndTime = item.videoEndTime / 1000
          item.videoTime = item.videoEndTime - item.videoStartTime
        })
        let total = 0
        this.videoList.forEach(item => {
          total += item.videoTime
          this.video.timeArr.push(total)
        })
        this.totalSeconds = total
        if (this.videoList.length > 0) {
          if (this.videoList.length === 1) {
            this.videoUrl = this.videoList[0].videoUrl
            this.videoCurrentIndex = 0
          } else {
            this.videoUrl = this.videoList[1].videoUrl
            this.videoCurrentIndex = 1
            this.calcVideo(this.video.timeArr[0] + 1)
          }
        }
      })
    },
    // 处理视频获取数据
    calcVideo(total, isPlaying = false) {
      if (this.videoList.length === 1) {
        this.video.currentTime = total
        this.videoContext.currentTime = total
        this.video.isPause = true
        return
      }
      if (this.videoList.length === 0) {
        const timeText = this.formatTime(total).split(':').slice(0, 3).join(':')
      }
      let index = -1
      for (let i = 0; i < this.video.timeArr.length - 1; i++) {
        if (this.video.timeArr[0] >= total) {
          if (this.videoCurrentIndex !== i) {
            this.videoUrl = this.videoList[0].videoUrl
          }
          this.videoTime = 0
          this.videoCurrentIndex = 0
          Promise.resolve().then(res => {
            console.log(res, 'calcVideo promise resolve res')
            this.video.currentTime = total
            this.videoContext.currentTime = total
            this.video.isClick = true
            this.video.copyTime = total
          })
          this.video.isPause = true
          index = 0
          break
        }
        if (total >= this.video.timeArr[i] && total <= this.video.timeArr[i + 1]) {
          index = i
          //   this.clickStatus = true
          this.videoUrl = this.videoList[index + 1].videoUrl
          this.videoCurrentIndex = index + 1
          this.videoTime = this.video.timeArr[i]
          this.video.isClick = true
          this.video.copyTime = total - this.video.timeArr[i]
          Promise.resolve().then(() => {
            this.video.currentTime = total - this.video.timeArr[i]
            this.videoContext.currentTime = total - this.video.timeArr[i]
          })
          this.video.isPause = true
          break
        }
        if (
          total >= this.video.timeArr[this.video.timeArr.length - 2] &&
          total <= this.video.timeArr[this.video.timeArr.length - 1]
        ) {
          this.videoUrl = this.videoList[this.videoList.length - 1].videoUrl
          this.videoCurrentIndex = this.videoList.length - 1
          this.videoTime = this.video.timeArr[this.video.timeArr.length - 2]
          //   this.clickStatus = true
          this.video.isClick = true
          this.video.copyTime = total - this.video.timeArr[this.video.timeArr.length - 2]
          Promise.resolve().then(() => {
            this.video.currentTime = total - this.video.timeArr[this.video.timeArr.length - 2]
            this.videoContext.currentTime =
              total - this.video.timeArr[this.video.timeArr.length - 2]
          })
          this.video.isPause = true
          break
        }
      }
      if (!isPlaying) {
        this.videoContext && this.videoContext.pause()
        this.video.isPause = true
      } else {
        setTimeout(() => {
          this.videoContext && this.videoContext.play()
        }, 100)
        this.video.isPause = false
      }
    },

    // 视频播放时
    timeupdate(e) {
      // 视频在播放
      this.video.isPause = false
      if (this.videoContext) {
        this.videoContext.playbackRate = this.ratio //倍速
        this.videoContext.volume = this.video.volume
        this.video.currentTime = parseInt(e.detail.currentTime)
      }
    },

    canplay() {
      if (this.video.isClick) {
        this.videoContext.currentTime = this.video.copyTime
        this.video.isClick = false
        this.videoContext.playbackRate = this.ratio
        this.videoContext.volume = this.video.volume
      }
    },

    videoEnd() {
      if (this.videoCurrentIndex === this.videoList.length - 1) {
        // alert(1)
        this.videoUrl = this.videoList[0].videoUrl
        this.videoCurrentIndex = 0
        this.videoTime = 0
      } else {
        this.videoTime = this.videoTime + this.video.currentTime
        this.videoCurrentIndex++
        this.videoUrl = this.videoList[this.videoCurrentIndex].videoUrl
      }
      Promise.resolve().then(res => {
        console.log(res, 'videoEnd promise resolve res')
        this.videoContext.play()
        this.video.isPause = false
        this.video.iconShow = false
      })
    },
    videoWaiting() {
      if (!this.video.isPause) {
        this.video.isLoading = true
      }
    },
    videoPlaying() {
      this.video.isLoading = false
    },
    toggleVideoStatus() {
      if (this.video.isPause) {
        this.video.isPause = false
        this.video.coverShow = false
        this.videoContext.play()
      } else {
        this.video.isPause = true
        this.videoContext.pause()
      }
    },

    clickTouchend() {
      if (this.video.isPause) {
        // 暂停状态
        this.video.isPause = false
        this.videoContext.play()
      } else {
        this.videoContext.pause()
        this.video.isPause = true
      }
    },
    changeRatio(ratio) {
      this.ratio = ratio
      this.videoContext.playbackRate = ratio
      this.$nextTick(() => {
        this.isShowChooseRatio = false
      })
    },
    // 选择倍速
    ratioText() {
      if (this.ratio === 1) {
        return '倍速'
      } else {
        return this.ratio + 'X'
      }
    },
    // 获取视频总时间
    videoTotalTime() {
      const time = this.totalSeconds
      if (!time) return
      let h = parseInt(time / 60 / 60)
      let m = Math.floor((time % 3600) / 60)
      let s = Math.floor(time % 60)
      // h >= 24 ? h - 24 : h
      h = h >= 10 ? h : `0${h}`
      m = m >= 10 ? m : `0${m}`
      s = s >= 10 ? s : `0${s}`
      return `${h}:${m}:${s}`
    },
    // 获取视频当前播放的时间
    getVideoTime() {
      const total = this.videoTime + this.video.currentTime
      // console.log(this.videoTime, this.video.currentTime)
      return this.formatTime(total)
    },
    formatTime(time) {
      // 格式化时分秒
      let h = Math.floor(time / 3600)
      let m = Math.floor((time % 3600) / 60)
      let s = Math.floor(time % 60)
      // h >= 24 ? h - 24 : h
      h = h >= 10 ? h : `0${h}`
      m = m >= 10 ? m : `0${m}`
      s = s >= 10 ? s : `0${s}`
      return `${h}:${m}:${s}`
    }
  }
}
</script>
<style lang="less" scoped>
.flex {
  display: flex;
}
.warp {
  flex-wrap: wrap;
}
.col {
  flex-direction: column;
}
.just-center {
  justify-content: center;
}
.just-start {
  justify-content: flex-start;
}
.just-end {
  justify-content: flex-end;
}
.just-a {
  justify-content: space-around;
}
.just-b {
  justify-content: space-between;
}
.align-center {
  align-items: center;
}
.align-end {
  align-items: flex-end;
}
.live_detail {
  width: 100vw;
  height: 100vh;
  position: relative;

  .video-stop {
    width: 98rpx;
    height: 98rpx;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 11;
    background: url(https://tagvvcloud-brandapp-1256030678.cos.ap-guangzhou.myqcloud.com/49e6cf22c7c94c7a92a61288d45d0ec8play.png)
      no-repeat;
    background-size: 100% 100%;
  }

  .video-con {
    // #ifdef MP-LARK
    position: fixed;
    align-items: unset !important;
    // #endif
    z-index: 100;
    // #ifdef MP-WEIXIN
    position: absolute;
    // #endif
    left: 0;
    bottom: 0;
    width: 100%;
    height: 240rpx;
    background: rgba(0, 0, 0, 0.5);
    flex-direction: column;

    .footer {
      // #ifdef MP-LARK
      margin-left: 24rpx;
      // #endif
      align-items: center;
      font-size: 28rpx;
      font-weight: 400;
      color: #ffffff;
      margin-top: 24rpx;
      .head {
        width: 96rpx;
        height: 96rpx;
        background: #d8d8d8;
        border-radius: 50%;
      }
      .f_other {
        margin-left: 24rpx;
        flex-direction: column;

        .count {
          margin-top: 16rpx;
          span {
            display: inline-block;
            margin-right: 80rpx;
            white-space: nowrap;
          }
          span:last-child {
            margin-right: unset;
          }
        }
      }
    }
    > div {
      width: calc(100% - 48rpx);
    }
    .top {
      justify-content: space-between !important;
      align-items: center;
      // #ifdef MP-LARK
      margin-left: 24rpx;
      margin-right: 24rpx;
      // #endif
    }
    .video-time {
      height: 28rpx;
      font-size: 20rpx;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: #ffffff;
      line-height: 28rpx;
      opacity: 0.4;
      .current-time {
        color: #fff;
      }
    }
    .ratio-play {
      position: relative;
      color: rgba(255, 255, 255, 0.6);
      .play-text {
        font-size: 24rpx;
        width: 80rpx;
        height: 40rpx;
        line-height: 40rpx;
        text-align: center;
        background: rgba(0, 0, 0, 0.5);
        border-radius: 6rpx;
      }
      .choose-ratio-list {
        width: 80rpx;
        position: absolute;
        padding: 10rpx 0;
        left: 0rpx;
        top: -234rpx;
        flex-direction: column-reverse;
        background: rgba(0, 0, 0, 0.7);
        border-radius: 2rpx;
        display: none;
        transition: all 0.2s;
        font-size: 20rpx;
        &.active {
          display: block;
        }
        .choose-item {
          text-align: center;
          line-height: 34rpx;
		  // #ifdef MP-LARK
			line-height: 42rpx;
		  // #endif
          width: 80rpx;
          transition: all 0.2s;
        }
      }
    }
    .progress {
      width: 360rpx;
      height: 4rpx;
      line-height: 4rpx;
      background: #ffffff;
      border-radius: 2rpx;
      align-items: center;
      position: relative;
      .line {
        width: 100%;
        height: 4rpx;
        background-color: #fff;
      }
      .point {
        cursor: pointer;
        width: 24rpx;
        height: 16rpx;
        background-color: #fff;
        border-radius: 8rpx;
        position: absolute;
        left: v-bind(left);
        top: 50%;
        transform: translateY(-50%);
      }
    }
  }
}
</style>

2022-10-8 更新

实现下载视频到相册的功能

直接上代码,这里下载视频的方法uni.downloadFile返回一个对象,里面有百分比,已下载的大小等信息,还有停止下载的方法(详见官网),可以实现下载进度条,停止下载等操作

    

    <div class="preview_btn" @click="saveVideo(videoUrl)">保存视频到相册</div>



     // 下载方法,传入要保存视频的路径
    download(url, _this) {
      // 下载视频,可以返回一个 downloadTask 对象来获取保存进度(百分比),详见官网
      uni.downloadFile({
        url,
        success: res => {
          if (res.statusCode === 200) {
            // 保存到相册
            uni.saveVideoToPhotosAlbum({
              filePath: res.tempFilePath,
              success: function () {
                uni.showToast({
                  title: '保存成功!',
                  icon: 'success'
                })
                setTimeout(() => {
                  uni.hideLoading()
                }, 1500);
              },
              fail: res => {
                console.log(res.errMsg)
                return uni.showToast({
                  title: res.errMsg,
                  icon: 'none'
                })
              },
              complete: res => {
                uni.hideLoading()
              }
            })
          }
        }
      })
    },

    // 将视频保存到相册,
    saveVideo(url) {
      console.log('saveVideo url-->', url)
      const _this = this
      if (!url) return
      uni.showLoading({ title: '保存中~', mask: true })
      //获取用户的当前设置。获取相册权限
      uni.getSetting({
        success: res => {
          //如果没有相册权限
          if (!res.authSetting['scope.writePhotosAlbum']) {
            //向用户发起授权请求
            uni.authorize({
              scope: 'scope.writePhotosAlbum',
              success: () => {
                //授权成功,开始下载视频
                _this.download(url, _this)
              },
              //授权失败
              fail: () => {
                uni.hideLoading()
                uni.showModal({
                  title: '您已拒绝获取相册权限',
                  content: '是否进入权限管理,调整授权?',
                  success: res => {
                    if (res.confirm) {
                      //调起客户端小程序设置界面,返回用户设置的操作结果。(重新让用户授权)
                      uni.openSetting({
                        success: res => {
                          console.log(res.authSetting)
                        }
                      })
                    } else if (res.cancel) {
                      return uni.showToast({
                        title: '已取消!',
                        icon: 'none'
                      })
                    }
                  }
                })
              }
            })
          } else {
            //如果已有相册权限,开始下载视频
            _this.download(url, _this)
          }
        },
        fail: res => {}
      })
    }

  • 17
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值