8.21提交
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
|
||||
<template>
|
||||
<view class="plv-player-drag-tips">
|
||||
<view class="plv-player-drag-tips__box">
|
||||
<image class="plv-player-drag-tips__direction-img" :src="imgSrc"
|
||||
@error="imageError">
|
||||
</image>
|
||||
<text class="plv-player-drag-tips__time">
|
||||
{{changeTimeFormat}} / {{durationFormat}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '@/common/util.js';
|
||||
import backImg from '@/static/skin/back.png';
|
||||
import forwardImg from '@/static/skin/forward.png'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
currentTime: {
|
||||
type: Number
|
||||
},
|
||||
|
||||
duration: {
|
||||
type: Number
|
||||
},
|
||||
|
||||
changeTime: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isForward: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
changeTime(newValue, oldValue){
|
||||
this.isForward = newValue > oldValue;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timeFormatType() {
|
||||
this.duration >= 3600 ? 2 : 1;
|
||||
},
|
||||
|
||||
changeTimeFormat() {
|
||||
return utils.formatTimeByDuration(this.changeTime, this.timeFormatType);
|
||||
},
|
||||
|
||||
durationFormat() {
|
||||
return utils.formatTimeByDuration(this.duration, this.timeFormatType)
|
||||
},
|
||||
|
||||
imgSrc() {
|
||||
return this.isForward ? forwardImg : backImg;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.plv-player-drag-tips {
|
||||
width: 250rpx;
|
||||
height: 160rpx;
|
||||
background: rgba(0,0,0,.7);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.plv-player-drag-tips__box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plv-player-drag-tips__direction-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.plv-player-drag-tips__time {
|
||||
font-size: 24rpx;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<view class="plv-player-gesture"
|
||||
@touchstart="touchstart"
|
||||
@touchmove="touchmove"
|
||||
@longpress="handleLongPress"
|
||||
@touchcancel="handleTouchCancel"
|
||||
@touchend="touchend">
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// 屏幕宽度(物理像素)
|
||||
holeWidth: Number,
|
||||
// 视频总时长
|
||||
duration: Number,
|
||||
// 视频当前播放时间
|
||||
currentTime: Number
|
||||
},
|
||||
|
||||
computed: {
|
||||
halfWidth() {
|
||||
return parseInt(this.holeWidth / 2);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 是否在左区域按下手势
|
||||
downLeft: false,
|
||||
// 滑动方向
|
||||
swipeDir: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.addHandler();
|
||||
},
|
||||
|
||||
methods:{
|
||||
addHandler() {
|
||||
// 增加父容器左右滑动监听
|
||||
let finalTime = -1;
|
||||
// 计算左右滑动seek进度
|
||||
const countTime = (position) => {
|
||||
const { holeWidth, duration, currentTime } = this;
|
||||
const precent = Math.abs(position) / holeWidth;
|
||||
const nowTime = currentTime || 0;
|
||||
const constSecond = duration < 60 ? duration : 60;
|
||||
let plusTime = parseInt(Math.abs(precent * constSecond));
|
||||
if (position < 0) plusTime = plusTime * -1;
|
||||
finalTime = nowTime + plusTime;
|
||||
finalTime = this.limit(finalTime, 0, duration);
|
||||
return finalTime;
|
||||
};
|
||||
|
||||
this.addGesture((type, position) => {
|
||||
switch (type) {
|
||||
// 下滑
|
||||
case -1:
|
||||
// console.log('//下滑');
|
||||
this.$emit('onGestureEvent', {
|
||||
type: this.downLeft ? 'LEFT_DOWN' : 'RIGHT_DOWN',
|
||||
position
|
||||
});
|
||||
break;
|
||||
// 上滑
|
||||
case -2:
|
||||
// console.log('//上滑');
|
||||
this.$emit('onGestureEvent', {
|
||||
type: this.downLeft ? 'LEFT_UP' : 'RIGHT_UP',
|
||||
position
|
||||
});
|
||||
break;
|
||||
// 左滑
|
||||
case 0:
|
||||
// console.log('//左滑');
|
||||
this.swipeDir = type;
|
||||
countTime(position);
|
||||
this.$emit('onGestureEvent', {
|
||||
type: 'SEEK_TIME_UPDATE',
|
||||
finalTime
|
||||
});
|
||||
break;
|
||||
// 右滑
|
||||
case 1:
|
||||
// console.log('//右滑');
|
||||
this.swipeDir = type;
|
||||
countTime(position);
|
||||
this.$emit('onGestureEvent', {
|
||||
type: 'SEEK_TIME_UPDATE',
|
||||
finalTime
|
||||
});
|
||||
break;
|
||||
// touchend for swipe
|
||||
case 3:
|
||||
if (this.swipeDir > -1) {
|
||||
this.$emit('onGestureEvent', {
|
||||
type: this.swipeDir ? 'SWIPE_RIGHT' : 'SWIPE_LEFT'
|
||||
});
|
||||
this.swipeDir = -1;
|
||||
}
|
||||
|
||||
if (finalTime > -1) {
|
||||
this.$emit('onGestureSeekTo', finalTime);
|
||||
finalTime = -1;
|
||||
}
|
||||
break;
|
||||
// doubleTap
|
||||
case 4:
|
||||
// console.log('>>>> 双击');
|
||||
this.$emit('onGestureEvent', {
|
||||
type: 'DOUBLE_CLICK'
|
||||
});
|
||||
break;
|
||||
// tap
|
||||
case 5:
|
||||
// console.log('>>>> 单击');
|
||||
this.handleClick();
|
||||
break;
|
||||
// longTap
|
||||
case 6:
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addGesture(callback) {
|
||||
const { options } = this;
|
||||
let startX, startY, moveEndX, moveEndY, X, Y, position, hasType;
|
||||
let T, lastTap, isJustTouch = true;
|
||||
|
||||
|
||||
const excuteCb = (type) => {
|
||||
// type 手势类型 -2 上滑 -1 下滑 0 左滑 1 右滑 2 just touch 3 touchend 4 doubleTap 5 tap 6 longTap
|
||||
if (callback && typeof callback === 'function')
|
||||
callback(type, position);
|
||||
};
|
||||
|
||||
const YPlus = 20;
|
||||
const YReduce = YPlus * -1;
|
||||
const XPlus = 50;
|
||||
const XReduce = XPlus * -1;
|
||||
|
||||
const isXchange = () => {
|
||||
if (X > XPlus || X < XReduce) {
|
||||
position = X;
|
||||
hasType = 'X';
|
||||
isJustTouch = false;
|
||||
}
|
||||
|
||||
if (X > XPlus) excuteCb(1);
|
||||
// 左滑
|
||||
else if (X < XReduce) excuteCb(0);
|
||||
};
|
||||
|
||||
const isYchange = () => {
|
||||
if (Y > YPlus || Y < YReduce) {
|
||||
position = Y;
|
||||
hasType = 'Y';
|
||||
isJustTouch = false;
|
||||
}
|
||||
|
||||
if (Y > YPlus) excuteCb(-1);
|
||||
// 上滑
|
||||
else if (Y < YReduce) excuteCb(-2);
|
||||
};
|
||||
|
||||
const countDirect = () => {
|
||||
if (hasType) {
|
||||
hasType === 'X' ? isXchange() : isYchange();
|
||||
return;
|
||||
}
|
||||
isXchange();
|
||||
isYchange();
|
||||
};
|
||||
|
||||
this.handleTouch = (type, e) => {
|
||||
switch(type) {
|
||||
case 'start':
|
||||
startX = e.changedTouches[0].pageX;
|
||||
startY = e.changedTouches[0].pageY;
|
||||
T = Date.now();
|
||||
hasType = '';
|
||||
isJustTouch = true;
|
||||
this.downLeft = startX < this.halfWidth ? true : false;
|
||||
|
||||
break;
|
||||
case 'move':
|
||||
moveEndX = e.changedTouches[0].pageX;
|
||||
moveEndY = e.changedTouches[0].pageY;
|
||||
X = moveEndX - startX;
|
||||
Y = moveEndY - startY;
|
||||
countDirect();
|
||||
break;
|
||||
case 'end':
|
||||
if (isJustTouch)
|
||||
if (lastTap && Date.now() - lastTap <= 300)
|
||||
excuteCb(4);
|
||||
else if (Date.now() - T < 1000)
|
||||
excuteCb(5);
|
||||
else
|
||||
excuteCb(6);
|
||||
else
|
||||
excuteCb(3);
|
||||
|
||||
lastTap = Date.now();
|
||||
isJustTouch = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
handleClick() {
|
||||
this.$emit('onGestureClick');
|
||||
},
|
||||
|
||||
touchstart(e) {
|
||||
this.handleTouch('start', e);
|
||||
},
|
||||
|
||||
touchmove(e) {
|
||||
this.handleTouch('move', e);
|
||||
},
|
||||
|
||||
touchend(e) {
|
||||
this.handleTouch('end', e);
|
||||
this.$emit('onTouchEnd');
|
||||
},
|
||||
|
||||
handleTouchCancel() {
|
||||
this.$emit('onTouchCancel');
|
||||
},
|
||||
|
||||
handleLongPress() {
|
||||
this.$emit('onLongPress');
|
||||
},
|
||||
|
||||
/**
|
||||
* 控制最小值,最大值
|
||||
* @param num
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns {*}
|
||||
*/
|
||||
limit(num, min, max) {
|
||||
if (num < min) return min;
|
||||
if (num > max) return max;
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.plv-player-gesture {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.plv-player-gesture__left {
|
||||
flex: 5;
|
||||
}
|
||||
|
||||
.plv-player-gesture__right {
|
||||
flex: 5;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view v-if="isShow" class="plv-player-icon">
|
||||
<image class="plv-player-icon__img" :src="imgUrl"></image>
|
||||
<text class="plv-player-icon__text">{{ precent + '%' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
imgUrl: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
precent: {
|
||||
type: 'Number',
|
||||
default: 100,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
precent(num) {
|
||||
if (!this.isShow) this.isShow = true;
|
||||
this.startClock();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isShow: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
startClock() {
|
||||
this.clearClock();
|
||||
|
||||
this.clock = setTimeout(() => {
|
||||
this.isShow = false;
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
clearClock() {
|
||||
if (this.clock) clearTimeout(this.clock);
|
||||
this.clock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.plv-player-icon {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plv-player-icon__img {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.plv-player-icon__text {
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<view v-if="isShow" class="plv-player-volume-wrap">
|
||||
<image class="plv-player-volume__img" :src="value === 0 ? muteImgUrl : imgUrl "></image>
|
||||
<view class="plv-player-volume__progress">
|
||||
<view class="plv-player-volume__progress__played" :style="'flex:' + value"></view>
|
||||
<view class="plv-player-volume__progress_surplus" :style="'flex:' + valueLeft"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
imgUrl: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
muteImgUrl: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: 'Number',
|
||||
default: 0.5,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(num) {
|
||||
if (!this.isShow) this.isShow = true;
|
||||
this.startClock();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
valueLeft() {
|
||||
return 1 - this.value;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isShow: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
startClock() {
|
||||
this.clearClock();
|
||||
|
||||
this.clock = setTimeout(() => {
|
||||
this.isShow = false;
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
clearClock() {
|
||||
if (this.clock) clearTimeout(this.clock);
|
||||
this.clock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.plv-player-volume-wrap {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plv-player-volume__img {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.plv-player-volume__progress {
|
||||
width: 260rpx;
|
||||
height: 8rpx;
|
||||
flex-direction: row;
|
||||
background-color: #09BB07;
|
||||
}
|
||||
|
||||
.plv-player-volume__progress__played {
|
||||
background-color: #007AFF;
|
||||
}
|
||||
|
||||
.plv-player-volume__progress_surplus {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<view class="custom-tabbar">
|
||||
<view
|
||||
class="tabbar-item"
|
||||
v-for="(item, index) in tabList"
|
||||
:key="index"
|
||||
@click="switchTab(index, item)"
|
||||
:class="{ active: currentTab === index }"
|
||||
>
|
||||
<!-- 图标容器 -->
|
||||
<view class="icon-container">
|
||||
<!-- <uni-icons
|
||||
:type="currentTab === index ? item.activeIcon : item.icon"
|
||||
:size="currentTab === index ? 28 : 24"
|
||||
:color="currentTab === index ? item.activeColor : item.color"
|
||||
></uni-icons> -->
|
||||
<image class="img" :src="currentTab === index ? item.activeIcon : item.icon" alt="" />
|
||||
<!-- 徽章 -->
|
||||
<view class="badge" v-if="item.badge && item.badge > 0">
|
||||
<uni-badge
|
||||
:text="item.badge > 99 ? '99+' : item.badge.toString()"
|
||||
type="error"
|
||||
size="small"
|
||||
></uni-badge>
|
||||
</view>
|
||||
|
||||
<!-- 红点 -->
|
||||
<view class="red-dot" v-if="item.showRedDot"></view>
|
||||
</view>
|
||||
|
||||
<!-- 文字 -->
|
||||
<text class="tab-text" :class="{ active: currentTab === index }">
|
||||
{{ item.text }}
|
||||
</text>
|
||||
|
||||
<!-- 动画效果 -->
|
||||
<!-- <view class="active-indicator" v-if="currentTab === index"></view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { onShow, onLoad } from "@dcloudio/uni-app";
|
||||
import home from '@/static/home_nor.png'
|
||||
import homeOn from '@/static/home_sel.png'
|
||||
import classroom from "@/static/classroom.png"
|
||||
import classroomOn from "@/static/classroomOn.png"
|
||||
import live from "@/static/live.png"
|
||||
import liveOn from "@/static/liveOn.png"
|
||||
import education from '@/static/education.png'
|
||||
import educationOn from '@/static/educationOn.png'
|
||||
import my from '@/static/my.png'
|
||||
import myOn from '@/static/myOn.png'
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
// 可以添加自定义props
|
||||
});
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits(['tabChange']);
|
||||
|
||||
// 当前选中的tab
|
||||
const currentTab = ref(0);
|
||||
|
||||
// tabbar数据
|
||||
const tabList = reactive([
|
||||
{
|
||||
icon: home,
|
||||
activeIcon: homeOn,
|
||||
text: '首页',
|
||||
color: '#999999',
|
||||
activeColor: '#007aff',
|
||||
badge: 0,
|
||||
showRedDot: false,
|
||||
pagePath: '/pages/index/index'
|
||||
},
|
||||
{
|
||||
icon: classroom,
|
||||
activeIcon: classroomOn,
|
||||
text: '患教学堂',
|
||||
color: '#999999',
|
||||
activeColor: '#007aff',
|
||||
badge: 5,
|
||||
showRedDot: false,
|
||||
pagePath: '/pages/education/education'
|
||||
},
|
||||
{
|
||||
icon: live,
|
||||
activeIcon: liveOn,
|
||||
text: '会议·直播',
|
||||
color: '#999999',
|
||||
activeColor: '#007aff',
|
||||
badge: 0,
|
||||
showRedDot: true,
|
||||
pagePath: '/pages/meeting/meeting'
|
||||
},
|
||||
{
|
||||
icon: education,
|
||||
activeIcon:educationOn,
|
||||
text: '继续教育',
|
||||
color: '#999999',
|
||||
activeColor: '#007aff',
|
||||
badge: 12,
|
||||
showRedDot: false,
|
||||
pagePath: '/pages/education/continuing'
|
||||
},
|
||||
{
|
||||
icon: my,
|
||||
activeIcon: myOn,
|
||||
text: '我的',
|
||||
color: '#999999',
|
||||
activeColor: '#007aff',
|
||||
badge: 0,
|
||||
showRedDot: false,
|
||||
pagePath: '/pages/profile/profile'
|
||||
}
|
||||
]);
|
||||
|
||||
// 切换tab
|
||||
const switchTab = (index, item) => {
|
||||
if (currentTab.value === index) return;
|
||||
|
||||
// 更新当前tab
|
||||
currentTab.value = index;
|
||||
|
||||
// 清除红点
|
||||
if (item.showRedDot) {
|
||||
item.showRedDot = false;
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
uni.switchTab({
|
||||
url: item.pagePath,
|
||||
fail: () => {
|
||||
// 如果页面不存在,使用navigateTo
|
||||
uni.navigateTo({
|
||||
url: item.pagePath,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 触发父组件事件
|
||||
emit('tabChange', {
|
||||
index,
|
||||
item
|
||||
});
|
||||
};
|
||||
|
||||
// 设置徽章数量
|
||||
const setBadge = (index, count) => {
|
||||
if (index >= 0 && index < tabList.length) {
|
||||
tabList[index].badge = count;
|
||||
}
|
||||
};
|
||||
|
||||
// 显示红点
|
||||
const showRedDot = (index) => {
|
||||
if (index >= 0 && index < tabList.length) {
|
||||
tabList[index].showRedDot = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏红点
|
||||
const hideRedDot = (index) => {
|
||||
if (index >= 0 && index < tabList.length) {
|
||||
tabList[index].showRedDot = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 设置当前tab
|
||||
const setCurrentTab = (index) => {
|
||||
if (index >= 0 && index < tabList.length) {
|
||||
currentTab.value = index;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前tab信息
|
||||
const getCurrentTab = () => {
|
||||
return {
|
||||
index: currentTab.value,
|
||||
item: tabList[currentTab.value]
|
||||
};
|
||||
};
|
||||
|
||||
// 更新tabbar数据
|
||||
const updateTabList = (newTabList) => {
|
||||
if (Array.isArray(newTabList)) {
|
||||
tabList.splice(0, tabList.length, ...newTabList);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
setBadge,
|
||||
showRedDot,
|
||||
hideRedDot,
|
||||
setCurrentTab,
|
||||
getCurrentTab,
|
||||
updateTabList,
|
||||
currentTab,
|
||||
tabList
|
||||
});
|
||||
|
||||
// 页面加载时获取当前页面路径
|
||||
onLoad(() => {
|
||||
nextTick(() => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPath = '/' + currentPage.route;
|
||||
|
||||
// 根据当前页面路径设置选中的tab
|
||||
const tabIndex = tabList.findIndex(item => item.pagePath === currentPath);
|
||||
if (tabIndex !== -1) {
|
||||
currentTab.value = tabIndex;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面显示时更新状态
|
||||
onShow(() => {
|
||||
// 可以在这里更新徽章数量等状态
|
||||
console.log('Tabbar onShow - 当前tab:', currentTab.value);
|
||||
});
|
||||
|
||||
// 组件挂载后
|
||||
onMounted(() => {
|
||||
console.log('Tabbar组件已挂载');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
height: 120rpx;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
z-index: 999;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.tabbar-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.img{
|
||||
width: 46rpx;
|
||||
height: 46rpx;
|
||||
}
|
||||
.icon-container {
|
||||
width:100%;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.red-dot {
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background-color: #ff0000;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #ffffff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tab-text.active {
|
||||
color: #8B2316;
|
||||
font-weight: 600;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.active-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: linear-gradient(90deg, #007aff, #0056b3);
|
||||
border-radius: 2rpx;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
width: 40rpx;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 点击效果 */
|
||||
.tabbar-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 750rpx) {
|
||||
.custom-tabbar {
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.custom-tabbar {
|
||||
background-color: #1c1c1e;
|
||||
border-top-color: #2c2c2e;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
.tab-text.active {
|
||||
color: #0a84ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<view class="wrap">
|
||||
<view :class="isFull ? 'player-full' : 'player'">
|
||||
<plv-player
|
||||
ref="vod"
|
||||
class="vod-player"
|
||||
seekType=1
|
||||
autoPlay=true
|
||||
disableScreenCAP=false
|
||||
rememberLastPosition=false
|
||||
@onPlayStatus="onPlayStatus"
|
||||
@onPlayError="onPlayError"
|
||||
@positionChange="positionChange"
|
||||
@onEnableSubtitle="onEnableSubtitle"
|
||||
@onSRTTextConfig="onSRTTextConfig"
|
||||
@onSRTTitle="onSRTTitle">
|
||||
</plv-player>
|
||||
<skin ref="skin"
|
||||
class="skin-control"
|
||||
:defaultVolume="defaultVolume"
|
||||
@onPlayBtnClick="onPlayBtnClick"
|
||||
@onToSeek="onToSeek"
|
||||
@onFullBtnClick="onFullBtnClick"
|
||||
@onHdBtnClick="onHdBtnClick"
|
||||
@onRateBtnClick="onRateBtnClick"
|
||||
@onVolumeChanged="onVolumeChanged"
|
||||
@onScalingBtnClick="onScalingBtnClick"
|
||||
@onScreenShot="onScreenShot"
|
||||
@onChangeSRTBtnClick="onChangeSRTBtnClick"
|
||||
@onChangeSRTSingleMode="onChangeSRTSingleMode"
|
||||
@onOpenSRT="onOpenSRT">
|
||||
</skin>
|
||||
</view>
|
||||
<view v-if="!isFull" style="padding: 20rpx;">
|
||||
<input class="uni-input" style="min-height: 35px; height: 35px; border: 1px solid #cccccc;font-size: 14px;" :value="videoVid"
|
||||
@input="onVidInput" placeholder="请输入视频vid" />
|
||||
<button type="primary" @click="setVid()">播放</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Skin from '@/components/plv-player-skin/skin.nvue';
|
||||
import DragTips from '@/components/plv-player-skin/drag-tips.nvue';
|
||||
const dom = weex.requireModule('dom');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Skin
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.vodPlayer = this.$refs.vod;
|
||||
this.skin = this.$refs.skin;
|
||||
// plus.screen.lockOrientation('portrait-primary');
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
title: '播放器Demo',
|
||||
// 播放器组件
|
||||
vodPlayer: null,
|
||||
// 控制栏组件
|
||||
skin: null,
|
||||
//是否全屏
|
||||
isFull: false,
|
||||
// 默认音量
|
||||
defaultVolume: 0.5,
|
||||
videoVid: 'cfb7a69a75b5004ddb137d7bd96aa7d7_c'
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
//输入vid
|
||||
onVidInput: function(event) {
|
||||
this.videoVid = event.detail.value;
|
||||
},
|
||||
|
||||
setVid() {
|
||||
if(!this.videoVid) {
|
||||
uni.showToast({
|
||||
title: "请输入视频Vid",
|
||||
icon: "none"
|
||||
})
|
||||
return;
|
||||
}
|
||||
const { vodPlayer } = this;
|
||||
vodPlayer.setVid({
|
||||
vid:this.videoVid,
|
||||
level:0
|
||||
}, (ret) => {
|
||||
this.text = JSON.stringify(ret);
|
||||
if (ret.errMsg != null) {
|
||||
uni.showToast({
|
||||
title: ret.errMsg,
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onPlayError(e){
|
||||
if (e.detail.errEvent != null) {
|
||||
uni.showToast({
|
||||
title:'playErrorEvent - '+e.detail.errEvent,
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
positionChange(e){
|
||||
this.skin.timeUpdate(e.detail.currentPosition);
|
||||
},
|
||||
|
||||
onEnableSubtitle(e){
|
||||
console.log("===1" + e.detail);
|
||||
this.skin.setEnableSubtitle(e.detail.isEnableSubtitle, e.detail.isEnableDoubleSubtitle);
|
||||
},
|
||||
|
||||
onSRTTextConfig(e) {
|
||||
console.log("===1 fontColor: " + e.detail.fontColor);
|
||||
this.skit.setSRTTextConfig(e.detail);
|
||||
},
|
||||
|
||||
onSRTTitle(e) {
|
||||
console.log("===1 onSRTTitle" + e.detail);
|
||||
|
||||
},
|
||||
|
||||
onPlayStatus(e){
|
||||
const { skin, vodPlayer } = this;
|
||||
const state = e.detail.playbackState;
|
||||
const preparedToPlay = e.detail.preparedToPlay;
|
||||
if (state != null) {
|
||||
this.skin.changePlayStatus(state === 'start');
|
||||
} else if (preparedToPlay != null) {
|
||||
this.updateDuration();
|
||||
this.updateLevels();
|
||||
this.updateScaling();
|
||||
}
|
||||
},
|
||||
|
||||
updateDuration() {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.getDuration(null, ret => {
|
||||
this.skin.updateDuration(ret.duration);
|
||||
});
|
||||
},
|
||||
|
||||
updateLevels() {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
|
||||
vodPlayer.getLevelNum(null, ret => {
|
||||
this.skin.updateLevels(ret.levelNum);
|
||||
});
|
||||
|
||||
vodPlayer.getCurrentLevel(null, ret => {
|
||||
this.skin.updateCurrentLevel(ret.currentLevel);
|
||||
});
|
||||
},
|
||||
|
||||
updateScaling() {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
|
||||
vodPlayer.getScalingMode({}, ret => {
|
||||
this.skin.updateScaling(ret.scalingMode);
|
||||
});
|
||||
},
|
||||
|
||||
onPlayBtnClick(isPlaying) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
isPlaying ? vodPlayer.start() : vodPlayer.pause();
|
||||
},
|
||||
|
||||
onHdBtnClick(level) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.changeLevel({level});
|
||||
},
|
||||
|
||||
onRateBtnClick(speed) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.setSpeed({speed});
|
||||
},
|
||||
|
||||
onScalingBtnClick(scalingMode) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.setScalingMode({scalingMode});
|
||||
},
|
||||
|
||||
onChangeSRTBtnClick(srt) {
|
||||
console.log("===1 " + srt);
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.changeSRT(srt);
|
||||
},
|
||||
|
||||
onChangeSRTSingleMode(isSingle) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.changeSRTSingleMode(isSingle);
|
||||
},
|
||||
|
||||
onOpenSRT(isOpen) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.setOpenSRT({
|
||||
openSrt: isOpen
|
||||
});
|
||||
},
|
||||
|
||||
onFullBtnClick(isFull) {
|
||||
// plus.screen.unlockOrientation();
|
||||
plus.navigator.setFullscreen(isFull);
|
||||
|
||||
//通过h5+实现横竖屏
|
||||
// isFull ? plus.screen.lockOrientation('landscape-primary') : plus.screen.lockOrientation('portrait-primary');
|
||||
|
||||
//通过原生实现横竖屏切换
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
isFull ? vodPlayer.changeToLandscape() : vodPlayer.changeToPortrait();
|
||||
|
||||
this.isFull = isFull;
|
||||
},
|
||||
|
||||
onToSeek(time) {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.seekTo({seconds: time});
|
||||
},
|
||||
|
||||
onVolumeChanged(value) {
|
||||
const { vodPlayer, skin } = this;
|
||||
if (!vodPlayer) return;
|
||||
vodPlayer.getVolume(null, ret => {
|
||||
const changedValue = ret.volume + value;
|
||||
|
||||
const realValue = this.limit(changedValue, 0, 1);
|
||||
|
||||
vodPlayer.setVolume({volume: realValue});
|
||||
|
||||
skin.updateVolumeValue(realValue);
|
||||
});
|
||||
},
|
||||
|
||||
onScreenShot() {
|
||||
const { vodPlayer } = this;
|
||||
if (!vodPlayer) return;
|
||||
|
||||
vodPlayer.snapshot(null, result =>{
|
||||
if(result.errMsg != null){
|
||||
console.log(result.errMsg)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 控制最小值,最大值
|
||||
* @param num
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns {*}
|
||||
*/
|
||||
limit(num, min, max) {
|
||||
if (num < min) return min;
|
||||
if (num > max) return max;
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 140rpx;
|
||||
}
|
||||
|
||||
.player {
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.player-full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vod-player {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skin-control {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hide {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user