关于opera老是弹出error console的问题

本文提供了解决Opera浏览器弹出错误控制台的详细步骤,包括检查选项设置、清除错误控制台过滤器及重启浏览器,旨在帮助用户有效解决此常见问题。

最近使用opera老是弹出error console,很是烦人,在此转帖一份,终于解决问题。详见13条,一定要按要求完成3步额

转自http://baoku.yunduan.cn/d/artitem/7396/1/9/0/7759/

【教程】Opera 浏览器 新手上路指南 [续]

2010-07-30 分享:HAPPY2712125 来源:http://hi.baidu.com/rongzhiyin/blog/item/04a384dce697c5accc116627.html 快来分享你的教程和攻略,还有Q币拿!

1、订阅新闻推送(Feeds)

菜单里面默认没有新闻推送(根据你的语言文件翻译为准,英文为Feeds)这个项目,但是我们浏览一些站点的时候,就会发现地址栏出现图中的图标:

043.png (4.16 KiB) 被浏览 140194 次

这就是新闻订阅了。点击它,会提示选择RSS或者Atom,选择之后,将询问我们是否订阅该新闻,确定之后,我们就会发现菜单栏多了一个新闻推送的项目,可以在里面管理每个订阅。

注:新闻订阅可以使你不用登录站点就查看到该站点更新的条目,前提是该站点支持新闻订阅。不过有的站点好像Opera无法自动发现,需要自己点击页面上的Feeds图标(就是上图中的那个)。

2、网址缩写和快捷网址

(1)我们可以通过地址栏输入某站点的网址缩写来访问已经加入到书签的某个网址

方法:添加某链接到书签的时候,在昵称那里,输入缩写,以后只要在地址栏里输入该缩写就可以打开该网址,中文缩写也行。——在书签管理每个书签的属性那里可以再次修改。注意,书签中的文件夹也可以设置昵称,在地址栏输入该历程,将打开该文件夹下的所有书签。如图:

044.png (14.5 KiB) 被浏览 140357 次

(2)用快捷键访问一个或者多个网址

方法:进入工具菜单的首选项>高级>快捷键>键盘设置>编辑,新建一个快捷键,比如F3,然后在后面的命令那里输入New page & Go to page,"网址1",如果需要一键访问多个网址,则输入New page & Go to page,"网址1" & New page & Go to page,"网址2" & New page & Go to page,"网址3",以此类推,注意网址要加引号。

3、双击笔记条目访问该笔记的来源网页

这是一个笔记的小技巧,不需要设置。当我们用右键菜单的“复制到笔记”复制一段文字到笔记本之后,在笔记本的条目列表框那里双击该条目名称,就可以打开刚才复制那段文字的网页,对于经常查找资料的朋友,这个功能相当贴心。

4、在笔记的右键菜单中添加搜索功能

在网页上拖动选择一段文本,然后点击右键选择搜索,就可以在搜索引擎中查找它。但是在笔记本中却没有这个功能。唯一的办法是复制文本,然后把它粘贴到笔记上方的搜索框。对于经常使用笔记的朋友,这个办法就不方便了。下面我们添加一条命令来完成这个功能。

打开standard_menu,找到[Edit Widget Popup Menu],在它的最下方添加:

Item, "搜索"=Copy & New page & Focus search field & Paste & go

修改后的笔记右键菜单如图:

045.png (11.41 KiB) 被浏览 140177 次

5、在右键菜单中添加查找功能

(1)右键点击页面空白处的右键菜单。

打开standard_menu,找到[Document Popup Menu],在它的最下方添加:

Item, "查找"=Find

完成后如图:

046.png (11.63 KiB) 被浏览 140175 次

(2)左键拖动选择一段文本,右键点击该文本出现的右键菜单——这条命令将自动粘贴这段拖选的文本到查找框,所以它和上面的命令不太一样,是个组合命令。

打开standard_menu,找到[Hotclick Popup Menu],在它的最下方添加:

Item, "查找"=Copy & Find & Focus search field & Paste

完成后如图:

047.png (8.98 KiB) 被浏览 139755 次

6、隐藏Opera到桌面右下角的托盘栏(老板键)(提供现成按钮)

这个就是常说的老板键了,在办公室同事或者老板来了可以很快的隐藏Opera。默认的快捷键是Ctrl+H(按Ctrl不松手,然后按H)。如果你觉得不方便,可以在首选项->高级->快捷键->键盘设置的编辑那里自己重新设置。

或者拖放下面的按钮到工具栏上:

注:老板键按钮去看Opera按钮大全。

另外,当我们使用了Opera的email功能,托盘会在启动的时候自动出现。如果你对托盘的右键菜单不满意,可以standard_menu.ini的[Tray Popup Menu]下面自行编辑。

7、导入IE等其他浏览器的收藏夹以及备份Opera的收藏夹(书签)

刚用Opera的朋友肯定第一件事就是把IE的收藏夹转到Opera中,方法是点击菜单中的文件->导入导出->导入Internet Explorer 收藏夹,这时会弹出一个窗口,显示当前Windows用户的IE收藏夹位置,确定即可。

同样的,这里也可以备份Opera的书签(即收藏夹)。文件->导入导出->导出 Opera 书签,然后选择保存位置即可。opera6.adr就是我们的保存的书签。下次要导入这个书签,在导入“导出导出”那里选择“导入 Opera 书签”。

8、隐藏菜单栏(提供现成按钮)

把下面这个按钮拖放到你的浏览器中就行了。点击一下为隐藏菜单栏,再点为显示,点击按钮上面的小三角形,会弹出菜单项目。

注:按钮去看Opera按钮大全。

9、页面无法及时刷新的问题

有时候在论坛回帖之后,可能发现看不到自己的回帖,原因是Opera缓存中已有的页面即使服务器更新,也不会及时更新,默认为5小时检查。进入首选项->高级->历史,设置检查文档为“总是”,检查图像为“5分钟”或者“总是”。另外,退出时清空缓存也可以选择上,否则有时候下次打开Opera自动打开的未关闭页面是缓存中的页面

048.png (23.98 KiB) 被浏览 140090 次

10、关闭/开启标签预览

当鼠标指到标签稍作停留,会出现该标签的网页略缩预览,如图:

049.png (153.02 KiB) 被浏览 140693 次

完全关闭的办法:进入工具菜单首选项关闭工具提示。不过这样按钮的提示也不会显示。

050.png (26.85 KiB) 被浏览 139643 次

如果你只是希望去掉网页略缩图,保留文字信息,如图:

051.png (63.88 KiB) 被浏览 139799 次

那么就不要取消工具提示,仅仅在地址栏输入opera:config,去掉Use Thumbnails in Tab Tooltips的勾选并保存就行了。

052.png (35.2 KiB) 被浏览 139271 次

11、设置标签栏位置

(1)设置到浏览器底部

053.png (125.86 KiB) 被浏览 139567 次

