Преглед на файлове

弹幕优化,自定义弹幕

XSLu08042 преди 2 седмици
родител
ревизия
4eb876e5c9
променени са 4 файла, в които са добавени 210 реда и са изтрити 54 реда
  1. 207 54
      pages_course/video.vue
  2. 3 0
      pages_user/patient.vue
  3. BIN
      static/images/danmu-off.png
  4. BIN
      static/images/danmu-on.png

+ 207 - 54
pages_course/video.vue

@@ -29,13 +29,24 @@
 			:picture-in-picture-mode="[]"
 			:show-background-playback-button="false"
 			:src="videoUrl"
-			:danmu-list="danmuList" 
+			>
+			<!-- :danmu-list="danmuList"
 			enable-danmu
-			danmu-btn>
+			danmu-btn -->
+				<template v-show="showDanmu==1">
+					<text v-for="(item, index) in activeDanmus" :key="item.id" class="danmu-item danmuMove" 
+						:style="{
+						  top: item.top + 'px',
+						  ...item.style,
+						  'animation-duration': '8s'
+						 }" @animationend="animationend(item,index)">
+						{{ item.text }}
+					</text>
+				</template>
 				<cover-view class="video-danmu-btnbox" :style="{display: isfull&&crtShow&&isLogin&&isAddKf==1 ? 'block':'none'}">
-				<cover-image class="video-danmu-image"
-				 src="https://cos.his.cdwjyyh.com/fs/20250418/beaf9df1a6204b8babc3e28d9b563c62.png"
-				 @click="openDanmu(1)"></cover-image>
+					<cover-image class="video-danmu-image"
+					 src="https://cos.his.cdwjyyh.com/fs/20250418/beaf9df1a6204b8babc3e28d9b563c62.png"
+					 @click="openDanmu(1)"></cover-image>
 				</cover-view>
 			</video>
 		</view>
@@ -103,7 +114,7 @@
 		<!-- 线路 -->
 		<view class="video-line" @click="openPop" v-if="isLogin&&isAddKf==1">
 			<image :src="baseUrl+'/images/changePlayer-icon.png'"></image>
-			<text>线路{{lineIndex + 1 | numberToChinese}}</text>
+			<text>线路{{numberToChinese(lineIndex + 1)}}</text>
 		</view>
 		<!-- 线路弹窗 -->
 		<uni-popup ref="popup" type="bottom"  class="full-width-popup">
@@ -116,7 +127,7 @@
 				<view class="popupbox-content">
 					<view :class="lineIndex == index ? 'line-item line-active': 'line-item'"
 						v-for="(it,index) in lineList" :key="index" @click="handleLine(index)">
-						线路{{index + 1 | numberToChinese}}</view>
+						线路{{numberToChinese(lineIndex + 1)}}</view>
 				</view>
 			</view>
 		</uni-popup>
@@ -129,6 +140,7 @@
 		<uni-popup ref="danmuPopup" type="bottom" style="z-index: 999;" @change="changeShowPopup">
 			<view class="danmuPopup" :style="{marginLeft:isfull ? statusBarHeight+'px': 0,marginBottom: danmuboxHeight+'px'}">
 				<view class="danmuPopup-head border-line">
+					<image class="danmu-icon" :src="showDanmu==0?'/static/images/danmu-off.png':'/static/images/danmu-on.png'" mode="heightFix" @click="switchDanmu()"></image>
 					<u-input 
 					class="danmuPopup-input" 
 					placeholder="发个弹幕吧~" 
@@ -139,7 +151,7 @@
 					:autoBlur="true" 
 					maxlength="140" 
 					clearable 
-					v-model="danmuIput"></u-input>
+					v-model.trim="danmuIput"></u-input>
 					<button class="danmuPopup-send"  :disabled="danmubtnLoading"  @click="sendDanmu">发送</button>
 				</view>
 			</view>
@@ -334,19 +346,21 @@
 				danmuboxHeight: 0,
 				user: {},
 				crtShow: true,
