Commit b73e5e103db93a7edfad3a1b51906a27efa1a046

Authored by 王鑫
1 parent 3252e971

fix():修复播放器组件轮播和全部关闭销毁播放器冲突问题

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>
... ...