054.png (79.81 KiB) 被浏览 139347 次

(2)设置标签栏在浏览器顶部,但是位于地址栏下。就像下图所示:

055.png (113 KiB) 被浏览 139309 次

来吧,开始设置。

056.png (77.93 KiB) 被浏览 139001 次

057.png (92.35 KiB) 被浏览 138925 次

058.png (86.76 KiB) 被浏览 138946 次

059.png (89.79 KiB) 被浏览 138556 次

12、去掉一些站点防止复制的隐藏文字

一些站点在它的页面使用了一些隐藏的字符,防止别人复制。在我们设置了最小字体12号之后,甚至可以看见这些乱码,相当不美观。

somh编写了一个脚本来解决这个问题,相当好用。请把你的站点,添加到该脚本中中(第十行“var hs=”那里,两个站点中间需要一个空格)。如果不清楚如何使用脚本,参见24、修复Google快照。

http://files.myopera.com/亡灵法师/files/hidden-chars-remover.js

13、弹出JavaScript错误控制台的问题

正常情况下不会遇到这个问题,但是浏览器崩溃、系统问题,以及用Opera调试大量脚本后,默认关闭的JavaScript错误控制台可能不断弹出来。如图:

060.png (28.86 KiB) 被浏览 138152 次

解决办法:

(1)检查一下是否已经关闭“出错时打开控制台”的选项。

061.png (34.53 KiB) 被浏览 138588 次

062.png (22.2 KiB) 被浏览 137918 次

(2)进入opera:config下UserPrefs的ErrorConsoleFilter,清空图中的项目并保存。

063.png (45.23 KiB) 被浏览 138145 次

(3)马上关闭并重新运行Opera,否则没有作用。

注意:接发电子邮件导致的错误提示是不能关闭的。

14、修改临时文件夹位置

Opera的临时文件夹也就是硬盘缓存,默认位置是profile下的cache4目录。有的朋友可能需要喜欢自己的路径。修改方法:

在地址栏输入[config]opera:config[/config],确定后进入UserPrefs下Cache Directory4,自己设置Cache Directory4里面的路径就行了。比如设置为C:\Documents and Settings\你的用户名\Local Settings\Temp或者C:\Documents and Settings\你的用户名\Local Settings\Temporary Internet Files。

注:一个小技巧,大内存(512M,最好1G以上)又经常使用Opera的朋友,可以使用RAMDisk之类的软件从内存中虚拟一个硬盘分区出来