-				isCheckRealUrl: false
+				isCheckRealUrl: false,
+				activeDanmus:[],
+				flagTime: 0,
+				danmuItemStyle:{
+					color: '#ffffff',
+					fontSize: '16px',
+					border: 'solid 1px #ffffff',
+					borderRadius: '5px',
+					padding: '2px 2px',
+					backgroundColor: 'rgba(255, 255, 255, 0.1)'
+				},
+				showDanmu: 1,
+				ctx: null
 			}
 		},
-		filters: {
-			numberToChinese(number) {
-				if (number) {
-					const chineseNumber = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
-					return chineseNumber[number - 1];
-				} else {
-					return ''
-				}
-			},
-		},
 		computed: {
 			isAnswer() {
 				return (item, name) => {
@@ -372,19 +386,9 @@
 			// 	this.loginByMp()
 			// }
 			var that=this;
-			// this.videoId=769;
-			// this.qwUserId=2110;
-			// this.corpId='ww5a88c4f879f204c5';
-			// this.linkType=0;
-			// this.urlOption.videoId=769;
-			// this.urlOption.courseId=79;
-			// this.urlOption.companyId=170;
-			// this.urlOption.companyUserId=3972;
-			// this.urlOption.qwUserId=1;
-			// this.urlOption.corpId='ww5a88c4f879f204c5';
-			// this.urlOption.qwExternalId=6213064;
-			// this.urlOption.qwUserId=2110;
-			// this.urlOption.linkType=0;
+			if (this.videoId) {
+				this.getH5CourseByVideo()
+			}
 			this.sortLink = this.urlOption.link || ''
 			this.getMenuButton()
 			// #ifndef H5
@@ -395,6 +399,7 @@
 				clearInterval(this.pingpangTimes)
 				this.socket = null
 			}
+			this.initTracks()
 		},
 		onShow() {
 			this.tipsOpen = false
@@ -406,9 +411,6 @@
 			} else {
 				this.user = {}
 			}
-			if (this.videoId) {
-				this.getH5CourseByVideo()
-			}
 			if(this.sortLink){
 				this.getLink()
 			} else {
@@ -468,6 +470,14 @@
 			this.clearIntegral()
 		},
 		methods: {
+			numberToChinese(number) {
+				if (number) {
+					const chineseNumber = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
+					return chineseNumber[number - 1];
+				} else {
+					return ''
+				}
+			},
 			keyboardHeightChange(res) {
 				// #ifndef H5
 				console.log("this.danmuboxHeight",this.danmuboxHeight)
@@ -496,6 +506,10 @@
 					}
 					this.playTime = currentTime
 				}
+				if (Math.floor(e.detail.currentTime) != this.flagTime) {
+					this.flagTime = Math.floor(e.detail.currentTime)
+					this.checkDanmu()
+				}
 			},
 			changeTime(that,e) {
 				that.playDurationSeek = 0
@@ -529,6 +543,7 @@
 			},
 			fullscreenchange(event) {
 				this.isfull = event.detail.fullScreen
+				this.initTracks()
 			},
 			controlstoggle(event) {
 				this.crtShow =  event.detail.show
@@ -1134,7 +1149,7 @@
 			},
 			// 发送弹幕
 			sendDanmu() {
-				if(this.danmuIput==''||this.danmuIput.trim()=='') {
+				if(this.danmuIput=='') {
 					uni.showToast({
 						title: '弹幕不能为空',
 						icon: 'none'
@@ -1152,6 +1167,16 @@
 							text: item.content,
 							time: item.timePoint ? Number(item.timePoint) : this.playTime,
 							color: "#FFFFFF",
+							mode: item.mode|| "scroll",
+							top: null,
+							style: {
+								color: item.isColor==1 ? item.color || this.danmuItemStyle.color : this.danmuItemStyle.color,//是否彩色1是0否
+								fontSize: item.fontSize || this.danmuItemStyle.fontSize, 
+								padding: this.danmuItemStyle.padding,
+								border:this.user.userId ==item.userId ? item.color ? `solid 1px ${item.color}`: this.danmuItemStyle.border : 'none',
+								borderRadius: this.user.userId==item.userId ? this.danmuItemStyle.borderRadius : 0,
+								backgroundColor: this.user.userId==item.userId ? this.danmuItemStyle.backgroundColor : 'transparent'
+							},
 						}))
 					} else {
 						this.danmuList = []
@@ -1196,7 +1221,7 @@
 					},
 				})
 				this.socket.onMessage((res) => {
-					console.log("收到消息parse",JSON.parse(res.data))
+					// console.log("收到消息parse",JSON.parse(res.data))
 					const redata = JSON.parse(res.data);
 					if(redata.cmd=="heartbeat"){
 						  //心跳
@@ -1226,7 +1251,6 @@
 						//重启
 						that.initSocket()
 					}
-					// that.msgEnd = true
 				})
 				//监听socket错误
 				this.socket.onError((err) => {
@@ -1252,9 +1276,9 @@
 						content: this.danmuIput,
 						timePoint: this.playTime, // 弹幕对应视频时间节点()秒
 						platform: 'uniapp',  //发送平台,app传值“app”,小程序传值“uniapp”
-						fontSize: '14px',
+						fontSize: '16px',
 						mode: "scroll",
-						color: "#fff",
+						color: "#ffffff",
 					};
 					this.socket.send({
 						data: JSON.stringify(data),
@@ -1264,7 +1288,6 @@
 							this.isSend = false;
 						},
 						fail: () => {
-							console.log("发送失败")
 							uni.showToast({
 								title: '发送失败',
 								icon: 'none'
@@ -1280,19 +1303,113 @@
 				if (!this.player) {
 					this.player = uni.createVideoContext('video-content-box');
 				}
-				this.player.sendDanmu({
+				// this.player.sendDanmu({
+				// 	text: content.content,
+				// 	color: "#FF0000",
+				// 	time: this.playTime + 1
+				// })
+				const id = content.userId +'_' + new Date().getTime()
+				const mystyle = {
+					color: content.color || this.danmuItemStyle.color,
+					fontSize: content.fontSize || this.danmuItemStyle.fontSize,
+					border: content.color ? `solid 1px ${content.color}`: this.danmuItemStyle.border,
+					borderRadius: this.danmuItemStyle.borderRadius,
+					padding: this.danmuItemStyle.padding,
+					backgroundColor: this.danmuItemStyle.backgroundColor
+				}
+				const otherstyle = {
+					color: content.color || this.danmuItemStyle.color,
+					fontSize: content.fontSize || this.danmuItemStyle.fontSize,
+					padding: this.danmuItemStyle.padding,
+				}
+				const mode = content.mode || "scroll"
+				const obj = {
+					id: content.id || id,
+					userId: content.userId,
 					text: content.content,
-					color: "#FF0000",
-					time: this.playTime + 1
+					time: this.flagTime + 1,
+					color: content.color || this.danmuItemStyle.color,
+					style: this.user.userId == content.userId ? mystyle : otherstyle,
+					top: null
+				}
+				console
+				if(this.showDanmu == 0) return
+				this.danmuList.push(obj)
+			},
+			initTracks() {
+				this.tracks = []
+				const trackHeight = 22; // 每行高度
+				const trackCount = 3
+				for (let i = 0; i < trackCount; i++) {
+					this.tracks.push({
+						top: i * trackHeight+10,
+						isFree: true
+					});
+				}
+			},
+			// 获取字体高度
+			getTextWidth(content) {
+				if (!this.ctx) {
+					this.ctx = uni.createCanvasContext('myCanvas')
+				}
+				const metrics = this.ctx.measureText(content)
+				return Math.ceil(metrics.width)
+			},
+			// 分配轨道
+			getFreeTrack(item) {
+				const width = this.getTextWidth(item.content)
+				const passWidth = width + uni.getSystemInfoSync().screenWidth
+				const duration = 8
+				for (let i = 0; i < this.tracks.length; i++) {
+					if (this.tracks[i].isFree) {
+						this.tracks[i].isFree = false;
+						// 等本条通过右边界的时间
+						let passtime = Math.ceil(duration * 1000 / passWidth * width)
+						passtime = passtime + 1000
+						// console.log("passtime==", passtime)
+						setTimeout(() => {
+							this.tracks[i].isFree = true;
+						}, passtime); // 5秒后释放轨道
+						return this.tracks[i].top;
+					}
+				}
+				// 无可用轨道
+				if (item.userId == this.user.userId) {
+					let trackHeight = this.tracks[this.tracks.length - 1].top
+					return Math.random() * trackHeight + 16 // 自己发的弹幕随机高度; // 无可用轨道
+				} else {
+					// console.log("无可用轨道")
+					return 'abandon'
+				}
+			},
+			// 检测并激活弹幕
+			checkDanmu() {
+				if(this.showDanmu == 0) return
+				// 筛选当前时间应出现的弹幕
+				const newDanmus = this.danmuList.filter((item) => Math.abs(item.time - this.flagTime) < 1)
+				// 分配轨道高度
+				newDanmus.forEach((item) => {
+					// 滚动弹幕随机高度
+					if(!item.top) {
+						item.top = this.getFreeTrack(item)
+					}
 				})
-				// setTimeout(()=>{
-				// 	this.player.sendDanmu({
-				// 		text: content.content,
-				// 		color: "#FF0000",
-				// 		time: this.playTime
-				// 	})
-				// },100)
-			}
+				// 过滤没有分配到空闲轨道弹幕
+				const aliveNewDanmus = newDanmus.filter((item) => item.top != 'abandon')
+				// 添加到活跃列表
+				this.activeDanmus = [...this.activeDanmus, ...aliveNewDanmus]
+			},
+			animationend(moveItem, i) {
+				// 移除动画结束的弹幕(性能优化)
+				this.activeDanmus = this.activeDanmus.filter((item) => item.id != moveItem.id)
+			},
+			switchDanmu() {
+				this.showDanmu = this.showDanmu == 1 ? 0:1
+				if(this.showDanmu == 0) {
+					this.activeDanmus = []
+					this.initTracks()
+				}
+			},
 		}
 	}
 </script>
@@ -1996,7 +2113,7 @@
 	}
 	.danmuPopup {
 		background-color: #fff;
-		padding-bottom: var(--window-bottom);
+		padding-bottom: calc(var(--window-bottom) + 10px);
 		&-head {
 			width: 100%;
 			padding: 10px;
@@ -2041,5 +2158,41 @@
 			color: #757575;
 		}
 	}
-	 
+	.danmu-item {
+		position: absolute;
+		top: 0;
+		white-space: nowrap;
+		font-size: 16px;
+		height: 20px;
+		display: inline-flex;
+		box-sizing: border-box;
+		align-items: center;
+	}
+	.danmuMove {
+		// animation: mymove 8s linear forwards;
+		// animation-duration: 8s;
+		animation-timing-function: linear;
+		animation-delay: 0s;
+		animation-iteration-count: 1;
+		animation-direction: normal;
+		animation-fill-mode: forwards;
+		animation-play-state: running;
+		animation-name: mymove;
+		will-change: transform;
+	}
+	
+	@keyframes mymove {
+		from {
+			transform: translateX(100vw);
+		}
+	
+		to {
+			transform: translateX(-100%);
+		}
+	}
+	.danmu-icon{
+		height: 24px;
+		width: 24px;
+		margin-right: 12px;
+	}
 </style>

+ 3 - 0
pages_user/patient.vue

@@ -41,6 +41,9 @@
 				this.getPatientList()
 			})
 		},
+		onUnload() {
+			uni.$off('refreshPatient')
+		},
 		methods: {
 			selectPatient(item){
 				uni.$emit('refreshOrderPatient',item);

BIN
static/images/danmu-off.png


BIN
static/images/danmu-on.png