Commit b73e5e103db93a7edfad3a1b51906a27efa1a046
1 parent
3252e971
fix():修复播放器组件轮播和全部关闭销毁播放器冲突问题
Showing
2 changed files
with
340 additions
and
334 deletions
web_src/src/components/DeviceList1078.vue
| ... | ... | @@ -148,6 +148,7 @@ export default { |
| 148 | 148 | // --- 侧边栏折叠逻辑 --- |
| 149 | 149 | updateSidebarState() { |
| 150 | 150 | this.sidebarState = !this.sidebarState; |
| 151 | + // 触发 resize 事件让播放器自适应 | |
| 151 | 152 | setTimeout(() => { |
| 152 | 153 | const event = new Event('resize'); |
| 153 | 154 | window.dispatchEvent(event); |
| ... | ... | @@ -180,37 +181,39 @@ export default { |
| 180 | 181 | }, |
| 181 | 182 | |
| 182 | 183 | // ========================================== |
| 183 | - // 【核心修复】1. 左键点击播放逻辑 | |
| 184 | + // 1. 左键点击播放逻辑 | |
| 184 | 185 | // ========================================== |
| 185 | 186 | nodeClick(data, node) { |
| 186 | 187 | if (this.isCarouselRunning) { |
| 187 | 188 | this.$message.warning("请先停止轮播再手动播放"); |
| 188 | 189 | return; |
| 189 | 190 | } |
| 190 | - // 判断是否为叶子节点(通道),没有子节点视为通道 | |
| 191 | + // 判断是否为叶子节点(通道) | |
| 191 | 192 | if (!data.children || data.children.length === 0) { |
| 192 | 193 | this.playSingleChannel(data); |
| 193 | 194 | } |
| 194 | 195 | }, |
| 195 | 196 | |
| 196 | - // 单路播放 API 请求 | |
| 197 | 197 | playSingleChannel(data) { |
| 198 | - // 假设 data.code 格式为 "deviceId_sim_channel" | |
| 199 | - // 根据您的接口调整这里的解析逻辑 | |
| 198 | + // data.code 格式假设为 "deviceId_sim_channel" | |
| 200 | 199 | let stream = data.code.replace('-', '_'); // 容错处理 |
| 201 | 200 | let arr = stream.split("_"); |
| 202 | 201 | |
| 203 | - // 假设 ID 结构是: id_sim_channel,取后两段 | |
| 204 | - if(arr.length < 3) return; | |
| 202 | + // 容错: 确保能解析出 sim 和 channel | |
| 203 | + if (arr.length < 3) { | |
| 204 | + console.warn("Invalid channel code:", data.code); | |
| 205 | + return; | |
| 206 | + } | |
| 205 | 207 | |
| 206 | 208 | this.$axios.get(`/api/jt1078/query/send/request/io/${arr[1]}/${arr[2]}`).then(res => { |
| 207 | - if(res.data.code === 0 || res.data.code === 200) { | |
| 208 | - const url = res.data.data.ws_flv; // 或者 wss_flv | |
| 209 | + if (res.data.code === 0 || res.data.code === 200) { | |
| 210 | + // 兼容 http/https 协议 | |
| 211 | + const url = (location.protocol === "https:") ? res.data.data.wss_flv : res.data.data.ws_flv; | |
| 209 | 212 | const idx = this.windowClickIndex - 1; |
| 210 | 213 | |
| 211 | - // 更新播放地址和信息 | |
| 214 | + // 使用 $set 确保数组响应式更新 | |
| 212 | 215 | this.$set(this.videoUrl, idx, url); |
| 213 | - this.$set(this.videoDataList, idx, { ...data, videoUrl: url }); | |
| 216 | + this.$set(this.videoDataList, idx, {...data, videoUrl: url}); | |
| 214 | 217 | |
| 215 | 218 | // 自动跳到下一个窗口 |
| 216 | 219 | const maxWindow = parseInt(this.windowNum) || 4; |
| ... | ... | @@ -224,18 +227,15 @@ export default { |
| 224 | 227 | }, |
| 225 | 228 | |
| 226 | 229 | // ========================================== |
| 227 | - // 【核心修复】2. 右键菜单逻辑 | |
| 230 | + // 2. 右键菜单逻辑 | |
| 228 | 231 | // ========================================== |
| 229 | 232 | nodeContextmenu(event, data, node) { |
| 230 | - // 只有设备节点(有子级)才显示菜单,通道节点不显示 | |
| 233 | + // 只有父设备节点(有子级)才显示菜单 | |
| 231 | 234 | if (data.children && data.children.length > 0) { |
| 232 | 235 | this.rightClickNode = node; |
| 233 | 236 | this.contextMenuVisible = true; |
| 234 | 237 | this.contextMenuLeft = event.clientX; |
| 235 | 238 | this.contextMenuTop = event.clientY; |
| 236 | - | |
| 237 | - // 阻止默认浏览器右键菜单 | |
| 238 | - // 注意:VehicleList 组件内部也需要处理 @contextmenu.prevent | |
| 239 | 239 | } |
| 240 | 240 | }, |
| 241 | 241 | |
| ... | ... | @@ -243,7 +243,6 @@ export default { |
| 243 | 243 | this.contextMenuVisible = false; |
| 244 | 244 | }, |
| 245 | 245 | |
| 246 | - // 处理右键菜单命令(一键播放该设备) | |
| 247 | 246 | handleContextCommand(command) { |
| 248 | 247 | this.hideContextMenu(); |
| 249 | 248 | if (command === 'playback') { |
| ... | ... | @@ -262,21 +261,20 @@ export default { |
| 262 | 261 | else this.windowNum = '4'; |
| 263 | 262 | |
| 264 | 263 | // 3. 构造批量请求参数 |
| 265 | - // 假设后端接受的格式处理 | |
| 266 | 264 | const ids = channels.map(c => { |
| 267 | - // 假设 code 是 id_sim_channel | |
| 265 | + // 假设 code 是 id_sim_channel,后端需要 sim-channel | |
| 268 | 266 | const parts = c.code.replaceAll('_', '-').split('-'); |
| 269 | - // 取 sim-channel 部分 | |
| 270 | 267 | return parts.slice(1).join('-'); |
| 271 | 268 | }); |
| 272 | 269 | |
| 273 | 270 | this.$axios.post('/api/jt1078/query/beachSend/request/io', ids).then(res => { |
| 274 | - if(res.data && res.data.data) { | |
| 271 | + if (res.data && res.data.data) { | |
| 275 | 272 | const list = res.data.data || []; |
| 276 | 273 | list.forEach((item, i) => { |
| 277 | 274 | if (channels[i]) { |
| 278 | - this.$set(this.videoUrl, i, item.ws_flv); | |
| 279 | - this.$set(this.videoDataList, i, { ...channels[i], videoUrl: item.ws_flv }); | |
| 275 | + const url = (location.protocol === "https:") ? item.wss_flv : item.ws_flv; | |
| 276 | + this.$set(this.videoUrl, i, url); | |
| 277 | + this.$set(this.videoDataList, i, {...channels[i], videoUrl: url}); | |
| 280 | 278 | } |
| 281 | 279 | }); |
| 282 | 280 | } else { |
| ... | ... | @@ -297,10 +295,12 @@ export default { |
| 297 | 295 | async checkCarouselPermission(actionName) { |
| 298 | 296 | if (!this.isCarouselRunning) return true; |
| 299 | 297 | try { |
| 300 | - await this.$confirm(`正在轮播,"${actionName}"将停止轮播,是否继续?`,'提示',{ type: 'warning' }); | |
| 298 | + await this.$confirm(`正在轮播,"${actionName}"将停止轮播,是否继续?`, '提示', {type: 'warning'}); | |
| 301 | 299 | this.stopCarousel(); |
| 302 | 300 | return true; |
| 303 | - } catch (e) { return false; } | |
| 301 | + } catch (e) { | |
| 302 | + return false; | |
| 303 | + } | |
| 304 | 304 | }, |
| 305 | 305 | |
| 306 | 306 | async closeAllVideo() { |
| ... | ... | @@ -313,16 +313,17 @@ export default { |
| 313 | 313 | if (!(await this.checkCarouselPermission('关闭当前窗口'))) return; |
| 314 | 314 | const idx = Number(this.windowClickIndex) - 1; |
| 315 | 315 | if (this.videoUrl && this.videoUrl[idx]) { |
| 316 | - this.$confirm(`确认关闭窗口 [${this.windowClickIndex}] ?`, '提示', { type: 'warning' }) | |
| 316 | + this.$confirm(`确认关闭窗口 [${this.windowClickIndex}] ?`, '提示', {type: 'warning'}) | |
| 317 | 317 | .then(() => { |
| 318 | 318 | this.$set(this.videoUrl, idx, null); |
| 319 | 319 | this.$set(this.videoDataList, idx, null); |
| 320 | - }).catch(()=>{}); | |
| 320 | + }).catch(() => { | |
| 321 | + }); | |
| 321 | 322 | } |
| 322 | 323 | }, |
| 323 | 324 | |
| 324 | 325 | // ========================================== |
| 325 | - // 4. 轮播逻辑 (保持之前定义的逻辑) | |
| 326 | + // 4. 轮播逻辑 (补全部分) | |
| 326 | 327 | // ========================================== |
| 327 | 328 | openCarouselConfig() { |
| 328 | 329 | if (this.isCarouselRunning) { |
| ... | ... | @@ -334,25 +335,182 @@ export default { |
| 334 | 335 | |
| 335 | 336 | stopCarousel() { |
| 336 | 337 | this.isCarouselRunning = false; |
| 337 | - if (this.carouselTimer) clearTimeout(this.carouselTimer); | |
| 338 | + if (this.carouselTimer) { | |
| 339 | + clearTimeout(this.carouselTimer); | |
| 340 | + this.carouselTimer = null; | |
| 341 | + } | |
| 342 | + this.$message.info("轮播已停止"); | |
| 338 | 343 | }, |
| 339 | 344 | |
| 340 | 345 | async startCarousel(config) { |
| 341 | 346 | this.carouselConfig = config; |
| 347 | + | |
| 348 | + // 1. 筛选目标设备 | |
| 342 | 349 | let targetNodes = []; |
| 350 | + if (config.sourceType === 'all_online') { | |
| 351 | + const collectOnline = (nodes) => { | |
| 352 | + nodes.forEach(node => { | |
| 353 | + if (node.abnormalStatus === 1) targetNodes.push(node); | |
| 354 | + if (node.children && node.children.length > 0) collectOnline(node.children); | |
| 355 | + }); | |
| 356 | + }; | |
| 357 | + collectOnline(this.deviceTreeData); | |
| 358 | + } else { | |
| 359 | + targetNodes = config.selectedNodes.filter(n => n.abnormalStatus === 1); | |
| 360 | + } | |
| 361 | + | |
| 362 | + if (targetNodes.length === 0) { | |
| 363 | + this.$message.warning("当前范围内没有在线设备可供轮播"); | |
| 364 | + return; | |
| 365 | + } | |
| 343 | 366 | |
| 344 | - // ... (保留您之前的 startCarousel 完整逻辑,这里简略以节省篇幅,请确保您之前的代码已粘贴进来) ... | |
| 345 | - // 如果没有之前的代码,请告诉我,我再发一遍完整的 startCarousel | |
| 346 | - // 这里简单模拟一个启动 | |
| 367 | + // 2. 初始化状态 | |
| 368 | + this.carouselDeviceList = targetNodes; | |
| 369 | + this.channelBuffer = []; | |
| 370 | + this.deviceCursor = 0; | |
| 347 | 371 | this.isCarouselRunning = true; |
| 348 | - this.$message.success("轮播已启动 (需要补全 startCarousel 完整逻辑)"); | |
| 372 | + this.isWithinSchedule = true; | |
| 373 | + | |
| 374 | + this.$message.success(`轮播已启动,共 ${targetNodes.length} 台在线设备`); | |
| 375 | + | |
| 376 | + // 3. 立即执行第一轮 | |
| 377 | + await this.executeFirstRound(); | |
| 378 | + }, | |
| 379 | + | |
| 380 | + async executeFirstRound() { | |
| 381 | + if (this.windowNum !== this.carouselConfig.layout) { | |
| 382 | + this.windowNum = this.carouselConfig.layout; | |
| 383 | + } | |
| 384 | + | |
| 385 | + const batch = await this.fetchNextBatchData(); | |
| 386 | + if (batch) { | |
| 387 | + this.applyVideoBatch(batch); | |
| 388 | + this.runCarouselLoop(); | |
| 389 | + } else { | |
| 390 | + this.$message.error("首轮加载失败,尝试重试..."); | |
| 391 | + this.runCarouselLoop(); | |
| 392 | + } | |
| 393 | + }, | |
| 394 | + | |
| 395 | + runCarouselLoop() { | |
| 396 | + if (!this.isCarouselRunning) return; | |
| 397 | + | |
| 398 | + const {runMode, timeRange, interval} = this.carouselConfig; | |
| 399 | + | |
| 400 | + // 1. 检查定时 | |
| 401 | + if (runMode === 'schedule') { | |
| 402 | + if (!this.checkTimeRange(timeRange[0], timeRange[1])) { | |
| 403 | + this.isWithinSchedule = false; | |
| 404 | + if (this.videoUrl.some(v => v)) this.closeAllVideoNoConfirm(); | |
| 405 | + this.carouselTimer = setTimeout(() => this.runCarouselLoop(), 10000); | |
| 406 | + return; | |
| 407 | + } | |
| 408 | + this.isWithinSchedule = true; | |
| 409 | + } | |
| 410 | + | |
| 411 | + // 2. 计算时间轴 | |
| 412 | + const PRELOAD_TIME = 15; | |
| 413 | + const intervalSec = Math.max(interval, 30); | |
| 414 | + const waitTime = (intervalSec - PRELOAD_TIME) * 1000; | |
| 415 | + | |
| 416 | + // 3. 计时循环 | |
| 417 | + this.carouselTimer = setTimeout(async () => { | |
| 418 | + if (!this.isCarouselRunning) return; | |
| 419 | + | |
| 420 | + const nextBatch = await this.fetchNextBatchData(); | |
| 421 | + | |
| 422 | + this.carouselTimer = setTimeout(() => { | |
| 423 | + if (!this.isCarouselRunning) return; | |
| 424 | + if (nextBatch) this.applyVideoBatch(nextBatch); | |
| 425 | + this.runCarouselLoop(); | |
| 426 | + }, PRELOAD_TIME * 1000); | |
| 427 | + | |
| 428 | + }, waitTime); | |
| 429 | + }, | |
| 430 | + | |
| 431 | + applyVideoBatch({urls, infos}) { | |
| 432 | + this.videoUrl = new Array(parseInt(this.windowNum)).fill(null); | |
| 433 | + setTimeout(() => { | |
| 434 | + urls.forEach((url, index) => { | |
| 435 | + setTimeout(() => { | |
| 436 | + this.$set(this.videoUrl, index, url); | |
| 437 | + this.$set(this.videoDataList, index, infos[index]); | |
| 438 | + }, index * 100); | |
| 439 | + }); | |
| 440 | + }, 200); | |
| 441 | + }, | |
| 442 | + | |
| 443 | + async fetchNextBatchData() { | |
| 444 | + let pageSize = parseInt(this.windowNum) || 4; | |
| 445 | + if (isNaN(pageSize)) pageSize = 4; | |
| 446 | + | |
| 447 | + // 填充缓冲区 | |
| 448 | + let safetyCounter = 0; | |
| 449 | + while (this.channelBuffer.length < pageSize && safetyCounter < 100) { | |
| 450 | + safetyCounter++; | |
| 451 | + if (this.deviceCursor >= this.carouselDeviceList.length) { | |
| 452 | + this.deviceCursor = 0; | |
| 453 | + } | |
| 454 | + | |
| 455 | + const device = this.carouselDeviceList[this.deviceCursor]; | |
| 456 | + if (device && device.children && device.children.length > 0) { | |
| 457 | + const codes = device.children | |
| 458 | + .filter(child => !child.disabled) | |
| 459 | + .map(child => child.code); | |
| 460 | + this.channelBuffer.push(...codes); | |
| 461 | + } | |
| 462 | + this.deviceCursor++; | |
| 463 | + } | |
| 464 | + | |
| 465 | + if (this.channelBuffer.length === 0) return null; | |
| 466 | + | |
| 467 | + const currentCodes = this.channelBuffer.splice(0, pageSize); | |
| 468 | + const streamParams = currentCodes.map(c => c.replaceAll('_', '-').split('-').slice(1).join('-')); | |
| 469 | + | |
| 470 | + try { | |
| 471 | + const res = await this.$axios.post('/api/jt1078/query/beachSend/request/io', streamParams, {timeout: 20000}); | |
| 472 | + if (res.data && res.data.data) { | |
| 473 | + const resultList = res.data.data; | |
| 474 | + const urls = new Array(pageSize).fill(''); | |
| 475 | + const infos = new Array(pageSize).fill(null); | |
| 476 | + | |
| 477 | + resultList.forEach((item, i) => { | |
| 478 | + if (i < currentCodes.length) { | |
| 479 | + const url = (location.protocol === "https:") ? item.wss_flv : item.ws_flv; | |
| 480 | + urls[i] = url; | |
| 481 | + infos[i] = { | |
| 482 | + code: currentCodes[i], | |
| 483 | + name: `通道 ${i + 1}`, | |
| 484 | + videoUrl: url | |
| 485 | + }; | |
| 486 | + } | |
| 487 | + }); | |
| 488 | + return {urls, infos}; | |
| 489 | + } | |
| 490 | + } catch (e) { | |
| 491 | + console.error("批量请求流地址失败", e); | |
| 492 | + } | |
| 493 | + return null; | |
| 349 | 494 | }, |
| 350 | 495 | |
| 351 | - // ... 其他轮播辅助函数 (fetchNextBatchData, applyVideoBatch 等) ... | |
| 352 | - // 请确保这些函数存在,否则轮播会报错 | |
| 353 | - fetchNextBatchData() {}, | |
| 354 | - runCarouselLoop() {}, | |
| 355 | - applyVideoBatch() {}, | |
| 496 | + checkTimeRange(startStr, endStr) { | |
| 497 | + if (!startStr || !endStr) return true; | |
| 498 | + const now = new Date(); | |
| 499 | + const current = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); | |
| 500 | + const parse = (str) => { | |
| 501 | + const [h, m, s] = str.split(':').map(Number); | |
| 502 | + return h * 3600 + m * 60 + s; | |
| 503 | + }; | |
| 504 | + const start = parse(startStr); | |
| 505 | + const end = parse(endStr); | |
| 506 | + if (end < start) return current >= start || current <= end; | |
| 507 | + return current >= start && current <= end; | |
| 508 | + }, | |
| 509 | + | |
| 510 | + closeAllVideoNoConfirm() { | |
| 511 | + this.videoUrl = new Array(parseInt(this.windowNum)).fill(null); | |
| 512 | + this.videoDataList = new Array(parseInt(this.windowNum)).fill(null); | |
| 513 | + }, | |
| 356 | 514 | |
| 357 | 515 | handleBeforeUnload(e) { |
| 358 | 516 | if (this.isCarouselRunning) { |
| ... | ... | @@ -419,7 +577,10 @@ export default { |
| 419 | 577 | cursor: pointer; |
| 420 | 578 | color: #606266; |
| 421 | 579 | } |
| 422 | -.fold-btn:hover { color: #409EFF; } | |
| 580 | + | |
| 581 | +.fold-btn:hover { | |
| 582 | + color: #409EFF; | |
| 583 | +} | |
| 423 | 584 | |
| 424 | 585 | .header-right-info { |
| 425 | 586 | margin-left: auto; |
| ... | ... | @@ -442,19 +603,24 @@ export default { |
| 442 | 603 | position: fixed; |
| 443 | 604 | background: #fff; |
| 444 | 605 | border: 1px solid #EBEEF5; |
| 445 | - box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); | |
| 606 | + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); | |
| 446 | 607 | z-index: 3000; |
| 447 | 608 | border-radius: 4px; |
| 448 | 609 | padding: 5px 0; |
| 449 | 610 | min-width: 120px; |
| 450 | 611 | } |
| 612 | + | |
| 451 | 613 | .menu-item { |
| 452 | 614 | padding: 8px 15px; |
| 453 | 615 | font-size: 14px; |
| 454 | 616 | color: #606266; |
| 455 | 617 | cursor: pointer; |
| 456 | 618 | } |
| 457 | -.menu-item:hover { background: #ecf5ff; color: #409EFF; } | |
| 619 | + | |
| 620 | +.menu-item:hover { | |
| 621 | + background: #ecf5ff; | |
| 622 | + color: #409EFF; | |
| 623 | +} | |
| 458 | 624 | |
| 459 | 625 | .carousel-status { |
| 460 | 626 | margin-left: 15px; | ... | ... |
web_src/src/components/common/EasyPlayer.vue
| ... | ... | @@ -17,9 +17,9 @@ |
| 17 | 17 | </div> |
| 18 | 18 | </div> |
| 19 | 19 | |
| 20 | - <div :id="uniqueId" ref="container" class="player-box"></div> | |
| 20 | + <div :id="uniqueId" :key="uniqueId" ref="container" class="player-box"></div> | |
| 21 | 21 | |
| 22 | - <div v-if="!hasUrl" class="idle-mask"> | |
| 22 | + <div v-show="!hasUrl" class="idle-mask"> | |
| 23 | 23 | <div class="idle-text">无信号</div> |
| 24 | 24 | </div> |
| 25 | 25 | |
| ... | ... | @@ -53,6 +53,7 @@ export default { |
| 53 | 53 | }, |
| 54 | 54 | data() { |
| 55 | 55 | return { |
| 56 | + // 初始 ID | |
| 56 | 57 | uniqueId: `player-box-${Date.now()}-${Math.floor(Math.random() * 1000)}`, |
| 57 | 58 | playerInstance: null, |
| 58 | 59 | |
| ... | ... | @@ -68,19 +69,17 @@ export default { |
| 68 | 69 | controlTimer: null, |
| 69 | 70 | retryCount: 0, |
| 70 | 71 | |
| 71 | - // 【配置区域】在这里修改 true/false 即可生效 | |
| 72 | 72 | controlsConfig: { |
| 73 | - showBottomBar: true, // 是否显示底栏 | |
| 74 | - showSpeed: false, // 【关键】设为 false 隐藏底部网速 | |
| 75 | - showCodeSelect: false, // 【关键】设为 false 隐藏解码选择 | |
| 76 | - | |
| 77 | - showPlay: true, // 播放暂停按钮 | |
| 78 | - showAudio: true, // 音量按钮 | |
| 79 | - showStretch: true, // 拉伸按钮 | |
| 80 | - showScreenshot: true, // 截图按钮 | |
| 81 | - showRecord: true, // 录制按钮 | |
| 82 | - showZoom: true, // 电子放大 | |
| 83 | - showFullscreen: true, // 全屏按钮 | |
| 73 | + showBottomBar: true, | |
| 74 | + showSpeed: false, | |
| 75 | + showCodeSelect: false, | |
| 76 | + showPlay: true, | |
| 77 | + showAudio: true, | |
| 78 | + showStretch: true, | |
| 79 | + showScreenshot: true, | |
| 80 | + showRecord: true, | |
| 81 | + showZoom: true, | |
| 82 | + showFullscreen: true, | |
| 84 | 83 | } |
| 85 | 84 | }; |
| 86 | 85 | }, |
| ... | ... | @@ -91,14 +90,12 @@ export default { |
| 91 | 90 | if (typeof url === 'string') return url.length > 0; |
| 92 | 91 | return !!url.videoUrl; |
| 93 | 92 | }, |
| 94 | - // 生成控制 CSS 类名 | |
| 95 | 93 | playerClassOptions() { |
| 96 | 94 | const c = this.controlsConfig; |
| 97 | 95 | return { |
| 98 | 96 | 'hide-bottom-bar': !c.showBottomBar, |
| 99 | - 'hide-speed': !c.showSpeed, // 对应下方 CSS | |
| 100 | - 'hide-code-select': !c.showCodeSelect, // 对应下方 CSS | |
| 101 | - | |
| 97 | + 'hide-speed': !c.showSpeed, | |
| 98 | + 'hide-code-select': !c.showCodeSelect, | |
| 102 | 99 | 'hide-btn-play': !c.showPlay, |
| 103 | 100 | 'hide-btn-audio': !c.showAudio, |
| 104 | 101 | 'hide-btn-stretch': !c.showStretch, |
| ... | ... | @@ -113,25 +110,35 @@ export default { |
| 113 | 110 | initialPlayUrl: { |
| 114 | 111 | handler(newUrl) { |
| 115 | 112 | const url = typeof newUrl === 'string' ? newUrl : (newUrl && newUrl.videoUrl) || ''; |
| 113 | + | |
| 116 | 114 | if (url) { |
| 115 | + // 有地址:准备播放 | |
| 117 | 116 | this.isLoading = true; |
| 118 | 117 | this.isError = false; |
| 119 | - this.$nextTick(() => { | |
| 120 | - if (this.playerInstance) this.destroy(); | |
| 121 | - setTimeout(() => { | |
| 118 | + | |
| 119 | + if (this.playerInstance) { | |
| 120 | + // 实例已存在(比如轮播切换下一路),直接切换地址,不要销毁 | |
| 121 | + this.play(url); | |
| 122 | + } else { | |
| 123 | + // 实例不存在(比如从全部关闭状态恢复),创建并播放 | |
| 124 | + this.$nextTick(() => { | |
| 122 | 125 | this.create(); |
| 123 | - this.play(url); | |
| 124 | - }, 50); | |
| 125 | - }); | |
| 126 | + setTimeout(() => this.play(url), 100); | |
| 127 | + }); | |
| 128 | + } | |
| 126 | 129 | } else { |
| 130 | + // 地址为空(全部关闭):彻底销毁 | |
| 127 | 131 | this.destroy(); |
| 128 | 132 | this.isLoading = false; |
| 133 | + this.isError = false; | |
| 129 | 134 | } |
| 130 | 135 | }, |
| 131 | 136 | immediate: true |
| 132 | 137 | }, |
| 133 | 138 | hasAudio() { |
| 134 | - if (this.hasUrl) this.destroyAndReplay(this.initialPlayUrl); | |
| 139 | + if (this.hasUrl && this.playerInstance) { | |
| 140 | + this.destroyAndReplay(this.initialPlayUrl); | |
| 141 | + } | |
| 135 | 142 | } |
| 136 | 143 | }, |
| 137 | 144 | beforeDestroy() { |
| ... | ... | @@ -156,16 +163,20 @@ export default { |
| 156 | 163 | |
| 157 | 164 | create() { |
| 158 | 165 | if (this.playerInstance) return; |
| 159 | - const container = this.$refs.container; | |
| 160 | - if (!container) return; | |
| 161 | - container.innerHTML = ''; | |
| 162 | 166 | |
| 163 | - if (container.clientWidth === 0 && this.retryCount < 5) { | |
| 164 | - this.retryCount++; | |
| 165 | - setTimeout(() => this.create(), 200); | |
| 167 | + const container = this.$refs.container; | |
| 168 | + // 容错:如果容器不存在,或者刚才被销毁还没渲染出来,延迟重试 | |
| 169 | + if (!container || container.clientWidth === 0) { | |
| 170 | + if (this.retryCount < 10) { // 增加重试次数 | |
| 171 | + this.retryCount++; | |
| 172 | + setTimeout(() => this.create(), 100); | |
| 173 | + } | |
| 166 | 174 | return; |
| 167 | 175 | } |
| 168 | 176 | |
| 177 | + // 再次确保清空内容 | |
| 178 | + container.innerHTML = ''; | |
| 179 | + | |
| 169 | 180 | if (!window.EasyPlayerPro) return; |
| 170 | 181 | |
| 171 | 182 | try { |
| ... | ... | @@ -178,10 +189,8 @@ export default { |
| 178 | 189 | WCS: true, |
| 179 | 190 | hasAudio: this.hasAudio, |
| 180 | 191 | isLive: true, |
| 181 | - loading: false, | |
| 182 | - isBand: true, // 保持开启以获取数据 | |
| 183 | - | |
| 184 | - // btns 配置只能控制原生有开关的按钮 | |
| 192 | + loading: false, // 使用我们的自定义 loading | |
| 193 | + isBand: true, | |
| 185 | 194 | btns: { |
| 186 | 195 | play: c.showPlay, |
| 187 | 196 | audio: c.showAudio, |
| ... | ... | @@ -200,9 +209,11 @@ export default { |
| 200 | 209 | }); |
| 201 | 210 | |
| 202 | 211 | this.playerInstance.on('play', () => { |
| 212 | + console.log(`[${this.uniqueId}] 播放成功`); | |
| 203 | 213 | this.hasStarted = true; |
| 204 | 214 | this.isLoading = false; |
| 205 | 215 | this.isError = false; |
| 216 | + // 播放成功后显示一下控制栏 | |
| 206 | 217 | this.showControls = true; |
| 207 | 218 | if (this.controlTimer) clearTimeout(this.controlTimer); |
| 208 | 219 | this.controlTimer = setTimeout(() => { |
| ... | ... | @@ -212,61 +223,73 @@ export default { |
| 212 | 223 | |
| 213 | 224 | this.playerInstance.on('error', (err) => { |
| 214 | 225 | console.error('Player Error:', err); |
| 215 | - this.triggerError('流媒体连接失败'); | |
| 226 | + this.triggerError('视频流连接异常'); | |
| 216 | 227 | }); |
| 217 | 228 | |
| 218 | 229 | } catch (e) { |
| 219 | - console.error("Create Error:", e); | |
| 230 | + console.error("Create Instance Error:", e); | |
| 231 | + // 如果创建报错,可能是容器脏了,执行销毁逻辑刷新DOM | |
| 232 | + this.destroy(); | |
| 220 | 233 | } |
| 221 | 234 | }, |
| 222 | 235 | |
| 223 | 236 | play(url) { |
| 224 | 237 | if (!url) return; |
| 238 | + | |
| 225 | 239 | if (!this.playerInstance) { |
| 226 | 240 | this.create(); |
| 227 | 241 | setTimeout(() => this.play(url), 200); |
| 228 | 242 | return; |
| 229 | 243 | } |
| 244 | + | |
| 230 | 245 | this.isLoading = true; |
| 231 | 246 | this.isError = false; |
| 232 | 247 | this.errorMessage = ''; |
| 233 | 248 | |
| 249 | + // 设置超时检测 | |
| 234 | 250 | setTimeout(() => { |
| 235 | 251 | if (this.isLoading) this.triggerError('连接超时,请重试'); |
| 236 | 252 | }, this.loadTimeout); |
| 237 | 253 | |
| 238 | 254 | this.playerInstance.play(url).catch(e => { |
| 255 | + console.warn("Play error:", e); | |
| 239 | 256 | this.triggerError('请求播放失败'); |
| 240 | 257 | }); |
| 241 | 258 | }, |
| 242 | 259 | |
| 260 | + // 【核心修复】彻底销毁并刷新DOM ID | |
| 243 | 261 | destroy() { |
| 262 | + // 1. 重置状态 | |
| 244 | 263 | this.hasStarted = false; |
| 245 | 264 | this.showControls = false; |
| 246 | 265 | this.netSpeed = '0KB/s'; |
| 266 | + this.isLoading = false; | |
| 267 | + this.isError = false; | |
| 268 | + | |
| 247 | 269 | const container = this.$refs.container; |
| 248 | - if (container) { | |
| 249 | - const video = container.querySelector('video'); | |
| 250 | - if (video) { | |
| 251 | - video.pause(); | |
| 252 | - video.src = ""; | |
| 253 | - video.load(); | |
| 254 | - video.remove(); | |
| 255 | - } | |
| 256 | - container.innerHTML = ''; | |
| 257 | - } | |
| 270 | + | |
| 271 | + // 2. 销毁实例 | |
| 258 | 272 | if (this.playerInstance) { |
| 259 | 273 | try { |
| 260 | 274 | this.playerInstance.destroy(); |
| 261 | - } catch (e) { | |
| 262 | - } | |
| 275 | + } catch(e) {} | |
| 263 | 276 | this.playerInstance = null; |
| 264 | 277 | } |
| 278 | + | |
| 279 | + // 3. 暴力清理旧 DOM | |
| 280 | + if (container) { | |
| 281 | + container.innerHTML = ''; | |
| 282 | + } | |
| 283 | + | |
| 284 | + // 4. 【关键】更新 uniqueId | |
| 285 | + // 这会强制 Vue 在下一次渲染时移除当前的 div,并创建一个全新的 div | |
| 286 | + // 从而彻底解决 "EasyPlayerPro err container" 错误 | |
| 287 | + this.uniqueId = `player-box-${Date.now()}-${Math.floor(Math.random() * 1000)}`; | |
| 265 | 288 | }, |
| 266 | 289 | |
| 267 | 290 | destroyAndReplay(url) { |
| 268 | - this.isLoading = true; | |
| 269 | 291 | this.destroy(); |
| 292 | + this.isLoading = true; | |
| 270 | 293 | this.$nextTick(() => { |
| 271 | 294 | this.create(); |
| 272 | 295 | if (url) { |
| ... | ... | @@ -289,276 +312,93 @@ export default { |
| 289 | 312 | }, |
| 290 | 313 | |
| 291 | 314 | setControls(config) { |
| 292 | - this.controlsConfig = {...this.controlsConfig, ...config}; | |
| 315 | + this.controlsConfig = { ...this.controlsConfig, ...config }; | |
| 293 | 316 | } |
| 294 | 317 | } |
| 295 | 318 | }; |
| 296 | 319 | </script> |
| 297 | 320 | |
| 298 | 321 | <style scoped> |
| 299 | -/* -------------------------------------------------- | |
| 300 | - 这里是组件内部样式,仅处理非 EasyPlayer 插件的部分 | |
| 301 | - -------------------------------------------------- | |
| 302 | -*/ | |
| 322 | +/* 基础布局 */ | |
| 303 | 323 | .player-wrapper { |
| 304 | - width: 100%; | |
| 305 | - height: 100%; | |
| 306 | - display: flex; | |
| 307 | - flex-direction: column; | |
| 308 | - position: relative; | |
| 309 | - background: #000; | |
| 310 | - overflow: hidden; | |
| 324 | + width: 100%; height: 100%; display: flex; flex-direction: column; | |
| 325 | + position: relative; background: #000; overflow: hidden; | |
| 311 | 326 | } |
| 312 | - | |
| 313 | 327 | .player-box { |
| 314 | - flex: 1; | |
| 315 | - width: 100%; | |
| 316 | - height: 100%; | |
| 317 | - background: #000; | |
| 318 | - position: relative; | |
| 319 | - z-index: 1; | |
| 328 | + flex: 1; width: 100%; height: 100%; background: #000; position: relative; z-index: 1; | |
| 320 | 329 | } |
| 321 | 330 | |
| 322 | 331 | /* 顶部栏 */ |
| 323 | 332 | .custom-top-bar { |
| 324 | - position: absolute; | |
| 325 | - top: 0; | |
| 326 | - left: 0; | |
| 327 | - width: 100%; | |
| 328 | - height: 40px; | |
| 329 | - background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)); | |
| 330 | - z-index: 200; | |
| 331 | - display: flex; | |
| 332 | - justify-content: space-between; | |
| 333 | - align-items: center; | |
| 334 | - padding: 0 15px; | |
| 335 | - box-sizing: border-box; | |
| 336 | - pointer-events: none; | |
| 337 | - transition: opacity 0.3s ease; | |
| 338 | -} | |
| 339 | - | |
| 340 | -.custom-top-bar.hide-bar { | |
| 341 | - opacity: 0; | |
| 342 | -} | |
| 343 | - | |
| 344 | -.top-bar-left .video-title { | |
| 345 | - color: #fff; | |
| 346 | - font-size: 14px; | |
| 347 | - font-weight: bold; | |
| 348 | -} | |
| 349 | - | |
| 350 | -.top-bar-right .net-speed { | |
| 351 | - color: #00ff00; | |
| 352 | - font-size: 12px; | |
| 353 | - font-family: monospace; | |
| 354 | -} | |
| 355 | - | |
| 356 | -/* 蒙层 */ | |
| 357 | -.status-mask { | |
| 358 | - position: absolute; | |
| 359 | - top: 0; | |
| 360 | - left: 0; | |
| 361 | - width: 100%; | |
| 362 | - height: 100%; | |
| 363 | - background-color: #000; | |
| 364 | - z-index: 50; | |
| 365 | - display: flex; | |
| 366 | - flex-direction: column; | |
| 367 | - align-items: center; | |
| 368 | - justify-content: center; | |
| 369 | -} | |
| 370 | - | |
| 333 | + position: absolute; top: 0; left: 0; width: 100%; height: 40px; | |
| 334 | + background: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0)); | |
| 335 | + z-index: 200; display: flex; justify-content: space-between; align-items: center; | |
| 336 | + padding: 0 15px; box-sizing: border-box; pointer-events: none; transition: opacity 0.3s ease; | |
| 337 | +} | |
| 338 | +.custom-top-bar.hide-bar { opacity: 0; } | |
| 339 | +.top-bar-left .video-title { color: #fff; font-size: 14px; font-weight: bold; } | |
| 340 | +.top-bar-right .net-speed { color: #00ff00; font-size: 12px; font-family: monospace; } | |
| 341 | + | |
| 342 | +/* 无信号遮罩 | |
| 343 | + z-index 必须高于 player-box (z-index 1) | |
| 344 | + 并且背景设为纯黑,以盖住可能残留的画面 | |
| 345 | +*/ | |
| 371 | 346 | .idle-mask { |
| 372 | - position: absolute; | |
| 373 | - top: 0; | |
| 374 | - left: 0; | |
| 375 | - width: 100%; | |
| 376 | - height: 100%; | |
| 377 | - background-color: #000; | |
| 378 | - z-index: 15; | |
| 379 | - display: flex; | |
| 380 | - align-items: center; | |
| 381 | - justify-content: center; | |
| 382 | -} | |
| 383 | - | |
| 384 | -.idle-text { | |
| 385 | - color: #555; | |
| 386 | - font-size: 14px; | |
| 387 | -} | |
| 388 | - | |
| 389 | -.status-text { | |
| 390 | - color: #fff; | |
| 391 | - margin-top: 15px; | |
| 392 | - font-size: 14px; | |
| 393 | - letter-spacing: 1px; | |
| 347 | + position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| 348 | + background-color: #000; z-index: 10; | |
| 349 | + display: flex; align-items: center; justify-content: center; | |
| 394 | 350 | } |
| 351 | +.idle-text { color: #555; font-size: 14px; } | |
| 395 | 352 | |
| 396 | -.error-content { | |
| 397 | - display: flex; | |
| 398 | - flex-direction: column; | |
| 399 | - align-items: center; | |
| 400 | - justify-content: center; | |
| 401 | -} | |
| 402 | - | |
| 403 | -.error-text { | |
| 404 | - color: #ff6d6d; | |
| 405 | -} | |
| 406 | - | |
| 407 | -/* Loading 动画 */ | |
| 408 | -.spinner-box { | |
| 409 | - width: 50px; | |
| 410 | - height: 50px; | |
| 411 | - display: flex; | |
| 412 | - justify-content: center; | |
| 413 | - align-items: center; | |
| 353 | +/* 状态蒙层 */ | |
| 354 | +.status-mask { | |
| 355 | + position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| 356 | + background-color: #000; z-index: 50; | |
| 357 | + display: flex; flex-direction: column; align-items: center; justify-content: center; | |
| 414 | 358 | } |
| 359 | +.status-text { color: #fff; margin-top: 15px; font-size: 14px; letter-spacing: 1px; } | |
| 360 | +.error-content { display: flex; flex-direction: column; align-items: center; justify-content: center; } | |
| 361 | +.error-text { color: #ff6d6d; } | |
| 415 | 362 | |
| 363 | +/* Loading */ | |
| 364 | +.spinner-box { width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; } | |
| 416 | 365 | .simple-spinner { |
| 417 | - width: 40px; | |
| 418 | - height: 40px; | |
| 419 | - border: 3px solid rgba(255, 255, 255, 0.2); | |
| 420 | - border-radius: 50%; | |
| 421 | - border-top-color: #409EFF; | |
| 422 | - animation: spin 0.8s linear infinite; | |
| 423 | -} | |
| 424 | - | |
| 425 | -@keyframes spin { | |
| 426 | - to { | |
| 427 | - transform: rotate(360deg); | |
| 428 | - } | |
| 366 | + width: 40px; height: 40px; border: 3px solid rgba(255, 255, 255, 0.2); | |
| 367 | + border-radius: 50%; border-top-color: #409EFF; animation: spin 0.8s linear infinite; | |
| 429 | 368 | } |
| 369 | +@keyframes spin { to { transform: rotate(360deg); } } | |
| 430 | 370 | </style> |
| 431 | 371 | |
| 432 | 372 | <style> |
| 433 | -/* 1. 控制网速显示显隐 */ | |
| 434 | -.player-wrapper.hide-speed .easyplayer-speed { | |
| 435 | - display: none !important; | |
| 436 | -} | |
| 437 | - | |
| 438 | -/* 2. 控制解码面板显隐 */ | |
| 439 | -.player-wrapper.hide-code-select .easyplayer-controls-code-wrap { | |
| 440 | - display: none !important; | |
| 441 | -} | |
| 442 | - | |
| 443 | -/* 3. 控制其他按钮显隐 (基于您提供的 controlsConfig) */ | |
| 444 | -.player-wrapper.hide-bottom-bar .easyplayer-controls { | |
| 445 | - display: none !important; | |
| 446 | -} | |
| 447 | - | |
| 448 | -/* 播放按钮 */ | |
| 449 | -.player-wrapper.hide-btn-play .easyplayer-play, | |
| 450 | -.player-wrapper.hide-btn-play .easyplayer-pause { | |
| 451 | - display: none !important; | |
| 452 | -} | |
| 453 | - | |
| 454 | -/* 音量按钮 */ | |
| 455 | -.player-wrapper.hide-btn-audio .easyplayer-audio-box { | |
| 456 | - display: none !important; | |
| 457 | -} | |
| 458 | - | |
| 459 | -/* 截图按钮 */ | |
| 460 | -.player-wrapper.hide-btn-screenshot .easyplayer-screenshot { | |
| 461 | - display: none !important; | |
| 462 | -} | |
| 463 | - | |
| 464 | -/* 全屏按钮 */ | |
| 465 | -.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen, | |
| 466 | -.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen-exit { | |
| 467 | - display: none !important; | |
| 468 | -} | |
| 469 | - | |
| 470 | - | |
| 471 | -/* --- 修正录制按钮样式 (强制应用) --- */ | |
| 472 | -.player-wrapper.hide-btn-record .easyplayer-record, | |
| 473 | -.player-wrapper.hide-btn-record .easyplayer-record-stop { | |
| 474 | - display: none !important; | |
| 475 | -} | |
| 476 | - | |
| 477 | -/* 强制覆盖录制按钮位置 */ | |
| 373 | +.player-wrapper.hide-speed .easyplayer-speed { display: none !important; } | |
| 374 | +.player-wrapper.hide-code-select .easyplayer-controls-code-wrap { display: none !important; } | |
| 375 | +.player-wrapper.hide-bottom-bar .easyplayer-controls { display: none !important; } | |
| 376 | +.player-wrapper.hide-btn-play .easyplayer-play, .player-wrapper.hide-btn-play .easyplayer-pause { display: none !important; } | |
| 377 | +.player-wrapper.hide-btn-audio .easyplayer-audio-box { display: none !important; } | |
| 378 | +.player-wrapper.hide-btn-screenshot .easyplayer-screenshot { display: none !important; } | |
| 379 | +.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen, .player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen-exit { display: none !important; } | |
| 380 | +.player-wrapper.hide-btn-record .easyplayer-record, .player-wrapper.hide-btn-record .easyplayer-record-stop { display: none !important; } | |
| 478 | 381 | .player-wrapper .easyplayer-recording { |
| 479 | - display: none; | |
| 480 | - position: absolute !important; | |
| 481 | - top: 50px !important; | |
| 482 | - left: 50% !important; | |
| 483 | - transform: translateX(-50%) !important; | |
| 484 | - z-index: 200 !important; | |
| 485 | - background: rgba(255, 0, 0, 0.6); | |
| 486 | - border-radius: 4px; | |
| 487 | - padding: 4px 12px; | |
| 488 | -} | |
| 489 | - | |
| 490 | -.player-wrapper .easyplayer-recording[style*="block"] { | |
| 491 | - display: flex !important; | |
| 492 | - align-items: center !important; | |
| 493 | - justify-content: center !important; | |
| 494 | -} | |
| 495 | - | |
| 496 | -.player-wrapper .easyplayer-recording-time { | |
| 497 | - margin: 0 8px; | |
| 498 | - font-size: 14px; | |
| 499 | - color: #fff; | |
| 500 | -} | |
| 501 | - | |
| 502 | -.player-wrapper .easyplayer-recording-stop { | |
| 503 | - height: auto !important; | |
| 504 | - cursor: pointer; | |
| 505 | -} | |
| 506 | - | |
| 507 | - | |
| 508 | -/* --- 修正拉伸按钮样式 (SVG图标) --- */ | |
| 509 | -.player-wrapper.hide-btn-stretch .easyplayer-stretch { | |
| 510 | - display: none !important; | |
| 511 | -} | |
| 512 | - | |
| 513 | -.player-wrapper .easyplayer-stretch { | |
| 514 | - font-size: 0 !important; | |
| 515 | - width: 34px !important; | |
| 516 | - height: 100% !important; | |
| 517 | - display: flex !important; | |
| 518 | - align-items: center; | |
| 519 | - justify-content: center; | |
| 520 | - cursor: pointer; | |
| 521 | -} | |
| 522 | - | |
| 382 | + display: none; position: absolute !important; top: 50px !important; left: 50% !important; | |
| 383 | + transform: translateX(-50%) !important; z-index: 200 !important; | |
| 384 | + background: rgba(255, 0, 0, 0.6); border-radius: 4px; padding: 4px 12px; | |
| 385 | +} | |
| 386 | +.player-wrapper .easyplayer-recording[style*="block"] { display: flex !important; align-items: center !important; justify-content: center !important; } | |
| 387 | +.player-wrapper .easyplayer-recording-time { margin: 0 8px; font-size: 14px; color: #fff; } | |
| 388 | +.player-wrapper .easyplayer-recording-stop { height: auto !important; cursor: pointer; } | |
| 389 | +.player-wrapper.hide-btn-stretch .easyplayer-stretch { display: none !important; } | |
| 390 | +.player-wrapper .easyplayer-stretch { font-size: 0 !important; width: 34px !important; height: 100% !important; display: flex !important; align-items: center; justify-content: center; cursor: pointer; } | |
| 523 | 391 | .player-wrapper .easyplayer-stretch::after { |
| 524 | - content: ''; | |
| 525 | - display: block; | |
| 526 | - width: 20px; | |
| 527 | - height: 20px; | |
| 392 | + content: ''; display: block; width: 20px; height: 20px; | |
| 528 | 393 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M128 128h298.667v85.333H195.2l201.6 201.6-60.373 60.374-201.6-201.6v231.466H49.067V49.067h78.933V128zM896 896H597.333v-85.333H828.8l-201.6-201.6 60.373-60.374 201.6 201.6V518.933h85.334v377.067h-78.934V896z' fill='%23ffffff'/%3E%3C/svg%3E"); |
| 529 | - background-repeat: no-repeat; | |
| 530 | - background-position: center; | |
| 531 | - background-size: contain; | |
| 532 | - opacity: 0.8; | |
| 533 | -} | |
| 534 | - | |
| 535 | -.player-wrapper .easyplayer-stretch:hover::after { | |
| 536 | - opacity: 1; | |
| 394 | + background-repeat: no-repeat; background-position: center; background-size: contain; opacity: 0.8; | |
| 537 | 395 | } |
| 538 | - | |
| 539 | - | |
| 540 | -/* --- 修正电子放大样式 --- */ | |
| 541 | -.player-wrapper.hide-btn-zoom .easyplayer-zoom, | |
| 542 | -.player-wrapper.hide-btn-zoom .easyplayer-zoom-stop { | |
| 543 | - display: none !important; | |
| 544 | -} | |
| 545 | - | |
| 396 | +.player-wrapper .easyplayer-stretch:hover::after { opacity: 1; } | |
| 397 | +.player-wrapper.hide-btn-zoom .easyplayer-zoom, .player-wrapper.hide-btn-zoom .easyplayer-zoom-stop { display: none !important; } | |
| 546 | 398 | .player-wrapper .easyplayer-zoom-controls { |
| 547 | - position: absolute !important; | |
| 548 | - top: 50px !important; | |
| 549 | - left: 50% !important; | |
| 550 | - transform: translateX(-50%); | |
| 551 | - z-index: 199 !important; | |
| 552 | - background: rgba(0, 0, 0, 0.6); | |
| 553 | - border-radius: 20px; | |
| 554 | - padding: 0 10px; | |
| 555 | -} | |
| 556 | - | |
| 557 | - | |
| 558 | -/* --- 整体显隐控制 --- */ | |
| 559 | -.player-wrapper.force-hide-controls .easyplayer-controls { | |
| 560 | - opacity: 0 !important; | |
| 561 | - visibility: hidden !important; | |
| 562 | - transition: opacity 0.3s ease; | |
| 399 | + position: absolute !important; top: 50px !important; left: 50% !important; | |
| 400 | + transform: translateX(-50%); z-index: 199 !important; | |
| 401 | + background: rgba(0,0,0,0.6); border-radius: 20px; padding: 0 10px; | |
| 563 | 402 | } |
| 403 | +.player-wrapper.force-hide-controls .easyplayer-controls { opacity: 0 !important; visibility: hidden !important; transition: opacity 0.3s ease; } | |
| 564 | 404 | </style> | ... | ... |