<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>透明桌面小人</title> <!-- Tailwind CSS (使用生产环境兼容的CDN) --> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@3.4.1/dist/tailwind.min.css" rel="stylesheet"> <!-- Font Awesome --> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> <!-- Spine Player --> <script src="spine-player.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@esotericsoftware/spine-player@4.2.21/dist/spine-player.min.css"> <script> tailwind.config = { theme: { extend: { colors: { primary: '#6366f1', secondary: '#8b5cf6', }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], } } } } </script> <style type="text/tailwindcss"> @layer utilities { .glass-effect { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); } .character-container { cursor: move; } .emoji-button { transition: all 0.2s ease; } .emoji-button:hover { transform: scale(1.2); } .emoji-button.active { border: 2px solid #6366f1; box-shadow: 0 0 10px rgba(99, 102, 241, 0.5); } } body { margin: 0; padding: 0; background-color: rgba(0, 0, 0, 0); overflow: hidden; } #character-container { position: absolute; top: 0; left: 0; width: 300px; height: 400px; z-index: 1000; user-select: none; } #spine-container { width: 100%; height: 350px; position: relative; } #controls { position: absolute; bottom: 0; left: 0; width: 100%; padding: 10px; display: flex; justify-content: center; gap: 10px; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); border-top: 1px solid rgba(255, 255, 255, 0.2); } .emoji-btn { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; background: rgba(255, 255, 255, 0.2); border: none; transition: all 0.2s ease; } .emoji-btn:hover { transform: scale(1.2); background: rgba(255, 255, 255, 0.3); } .emoji-btn.active { background: rgba(99, 102, 241, 0.3); border: 2px solid #6366f1; } #close-btn { position: absolute; top: 5px; right: 5px; width: 30px; height: 30px; border-radius: 50%; background: rgba(255, 255, 255, 0.2); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10; transition: all 0.2s ease; } #close-btn:hover { background: rgba(255, 0, 0, 0.3); } </style> </head> <body> <div id="character-container"> <div id="close-btn"> <i class="fa fa-times" aria-hidden="true"></i> </div> <div id="spine-container"></div> <div id="controls"> <button class="emoji-btn active" data-emoji="emoji_0">😊</button> <button class="emoji-btn" data-emoji="emoji_1">😂</button> <button class="emoji-btn" data-emoji="emoji_2">😍</button> <button class="emoji-btn" data-emoji="emoji_3">😢</button> <button class="emoji-btn" data-emoji="emoji_4">😡</button> </div> </div> <script> // 浏览器兼容性检查 function checkBrowserCompatibility() { // 检测浏览器类型和版本 const browserInfo = getBrowserInfo(); // 检查WebGL支持 function isWebGLSupported() { try { const canvas = document.createElement('canvas'); return !!(window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))); } catch (e) { return false; } } // 检查backdrop-filter支持 function checkBackdropFilterSupport() { if (typeof CSS !== 'undefined' && typeof CSS.supports !== 'undefined') { return CSS.supports('backdrop-filter', 'blur(10px)') || CSS.supports('-webkit-backdrop-filter', 'blur(10px)'); } return false; } // 检查必要的API支持 const requiredAPIs = { 'requestAnimationFrame': typeof window.requestAnimationFrame !== 'undefined', 'WebGL': isWebGLSupported(), 'Canvas': typeof HTMLCanvasElement !== 'undefined', 'TypedArrays': typeof Uint8Array !== 'undefined', 'backdrop-filter': checkBackdropFilterSupport() }; // 检查是否所有必要API都支持 const allSupported = Object.values(requiredAPIs).every(val => val); // 针对Edge浏览器的特定检查和处理 if (browserInfo.name === 'Edge') { console.log(`检测到Edge浏览器,版本: ${browserInfo.version}`); // Edge特定的兼容性处理 handleEdgeSpecificIssues(browserInfo.version); // 如果是Edge浏览器,即使WebGL检测失败,也尝试继续 if (!requiredAPIs.WebGL) { console.warn('Edge浏览器中WebGL检测失败,尝试使用软件渲染'); // 在Edge中,即使WebGL检测失败,也尝试初始化 requiredAPIs.WebGL = true; // 标记为支持,以便继续 } } if (!allSupported) { console.warn('浏览器兼容性问题:', requiredAPIs); // 显示兼容性警告 const warningDiv = document.createElement('div'); warningDiv.className = 'fixed top-0 left-0 w-full bg-red-500 text-white z-50 flex items-center justify-center'; warningDiv.innerHTML = ` <div class="p-4 max-w-md"> <h3 class="text-xl font-bold mb-2">浏览器兼容性问题</h3> <p>您的浏览器可能不支持某些功能,应用可能无法正常工作。</p> <p class="mt-2">建议使用最新版本的Chrome、Firefox、Edge或Safari浏览器。</p> ${browserInfo.name === 'Edge' ? ` <p class="mt-2 text-yellow-200">Edge浏览器提示:</p> <ul class="mt-1 list-disc list-inside text-sm text-yellow-200"> <li>确保已启用硬件加速</li> <li>更新显卡驱动程序</li> <li>尝试重启浏览器</li> </ul> ` : ''} <button id="close-warning" class="mt-4 px-4 py-2 bg-white text-red-500 rounded-full">关闭</button> </div> `; document.body.appendChild(warningDiv); // 关闭警告按钮 document.getElementById('close-warning').addEventListener('click', () => { warningDiv.remove(); }); } return true; // 即使有兼容性问题,也尝试继续运行 } // 获取浏览器信息 function getBrowserInfo() { const userAgent = navigator.userAgent; let browserName = '未知浏览器'; let browserVersion = '未知版本'; // 检测浏览器类型 if (userAgent.includes('Chrome')) { browserName = 'Chrome'; browserVersion = userAgent.match(/Chrome\/(\d+)/)[1]; } else if (userAgent.includes('Firefox')) { browserName = 'Firefox'; browserVersion = userAgent.match(/Firefox\/(\d+)/)[1]; } else if (userAgent.includes('Safari')) { browserName = 'Safari'; browserVersion = userAgent.match(/Version\/(\d+)/)[1]; } else if (userAgent.includes('Edge')) { browserName = 'Edge'; browserVersion = userAgent.match(/Edge\/(\d+)/) ? userAgent.match(/Edge\/(\d+)/)[1] : (userAgent.match(/Edg\/(\d+)/) ? userAgent.match(/Edg\/(\d+)/)[1] : '未知'); } else if (userAgent.includes('Opera') || userAgent.includes('OPR')) { browserName = 'Opera'; browserVersion = userAgent.match(/(Opera|OPR)\/(\d+)/)[2]; } return { name: browserName, version: browserVersion }; } // 处理Edge特定问题 function handleEdgeSpecificIssues(version) { const edgeVersion = parseInt(version); // Edge旧版本处理 if (edgeVersion < 79) { console.warn('检测到旧版本Edge浏览器,可能存在兼容性问题'); // 为旧版本Edge添加backdrop-filter回退样式 addEdgeBackdropFilterFallback(); } // 修复Edge中的WebGL问题 fixEdgeWebGLIssues(); } // 为旧版本版本Edge添加backdrop-filter回退样式 function addEdgeBackdropFilterFallback() { const style = document.createElement('style'); style.textContent = ` /* Edge旧版本backdrop-filter回退样式 */ .glass-effect, #controls, #close-btn { background: rgba(255, 255, 255, 0.3) !important; } `; document.head.appendChild(style); } // 修复Edge WebGL问题 function fixEdgeWebGLIssues() { // Edge中WebGL上下文获取可能问题的修复 const originalGetContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function(contextId, options) { try { // 尝试获取WebGL上下文 const context = originalGetContext.call(this, contextId, options); if (context) { return context; } // 如果失败,尝试使用experimental-webgl if (contextId === 'webgl') { console.log('尝试使用experimental-webgl'); return originalGetContext.call(this, 'experimental-webgl', options); } } catch (error) { console.error('获取WebGL上下文失败:', error); // 在Edge中,尝试使用不同的参数 if (contextId === 'webgl' || contextId === 'experimental-webgl') { try { console.log('尝试使用简化参数获取WebGL上下文'); return originalGetContext.call(this, contextId, { alpha: true, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: true }); } catch (error2) { console.error('使用简化参数获取WebGL上下文也失败:', error2); } } } return null; }; console.log('Edge WebGL修复已应用'); } // 全局变量 let player; let isDragging = false; let offsetX, offsetY; let currentEmoji = 'emoji_0'; let animationFrameId = null; // DOM元素 const characterContainer = document.getElementById('character-container'); const spineContainer = document.getElementById('spine-container'); const closeBtn = document.getElementById('close-btn'); const emojiBtns = document.querySelectorAll('.emoji-btn'); // 初始化Spine动画 function initSpine() { try { // 获取浏览器信息 const browserInfo = getBrowserInfo(); // 创建Spine播放器配置 const playerConfig = { jsonUrl: "assets/1310.json", atlasUrl: "assets/1310.atlas", alpha: true, backgroundColor: "#00000000", preserveDrawingBuffer: true, showLoading: false, showControls: false, animation: "idle", success: function(player) { console.log("Spine动画加载成功"); // 默认显示第一个表情 setEmoji(currentEmoji); // 启动动画循环 startAnimationLoop(); }, error: function(reason) { console.error("Spine动画加载失败:", reason); // 显示错误信息 showError(`动画加载失败: ${reason.message || reason}`); // 回退到简单的表情显示 fallbackToSimpleEmoji(); }, viewport: { padLeft: "0%", padRight: "0%", padTop: "0%", padBottom: "0%" } }; // Edge特定配置 if (browserInfo.name === 'Edge') { console.log('为Edge浏览器应用特定配置'); // 在Edge中使用更简单的配置 playerConfig.preserveDrawingBuffer = true; playerConfig.alpha = true; // 为Edge添加额外的错误处理 const originalErrorHandler = playerConfig.error; playerConfig.error = function(reason) { console.error("Edge中Spine动画加载失败:", reason); // Edge特定的错误处理 if (reason.message && reason.message.includes('WebGL')) { showError(`WebGL错误: ${reason.message}<br>在Edge中,您可能需要启用硬件加速或更新显卡驱动。`); } else { showError(`动画加载失败: ${reason.message || reason}`); } // 回退到简单的表情显示 fallbackToSimpleEmoji(); // 调用原始错误处理 if (originalErrorHandler) originalErrorHandler(reason); }; } // 创建Spine播放器 player = new spine.SpinePlayer("spine-container", playerConfig); } catch (error) { console.error("Spine初始化异常:", error); // 获取浏览器信息 const browserInfo = getBrowserInfo(); if (browserInfo.name === 'Edge' && error.message.includes('WebGL')) { showError(`初始化失败: ${error.message}<br>在Edge中,您可能需要启用硬件加速或更新显卡驱动。`); } else { showError(`初始化失败: ${error.message}`); } fallbackToSimpleEmoji(); } } // 显示错误信息 function showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'absolute inset-0 bg-red-900 bg-opacity-80 text-white flex flex-col items-center justify-center p-4 z-10'; errorDiv.innerHTML = ` <h3 class="text-xl font-bold mb-2">加载错误</h3> <p class="text-center mb-4">${message}</p> <p class="text-sm text-gray-300">请尝试检查Spine资源文件是否存在或格式是否正确</p> `; document.getElementById('spine-container').appendChild(errorDiv); } // 回退到简单的表情显示 function fallbackToSimpleEmoji() { const container = document.getElementById('spine-container'); container.innerHTML = ''; const emojiDiv = document.createElement('div'); emojiDiv.id = 'fallback-emoji'; emojiDiv.className = 'w-full h-full flex items-center justify-center text-6xl'; emojiDiv.textContent = '😊'; container.appendChild(emojiDiv); // 更新表情的函数 window.setFallbackEmoji = function(emojiName) { const emojiMap = { 'emoji_0': '😊', 'emoji_1': '😂', 'emoji_2': '😍', 'emoji_3': '😢', 'emoji_4': '😡' }; emojiDiv.textContent = emojiMap[emojiName] || '😊'; }; console.log("已回退到简单表情模式"); } // 动画循环 function startAnimationLoop() { let lastTime = 0; function animate(currentTime) { if (!lastTime) lastTime = currentTime; const delta = (currentTime - lastTime) / 1000; // 转换为秒 lastTime = currentTime; if (player && player.update) { try { player.update(delta); } catch (error) { console.error("动画更新错误:", error); } } animationFrameId = requestAnimationFrame(animate); } // 停止之前的动画循环 if (animationFrameId) { cancelAnimationFrame(animationFrameId); } animationFrameId = requestAnimationFrame(animate); } // 设置表情 function setEmoji(emojiName) { currentEmoji = emojiName; // 更新按钮状态 emojiBtns.forEach(btn => { if (btn.dataset.emoji === emojiName) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // 检查是否在回退模式 if (window.setFallbackEmoji) { window.setFallbackEmoji(emojiName); return; } if (!player) return; try { // 在Spine中设置表情 // 这里假设Spine动画中有对应的皮肤或附件来切换表情 // 如果是通过皮肤切换 if (player.skeleton && player.skeleton.data && player.skeleton.data.findSkin) { if (player.skeleton.data.findSkin(emojiName)) { player.skeleton.setSkin(emojiName); player.skeleton.setToSetupPose(); if (player.update) player.update(0); return; } } // 如果是通过附件切换 if (player.skeleton && player.skeleton.findSlot) { const slot = player.skeleton.findSlot("face"); if (slot && player.skeleton.getAttachment) { const attachment = player.skeleton.getAttachment(slot.index, emojiName); if (attachment) { slot.setAttachment(attachment); if (player.update) player.update(0); return; } } } // 如果是通过动画切换 if (player.animationState && player.animationState.data && player.animationState.data.findAnimation) { if (player.animationState.data.findAnimation(emojiName)) { player.animationState.setAnimation(0, emojiName, false); // 播放完表情动画后回到idle if (player.animationState.data.findAnimation("idle")) { player.animationState.addAnimation(0, "idle", true, 0); } return; } } console.warn(`未找到表情: ${emojiName}`); } catch (error) { console.error("设置表情错误:", error); } } // 窗口拖动功能 function initDrag() { characterContainer.addEventListener('mousedown', startDrag); characterContainer.addEventListener('touchstart', startDrag, { passive: false }); function startDrag(e) { // 忽略控件区域的拖动 if (e.target.closest('#controls') || e.target.closest('#close-btn')) { return; } isDragging = true; if (e.type === 'mousedown') { offsetX = e.clientX - characterContainer.getBoundingClientRect().left; offsetY = e.clientY - characterContainer.getBoundingClientRect().top; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); } else { offsetX = e.touches[0].clientX - characterContainer.getBoundingClientRect().left; offsetY = e.touches[0].clientY - characterContainer.getBoundingClientRect().top; document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('touchend', stopDrag); } e.preventDefault(); } function drag(e) { if (!isDragging) return; let clientX, clientY; if (e.type === 'mousemove') { clientX = e.clientX; clientY = e.clientY; } else { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; e.preventDefault(); // 防止滚动 } const x = clientX - offsetX; const y = clientY - offsetY; // 限制在屏幕范围内 const maxX = window.innerWidth - characterContainer.offsetWidth; const maxY = window.innerHeight - characterContainer.offsetHeight; const boundedX = Math.max(0, Math.min(x, maxX)); const boundedY = Math.max(0, Math.min(y, maxY)); characterContainer.style.left = `${boundedX}px`; characterContainer.style.top = `${boundedY}px`; } function stopDrag() { isDragging = false; document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchmove', drag); document.removeEventListener('touchend', stopDrag); } } // 初始化关闭按钮 function initCloseBtn() { closeBtn.addEventListener('click', () => { // 在Electron环境中关闭窗口 if (window.electron) { window.electron.closeWindow(); } else { // 在浏览器中隐藏 characterContainer.style.display = 'none'; console.log('窗口已关闭'); } }); } // 初始化表情按钮 function initEmojiBtns() { emojiBtns.forEach(btn => { btn.addEventListener('click', () => { setEmoji(btn.dataset.emoji); }); }); } // 窗口大小调整 function initResize() { // 简单的缩放功能,通过滚轮缩放角色大小 characterContainer.addEventListener('wheel', (e) => { e.preventDefault(); const currentWidth = characterContainer.offsetWidth; const currentHeight = characterContainer.offsetHeight; // 缩放因子 const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1; const newWidth = Math.max(100, Math.min(currentWidth * scaleFactor, 500)); const newHeight = Math.max(150, Math.min(currentHeight * scaleFactor, 700)); characterContainer.style.width = `${newWidth}px`; characterContainer.style.height = `${newHeight}px`; // 调整Spine容器高度 const spineHeight = newHeight - 60; // 减去控制栏高度 document.getElementById('spine-container').style.height = `${spineHeight}px`; // 如果播放器已初始化,重新调整大小 if (player && player.resize) { player.resize(); } }); } // 初始化 document.addEventListener('DOMContentLoaded', () => { // 获取浏览器信息 const browserInfo = getBrowserInfo(); console.log(`当前浏览器: ${browserInfo.name} ${browserInfo.version}`); // 检查浏览器兼容性 const isCompatible = checkBrowserCompatibility(); // 为Edge浏览器添加特定样式 if (browserInfo.name === 'Edge') { document.body.classList.add('edge-browser'); // 添加Edge特定的CSS样式 const edgeStyle = document.createElement('style'); edgeStyle.textContent = ` /* Edge浏览器特定样式 */ .edge-browser #controls, .edge-browser #close-btn { -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); } /* Edge中可能需要调整z-index */ .edge-browser #character-container { z-index: 2147483647; } `; document.head.appendChild(edgeStyle); } // 初始化Spine initSpine(); // 初始化其他功能 initDrag(); initCloseBtn(); initEmojiBtns(); initResize(); // 随机位置初始化 const randomX = Math.floor(Math.random() * (window.innerWidth - 300)); const randomY = Math.floor(Math.random() * (window.innerHeight - 400)); characterContainer.style.left = `${randomX}px`; characterContainer.style.top = `${randomY}px`; }); // 页面卸载时清理 window.addEventListener('beforeunload', () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } }); // 与Electron主进程通信 if (window.electron) { // 接收主进程的消息 window.electron.receive('update-emoji', (emoji) => { setEmoji(emoji); }); window.electron.receive('update-position', (x, y) => { characterContainer.style.left = `${x}px`; characterContainer.style.top = `${y}px`; }); } </script> </body> </html> import sys import os import json from pathlib import Path from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtCore import Qt, QUrl, QPoint, pyqtSlot from PyQt5.QtGui import QRegion, QPixmap, QPainter, QColor class TransparentWindow(QMainWindow): def __init__(self): super().__init__() # 初始化窗口设置 self.init_window() # 初始化Web视图 self.init_web_view() # 加载配置 self.load_config() # 设置窗口位置 self.set_initial_position() def init_window(self): # 设置窗口标志 self.setWindowFlags( Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SubWindow ) # 设置窗口透明 self.setAttribute(Qt.WA_TranslucentBackground) self.setAttribute(Qt.WA_NoSystemBackground, False) # 设置窗口大小 self.setFixedSize(300, 400) # 创建中心部件 self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.layout = QVBoxLayout(self.central_widget) self.layout.setContentsMargins(0, 0, 0, 0) def init_web_view(self): # 创建WebEngineView self.web_view = QWebEngineView() # 设置Web通道用于与JavaScript通信 from PyQt5.QtWebChannel import QWebChannel self.channel = QWebChannel() self.channel.registerObject("backend", self) self.web_view.page().setWebChannel(self.channel) # 设置WebEngine设置 settings = self.web_view.settings() settings.setAttribute(settings.JavascriptEnabled, True) settings.setAttribute(settings.LocalContentCanAccessRemoteUrls, True) settings.setAttribute(settings.LocalContentCanAccessFileUrls, True) # 添加到布局 self.layout.addWidget(self.web_view) # 加载HTML文件 html_path = Path(__file__).parent / "desktop-character.html" self.web_view.load(QUrl.fromLocalFile(str(html_path))) def load_config(self): # 加载配置文件 config_path = Path(__file__).parent / "config.json" if config_path.exists(): with open(config_path, 'r', encoding='utf-8') as f: self.config = json.load(f) else: self.config = { "position": {"x": 100, "y": 100}, "emoji": "emoji_0", "size": {"width": 300, "height": 400} } def save_config(self): # 保存配置文件 config_path = Path(__file__).parent / "config.json" with open(config_path, 'w', encoding='utf-8') as f: json.dump(self.config, f, ensure_ascii=False, indent=2) def set_initial_position(self): # 设置初始位置 x = self.config.get("position", {}).get("x", 100) y = self.config.get("position", {}).get("y", 100) self.move(x, y) @pyqtSlot() def closeWindow(self): # 保存当前位置 pos = self.pos() self.config["position"] = {"x": pos.x(), "y": pos.y()} self.save_config() # 关闭窗口 self.close() @pyqtSlot(str) def updateEmoji(self, emoji): # 更新表情配置 self.config["emoji"] = emoji self.save_config() def mousePressEvent(self, event): # 记录鼠标按下时的位置 if event.button() == Qt.LeftButton: self.drag_position = event.globalPos() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): # 拖动窗口 if event.buttons() == Qt.LeftButton: self.move(event.globalPos() - self.drag_position) event.accept() def mouseReleaseEvent(self, event): # 保存位置 if event.button() == Qt.LeftButton: pos = self.pos() self.config["position"] = {"x": pos.x(), "y": pos.y()} self.save_config() def main(): # 设置环境变量以支持透明背景 os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--enable-transparent-visuals --disable-gpu" app = QApplication(sys.argv) # 设置应用程序样式 app.setStyleSheet(""" QMainWindow { background: transparent; } QWebEngineView { background: transparent; } """) window = TransparentWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main() 浏览器兼容性问题 您的浏览器可能不支持某些功能,应用可能无法正常工作。 建议使用最新版本的Chrome、Firefox、Edge或Safari浏览器
11-06
import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'; import { isFunction } from '@/utils/is'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; import 'videojs-playlist'; import './PlaylistButton'; import './index.scss'; export const DEFAULT_CONTROL_BAR_OPTIONS = [ // 播放/暂停 { name: 'playToggle' }, // 音量调整 { name: 'volumePanel', inline: false, }, // 未知 { name: 'currentTimeDisplay' }, // 未知 { name: 'timeDivider' }, // 未知 { name: 'durationDisplay' }, // 播放进度条 { name: 'progressControl' }, // 播放倍速 { name: 'playbackRateMenuButton' }, // 播放视频列表 { name: 'playlistButton' }, // 全屏 { name: 'fullscreenToggle' }, ] as const; // 扩展videojs类型 declare global { interface Window { videojs: any; } } // ControlBar 支持的组件类型名称 export type TControlBarNames = typeof DEFAULT_CONTROL_BAR_OPTIONS[number]['name']; // ControlBar 组件单项支持的配置 export type TControlBarOption = { name: TControlBarNames; line?: boolean; content?: string; }; // ControlBar 配置项集合 export type TControlBarOptions = (TControlBarNames | TControlBarOption)[]; // 外部支持传入的ControlBar配置 export type TControlBarOptionsProps = ((options: TControlBarOption[]) => TControlBarOptions) | TControlBarOptions; // 循环播放模式枚举 export enum LoopMode { NONE = 'none', // 不循环 SINGLE = 'single', // 单视频循环 SEQUENCE = 'sequence', // 顺序循环整个播放列表 SHUFFLE = 'shuffle', // 随机循环整个播放列表 } // 视频项接口 export interface VideoItem { sources: Array<{ src: string; type: string; }>; poster?: string; title?: string; summary?: string; } // 组件属性接口 interface MifcVideoProps { videoList: VideoItem[]; autoplay?: boolean; loop?: boolean | LoopMode; // 支持布尔值或循环模式 width?: string | number; height?: string | number; onVideoChange?: (index: number, isAutoPlay?: boolean) => void; onPlay?: () => void; onPause?: (currentTime: number) => void; onEnded?: () => void; onRef?: (ref: MifcVideoRef) => void; children?: React.ReactNode; controlBarOptions?: TControlBarOptionsProps; onPlaylistToggle?: (visible: boolean) => void; } // 组件引用接口 export interface MifcVideoRef { playByUrl: (videoUrl: string) => void; play: () => void; pause: () => void; } const MifcVideo = forwardRef<MifcVideoRef, MifcVideoProps>( ( { videoList, autoplay = false, loop = LoopMode.SEQUENCE, // 默认顺序循环 width = '100%', height = '100%', onVideoChange, onPlay, onPause, onEnded, onRef, children, controlBarOptions, onPlaylistToggle }, ref, ) => { const videoRef = useRef<HTMLVideoElement>(null); const playlistRef = useRef<HTMLDivElement>(null); const childrenRef = useRef<HTMLDivElement>(null); const playerRef = useRef<any>(null); const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null); const originalPlaylistRef = useRef<VideoItem[]>([]); const [isPlaylistVisible, setIsPlaylistVisible] = useState(false); const isPlaylistVisibleRef = useRef(isPlaylistVisible); const [currentVideoIndex, setCurrentVideoIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [autoplayBlocked, setAutoplayBlocked] = useState(false); const [isPlaylistScrollable, setIsPlaylistScrollable] = useState(false); const [isMouseNearLeftEdge, setIsMouseNearLeftEdge] = useState(false); const [showControls, setShowControls] = useState(false); const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null); const [isMobile, setIsMobile] = useState(false); const isAutoPlayingRef = useRef(false); const touchStartRef = useRef({ x: 0, y: 0 }); const lastCallTimeRef = useRef(0); const [isFullscreen, setIsFullscreen] = useState(false); // 新增:跟踪全屏状态 const userInteractedRef = useRef(false); // 新增:跟踪用户是否已交互 // 暴露方法给父组件 useImperativeHandle(ref, () => ({ playByUrl: (videoUrl: string) => { if (playerRef.current) { // 设置自动播放标记 isAutoPlayingRef.current = true; // 创建临时视频项 const tempVideo: VideoItem = { sources: [{ src: videoUrl, type: 'video/mp4' }], title: 'AI推荐视频', summary: '正在播放AI推荐的视频', }; // 将临时视频添加到播放列表并播放 const currentPlaylist = (playerRef.current as any).playlist(); const videoIndex = currentPlaylist.findIndex((video: VideoItem) => video.sources.some((source) => source.src === videoUrl), ); if (videoIndex >= 0) { // 如果视频已在播放列表中,直接切换到该视频 switchVideo(videoIndex); // onVideoChange 会通过 playlistitem 事件自动触发 } else { // 如果视频不在播放列表中,添加到列表并播放 const newPlaylist = [...currentPlaylist, tempVideo]; (playerRef.current as any).playlist(newPlaylist, newPlaylist.length - 1); // onVideoChange 会通过 playlistitem 事件自动触发 } // 尝试播放 const playPromise = playerRef.current.play(); if (playPromise !== undefined) { playPromise.catch((error: any) => { console.warn('播放视频失败:', error); }); } } }, play: () => { if (playerRef.current) { const playPromise = playerRef.current.play(); if (playPromise !== undefined) { playPromise.catch((error: any) => { console.warn('恢复播放失败:', error); }); } } }, pause: () => { if (playerRef.current) { playerRef.current.pause(); } }, })); // 检测是否为移动端 const checkMobile = () => { const userAgent = navigator.userAgent.toLowerCase(); const isMobileDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isMobileResult = isMobileDevice || isTouchDevice; setIsMobile(isMobileResult); return isMobileResult; }; useEffect(() => { checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // 显示控制器 const showControlsWithTimeout = useCallback(() => { setShowControls(true); // 清除之前的定时器 if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); } // 3秒后自动隐藏控制器 controlsTimeoutRef.current = setTimeout(() => { setShowControls(false); }, 3000); }, []); // 隐藏控制器 const hideControls = useCallback(() => { setShowControls(false); if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); controlsTimeoutRef.current = null; } }, []); // 自定义setter函数,同时更新状态和ref const setIsPlaylistVisibleWithRef = useCallback((value: boolean) => { setIsPlaylistVisible(value); isPlaylistVisibleRef.current = value; // 立即更新ref // 通知外部播放列表可见性变化 onPlaylistToggle?.(value); }, [onPlaylistToggle]); // 显示播放列表 //showPlaylist`函数不能添加isMobile为依赖项,因为当前函数被用作useEffect的依赖项,而该useEffect用于初始化播放器。当`isMobile`状态改变时,`showPlaylist`函数会重新创建,导致`useEffect`依赖项变化,从而重新初始化播放器。在移动端,可能由于重新初始化播放器时自动播放被阻止,导致黑屏 const showPlaylist = useCallback(() => { // 防止快速连续调用 const now = Date.now(); if (now - lastCallTimeRef.current < 300) return; lastCallTimeRef.current = now; const currentVisible = isPlaylistVisibleRef.current; const mobileIsNo = checkMobile(); if (currentVisible) { setIsPlaylistVisibleWithRef(false); } else { setIsPlaylistVisibleWithRef(true); } // 清除之前的定时器 if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } if (!mobileIsNo) { // 1.5秒后自动隐藏 // hideTimeoutRef.current = setTimeout(() => { // setIsPlaylistVisibleWithRef(false); // }, 1500); } }, [setIsPlaylistVisibleWithRef]); // 隐藏播放列表 const hidePlaylist = useCallback((e?: React.MouseEvent | React.TouchEvent) => { // 阻止事件冒泡到父元素,避免触发视频播放/暂停 if (e) { e.stopPropagation(); e.preventDefault(); // 阻止默认行为 } setIsPlaylistVisibleWithRef(false); if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } }, [setIsPlaylistVisibleWithRef]); // 切换视频 const switchVideo = useCallback((index: number) => { if (playerRef.current && playerRef.current.playlist) { playerRef.current.playlist.currentItem(index); // setCurrentVideoIndex 和 onVideoChange 会通过 playlistitem 事件自动触发 // 不需要在这里重复调用 // 重置隐藏定时器 if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } hideTimeoutRef.current = setTimeout(() => { setIsPlaylistVisibleWithRef(false); }, 1000); } }, []); // 设置循环播放模式 const setLoopMode = useCallback((mode: LoopMode) => { if (!playerRef.current) return; switch (mode) { case LoopMode.NONE: // 不循环播放 (playerRef.current as any).playlist.repeat(false); (playerRef.current as any).playlist.autoadvance(0); break; case LoopMode.SINGLE: // 单视频循环 (playerRef.current as any).playlist.repeat(false); (playerRef.current as any).playlist.autoadvance(0); // 设置videojs的loop属性 playerRef.current.loop(true); break; case LoopMode.SEQUENCE: // 顺序循环整个播放列表 (playerRef.current as any).playlist.repeat(true); (playerRef.current as any).playlist.autoadvance(0); playerRef.current.loop(false); break; case LoopMode.SHUFFLE: // 随机循环整个播放列表 (playerRef.current as any).playlist.repeat(false); (playerRef.current as any).playlist.autoadvance(0); playerRef.current.loop(false); // 启用随机播放 (playerRef.current as any).playlist.shuffle({ rest: false }); break; } }, []); // 初始化播放器配置项 const initPlayerOptions = useCallback(()=> { let controlBarOpts = [...DEFAULT_CONTROL_BAR_OPTIONS] as TControlBarOptions; if (controlBarOptions && isFunction(controlBarOptions)) { controlBarOpts = controlBarOptions(controlBarOpts as TControlBarOption[]); } return { controls: true, autoplay, loop: false, // 初始不设置循环,由循环模式控制 fluid: true, responsive: true, playbackRates: [0.5, 1, 1.25, 1.5, 2], playsinline: true, // 防止移动端自动全屏 controlBar: { children: controlBarOpts }, }; }, [autoplay]); // 初始化播放器 useEffect(() => { if (!videoRef.current || !videoList.length) return; // 保存原始播放列表 originalPlaylistRef.current = [...videoList]; // 初始化videojs播放器 const player = videojs(videoRef.current, { ...initPlayerOptions() }); playerRef.current = player; // 添加播放列表组件 playerRef.current.addChild('Component', { el: playlistRef.current, }); playerRef.current.addChild('Component', { el: childrenRef.current, }); // 等待播放器准备就绪后初始化播放列表 player.ready(() => { // 使用正确的播放列表API初始化 (player as any).playlist(videoList, 0); // 设置循环播放模式 const loopMode = typeof loop === 'boolean' ? (loop ? LoopMode.SEQUENCE : LoopMode.NONE) : loop; setLoopMode(loopMode); // 设置自动播放 if (autoplay) { // 确保播放列表已加载 setTimeout(() => { try { (player as any).playlist.first(); const playPromise = player.play(); // 处理自动播放可能被浏览器阻止的情况 if (playPromise !== undefined) { playPromise.catch((error: any) => { console.warn('自动播放被阻止:', error); setAutoplayBlocked(true); }); } } catch (error) { console.warn('自动播放失败:', error); setAutoplayBlocked(true); } }, 100); } }); // 设置事件监听器 player.on('play', () => { setIsPlaying(true); // showPlaylist(); onPlay?.(); }); player.on('pause', () => { setIsPlaying(false); const currentTime = player.currentTime() || 0; onPause?.(currentTime); }); player.on('ended', () => { setIsPlaying(false); onEnded?.(); isAutoPlayingRef.current = false; }); player.on('timeupdate', () => { const currentTime = player.currentTime(); const duration = player.duration(); // 在视频即将结束时显示播放列表(剩余3秒) if (duration && currentTime && currentTime > 0 && duration - currentTime <= 3) { // 注释掉这里,因为会触发多次,直接播放完下一个播放就行不需要展开播放列表 // showPlaylist(); } }); // 播放列表项变化事件 player.on('playlistitem', (event: any, data: any) => { const currentIndex = (player as any).playlist.currentItem(); setCurrentVideoIndex(currentIndex); onVideoChange?.(currentIndex, isAutoPlayingRef.current); // 根据自动播放标记传递参数 // // 重置自动播放标记 // isAutoPlayingRef.current = false; }); // 播放列表按钮点击事件 player.on('playlistButtonClick', (e?: React.MouseEvent | React.TouchEvent) => { // 彻底阻止事件传播 if (e) { e.stopPropagation(); e.preventDefault(); } showPlaylist(); }); // 监听全屏变化事件 player.on('fullscreenchange', () => { setIsFullscreen(player?.isFullscreen() || false); }); // 清理函数 return () => { if (player) { player.dispose(); } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } if (playerRef.current) { playerRef.current.dispose(); playerRef.current = null; } }; }, [videoList, autoplay, loop, showPlaylist, onVideoChange, onPlay, onPause, onEnded, setLoopMode, setIsPlaylistVisibleWithRef]); // 播放列表点击事件处理 const handlePlaylistItemClick = useCallback( (index: number, e?: React.MouseEvent | React.TouchEvent) => { if (e) { e.stopPropagation(); // 阻止事件冒泡 e.preventDefault(); // 阻止默认行为 } switchVideo(index); isAutoPlayingRef.current = false; }, [switchVideo], ); // 播放列表项的触摸事件处理 const handlePlaylistItemTouchStart = useCallback((e: React.TouchEvent, index: number) => { // 记录触摸起始位置 const touch = e.touches[0]; touchStartRef.current = { x: touch.clientX, y: touch.clientY, }; e.stopPropagation(); }, []); // 播放列表项的触结束事件处理 const handlePlaylistItemTouchEnd = useCallback( (e: React.TouchEvent, index: number) => { // 计算触摸移动距离 const touch = e.changedTouches[0]; const deltaX = Math.abs(touch.clientX - touchStartRef.current.x); const deltaY = Math.abs(touch.clientY - touchStartRef.current.y); // 只有在移动距离小于阈值时才触发点击事件 // 这样可以防止滑动时误触发点击 if (deltaX < 10 && deltaY < 10) { handlePlaylistItemClick(index, e); } e.stopPropagation(); }, [handlePlaylistItemClick], ); // 播放列表浮层的点击处理 const handlePlaylistOverlayClick = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); }, []); // 播放列表鼠标进入事件 const handlePlaylistMouseEnter = useCallback(() => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } // 在播放列表区域时保持控制器显示 showControlsWithTimeout(); }, [showControlsWithTimeout]); // 播放列表鼠标离开事件 const handlePlaylistMouseLeave = useCallback(() => { // 只有在鼠标不在左侧边缘时才启动自动隐藏定时器 if (!isMouseNearLeftEdge) { hideTimeoutRef.current = setTimeout(() => { setIsPlaylistVisibleWithRef(false); }, 1000); } }, [isMouseNearLeftEdge]); // 处理用户交互开始播放 const handleUserInteraction = useCallback(() => { if (autoplayBlocked && playerRef.current) { // 标记用户已交互 userInteractedRef.current = true; // 取消静音 playerRef.current.muted(false); const playPromise = playerRef.current.play(); if (playPromise !== undefined) { playPromise .then(() => setAutoplayBlocked(false)) .catch((error: any) => { console.warn('播放失败:', error); }); } } }, [autoplayBlocked]); // 检测播放列表内容是否可滚动 const checkPlaylistScrollable = useCallback(() => { const playlistContent = document.querySelector('.playlist-content'); if (playlistContent) { const isScrollable = playlistContent.scrollHeight > playlistContent.clientHeight; setIsPlaylistScrollable(isScrollable); } }, []); // 处理播放器点击事件 const handlePlayerClick = useCallback( (event?: React.MouseEvent | React.TouchEvent) => { // 如果是移动端且是触摸事件,不处理点击(由触摸事件处理) if (isMobile && event && 'touches' in event) { return; } // 检查触摸目标是否是播放列表按钮或相关元素 const target = event?.target as Element; const isPlaylistButton = target?.closest('.vjs-playlist-button'); if (isPlaylistButton) return; if (playerRef.current) { if (isPlaying) { playerRef.current.pause(); } else { // 在iOS上,确保用户已交互 if (isMobile && !userInteractedRef.current) { userInteractedRef.current = true; setAutoplayBlocked(false); } playerRef.current.play(); } } }, [isPlaying, isMobile], ); // 处理触摸开始事件(移动端) const handleTouchStart = useCallback( (event: React.TouchEvent) => { // 显示控制器 showControlsWithTimeout(); // 检查触摸目标是否是播放列表按钮或相关元素 const target = event.target as Element; const isPlaylistButton = target.closest('.vjs-playlist-button') || target.closest('.playlist-toggle-btn') || target.closest('.playlist-overlay'); // 在移动端触摸播放视频时关闭侧边栏,但排除播放列表相关元素 if (isMobile && !isPlaylistButton) { hidePlaylist(); } }, [showControlsWithTimeout, isMobile], ); // 处理触摸结束事件(移动端) const handleTouchEnd = useCallback( (event: React.TouchEvent) => { // 检查是否在播放器区域点击(非控制栏区域) const touch = event.changedTouches[0]; const container = event.currentTarget; const rect = container.getBoundingClientRect(); const touchY = touch.clientY - rect.top; // 检查是否点击在控制栏区域(底部20%区域) const isInControlBarArea = touchY > rect.height * 0.8; // 只有在非控制栏区域点击时才触发播放/暂停 if (!isInControlBarArea) { handlePlayerClick(); } }, [handlePlayerClick], ); // 处理触摸移动事件(移动端) const handleTouchMove = useCallback((event: React.TouchEvent) => { // 移动端不需要处理触摸移动事件 }, []); // 处理鼠标移动到左侧边缘(仅桌面端) const handleMouseMove = useCallback( (event: React.MouseEvent) => { const container = event.currentTarget; const rect = container.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const edgeThreshold = 50; // 左侧边缘检测区域宽度 // 显示控制器 showControlsWithTimeout(); // 仅在桌面端处理左侧边缘检测 // if (!isMobile) { // if (mouseX <= edgeThreshold) { // setIsMouseNearLeftEdge(true); // // 只有在播放列表未显示且不在播放列表区域内时才显示 // if (!isPlaylistVisible) { // showPlaylist(); // } // } else { // setIsMouseNearLeftEdge(false); // } // } }, [isPlaylistVisible, showPlaylist, showControlsWithTimeout, isMobile], ); // 处理鼠标离开容器(仅桌面端) const handleMouseLeave = useCallback( (event: React.MouseEvent) => { // 仅在桌面端处理鼠标离开事件 if (!isMobile) { setIsMouseNearLeftEdge(false); hideControls(); } }, [hideControls, isMobile], ); // 检测播放列表可滚动状态 useEffect(() => { if (isPlaylistVisible) { // 延迟检测,确保DOM已更新 setTimeout(checkPlaylistScrollable, 100); // 监听窗口大小变化 window.addEventListener('resize', checkPlaylistScrollable); return () => window.removeEventListener('resize', checkPlaylistScrollable); } }, [isPlaylistVisible, videoList, checkPlaylistScrollable]); // 控制videojs控制器的显示/隐藏 useEffect(() => { if (playerRef.current) { const controlBar = playerRef.current.controlBar; if (controlBar) { if (showControls) { controlBar.el().classList.add('vjs-control-bar-visible'); } else { controlBar.el().classList.remove('vjs-control-bar-visible'); } } } }, [showControls]); // 组件卸载时清理定时器 useEffect(() => { return () => { if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); } }; }, []); return ( <div className={`mifc-video-container ${isFullscreen ? 'fullscreen-mode' : ''}`} style={{ width, height }} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} onTouchMove={handleTouchMove} onContextMenu={(e) => e.preventDefault()}> {/* 视频播放器 */} <div className={`video-player-wrapper ${!isMobile && isMouseNearLeftEdge ? 'edge-hover' : ''}`} onClick={handlePlayerClick} style={{ touchAction: 'manipulation' }}> <video ref={videoRef} className="video-js vjs-default-skin" data-setup="{}" muted={autoplay} playsInline onContextMenu={(e) => e.preventDefault()} style={{ pointerEvents: 'auto' }} /> {/* 自动播放被阻止时的提示 */} {autoplayBlocked && ( <div className="autoplay-blocked-overlay" onClick={handleUserInteraction}> <div className="autoplay-blocked-content"> <div className="autoplay-icon">▶️</div> <div className="autoplay-text"> <h3>点击开始播放</h3> <p>浏览器阻止了自动播放,请点击此处开始播放</p> </div> </div> </div> )} {/* 暂停时显示的播放按钮 {!isPlaying && !autoplayBlocked && ( <div className="play-button-overlay" onClick={handlePlayerClick}> <div className="play-button"> <svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="40" cy="40" r="40" fill="rgba(0, 0, 0, 0.7)"/> <path d="M32 24L56 40L32 56V24Z" fill="white"/> </svg> </div> </div> )} */} </div> {/* 播放列表浮层 */} <div ref={playlistRef} className={`playlist-overlay ${isPlaylistVisible && !isFullscreen ? 'visible' : ''} ${isFullscreen ? 'fullscreen' : ''} ${isPlaylistVisible && isFullscreen ? 'fullscreen-visible' : ''}`} onMouseEnter={handlePlaylistMouseEnter} onMouseLeave={handlePlaylistMouseLeave} onClick={handlePlaylistOverlayClick} // 阻止播放列表浮层的点击事件冒泡 onTouchStart={handlePlaylistOverlayClick} // 阻止触摸事件冒泡 onTouchEnd={handlePlaylistOverlayClick} // 添加触摸结束处理 > <div className={`playlist-content ${isPlaylistScrollable ? 'scrollable' : ''}`} onClick={handlePlaylistOverlayClick} // 内容区域也阻止点击 onTouchStart={handlePlaylistOverlayClick} // 内容区域也阻止触摸 > {videoList.map((video, index) => ( <div key={index} className={`playlist-item ${index === currentVideoIndex ? 'active' : ''}`} onClick={(e) => handlePlaylistItemClick(index, e)} onTouchStart={(e) => handlePlaylistItemTouchStart(e, index)} // 添加触摸开始处理 onTouchEnd={(e) => handlePlaylistItemTouchEnd(e, index)} // 添加触摸结束处理 > <div className="playlist-item-content"> <div className="playlist-item-title">{video.title || `视频 ${index + 1}`}</div> <div className="playlist-item-summary">{video.summary || '点击播放视频'}</div> </div> </div> ))} </div> {/* 右侧收起按钮 */} <div className="playlist-toggle-btn" onClick={hidePlaylist} onTouchEnd={hidePlaylist} // 添加触摸事件处理 ></div> </div> <div ref={childrenRef}>{children}</div> </div> ); }, ); export default MifcVideo; 当前代码在返回当前页面的时候视频播放的尺寸不对了
09-04
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值