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 => {}
})
}