您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
影视信息提取与排版工具,添加AI文字生成功能
// ==UserScript== // @name 豆瓣+TMDB影视工具(AI增强) // @namespace tampermonkey // @version 1.0 // @description 影视信息提取与排版工具,添加AI文字生成功能 // @author 绘梦 // @match https://pan1.me/?thread-create-*.htm // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_log // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect www.douban.com // @connect accounts.douban.com // @connect search.douban.com // @connect movie.douban.com // @connect m.douban.com // @connect doubanio.com // @connect search.doubanio.com // @connect tv.douban.com // @connect doubanio.com // @run-at document-end // @license MIT // 新增:声明MIT许可证 // ==/UserScript== (function () { 'use strict'; // === Douban anti-crawl limiter === // 兼顾速度与风控:放宽总频率,但引入“批量节流”与图片并发池 const D_RATE = { maxPerMin: 26, minDelay: 380, maxDelay: 900 }; let dLastTs = 0, dTokens = D_RATE.maxPerMin, dWindow = Date.now(); let doubanCooldownUntil = 0; function inCooldown() { return Date.now() < doubanCooldownUntil; } function triggerCooldown(sec = 90) { doubanCooldownUntil = Date.now() + sec*1000; } function waitDoubanSlot() { return new Promise(res => { const refill = () => { const now = Date.now(); if (now - dWindow >= 60000) { dWindow = now; dTokens = D_RATE.maxPerMin; } if (dTokens > 0) { dTokens--; const jitter = D_RATE.minDelay + Math.floor(Math.random()*(D_RATE.maxDelay-D_RATE.minDelay)); const gap = Math.max(0, D_RATE.minDelay - (now - dLastTs)); const delay = Math.max(jitter, gap); setTimeout(()=>{ dLastTs = Date.now(); res(); }, delay); } else setTimeout(refill, 300); }; refill(); }); } const UA_POOL = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15' ]; function isDoubanUrl(u){ try{ return /douban\.(com|io)|doubanio\.com/.test(new URL(u).hostname) || /douban/gi.test(u); }catch(_){ return /douban/.test(String(u)); } } function doubanRequest(opts){ return new Promise(async (resolve,reject)=>{ if (!opts || !opts.url) return reject(new Error('bad-opts')); const isDouban = isDoubanUrl(opts.url); if (isDouban && inCooldown()) return reject(new Error('cooldown')); if (isDouban) await waitDoubanSlot(); GM_xmlhttpRequest({ ...opts, headers: { 'User-Agent': UA_POOL[Math.floor(Math.random()*UA_POOL.length)], 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://movie.douban.com/', ...(opts.headers||{}) }, onload: (res)=>{ if (isDouban) { const txt = res.responseText || ''; const guarded = (res.finalUrl && /accounts\.|j\/app\/user\/check/.test(res.finalUrl)) || /请证明你是人类|嗯…/.test(txt); if (guarded) { triggerCooldown(90); reject(new Error('douban-guard')); return; } } resolve(res); }, onerror: (e)=> reject(e), ontimeout: ()=> reject(new Error('timeout')) }); }); } // 默认配置(用户可以通过配置界面修改) // 预连接常用域名,降低首包延迟 try { (function preconnectHosts(hosts){ try { const head = document.head || document.getElementsByTagName('head')[0]; if (!head) return; hosts.forEach(h => { try { const l1 = document.createElement('link'); l1.rel = 'preconnect'; l1.href = h; l1.crossOrigin = 'anonymous'; head.appendChild(l1); const l2 = document.createElement('link'); l2.rel = 'dns-prefetch'; l2.href = h; head.appendChild(l2); } catch(e) {} }); } catch (e) {} })([ 'https://movie.douban.com', 'https://search.douban.com', 'https://m.douban.com', 'https://www.douban.com', 'https://doubanio.com', 'https://api.themoviedb.org', 'https://www.themoviedb.org', 'https://image.tmdb.org' ]); } catch (e) {} const DEFAULT_CONFIG = { TMDB: { API_KEY: '', ACCESS_TOKEN: '', BASE_URL: 'https://api.themoviedb.org/3', IMAGE_BASE_URL: 'https://image.tmdb.org/t/p/', POSTER_SIZE: 'w780', STILL_SIZE: 'w780', // 新增:列表与选中分级尺寸,提升“看板加载速度+选中质量” LIST_POSTER_SIZE: 'w342', LIST_STILL_SIZE: 'w300', SELECTED_POSTER_SIZE: 'original', SELECTED_STILL_SIZE: 'original', DOUBAN_QUALITY: { PRIORITY: ['raw', 'l', 'm'], TIMEOUT: 3000, RETRY: 1 }, IMAGE_CANDIDATES_COUNT: 5, POSTER_PER_ROW: 5, STILL_PER_ROW: 5 }, AI: { API_ENDPOINT: 'https://api.openai.com/v1/chat/completions', DEFAULT_MODEL: 'gpt-3.5-turbo', API_KEY: '', PROVIDER: 'openai', FEATURES: [ { id: 'summary', name: '生成剧情简介', placeholder: '请输入剧情简介要求,例如:详细、简洁、适合推荐等' }, { id: 'comment', name: '生成评论摘要', placeholder: '请输入评论摘要要求,例如:正面、客观、有深度等' }, { id: 'tagline', name: '生成宣传标语', placeholder: '请输入宣传标语要求,例如:吸引人、简洁有力等' }, { id: 'analysis', name: '生成深度分析', placeholder: '请输入分析要求,例如:主题分析、角色分析、视听语言分析等' }, { id: 'post_format', name: '资源帖排版美化', placeholder: '请输入排版要求,例如:适合论坛发布、美观、信息完整等' }, { id: 'content_optimize', name: '内容优化建议', placeholder: '请输入优化方向,例如:SEO优化、吸引流量、符合平台规范等' }, { id: 'format_check', name: '排版合规检查', placeholder: '请输入检查重点,例如:版权信息、敏感词、排版结构等' }, { id: 'modular_design', name: '模块化排版设计', placeholder: '请输入设计需求,例如:分章节、醒目重点、便于阅读等' } ], // API获取指南(2025年版) API_GUIDE: { zhihu: 'https://www.zhihu.com/question/492416413', // AI API汇总 official: { glm4: 'https://console.baai.ac.cn/', qwen: 'https://modelscope.cn/', xunfei: 'https://www.xfyun.cn/', huggingface: 'https://huggingface.co/', gemini: 'https://makersuite.google.com/' } }, // 影视资源帖排版美化智能体框架设定 POST_FORMAT_GUIDELINES: { // 核心工具矩阵:按能力分级的编辑器选型 BASIC_EDITORS: [ {name: '键盘喵速排', features: ['影视推荐专属模板库', '拖拽式操作', '发布前自动检测高风险字符', '响应式布局模块']}, {name: 'Canva影视模板库', features: ['16:9黄金比例优化', '一键替换海报素材', '移动端缩略图自动生成']}, {name: '163Editor', features: ['双视图编辑模式', '引用块展示影评', '电影评分组件', '实时敏感词检测']} ], ADVANCED_TOOLS: [ {name: 'VS Code插件组合', features: ['Prettier自动格式化', 'Stylelint CSS兼容性检测', 'Live Server实时预览', 'HTML Snippets代码片段']}, {name: 'CKEditor4', features: ['精准表格工具', '海报自动压缩', '代码块功能', '可配置影视专用样式']} ], MARKDOWN_WORKFLOW: [ {name: 'Typora+Marked.js', features: ['::: movie语法块', '图片对齐指令', '标准标题层级', '自动过滤危险HTML标签']}, {name: 'Markdown2Html', features: ['theme: cinema参数', '图片自适应', '防盗链优化', '模板复用功能']} ], // 辅助工具链:从美化到合规的全流程支持 VISUAL_ENHANCEMENT: { COLOR_SYSTEM: { horror: ['#2d3142', '#ef8354'], romance: ['#f8b195', '#f8e1d1'], action: ['#335c67', '#e09f3e'], drama: ['#3a0ca3', '#4361ee'], comedy: ['#ffb347', '#fdfd96'], sciFi: ['#4169e1', '#87cefa'], fantasy: ['#9370db', '#e6e6fa'], anime: ['#ff69b4', '#ffb6c1'], documentary: ['#708090', '#d3d3d3'] }, IMAGE_OPTIMIZATION: { MAX_WIDTH: 800, MAX_SIZE_KB: 500, QUALITY: 85, UNIFORM_BORDER: '2px solid #e0e0e0', UNIFORM_RADIUS: '5px' }, TYPOGRAPHY: { FONT_FAMILY: 'Microsoft Yahei, sans-serif', LINE_HEIGHT: 1.7, PARAGRAPH_SPACING: '20px', HIGHLIGHT_STYLE: 'background:#fff380;padding:0 3px;border-radius:2px' } }, COMPLIANCE_CHECKS: { SENSITIVE_WORDS: { TOOL: 'sensitive-word', KEYWORDS: ['盗版', '枪版', '百度云', '网盘'], REPLACEMENTS: { '百度云': '合规平台', '免费观看': '正版渠道观看', '资源获取': '内容获取' } }, COPYRIGHT_RISK: [ '优先使用官方宣传海报', '注明"用于影视推荐合理使用"', '用户影评注明来源', '专业影评引用保留作者署名且不超过原文1/3', '仅推荐正规视频平台链接' ], PLATFORM_RULES: { COMMON: ['避免特殊符号(★、→等)', '评分使用文本或CSS实现', '包含"支持正版影视"声明'], MOBILE_OPTIMIZATION: ['按钮最小尺寸44×44px', '删除PC端悬浮效果', '增大触摸区域'] } }, // 模块化排版框架:影视资源帖专属结构 CORE_MODULES: { POSTER_AREA: { HTML: '<div class="poster-container"><img src="海报URL" alt="电影名称海报" class="responsive-poster"><p class="poster-caption">电影官方海报 | 来源:豆瓣电影</p></div>', CSS: '.poster-container {position: relative; padding-bottom: 56.25%; overflow: hidden;} .responsive-poster {position: absolute; width: 100%; height: 100%; object-fit: cover; border-radius: 8px;}' }, INFO_CARD: { HTML: '<div class="info-grid"><div class="info-item"><strong>导演</strong>:张艺谋</div><div class="info-item"><strong>类型</strong>:剧情 / 历史</div><div class="info-item"><strong>上映</strong>:2023-09-30</div><div class="info-item"><strong>评分</strong>:<span class="score">9.2</span></div></div>', CSS: '.info-grid {display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 15px;} .score {background: #2a9d8f; color: white; padding: 2px 8px; border-radius: 12px;}' }, SYNOPSIS: { HTML: '<div class="synopsis"><p class="safe-content">影片讲述了...(无剧透版本)</p><details class="full-content"><summary>点击查看完整剧情</summary><p>完整剧情描述...</p></details></div>', CSS: 'summary {list-style: none; color: #2a9d8f; cursor: pointer;} summary::-webkit-details-marker {display: none;}' }, REVIEWS: { HTML: '<div class="reviews"><div class="official-review"><p class="reviewer">【官方推荐】</p><p class="content">专业影评内容...</p></div><div class="user-review"><p class="reviewer">观众 @电影爱好者</p><p class="content">用户评论内容...</p></div></div>', CSS: '.official-review {border-left: 3px solid #2a9d8f; padding-left: 15px; margin-bottom: 15px;} .user-review {border-left: 3px solid #94a3b8; padding-left: 15px;} .reviewer {font-weight: bold; margin-bottom: 5px;}' } }, // 合规发布流程:从编辑到上线的校验清单 PUBLISH_FLOW: { PREPROCESSING: [ '素材合规检查:校验海报使用权,符合"宣传性质合理使用"原则', '主题色设定:根据影片类型确定三色体系,全文色彩不超过4种', '模块规划:按"头图→信息卡→剧情→影评→资源提示"搭建框架' ], EDITING: [ '文本净化:运行sensitive-word工具扫描全文,替换风险表述', '样式统一:格式化代码,确保margin统一为25px,padding为15px', '响应式测试:在375px、768px、1200px三个断点预览' ], FINAL_CHECK: [ '功能测试:点击所有折叠面板和链接按钮,检查交互正常', '合规复查:确认无特殊符号,包含"支持正版影视"声明', '多平台适配:生成掘金版、公众号版、知乎版等不同版本' ] }, // 避坑指南:影视排版高频问题解决方案 PITFALL_GUIDE: { TECHNICAL_ISSUES: [ '图片溢出:使用object-fit: cover裁剪中心区域,配合max-width: 100%', '代码过滤:平台过滤style标签时,转换为内联样式', '加载速度:海报采用渐进式加载,长文本使用分段加载' ], EXPERIENCE_OPTIMIZATION: [ '阅读体验:剧情文本行高1.7倍,段落间距20px', '重点突出:使用主色加粗而非特殊符号标注重点内容', '特殊符号:统一替换为【】,评分星星用★文本替代' ], PLATFORM_ADAPTATION: [ '掘金:保留代码高亮,优化图片懒加载', '微信公众号:简化样式,优化防盗链格式', '知乎:优化首图尺寸,调整段落间距' ] } } } }; // 配置管理功能 function getConfig() { const config = { TMDB: { ...DEFAULT_CONFIG.TMDB, API_KEY: GM_getValue('tmdb_api_key', ''), ACCESS_TOKEN: GM_getValue('tmdb_access_token', '') }, AI: { ...DEFAULT_CONFIG.AI, API_ENDPOINT: GM_getValue('ai_api_endpoint', DEFAULT_CONFIG.AI.API_ENDPOINT), API_KEY: GM_getValue('ai_api_key', ''), DEFAULT_MODEL: GM_getValue('ai_model', DEFAULT_CONFIG.AI.DEFAULT_MODEL), PROVIDER: GM_getValue('ai_provider', 'openai') } }; return config; } function saveConfig(config) { GM_setValue('tmdb_api_key', config.TMDB.API_KEY); GM_setValue('tmdb_access_token', config.TMDB.ACCESS_TOKEN); GM_setValue('ai_api_endpoint', config.AI.API_ENDPOINT); GM_setValue('ai_api_key', config.AI.API_KEY); GM_setValue('ai_model', config.AI.DEFAULT_MODEL); GM_setValue('ai_provider', config.AI.PROVIDER); } // 创建配置管理界面 function createConfigDialog() { const dialog = document.createElement('div'); dialog.id = 'config-dialog'; dialog.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 50; display: flex; justify-content: center; align-items: center; font-family: 'Microsoft YaHei', sans-serif; `; const configPanel = document.createElement('div'); configPanel.style.cssText = ` background: white; border-radius: 10px; padding: 30px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); `; const currentConfig = getConfig(); configPanel.innerHTML = ` <h2 style="margin: 0 0 20px 0; color: #333; text-align: center;">🔧 脚本配置管理</h2> <div style="margin-bottom: 25px;"> <h3 style="color: #2563eb; margin: 0 0 15px 0; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px;">📡 TMDB API 配置</h3> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #374151;">API Key:</label> <input type="text" id="tmdb-api-key" value="${currentConfig.TMDB.API_KEY}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px;" placeholder="请输入您的TMDB API Key"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #374151;">Access Token:</label> <input type="text" id="tmdb-access-token" value="${currentConfig.TMDB.ACCESS_TOKEN}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px;" placeholder="请输入您的TMDB Access Token"> </div> <div style="background: #f3f4f6; padding: 10px; border-radius: 5px; margin-bottom: 15px;"> <p style="margin: 0; font-size: 12px; color: #6b7280;"> <strong>获取方法:</strong><br> 1. 登录(不可用)/注册(不可用)TMDB账号:访问 <a href="https://www.themoviedb.org/" target="_blank" style="color: #2563eb;">TMDB官网</a> ,登录(不可用)或注册(不可用)账号。<br> 2. 进入API设置:点击右上角头像→Settings→左侧API选项。<br> 3. 获取v3 API Key:在"API Keys (v3 auth)"区域,创建并复制Key。<br> 4. 获取v4 Access Token:在"Access Tokens (v4 auth)"区域,生成并复制Token。<br> <br> 注意:遵守TMDB服务条款,界面可能微调。 </p> </div> <div style="margin-top:10px; padding:10px; background:#f9fafb; border-radius:6px; border-left:3px solid #8b5cf6;"> <p style="margin:0; font-size:12px; color:#6b7280;"> <strong>推荐服务商:</strong><br> • <strong>OpenAI:</strong> <a href="https://platform.openai.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>Claude:</strong> <a href="https://console.anthropic.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>Gemini:</strong> <a href="https://makersuite.google.com/app/apikey" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>豆包:</strong> <a href="https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>通义千问:</strong> <a href="https://dashscope.console.aliyun.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>智谱AI (GLM):</strong> <a href="https://open.bigmodel.cn/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>讯飞星火:</strong> <a href="https://console.xfyun.cn/services/bm3" target="_blank" style="color:#2563eb;">获取API Key</a> </p> </div> </div> <div style="margin-bottom: 25px;"> <h3 style="color: #2563eb; margin: 0 0 15px 0; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px;">🤖 AI API 配置</h3> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #374151;">API端点:</label> <select id="ai-provider" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px; margin-bottom: 10px;"> <option value="openai" ${currentConfig.AI.PROVIDER === 'openai' ? 'selected' : ''}>OpenAI</option> <option value="claude" ${currentConfig.AI.PROVIDER === 'claude' ? 'selected' : ''}>Claude (Anthropic)</option> <option value="gemini" ${currentConfig.AI.PROVIDER === 'gemini' ? 'selected' : ''}>Gemini (Google)</option> <option value="doubao" ${currentConfig.AI.PROVIDER === 'doubao' ? 'selected' : ''}>豆包</option> <option value="tongyi" ${currentConfig.AI.PROVIDER === 'tongyi' ? 'selected' : ''}>通义千问</option> <option value="glm" ${currentConfig.AI.PROVIDER === 'glm' ? 'selected' : ''}>智谱AI (GLM)</option> <option value="spark" ${currentConfig.AI.PROVIDER === 'spark' ? 'selected' : ''}>讯飞星火</option> <option value="qwen" ${currentConfig.AI.PROVIDER === 'qwen' ? 'selected' : ''}>通义千问 (Qwen)</option> <option value="zhipu" ${currentConfig.AI.PROVIDER === 'zhipu' ? 'selected' : ''}>智谱AI</option> <option value="custom" ${currentConfig.AI.PROVIDER === 'custom' ? 'selected' : ''}>自定义</option> </select> <div style="color: #6b7280; font-size: 12px; margin-top: 5px;">选择您使用的AI服务提供商,不同提供商可能需要不同的API端点和参数设置</div> <input type="text" id="ai-api-endpoint" value="${currentConfig.AI.API_ENDPOINT}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px;" placeholder="API端点地址"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #374151;">API Key:</label> <input type="password" id="ai-api-key" value="${currentConfig.AI.API_KEY}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px;" placeholder="请输入您的AI API Key"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #374151;">模型名称:</label> <input type="text" id="ai-model" value="${currentConfig.AI.DEFAULT_MODEL}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px;" placeholder="例如: gpt-3.5-turbo, claude-3-sonnet-20240229"> </div> <div style="background: #f3f4f6; padding: 10px; border-radius: 5px; margin-bottom: 15px;"> <p style="margin: 0; font-size: 12px; color: #6b7280;"> <strong>推荐服务商:</strong><br> • <strong>OpenAI:</strong> <a href="https://platform.openai.com/" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>Claude:</strong> <a href="https://console.anthropic.com/" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>Gemini:</strong> <a href="https://makersuite.google.com/app/apikey" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>豆包:</strong> <a href="https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>通义千问:</strong> <a href="https://dashscope.console.aliyun.com/" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>智谱AI (GLM):</strong> <a href="https://open.bigmodel.cn/" target="_blank" style="color: #2563eb;">获取API Key</a><br> • <strong>讯飞星火:</strong> <a href="https://console.xfyun.cn/services/bm3" target="_blank" style="color: #2563eb;">获取API Key</a> </p> </div> </div> <div style="text-align: center; margin-top: 30px;"> <button id="save-config" style="background: #10b981; color: white; border: none; padding: 12px 30px; border-radius: 5px; font-size: 16px; cursor: pointer; margin-right: 10px;"> 💾 保存配置 </button> <button id="close-config" style="background: #6b7280; color: white; border: none; padding: 12px 30px; border-radius: 5px; font-size: 16px; cursor: pointer;"> ❌ 关闭 </button> </div> `; dialog.appendChild(configPanel); document.body.appendChild(dialog); // 绑定事件 document.getElementById('save-config').onclick = () => { const newConfig = { TMDB: { ...DEFAULT_CONFIG.TMDB, API_KEY: document.getElementById('tmdb-api-key').value.trim(), ACCESS_TOKEN: document.getElementById('tmdb-access-token').value.trim() }, AI: { ...DEFAULT_CONFIG.AI, API_ENDPOINT: document.getElementById('ai-api-endpoint').value.trim(), API_KEY: document.getElementById('ai-api-key').value.trim(), DEFAULT_MODEL: document.getElementById('ai-model').value.trim(), PROVIDER: document.getElementById('ai-provider').value } }; saveConfig(newConfig); showNotification('配置已保存!', 'success'); document.body.removeChild(dialog); }; document.getElementById('close-config').onclick = () => { document.body.removeChild(dialog); }; // 提供商选择变化时更新端点 document.getElementById('ai-provider').onchange = () => { const provider = document.getElementById('ai-provider').value; const endpointInput = document.getElementById('ai-api-endpoint'); switch(provider) { case 'openai': endpointInput.value = 'https://api.openai.com/v1/chat/completions'; break; case 'claude': endpointInput.value = 'https://api.anthropic.com/v1/messages'; break; case 'gemini': endpointInput.value = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; break; default: endpointInput.value = ''; } }; // 点击背景关闭 dialog.onclick = (e) => { if (e.target === dialog) { document.body.removeChild(dialog); } }; } const COMMON_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', // 避免跳转到 app/user/check 等登录(不可用)检测页 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'same-origin' }; // 存储变量 let selectedPosterUrl = ''; let selectedStillUrl = ''; // AI区域多选集合(不影响主流程单选逻辑) let aiSelectedPosterUrls = new Set(); let aiSelectedStillUrls = new Set(); // AI侧独立的影片信息,避免与主流程共享状态 let aiCurrentMovieInfo = null; // AI请求控制器(用于终止) let aiCurrentRequest = null; let aiAbortReject = null; // 主功能与AI隔离标记 let isMainFlowActive = false; // 控制台防刷屏:仅提示一次未检测到编辑器 let editorNotFoundLogged = false; // 防重复绑定AI事件 let aiEventsBound = false; let currentMovieInfo = null; let currentComments = []; let sourceCodeElement = null; let panelObserver = null; let isPanelInitialized = false; let currentEditor = null; let posterPage = 1; // 海报当前页(初始1) let stillPage = 1; // 剧照当前页(初始1) let isLoadingPosters = false; let isLoadingStills = false; let posterContainer = null; let stillContainer = null; let panel = null; let selectedPosterEl = null; let selectedStillEl = null; // 排版美化样式库 const FORMAT_STYLES = [ { name: '主标题', icon: 'fa-header', tag: 'h1', category: '标题', styles: { 'color': '#1e40af', 'font-size': '24px', 'font-weight': 'bold', 'margin': '20px 0 15px 0', 'padding-bottom': '8px', 'border-bottom': '2px solid #dbeafe' }, preview: true, apply: (selectedText) => { const content = selectedText || '主标题示例'; return `<h1 style="color:#1e40af;font-size:24px;font-weight:bold;margin:20px 0 15px 0;padding-bottom:8px;border-bottom:2px solid #dbeafe;">${content}</h1>`; } }, { name: '副标题', icon: 'fa-header', tag: 'h2', category: '标题', styles: { 'color': '#2563eb', 'font-size': '20px', 'font-weight': 'bold', 'margin': '18px 0 12px 0', 'padding-bottom': '5px', 'border-bottom': '1px solid #dbeafe' }, preview: true, apply: (selectedText) => { const content = selectedText || '副标题示例'; return `<h2 style="color:#2563eb;font-size:20px;font-weight:bold;margin:18px 0 12px 0;padding-bottom:5px;border-bottom:1px solid #dbeafe;">${content}</h2>`; } }, { name: '三级标题', icon: 'fa-header', tag: 'h3', category: '标题', styles: { 'color': '#3b82f6', 'font-size': '18px', 'font-weight': 'bold', 'margin': '15px 0 10px 0' }, preview: true, apply: (selectedText) => { const content = selectedText || '三级标题示例'; return `<h3 style="color:#3b82f6;font-size:18px;font-weight:bold;margin:15px 0 10px 0;">${content}</h3>`; } }, { name: '正文段落', icon: 'fa-paragraph', tag: 'p', category: '文本', styles: { 'color': '#333', 'font-size': '14px', 'line-height': '1.8', 'margin': '8px 0', 'text-indent': '2em' }, preview: true, apply: (selectedText) => { const content = selectedText || '这是一段正文示例,包含标准的段落格式和首行缩进,适合用于大部分内容的展示。'; return `<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">${content}</p>`; } }, { name: '引用文本', icon: 'fa-quote-right', tag: 'blockquote', category: '文本', styles: { 'color': '#666', 'font-size': '13px', 'line-height': '1.6', 'margin': '10px 0', 'padding': '10px 15px', 'border-left': '3px solid #2196F3', 'background': '#f8f9fa' }, preview: true, apply: (selectedText) => { const content = selectedText || '这是一段引用文本示例,通常用于引用他人的话语或特殊说明内容。'; return `<blockquote style="color:#666;font-size:13px;line-height:1.6;margin:10px 0;padding:10px 15px;border-left:3px solid #2196F3;background:#f8f9fa;">${content}</blockquote>`; } }, { name: '无序列表', icon: 'fa-list-ul', tag: 'ul', category: '列表', styles: { 'margin': '10px 0 10px 20px', 'padding': '0' }, itemStyles: { 'color': '#444', 'font-size': '14px', 'line-height': '1.7', 'margin': '5px 0', 'list-style-type': 'disc' }, preview: true, apply: (selectedText) => { const content = selectedText || '列表项1\n列表项2\n列表项3'; const items = content.split('\n').map(item => `<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;list-style-type:disc;">${item.trim()}</li>` ).join(''); return `<ul style="margin:10px 0 10px 20px;padding:0;">${items}</ul>`; } }, { name: '有序列表', icon: 'fa-list-ol', tag: 'ol', category: '列表', styles: { 'margin': '10px 0 10px 20px', 'padding': '0' }, itemStyles: { 'color': '#444', 'font-size': '14px', 'line-height': '1.7', 'margin': '5px 0' }, preview: true, apply: (selectedText) => { const content = selectedText || '步骤一\n步骤二\n步骤三'; const items = content.split('\n').map(item => `<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;">${item.trim()}</li>` ).join(''); return `<ol style="margin:10px 0 10px 20px;padding:0;">${items}</ol>`; } }, { name: '分隔线', icon: 'fa-minus', tag: 'hr', category: '布局', styles: { 'border': 'none', 'border-top': '1px solid #e0e0e0', 'margin': '20px 0', 'height': '1px' }, preview: true, apply: () => { return `<hr style="border:none;border-top:1px solid #e0e0e0;margin:20px 0;height:1px;">`; } }, { name: '高亮文本', icon: 'fa-highlighter', tag: 'span', category: '文本', styles: { 'background': '#fff380', 'padding': '0 3px', 'border-radius': '2px' }, preview: true, apply: (selectedText) => { const content = selectedText || '需要高亮的文本'; return `<span style="background:#fff380;padding:0 3px;border-radius:2px;">${content}</span>`; } }, { name: '链接样式', icon: 'fa-link', tag: 'a', category: '文本', styles: { 'color': '#2563eb', 'text-decoration': 'none', 'border-bottom': '1px dashed #93c5fd', 'padding': '0 1px' }, preview: true, apply: (selectedText) => { const content = selectedText || '链接文本'; return `<a href="#" style="color:#2563eb;text-decoration:none;border-bottom:1px dashed #93c5fd;padding:0 1px;">${content}</a>`; } }, { name: '居中文本', icon: 'fa-align-center', tag: 'div', category: '布局', styles: { 'text-align': 'center', 'margin': '10px 0', 'color': '#4b5563' }, preview: true, apply: (selectedText) => { const content = selectedText || '这段文本会居中显示'; return `<div style="text-align:center;margin:10px 0;color:#4b5563;">${content}</div>`; } }, { name: '影视卡片', icon: 'fa-film', tag: 'div', category: '特殊', preview: true, apply: (selectedText) => { const title = selectedText || '影视名称'; return ` <div style="border:1px solid #e5e7eb;border-radius:6px;padding:15px;margin:15px 0;box-shadow:0 1px 3px rgba(0,0,0,0.05);"> <h3 style="margin-top:0;color:#1e40af;">${title}</h3> <p style="margin-bottom:0;color:#4b5563;font-size:14px;">这里可以添加影视的简要说明或推荐理由...</p> </div> `; } }, { name: '装饰标题', icon: 'fa-magic', tag: 'h3', category: '标题', preview: true, apply: (selectedText) => { const content = selectedText || '装饰标题示例'; return `<h3 style="position:relative;color:#db2777;font-size:18px;font-weight:700;margin:18px 0 12px 0;padding-left:10px;"> <span style="position:absolute;left:0;top:3px;width:4px;height:18px;background:#f472b6;border-radius:2px;"></span> ${content} </h3>`; } }, { name: '序号标题', icon: 'fa-list-ol', tag: 'h3', category: '标题', preview: true, apply: (selectedText) => { const content = selectedText || '01 序号标题示例'; return `<h3 style="color:#1e40af;font-size:18px;font-weight:700;margin:16px 0;">${content}</h3>`; } }, { name: '信息提示', icon: 'fa-info-circle', tag: 'div', category: '文本', preview: true, apply: (selectedText) => { const content = selectedText || '这是信息提示内容'; return `<div style="background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;padding:10px 12px;border-radius:8px;">${content}</div>`; } }, { name: '成功提示', icon: 'fa-check-circle', tag: 'div', category: '文本', preview: true, apply: (selectedText) => { const content = selectedText || '操作已成功完成'; return `<div style="background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46;padding:10px 12px;border-radius:8px;">${content}</div>`; } }, { name: '警告提示', icon: 'fa-exclamation-triangle', tag: 'div', category: '文本', preview: true, apply: (selectedText) => { const content = selectedText || '请注意可能的风险'; return `<div style="background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;padding:10px 12px;border-radius:8px;">${content}</div>`; } }, { name: '错误提示', icon: 'fa-times-circle', tag: 'div', category: '文本', preview: true, apply: (selectedText) => { const content = selectedText || '发生错误,请重试'; return `<div style="background:#fef2f2;border:1px solid #fecaca;color:#991b1b;padding:10px 12px;border-radius:8px;">${content}</div>`; } }, { name: '代码块', icon: 'fa-code', tag: 'pre', category: '文本', preview: true, apply: (selectedText) => { const content = (selectedText || 'const hello = "world";').replace(/</g,'<').replace(/>/g,'>'); return `<pre style="background:#0b1021;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto;font:12px/1.6 Consolas,Monaco,monospace;">${content}</pre>`; } }, { name: '任务清单', icon: 'fa-tasks', tag: 'ul', category: '列表', preview: true, apply: (selectedText) => { const content = selectedText || '[ ] 待办一\n[x] 已完成项\n[ ] 待办二'; const items = content.split('\n').map(line => { const done = /\[x\]/i.test(line); const text = line.replace(/\[[ xX]\]\s*/,''); return `<li style="list-style:none;margin:6px 0;font-size:14px;color:#374151;"> <span style="display:inline-block;width:14px;height:14px;margin-right:8px;border:1px solid #d1d5db;border-radius:3px;background:${done?'#10b981':'#fff'};"></span>${text} </li>`; }).join(''); return `<ul style="margin:10px 0 10px 4px;padding:0;">${items}</ul>`; } }, { name: '标签云', icon: 'fa-tags', tag: 'div', category: '列表', preview: true, apply: (selectedText) => { const content = selectedText || '剧情|动作|冒险|爱情|科幻|动画'; const chips = content.split(/\||,|\s+/).filter(Boolean).slice(0,12).map(t => `<span style="display:inline-block;margin:4px 6px 0 0;padding:3px 10px;border-radius:12px;background:#fff0f6;border:1px solid #fbcfe8;font-size:12px;color:#be185d;">${t}</span>`).join(''); return `<div style="margin:8px 0;">${chips}</div>`; } }, { name: '两列栅格', icon: 'fa-columns', tag: 'div', category: '布局', preview: true, apply: (selectedText) => { const a = '左侧内容'; const b = '右侧内容'; return `<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:10px 0;"> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;color:#374151;">${a}</div> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;color:#374151;">${b}</div> </div>`; } }, { name: '三列栅格', icon: 'fa-th', tag: 'div', category: '布局', preview: true, apply: () => { return `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:10px 0;"> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;color:#374151;">区块A</div> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;color:#374151;">区块B</div> <div style="background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;color:#374151;">区块C</div> </div>`; } }, { name: '图片说明卡', icon: 'fa-image', tag: 'div', category: '布局', preview: true, apply: () => { return `<div style="display:flex;gap:12px;align-items:flex-start;border:1px solid #e5e7eb;border-radius:8px;padding:10px;"> <img src="https://picsum.photos/120/80" style="width:120px;height:80px;border-radius:6px;object-fit:cover;" alt="图"/> <div style="color:#4b5563;font-size:14px;line-height:1.7;">这里是配图说明文字,可替换为剧情简介、片段描述或使用说明。</div> </div>`; } }, { name: '强调按钮', icon: 'fa-hand-pointer-o', tag: 'a', category: '特殊', preview: true, apply: (selectedText) => { const content = selectedText || '点此查看'; return `<a href="#" style="display:inline-block;background:linear-gradient(135deg,#ec4899 0%,#be185d 100%);color:#fff;padding:8px 14px;border-radius:8px;text-decoration:none;box-shadow:0 2px 6px rgba(236,72,153,.25);">${content}</a>`; } }, { name: '下载按钮', icon: 'fa-download', tag: 'a', category: '特殊', preview: true, apply: (selectedText) => { const content = selectedText || '下载资源'; return `<a href="#" style="display:inline-block;background:#10b981;color:#fff;padding:8px 14px;border-radius:8px;text-decoration:none;box-shadow:0 2px 6px rgba(16,185,129,.25);">${content}</a>`; } }, { name: '流程步骤', icon: 'fa-arrow-right', tag: 'ol', category: '列表', preview: true, apply: (selectedText) => { const content = selectedText || '准备素材\n编辑排版\n发布分享'; const items = content.split('\n').map((t, i) => `<li style="counter-increment:step;margin:8px 0;padding-left:28px;position:relative;color:#374151;"> <span style="position:absolute;left:0;top:0;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:#ec4899;color:#fff;font-size:12px;">${i+1}</span>${t} </li>`).join(''); return `<ol style="list-style:none;margin:10px 0;padding:0;">${items}</ol>`; } }, { name: '折叠面板', icon: 'fa-chevron-down', tag: 'details', category: '布局', preview: true, apply: (selectedText) => { const content = selectedText || '这里是可展开的详细说明内容'; return `<details style="border:1px solid #e5e7eb;border-radius:8px;padding:10px;background:#fff;"> <summary style="cursor:pointer;color:#db2777;font-weight:600;outline:none;">点击展开/收起</summary> <div style="margin-top:8px;color:#4b5563;font-size:14px;line-height:1.7;">${content}</div> </details>`; } }, { name: '两列表格', icon: 'fa-table', tag: 'table', category: '布局', preview: true, apply: () => { return `<table style="width:100%;border-collapse:collapse;margin:10px 0;"> <tr> <td style="width:30%;background:#fff0f6;border:1px solid #fbcfe8;padding:8px;color:#be185d;">属性</td> <td style="border:1px solid #fbcfe8;padding:8px;color:#374151;">值</td> </tr> <tr> <td style="background:#fff0f6;border:1px solid #fbcfe8;padding:8px;color:#be185d;">导演</td> <td style="border:1px solid #fbcfe8;padding:8px;color:#374151;">——</td> </tr> </table>`; } }, { name: '引用卡片', icon: 'fa-quote-left', tag: 'div', category: '文本', preview: true, apply: (selectedText) => { const content = selectedText || '这是一段加框的引用卡片内容,用于强调引用。'; return `<div style="border-left:4px solid #93c5fd;background:#f0f9ff;padding:10px 12px;border-radius:6px;color:#1e40af;">${content}</div>`; } }, { name: '下沉首字', icon: 'fa-bold', tag: 'p', category: '文本', preview: true, apply: (selectedText) => { const content = (selectedText || '首字下沉示例段落,用于提升视觉层次与阅读趣味。'); const first = content.slice(0,1); const rest = content.slice(1); return `<p style="font-size:14px;line-height:1.9;color:#333;margin:10px 0;"> <span style="float:left;font-size:36px;line-height:1;height:36px;margin:6px 8px 0 0;color:#db2777;font-weight:700;">${first}</span>${rest} </p>`; } }, { name: '图片区块', icon: 'fa-picture-o', tag: 'div', category: '布局', preview: true, apply: () => { return `<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:10px 0;"> ${[1,2,3,4].map(i=>`<img src="https://picsum.photos/200/120?${i}" style="width:100%;height:120px;object-fit:cover;border-radius:6px;" alt="图${i}"/>`).join('')} </div>`; } }, { name: '免责声明', icon: 'fa-shield', tag: 'div', category: '特殊', preview: true, apply: () => { return `<div style="background:#fff7ed;border:1px dashed #fdba74;color:#9a3412;padding:10px 12px;border-radius:8px;font-size:12px;"> 仅供学习与交流,禁止用于商业用途,版权归原平台与权利人所有。 </div>`; } }, { name: '按钮组', icon: 'fa-bars', tag: 'div', category: '特殊', preview: true, apply: () => { return `<div style="display:flex;gap:8px;flex-wrap:wrap;margin:8px 0;"> <a href="#" style="background:#ec4899;color:#fff;padding:6px 12px;border-radius:6px;text-decoration:none;">主操作</a> <a href="#" style="background:#9ca3af;color:#fff;padding:6px 12px;border-radius:6px;text-decoration:none;">次操作</a> </div>`; } }, { name: '彩色分隔条', icon: 'fa-minus', tag: 'hr', category: '布局', preview: true, apply: () => `<div style="height:6px;background:linear-gradient(90deg,#ec4899,#a78bfa,#60a5fa,#34d399);border-radius:999px;margin:16px 0;"></div>` } ]; // 自动填充和保存相关函数 function autoClickSourceBtn() { return new Promise((resolve) => { const modalSourceBtn = document.querySelector('#myModal-code .btn, #source-code-btn'); if (modalSourceBtn && modalSourceBtn.textContent.includes('源代码')) { modalSourceBtn.click(); setTimeout(() => resolve(true), 600); return; } const textButtons = [...document.querySelectorAll('button'), ...document.querySelectorAll('a')] .filter(elem => elem.textContent.trim().includes('源代码')); if (textButtons.length > 0) { textButtons[0].click(); setTimeout(() => resolve(true), 300); return; } const tinyMceBtn = document.querySelector('.tox-tbtn[title="源代码"]'); const oldTinyMceBtn = Array.from(document.querySelectorAll('.mce-btn')).find(elem => elem.textContent.includes('源代码')); const ckSourceLabel = document.querySelector('.cke_button__source_label'); if (tinyMceBtn) { tinyMceBtn.click(); setTimeout(() => resolve(true), 300); } else if (oldTinyMceBtn) { oldTinyMceBtn.click(); setTimeout(() => resolve(true), 300); } else if (ckSourceLabel && ckSourceLabel.closest('.cke_button')) { ckSourceLabel.closest('.cke_button').click(); setTimeout(() => resolve(true), 300); } else { resolve(true); } }); } // 关闭TinyMCE/CKEditor等源代码对话框或退出源码模式 function closeSourceDialogIfAny() { try { // TinyMCE 源代码对话框常见关闭按钮 const closeBtns = [ '.tox-dialog__footer .tox-button', '.tox-dialog__close', '.modal .close', '.dialog .close', '[aria-label="Close"]' ]; for (const sel of closeBtns) { const el = document.querySelector(sel); if (el) { el.click(); break; } } } catch (_) {} try { // 如果仍在源码模式,尝试再次点击按钮退出 const btn = document.querySelector('.tox-tbtn[title="源代码"], #source-code-btn'); if (btn) btn.click(); } catch (_) {} } function autoFillSourceBox(html) { return new Promise((resolve) => { // 内容非空:确保HTML内容不为空 const safeHtml = html || `<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px;">内容已自动生成</div>`; let retryCount = 0; const maxRetry = 20; const interval = 300; const tryFill = setInterval(() => { retryCount++; const editorSelectors = [ '#myModal-code textarea', 'textarea.tox-textarea', 'textarea.mce-textbox', 'textarea.cke_source', 'textarea[name="message"]', 'textarea[name="content"]', '#editor_content', '#content', 'textarea[rows="20"][cols="80"]', '.CodeMirror textarea', '.editor-textarea', // 扩展选择器范围,增强兼容性 '.content-editor textarea', '.article-editor textarea', 'div[contenteditable="true"]', '[role="textbox"]' ]; let targetBox = null; for (const selector of editorSelectors) { const elem = document.querySelector(selector); if (elem && elem.style.display !== 'none' && elem.offsetParent !== null) { targetBox = elem; sourceCodeElement = elem; currentEditor = getCurrentEditor(); break; } } if (targetBox) { const codeMirror = targetBox.closest('.CodeMirror'); if (codeMirror) { // 处理CodeMirror编辑器的多种情况 if (codeMirror.CodeMirror) { // 标准CodeMirror实例 try { codeMirror.CodeMirror.setValue(safeHtml); codeMirror.CodeMirror.getDoc().markClean(); codeMirror.CodeMirror.getDoc().changed(); // 触发额外的事件来模拟真实用户输入 codeMirror.CodeMirror.focus(); codeMirror.CodeMirror.refresh(); // 尝试通过编辑器的内部API触发变化通知,但避免触发验证 if (codeMirror.CodeMirror.on) { try { codeMirror.CodeMirror.on('change', codeMirror.CodeMirror.getDoc()); } catch (e) { // 静默忽略可能的错误 } } } catch (cmError) { console.log('CodeMirror编辑器操作失败:', cmError); // 降级处理:直接操作textarea targetBox.value = safeHtml; triggerNonBubblingEvents(targetBox); } } else { // 直接操作textarea(更贴近用户输入行为) targetBox.value = safeHtml; triggerNonBubblingEvents(targetBox); } } else { // 普通文本框处理或contenteditable元素 if (targetBox.isContentEditable) { // 处理contenteditable元素 targetBox.innerHTML = safeHtml; triggerNonBubblingEvents(targetBox); } else { // 普通文本框处理 targetBox.value = safeHtml; triggerNonBubblingEvents(targetBox); } } // 增加短暂延迟确保所有事件都被处理 setTimeout(() => { clearInterval(tryFill); resolve(true); }, 500); // 延长延迟时间,确保更可靠的填充 return; } if (retryCount >= maxRetry) { clearInterval(tryFill); // 粘贴失败时自动复制内容到剪贴板 try { navigator.clipboard.writeText(safeHtml).then(() => { showStatus('内容自动复制到剪贴板,请手动粘贴!', true); }).catch(err => { showStatus('内容粘贴失败,剪贴板复制也失败,请手动复制内容!', true); console.error('剪贴板写入失败:', err); }); } catch (e) { showStatus('内容粘贴失败,请手动复制内容!', true); console.error('自动复制功能失败:', e); } resolve(false); } }, interval); }); } // 触发阻断:创建不冒泡的事件并触发,防止触发表单验证 function triggerNonBubblingEvents(element) { if (!element) return; // 创建不冒泡的事件 const createNonBubblingEvent = (type, eventType = 'Event') => { let event; if (eventType === 'KeyboardEvent') { event = new KeyboardEvent(type, { bubbles: false, cancelable: true, key: 'Enter' }); } else { event = new Event(type, { bubbles: false, cancelable: true }); } // 定义空的stopPropagation方法,确保事件不会冒泡 Object.defineProperty(event, 'stopPropagation', { value: function() {}, writable: false }); return event; }; try { // 触发基础事件 element.dispatchEvent(createNonBubblingEvent('focus')); element.dispatchEvent(createNonBubblingEvent('input')); element.dispatchEvent(createNonBubblingEvent('change')); element.dispatchEvent(createNonBubblingEvent('compositionend')); // 触发键盘事件 element.dispatchEvent(createNonBubblingEvent('keydown', 'KeyboardEvent')); element.dispatchEvent(createNonBubblingEvent('keypress', 'KeyboardEvent')); element.dispatchEvent(createNonBubblingEvent('keyup', 'KeyboardEvent')); // 短暂聚焦然后失焦,完成编辑过程 element.focus(); if (element.setSelectionRange && typeof element.value === 'string') { element.setSelectionRange(element.value.length, element.value.length); } setTimeout(() => { element.dispatchEvent(createNonBubblingEvent('blur')); }, 100); } catch (eventError) { console.log('触发事件时发生错误:', eventError); } } function autoClickSaveBtn() { return new Promise((resolve) => { const saveButtons = [ ...document.querySelectorAll('button'), ...document.querySelectorAll('a') ].filter(elem => { const text = elem.textContent.trim(); return text === '保存' || text === '保存草稿' || text === '保存内容'; }); if (saveButtons.length > 0) { saveButtons[0].click(); setTimeout(() => resolve(true), 500); return; } const commonSaveBtn = document.querySelector('button.save, .save-button, [type="submit"][value="保存"]'); if (commonSaveBtn && !commonSaveBtn.textContent.includes('发布')) { commonSaveBtn.click(); setTimeout(() => resolve(true), 500); return; } resolve(false); }); } // 统一写入助手:尽最大努力把HTML写入任一编辑器(TinyMCE/CodeMirror/textarea/iframe/contenteditable) async function writeHtmlToAnyEditor(html) { try { const safeHtml = html || ''; // 1) TinyMCE 优先 try { const tiny = window.tinymce || window.tinyMCE; let ed = null; if (tiny) { ed = tiny.activeEditor || (tiny.editors && tiny.editors[0]) || (tiny.EditorManager && tiny.EditorManager.activeEditor) || null; if (!ed && typeof tiny.get === 'function') { // 尝试根据textarea id获取 const ta = document.querySelector('textarea[id]'); if (ta) { ed = tiny.get(ta.id) || ed; } } } if (ed && typeof ed.setContent === 'function') { ed.setContent(safeHtml); try { if (typeof ed.setDirty === 'function') ed.setDirty(true); } catch(_) {} try { if (tiny && typeof tiny.triggerSave === 'function') tiny.triggerSave(); } catch(_) {} return true; } } catch (_) {} // 2) 使用现有的getCurrentEditor工具(CodeMirror/textarea) try { const editor = getCurrentEditor && getCurrentEditor(); if (editor) { if (editor.type === 'codemirror' && editor.instance && typeof editor.instance.setValue === 'function') { editor.instance.setValue(safeHtml); return true; } else if (editor.instance) { editor.instance.value = safeHtml; editor.instance.dispatchEvent(new Event('input', { bubbles: true })); return true; } } } catch(_) {} // 3) TinyMCE iframe 兜底 try { const iframe = document.querySelector('.tox-edit-area iframe, iframe.tox-edit-area__iframe, .mce-edit-area iframe'); if (iframe && iframe.contentDocument && iframe.contentDocument.body) { iframe.contentDocument.body.innerHTML = safeHtml; iframe.contentDocument.body.dispatchEvent(new Event('input', { bubbles: true })); return true; } } catch(_) {} // 4) contenteditable 兜底 try { const editable = document.querySelector('[contenteditable="true"], [contenteditable=true]'); if (editable) { editable.innerHTML = safeHtml; return true; } } catch(_) {} // 5) 直接向可见textarea写入 try { const ta = Array.from(document.querySelectorAll('textarea')).find(t => t.offsetParent !== null); if (ta) { ta.value = safeHtml; ta.dispatchEvent(new Event('input', { bubbles: true })); return true; } } catch(_) {} return false; } catch (e) { return false; } } function autoFillTitleInput(title) { return new Promise((resolve) => { const titleInput = document.querySelector('input[placeholder="标题"], input[name="title"], #title'); if (!titleInput) { resolve(false); return; } titleInput.value = title || (currentMovieInfo?.title || '影视内容分享'); titleInput.dispatchEvent(new Event('input', { bubbles: true })); titleInput.dispatchEvent(new Event('change', { bubbles: true })); const keydownEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }); const keyupEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }); titleInput.dispatchEvent(keydownEvent); titleInput.dispatchEvent(keyupEvent); resolve(true); }); } async function fillAndSaveSource(html, enableAutoSave = false) { try { // 边界隔离:此函数主要负责内容填充,根据enableAutoSave参数决定是否执行保存操作 // 内容非空:确保标题不为空,即使没有影视信息也提供默认值 const movieTitle = currentMovieInfo?.title || '影视内容分享'; await autoFillTitleInput(movieTitle); showStatus('正在切换到源代码模式...', false); const switched = await autoClickSourceBtn(); if (!switched) { showStatus('未检测到源代码按钮,尝试直接填充...', false); } showStatus('正在填充内容到编辑框...', false); // 内容非空:确保HTML内容不为空 const safeHtml = html || `<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px;">内容已自动生成</div>`; const filled = await autoFillSourceBox(safeHtml); if (filled) { // 轻量延迟,确保内容写入完成 await new Promise(resolve => setTimeout(resolve, 120)); // 内容非空:检查并填充所有可能的必填字段,扩展选择器范围 const allInputSelectors = [ 'input[required]:not([type="hidden"])', 'textarea[required]:not([type="hidden"])', 'input[placeholder*="必填"], textarea[placeholder*="必填"]', 'input[name="content"], textarea[name="content"]', 'input[name="text"], textarea[name="text"]', 'input[data-required="true"], textarea[data-required="true"]', '[ng-required="true"]', '.required-field', '.required-input', '#content-input, #text-input', '.editor-container textarea', '.main-content textarea' ]; // 合并所有选择器 const allInputs = document.querySelectorAll(allInputSelectors.join(',')); // 内容非空:预填充所有可能的输入框 allInputs.forEach(input => { // 检查元素是否可见且无内容 if (!input.value.trim() && input.style.display !== 'none' && input.offsetParent !== null) { // 为不同类型的输入框提供合适的默认值 if (input.type === 'text' || input.type === 'textarea' || input.tagName.toLowerCase() === 'textarea') { // 为文本输入框填充更详细的默认内容 const placeholder = input.getAttribute('placeholder') || '内容已自动生成'; input.value = placeholder; // 触发阻断:阻止事件冒泡,防止触发表单验证 const createBubblesBlockedEvent = (type) => { const event = new Event(type, { bubbles: false, cancelable: true }); Object.defineProperty(event, 'stopPropagation', { value: function() {} }); return event; }; // 触发更多事件以模拟真实用户输入,但阻止冒泡 const events = [ createBubblesBlockedEvent('focus'), createBubblesBlockedEvent('input'), createBubblesBlockedEvent('change'), createBubblesBlockedEvent('blur') ]; events.forEach(event => { input.dispatchEvent(event); }); // 特殊事件:键盘事件和组合事件,同样阻止冒泡 const createKeyboardEventNoBubbles = (type, key) => { const event = new KeyboardEvent(type, { key, bubbles: false }); Object.defineProperty(event, 'stopPropagation', { value: function() {} }); return event; }; const keyboardEvents = [ createKeyboardEventNoBubbles('keydown', 'a'), createKeyboardEventNoBubbles('keypress', 'a'), createKeyboardEventNoBubbles('keyup', 'a') ]; keyboardEvents.forEach(event => { input.dispatchEvent(event); }); } } }); // 额外检查并设置编辑器内容的标记,防止验证时认为内容为空 if (window.editor && typeof window.editor.setContent === 'function') { try { window.editor.setContent(safeHtml); // 强制设置编辑器内部状态为已编辑 if (window.editor && typeof window.editor.isDirty === 'function') { window.editor.isDirty = () => true; } } catch (editorError) { console.log('设置编辑器内容失败:', editorError); } } // 快速保存:先尝试直接保存,失败再回退补齐必填项后再保存 if (enableAutoSave) { let saved = await autoClickSaveBtn(); if (!saved) { const selectors = [ 'input[required]:not([type="hidden"])', 'textarea[required]:not([type="hidden"])', 'input[placeholder*="必填"], textarea[placeholder*="必填"]', 'input[name="content"], textarea[name="content"]', 'input[name="text"], textarea[name="text"]', 'input[data-required="true"], textarea[data-required="true"]', '[ng-required="true"]', '.required-field', '.required-input', '#content-input, #text-input', '.editor-container textarea', '.main-content textarea' ]; document.querySelectorAll(selectors.join(',')).forEach(input => { if (!input.value || !String(input.value).trim()) { const placeholder = input.getAttribute('placeholder') || '内容已自动生成'; input.value = placeholder; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } }); await new Promise(r => setTimeout(r, 120)); saved = await autoClickSaveBtn(); } showStatus(saved ? '内容填充完成并已自动保存' : '内容填充完成,请手动保存', !saved); } else { showStatus('内容填充完成', false); } return true; } else { // 异常兜底:复制到剪贴板并显示手动粘贴按钮 try { GM_setClipboard(safeHtml); } catch (clipboardError) { console.log('复制到剪贴板失败:', clipboardError); } showStatus('自动填充失败,内容已复制到剪贴板,请手动粘贴', true); const pasteBtn = document.getElementById('paste-btn'); if (pasteBtn) pasteBtn.style.display = 'inline-block'; return false; } } catch (error) { console.error('填充过程中发生错误:', error); // 异常兜底:尝试从localStorage恢复备份 try { const backupHtml = localStorage.getItem('backup-movie-html'); if (backupHtml) { GM_setClipboard(backupHtml); showStatus('处理过程中出现错误,已将备份内容复制到剪贴板,请手动粘贴', true); const pasteBtn = document.getElementById('paste-btn'); if (pasteBtn) pasteBtn.style.display = 'inline-block'; } else { showStatus('处理过程中出现错误,请刷新页面重试', true); } } catch (restoreError) { showStatus('处理过程中出现错误,请刷新页面重试', true); } return false; } } // 内容生成函数 function generateHTML(movie, comments, posterDataURL, stillDataURL) { // 强制将TMDB缩略图升级为original,避免排版放大模糊 const finalPosterUrl = posterDataURL ? toTMDBOriginal(posterDataURL) : 'https://picsum.photos/680/480?default-poster'; const finalStillUrl = stillDataURL ? toTMDBOriginal(stillDataURL) : 'https://picsum.photos/800/450?default-still'; const runtime = movie.runtime === 'null' || !movie.runtime ? '未知片长' : movie.runtime; let imdbHtml = ''; if (movie.imdbId && movie.imdbId !== '暂无') { imdbHtml = `<span> </span><strong style="box-sizing: border-box; font-weight: bolder;">IMDb:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="https://www.imdb.com/title/${movie.imdbId}/" target="_blank" rel="noopener"><span> </span>${movie.imdbId}</a></span>`; } const introHtml = movie.intro .split('\n') .filter(para => para.trim()) .map(para => `<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">${para.trim()}</div>`) .join(''); let commentsHtml = ''; // 统一为“观众热评”,并与模板1一致:TMDB无热评时回退为简介/标语摘录 const isTmdbSrc = (movie && (movie.source === 'TMDB' || (movie.url && movie.url.includes('themoviedb.org')) || movie.tmdbId)); const firstRaw = Array.isArray(comments) && comments.length > 0 ? comments[0] : null; let firstText = ''; if (firstRaw) { if (typeof firstRaw === 'string') { firstText = firstRaw; } else if (typeof firstRaw === 'object') { firstText = ( firstRaw.content || firstRaw.text || firstRaw.comment || firstRaw.quote || '' ).toString(); } } let commentInnerHtml = ''; if (firstText && firstText.trim()) { commentInnerHtml = firstText.trim(); } else if (isTmdbSrc) { let snippet = (movie.intro || movie.tagline || '').toString().trim(); if (snippet.length > 80) snippet = snippet.slice(0, 78) + '…'; const rvLink = movie.tmdbId ? `https://www.themoviedb.org/${movie.mediaType || 'movie'}/${movie.tmdbId}/reviews` : ''; const moreHtml = rvLink ? ` <a href="${rvLink}" target="_blank" rel="noopener" style="color:#e64a19;text-decoration:none;border-bottom:1px dashed #ffccbc;">更多评价</a>` : ''; commentInnerHtml = snippet ? `<p style="margin:0 0 8px 0;">${snippet}</p><p style="margin:0;text-align:right;color:#e64a19;font-style:italic;font-size:14px;">—— 看点摘录${moreHtml}</p>` : '暂无热评,分享你的观影感受吧~'; } else { commentInnerHtml = '暂无热评,分享你的观影感受吧~'; } commentsHtml = ` <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.5rem;">观众热评:</h3> <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; line-height:1.8;">${commentInnerHtml}</div>`; // 处理额外信息(新添加的字段) let additionalInfo = ''; let hasAdditionalInfo = false; const additionalInfoItems = []; // 原始标题 if (movie.originalTitle && movie.originalTitle !== movie.title) { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">原始名称:</strong>${movie.originalTitle}</p>`); hasAdditionalInfo = true; } // 奖项信息 if (movie.awards && movie.awards.length > 0) { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">主要奖项:</strong>${movie.awards.slice(0, 3).join(';')}</p>`); hasAdditionalInfo = true; } // 关键字 if (movie.keywords && movie.keywords !== '') { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">关键字:</strong>${movie.keywords}</p>`); hasAdditionalInfo = true; } // 预算和票房(电影) if (movie.budget && movie.budget !== '未知') { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">预算:</strong>${movie.budget}</p>`); hasAdditionalInfo = true; } if (movie.revenue && movie.revenue !== '未知') { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">票房:</strong>${movie.revenue}</p>`); hasAdditionalInfo = true; } // 流媒体平台信息 if (movie.streamingPlatforms && movie.streamingPlatforms.length > 0) { additionalInfoItems.push(`<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">流媒体平台:</strong>${movie.streamingPlatforms.join('、')}</p>`); hasAdditionalInfo = true; } // 如果有额外信息,构建HTML if (hasAdditionalInfo) { additionalInfo = `<div style="box-sizing: border-box; margin-top: 15px; padding: 15px; background-color: rgb(248, 249, 250); border-radius: 0.5rem; border: 1px solid rgb(222, 226, 230);"> ${additionalInfoItems.join('\n')} </div>`; } return ` <div class="card border" style="box-sizing: border-box; position: relative; display: flex; flex-direction: column; min-width: 0px; overflow-wrap: break-word; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255); border: none; border-radius: 0.75rem; margin-bottom: 1rem; box-shadow: rgba(46, 45, 116, 0.05) 0px 0.25rem 1.875rem; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> <div class="movie-info" style="box-sizing: border-box; flex: 1 1 400px; padding: 20px;"><img class="lazy img-responsive" style="box-sizing: border-box; vertical-align: middle; border: 1px solid transparent; max-width: 100%; -webkit-user-drag: none; margin-bottom: 0.5rem; height: auto; width: 100%; cursor: pointer; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges;" src="${finalPosterUrl}" data-original="${finalPosterUrl}"><br style="box-sizing: border-box;"> <div class="movie-info-content" style="box-sizing: border-box; word-break: break-word; overflow-wrap: anywhere;"> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">名称:</strong>${movie.title}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">又名:</strong>${movie.alsoKnown || '无'}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">导演:</strong>${movie.director}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">编剧:</strong>${movie.writer}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">主演:</strong>${movie.actor}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">类型:</strong>${movie.genreTags.join('、') || '未知'}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">制片地区:</strong>${movie.region}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">上映时间:</strong>${movie.release}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">影视语言:</strong>${movie.lang}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">评分:</strong><span style="box-sizing: border-box; color: rgb(0, 132, 255); font-weight: 600;"><span> </span>${movie.rating}</span><span> </span><strong style="box-sizing: border-box; font-weight: bolder;">豆瓣ID:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="${movie.mediaType === 'tv' ? 'https://tv.douban.com/subject/' : 'https://movie.douban.com/subject/'}${movie.doubanId || movie.tmdbId}/" target="_blank" rel="noopener"><span> </span>${movie.doubanId || movie.tmdbId}</a></span>${imdbHtml}</p> <p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">片长:</strong>${runtime}</p> ${additionalInfo} </div> </div> </div> <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">影视简介:</h3> <div style="background:#ffffff; color:#1f2937; line-height:1.8; font-size:14px; border:1px solid #e5e7eb; border-radius:8px; padding:12px; box-shadow:0 1px 3px rgba(0,0,0,0.04);"> ${introHtml} </div> <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> </div> <div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> <h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">精彩剧照:</h3> <img src="${finalStillUrl}" style="box-sizing: border-box; vertical-align: middle; border-style: none; max-width: 100%; height: auto; border-radius: 4px; margin-bottom: 1rem;" alt="${movie.title} 剧照"> </div> ${commentsHtml} `; } // 创建AI功能面板内容(不创建独立面板,只返回HTML内容) function createAIPanelContent() { return ` <!-- AI功能选择 --> <div style="margin-bottom:8px;"> <label style="display:inline-block; width:80px; font-weight:500; color:#4b5563; font-size:12px;">功能类型:</label> <select id="ai-function-select" style="width:calc(100% - 90px); padding:4px; border:1px solid #d1d5db; border-radius:3px; font-size:12px; max-height: 200px;"> <option value="post_format" selected>资源帖排版美化</option> <option value="free_text">无预设生成</option> </select> </div> <!-- 风格选择 --> <div style="margin-bottom:8px;"> <label style="display:inline-block; width:80px; font-weight:500; color:#4b5563; font-size:12px;">输出风格:</label> <select id="ai-style-select" style="width:calc(100% - 90px); padding:4px; border:1px solid #d1d5db; border-radius:3px; font-size:12px;"> <option value="">自动(不指定)</option> <option value="专业严谨">专业严谨</option> <option value="简洁实用">简洁实用</option> <option value="活泼有趣">活泼有趣</option> <option value="学术深度">学术深度</option> <option value="幽默风趣">幽默风趣</option> <option value="文艺细腻">文艺细腻</option> <option value="复古胶片">复古胶片</option> <option value="赛博科幻">赛博科幻</option> <option value="国潮风格">国潮风格</option> <option value="万象合流">万象合流(跨文化全风格自适应)</option> </select> </div> <!-- AI提示输入 --> <div style="margin-bottom:8px;"> <label style="display:block; font-weight:500; color:#4b5563; font-size:12px; margin-bottom:4px;">生成提示:</label> <textarea id="ai-prompt-input" rows="3" placeholder="请输入剧情简介要求,例如:详细、简洁、适合推荐等" style="width:100%; padding:6px; border:1px solid #d1d5db; border-radius:4px; font-size:12px; resize:vertical;"></textarea> </div> <!-- AI检索与图片选择(精简版) --> <div style="margin-bottom:8px; padding:8px; background:#fff; border:1px solid #e5e7eb; border-radius:4px;"> <div id="ai-toggle-row" style="display:flex; gap:12px; align-items:stretch; margin-bottom:8px; flex-wrap:wrap;"> <label class="ai-flag" style="display:flex; align-items:center; gap:6px; padding:6px 10px; border:1px solid #e5e7eb; border-radius:6px; background:#f9fafb; font-size:12px; color:#374151;"> <input type="checkbox" id="ai-auto-title" checked> <span>选片名入提示</span> </label> <label class="ai-flag" id="ai-deep-wrap" style="display:flex; align-items:center; gap:6px; padding:6px 10px; border:1px solid #e5e7eb; border-radius:6px; background:#f9fafb; font-size:12px; color:#374151;"> <input type="checkbox" id="ai-deep-think"> <span>深度思考</span> </label> <label class="ai-flag" id="ai-web-wrap" style="display:flex; align-items:center; gap:6px; padding:6px 10px; border:1px solid #e5e7eb; border-radius:6px; background:#f9fafb; font-size:12px; color:#374151;"> <input type="checkbox" id="ai-web-browse"> <span>联网补充</span> </label> <span id="ai-feature-tip" style="align-self:center; font-size:11px; color:#9ca3af;"></span> </div> <div style="display:flex; gap:6px; align-items:center; margin-bottom:6px;"> <input id="ai-search-input" type="text" placeholder="输入片名或粘贴链接(自动弹出结果)" style="flex:1; padding:6px; border:1px solid #d1d5db; border-radius:4px; font-size:12px;"> </div> <div id="ai-search-status" style="display:none; font-size:12px; color:#6b7280; margin-bottom:6px;">正在加载...</div> <div id="ai-search-results" style="display:none; border:1px solid #e5e7eb; border-radius:6px; background:#fff; max-height:260px; overflow:auto; margin-bottom:6px;"> </div> <div id="ai-image-selection" style="display:none; margin-top:4px;"> <div style="margin-bottom:8px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;"> <strong style="font-size:12px; color:#374151;">🖼️ 海报(可多选)</strong> <button id="ai-load-more-posters" style="display:none; background:#f472b6; color:#fff; border:none; padding:4px 8px; border-radius:6px; font-size:12px; cursor:pointer;">加载更多海报</button> </div> <div id="ai-poster-candidates" style="display:grid; grid-template-columns:repeat(5,1fr); gap:8px; min-height:120px; background:#fff; border:1px solid #f3d5d9; border-radius:6px; padding:6px;"></div> </div> <div> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;"> <strong style="font-size:12px; color:#374151;">🎬 剧照(可多选)</strong> <button id="ai-load-more-stills" style="display:none; background:#f472b6; color:#fff; border:none; padding:4px 8px; border-radius:6px; font-size:12px; cursor:pointer;">加载更多剧照</button> </div> <div id="ai-still-candidates" style="display:grid; grid-template-columns:repeat(5,1fr); gap:8px; min-height:120px; background:#fff; border:1px solid #f3d5d9; border-radius:6px; padding:6px;"></div> </div> </div> </div> <!-- AI生成按钮 --> <div style="margin-bottom:8px;"> <button id="generate-ai-text" style="background:#6366f1; color:white; border:none; padding:6px 16px; border-radius:4px; cursor:pointer; font-size:12px;"> <i class="fa fa-magic" style="margin-right:5px;"></i>生成AI文本 </button> <button id="abort-ai-generate" style="background:#ef4444; color:white; border:none; padding:6px 12px; border-radius:4px; cursor:pointer; font-size:12px; margin-left:8px; display:none;"> 终止 </button> <button id="ai-clear" style="background:#ef4444; color:white; border:none; padding:6px 12px; border-radius:4px; cursor:pointer; font-size:12px; margin-left:8px;"> 清理 </button> </div> <!-- AI结果展示区域 --> <div id="ai-result-area" style="display:none; margin-top:10px; padding:8px; background:#fff; border:1px solid #e5e7eb; border-radius:4px;"> <h5 style="margin:0 0 6px 0; color:#4b5563; font-size:12px;">生成结果:</h5> <div id="ai-result-content" style="font-size:12px; line-height:1.6; color:#374151; min-height:50px; max-height:200px; overflow-y:auto;"></div> <div style="margin-top:6px; display:flex; gap:8px;"> <button id="copy-ai-result" style="background:#3b82f6; color:white; border:none; padding:3px 10px; border-radius:3px; cursor:pointer; font-size:11px;"> <i class="fa fa-copy" style="margin-right:3px;"></i>复制结果 </button> <button id="insert-ai-result" style="background:#8b5cf6; color:white; border:none; padding:3px 10px; border-radius:3px; cursor:pointer; font-size:11px;"> <i class="fa fa-pencil" style="margin-right:3px;"></i>插入到编辑框 </button> </div> </div>`; } // 切换标签页功能 function switchTab(tabId) { // 隐藏所有内容区域 document.getElementById('main-content-area').style.display = 'none'; document.getElementById('ai-content-area').style.display = 'none'; document.getElementById('settings-content-area').style.display = 'none'; // 获取标签元素 const mainTab = document.getElementById('main-tab'); const aiTab = document.getElementById('ai-tab'); const settingsTab = document.getElementById('settings-tab'); // 重置所有标签样式 [mainTab, aiTab, settingsTab].forEach(tab => { // 完全重置所有内联样式 tab.style.background = '#fff'; tab.style.color = '#6b7280'; tab.style.fontWeight = 'normal'; tab.style.boxShadow = 'none'; tab.style.borderBottom = '1px solid #f3d5d9'; tab.style.backgroundImage = 'none'; }); // 显示选中的内容区域并激活对应的标签 if (tabId === 'main') { document.getElementById('main-content-area').style.display = 'block'; mainTab.style.background = 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)'; mainTab.style.color = 'white'; mainTab.style.fontWeight = '500'; mainTab.style.boxShadow = '0 -2px 6px rgba(236, 72, 153, 0.3)'; mainTab.style.borderBottom = 'none'; showStatus('控制面板已准备就绪', false, 'main'); // 进入主功能:隔离AI isMainFlowActive = true; } else if (tabId === 'ai') { document.getElementById('ai-content-area').style.display = 'block'; aiTab.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)'; aiTab.style.color = 'white'; aiTab.style.fontWeight = '500'; aiTab.style.boxShadow = '0 -2px 6px rgba(139, 92, 246, 0.3)'; aiTab.style.borderBottom = 'none'; // 进入AI:解除隔离 isMainFlowActive = false; // 重新绑定AI事件监听器,确保按钮可以正常响应 bindAIEventListeners(); } else if (tabId === 'settings') { document.getElementById('settings-content-area').style.display = 'block'; settingsTab.style.background = 'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)'; settingsTab.style.color = 'white'; // 确保文字颜色为白色 settingsTab.style.fontWeight = '500'; settingsTab.style.boxShadow = '0 -2px 6px rgba(99, 102, 241, 0.3)'; settingsTab.style.borderBottom = 'none'; showStatus('设置面板已准备就绪', false, 'settings'); // 重新绑定设置面板事件监听器,确保按钮可以正常响应 bindSettingsEventListeners(); } } // 确保switchTab函数在全局作用域可访问 - 使用更可靠的方式暴露给外部HTML调用 if (typeof unsafeWindow !== 'undefined') { // 在Tampermonkey等用户脚本管理器中使用unsafeWindow unsafeWindow.switchTab = switchTab; } else { // 标准浏览器环境下使用window window.switchTab = switchTab; } // 额外添加到document对象作为后备方案 document.switchTab = switchTab; // 控制面板创建与定位 - 精准放置到标记位置(空值修复版+美化版) function createPanel() { panel = document.createElement('div'); panel.id = 'douban-tmdb-panel'; panel.style.cssText = ` background: #fff; border: 1px solid #f3d5d9; border-radius: 8px; padding: 15px; margin: 20px 0; box-shadow: 0 2px 8px rgba(236, 72, 153, 0.08); z-index: 0; position: static; box-sizing: border-box; width: 100%; min-width: 320px; max-width: 1400px; /* 扩大宽屏最大宽度,优化搜索结果显示 */ transition: margin-bottom 0.15s cubic-bezier(0.4, 0, 0.2, 1); overflow: visible; transform: none !important; transform-origin: unset !important; `; // 添加响应式CSS规则(精简与合并断点,移除极端断点) const responsiveCSS = ` <style> /* 仅竖排时允许过渡,恢复横排时不做过渡 */ #input-container.stacked-animate { transition: gap 160ms ease, flex-direction 160ms ease, align-items 160ms ease; } #input-container.no-transition { transition: none !important; } /* 修复点:当横排闪切时,连同子元素一起禁用过渡/动画,消除迟滞感 */ #input-container.no-transition * { transition: none !important; animation: none !important; } /* 修复点:全局级别的临时禁用过渡(通过给body加类名触发),用于竖→横闪切时彻底清零动画 */ body.no-transition-global *, body.no-transition-global *::before, body.no-transition-global *::after { transition: none !important; animation: none !important; } /* 修复点:>800px 时预置为横排顶部对齐,避免JS接管前出现"先居中"一帧 */ @media screen and (min-width: 801px) { #input-container { flex-direction: row !important; align-items: flex-start !important; gap: 15px !important; } /* 搜索容器与链接容器:强制顶部对齐 */ div[style*="flex: 1 1 250px"][style*="min-width: 200px"][style*="position: relative"], #media-url-container { display: flex !important; align-items: flex-start !important; } /* 包装器避免横排时拉到中线 */ #media-url-wrapper { align-items: stretch !important; } /* 修复点:横排时彻底移除输入框自身过渡,避免宽度变化被动画化 */ #search-movie, #media-url, #fetch-btn { transition: none !important; } /* 终极修复:横排模式下,输入区整棵子树禁用过渡/动画,保证阈值切换"闪切" */ #input-container, #input-container * { transition: none !important; animation: none !important; } } /* <= 800px:垂直布局与全宽输入 */ @media screen and (max-width: 800px) { #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } #input-container { flex-direction: column !important; align-items: stretch !important; gap: 12px !important; } #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; display: flex !important; flex-direction: column !important; align-items: stretch !important; } #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; width: 100% !important; } #search-movie, #media-url { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } #fetch-btn { border-radius: 6px !important; width: 100% !important; margin-left: 0 !important; font-size: 12px !important; padding: 6px 10px !important; flex-shrink: 0 !important; } #search-results { position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; width: 100% !important; margin: 4px 0 0 0 !important; z-index: 1000 !important; } label[style*="width:70px"] { width: 70px !important; font-size: 11px !important; } } /* <= 650px:标签更紧凑,输入框减小内边距 */ @media screen and (max-width: 650px) { #search-movie, #media-url { font-size: 12px !important; padding: 6px 10px !important; } } /* <= 360px:极窄屏输入区紧凑优化(不修改#search-results) */ @media screen and (max-width: 360px) { #input-container { gap: 8px !important; } label[style*="width:70px"], #media-url-label { width: 60px !important; font-size: 11px !important; } #search-movie, #media-url { padding: 5px 8px !important; font-size: 12px !important; } } /* <= 320px:超窄屏输入区兜底(不修改#search-results) */ @media screen and (max-width: 320px) { #input-container { gap: 6px !important; } label[style*="width:70px"], #media-url-label { width: 56px !important; font-size: 10.5px !important; } #search-movie, #media-url { padding: 4px 7px !important; font-size: 11.5px !important; } } </style> `; panel.innerHTML = responsiveCSS + panel.innerHTML; panel.innerHTML = ` <!-- 标签页导航 --> <div style="display:flex; margin:0 0 20px 0; border-bottom:1px solid #f3d5d9; flex-wrap: wrap; gap: 2px;"> <button id="main-tab" onclick="switchTab('main')" style="padding:8px 16px; border:none; background:linear-gradient(135deg, #ec4899 0%, #be185d 100%); color:white; font-size:12px; font-weight:500; cursor:pointer; border-radius:8px 8px 0 0; transition: background 0.2s ease, box-shadow 0.2s ease; box-shadow: 0 -2px 6px rgba(236, 72, 153, 0.3); flex: 1 1 auto; min-width: 120px;">🎬 豆瓣+TMDB影视工具</button> <button id="ai-tab" onclick="switchTab('ai')" style="padding:8px 16px; border:none; background:#fff; color:#be185d; font-size:12px; cursor:pointer; border-radius:8px 8px 0 0; transition: background 0.2s ease, color 0.2s ease; border-bottom: 1px solid #f3d5d9; flex: 1 1 auto; min-width: 120px;">🤖 AI文字生成工具</button> <button id="settings-tab" onclick="switchTab('settings')" style="padding:8px 16px; border:none; background:#fff; color:#6b7280; font-size:12px; cursor:pointer; border-radius:8px 8px 0 0; transition: background 0.2s ease, color 0.2s ease; border-bottom: 1px solid #f3d5d9; flex: 1 1 auto; min-width: 80px;">⚙️ 设置</button> <style> #main-tab, #ai-tab, #settings-tab { font-family: 'Microsoft YaHei', sans-serif; margin-right: 4px; position: relative; overflow: hidden; } #main-tab:hover:not(#main-tab[style*="background:linear-gradient"]), #ai-tab:hover:not(#ai-tab[style*="background:linear-gradient"]), #settings-tab:hover:not(#settings-tab[style*="background:linear-gradient"]) { background-color: #fff5f7; transform: translateY(-1px); border-bottom-color: #f472b6; } #main-tab::after, #ai-tab::after, #settings-tab::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 3px; background: linear-gradient(90deg, #ec4899, #8b5cf6); transform: scaleX(0); transition: transform 0.3s ease; z-index: 1; } #main-tab:hover::after, #ai-tab:hover::after, #settings-tab:hover::after { transform: scaleX(1); } /* 响应式标签页按钮 */ @media screen and (max-width: 600px) { #main-tab, #ai-tab, #settings-tab { padding: 6px 10px !important; font-size: 11px !important; min-width: 80px !important; } } @media screen and (max-width: 480px) { #main-tab, #ai-tab, #settings-tab { padding: 5px 8px !important; font-size: 10px !important; min-width: 60px !important; } } </style> </div> <!-- 主功能区域(新增预览模式开关) --> <div id="main-content-area" style="display:block;"> <!-- 排版美化工具区域 --> <div style="margin-bottom:20px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <h4 style="margin:0 0 12px 0; color:#be185d; font-size:14px; font-weight:600; display:flex; justify-content:space-between; align-items:center;"> 🎨 排版美化工具 <span id="format-preview-toggle" style="font-size:12px; color:#db2777; cursor:pointer; text-decoration:underline; transition: color 0.2s ease;">显示预览</span> </h4> <!-- 样式分类标签 --> <div id="format-categories" style="display:flex; gap:8px; margin-bottom:10px; overflow-x:auto; padding-bottom:4px; border-bottom:1px solid #f3d5d9;"> <!-- 分类标签通过JS生成 --> </div> <!-- 样式按钮区域 --> <div style="display:flex; flex-wrap:wrap; gap:6px;" id="format-buttons"> <!-- 美化按钮通过JS生成 --> </div> <!-- 样式预览区域 --> <div id="format-preview" style="margin-top:10px; padding:10px; background:#fff; border:1px solid #f3d5d9; border-radius:8px; display:none; max-height:200px; overflow-y:auto; font-family:'Microsoft YaHei', sans-serif;"> <div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div> </div> </div> <!-- 输入区(添加预览模式控制和日志容器) --> <div style="margin-bottom:20px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <!-- 搜索框和链接框的水平对齐容器 --> <div style="display:flex; gap:15px; margin-bottom:12px; align-items:flex-start; min-width: 0; flex-wrap: wrap;" id="input-container"> <!-- 搜索影片区域 --> <div style="flex: 1 1 250px; display:flex; align-items:center; min-width: 200px; overflow: visible;"> <label style="width:70px; font-weight:500; color:#6b7280; margin-right:8px; flex-shrink: 0; font-size: 12px; line-height: 1.5; text-align: right; padding-right: 4px;">搜索影片:</label> <div style="flex:1; display:flex; align-items:center; min-width: 0; position: relative;"> <input type="search" id="search-movie" placeholder="完美世界" style="width: 100%; padding:6px 10px; border:1px solid #d1d5db; border-radius:6px; font-size:12px; font-weight:400; color:#374151; transition: all 0.2s ease; outline:none; background:#fff; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); line-height: 1.5;"> <div id="search-loading" style="position:absolute; right:10px; top:35%; transform: translateY(-50%); color:#6b7280; display:none; font-size: 11px; font-weight:500; background: rgba(255, 255, 255, 0.9); padding: 2px 6px; border-radius: 4px; z-index: 1001;"> <i>搜索中...</i> </div> </div> </div> <!-- 影视链接区域 --> <div style="flex: 1 1 250px; display:flex; align-items:center; min-width: 200px;" id="media-url-container"> <label style="width:70px; font-weight:500; color:#6b7280; margin-right:8px; flex-shrink: 0; font-size: 12px; line-height: 1.5; text-align: right; padding-right: 4px;" id="media-url-label">影视链接:</label> <div style="flex:1; display:flex; align-items:center; min-width: 0;" id="media-url-wrapper"> <input type="text" id="media-url" name="dummy-media-url" placeholder="豆瓣或TMDB链接" autocomplete="new-password" spellcheck="false" data-lpignore="true" data-form-type="other" aria-label="影视链接输入框" aria-autocomplete="none" data-passwordmanager="false" data-disable-pwdmgr="true" data-1password-ignore="true" data-lastpass-ignore="true" data-autofill="disabled" onfocus="this.removeAttribute('readonly');" onblur="this.removeAttribute('readonly');" style="flex:1; min-width: 100px; padding:6px 10px; border:1px solid #d1d5db; border-radius:6px; font-size:12px; font-weight:400; color:#374151; transition: all 0.2s ease; outline:none; background:#fff; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); line-height: 1.5;"> <button id="fetch-btn" style="display:none; opacity:0.6; pointer-events:none; background:#ec4899; color:white; border:none; padding:6px 12px; border-radius:6px; cursor:not-allowed; font-size:12px; font-weight:500; transition: all 0.3s ease; flex-shrink: 0; line-height: 1.5; margin-left: 4px;">提取</button> </div> </div> </div> <!-- 搜索结果显示框独立容器,确保不影响输入框对齐 --> <div id="search-results" style="position:relative; top:0; left:0; right:0; z-index:1000; background:#fff; border:1px solid #f3d5d9; border-radius:6px; max-height:400px; overflow-y:auto; display:none; visibility:hidden; opacity:0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin-top:8px; width:100%;"></div> <style> #search-movie:focus, #media-url:focus { border-color: #f472b6; box-shadow: 0 0 0 2px rgba(244, 114, 182, 0.1); } #media-url:focus + #fetch-btn { background:#db2777; } #fetch-btn.active { opacity:1; pointer-events:auto; cursor:pointer; background:#db2777; } /* 响应式搜索区域 - 极窄窗口优化 */ @media screen and (max-width: 800px) { /* 强制搜索区域垂直布局 - 更早触发 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } /* 确保每个区域内部改为垂直排列(标签在上,输入框在下) */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="display:flex"][style*="align-items:center"] { flex-direction: column !important; align-items: stretch !important; } /* 调整标签样式,适应垂直布局 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"] label { width: auto !important; margin-right: 0 !important; margin-bottom: 4px !important; text-align: left !important; padding-right: 0 !important; } /* 调整搜索中指示器位置,适应垂直布局 - 提高优先级 */ #douban-tmdb-panel #search-loading { position: absolute !important; right: 10px !important; top: 35% !important; transform: translateY(-50%) !important; z-index: 1001 !important; } /* 确保搜索框容器有正确的定位基准 */ #douban-tmdb-panel div[style*="flex:1"][style*="position: relative"] { position: relative !important; } /* 确保输入框容器在移动端保持对齐 */ #input-container { flex-direction: column !important; align-items: stretch !important; gap: 12px !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 影视链接容器优化 */ #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; display: flex !important; flex-direction: column !important; align-items: stretch !important; } /* 影视链接包装器优化 - 强制垂直布局 */ #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; width: 100% !important; } /* 搜索输入框优化 */ #search-movie { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; border-radius: 6px !important; } /* 提取按钮优化 */ #fetch-btn { border-radius: 6px !important; width: 100% !important; margin-left: 0 !important; font-size: 12px !important; padding: 6px 10px !important; flex-shrink: 0 !important; } /* 搜索结果框位置调整 - 独立容器,确保输入框水平对齐 */ #search-results { position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; width: 100% !important; margin-top: 8px !important; margin-left: 0 !important; margin-right: 0 !important; z-index: 1000 !important; margin-top: 4px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 70px !important; font-size: 11px !important; } } @media screen and (max-width: 800px) { /* 强制搜索区域垂直布局 - 提前触发 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } /* 确保每个区域内部改为垂直排列(标签在上,输入框在下) */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="display:flex"][style*="align-items:center"] { flex-direction: column !important; align-items: stretch !important; } /* 调整标签样式,适应垂直布局 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"] label { width: auto !important; margin-right: 0 !important; margin-bottom: 4px !important; text-align: left !important; padding-right: 0 !important; } /* 调整搜索中指示器位置,适应垂直布局 - 提高优先级 */ #douban-tmdb-panel #search-loading { position: absolute !important; right: 10px !important; top: 35% !important; transform: translateY(-50%) !important; z-index: 1001 !important; } /* 确保搜索框容器有正确的定位基准 */ #douban-tmdb-panel div[style*="flex:1"][style*="position: relative"] { position: relative !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 影视链接容器优化 */ #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; } /* 影视链接包装器优化 - 强制垂直布局 */ #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; width: 100% !important; } /* 搜索输入框优化 */ #search-movie { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; border-radius: 6px !important; } /* 提取按钮优化 */ #fetch-btn { border-radius: 6px !important; width: 100% !important; margin-left: 0 !important; font-size: 12px !important; padding: 6px 10px !important; flex-shrink: 0 !important; } /* 搜索结果框位置调整 - 独立容器,确保输入框水平对齐 */ #search-results { position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; width: 100% !important; margin-top: 8px !important; margin-left: 0 !important; margin-right: 0 !important; z-index: 1000 !important; margin-top: 4px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 70px !important; font-size: 11px !important; } } @media screen and (max-width: 650px) { /* 强制搜索区域垂直布局 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 影视链接容器优化 */ #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; } /* 影视链接包装器优化 - 强制垂直布局 */ #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; width: 100% !important; } /* 搜索输入框优化 */ #search-movie { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: 100% !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; border-radius: 6px !important; } /* 提取按钮优化 */ #fetch-btn { border-radius: 6px !important; width: 100% !important; margin-left: 0 !important; font-size: 12px !important; padding: 6px 10px !important; flex-shrink: 0 !important; } /* 搜索结果框位置调整 - 独立容器,确保输入框水平对齐 */ #search-results { position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; width: 100% !important; margin-top: 8px !important; margin-left: 0 !important; margin-right: 0 !important; z-index: 1000 !important; margin-top: 4px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 70px !important; font-size: 11px !important; } } </style> <!-- 搜索结果交互效果CSS --> <style> /* 搜索结果框初始状态 - 独立容器,确保输入框水平对齐 */ #search-results { opacity: 0 !important; transform: translate3d(0, -8px, 0) scale(0.98) !important; transition: opacity 0.2s ease-out, transform 0.2s ease-out, max-height 0.2s ease-out !important; overflow-x: hidden !important; overflow-y: hidden !important; max-height: 0 !important; /* 初始折叠,避免空白 */ transform-origin: top center !important; will-change: opacity, transform, max-height !important; contain: layout style paint !important; position: relative !important; margin-top: 8px !important; margin-left: 0 !important; margin-right: 0 !important; } /* 搜索结果框显示动画 - 独立容器版本 */ #search-results.show { opacity: 1 !important; transform: translate3d(0, 0, 0) scale(1) !important; max-height: 60vh !important; /* 展开时自适应视口高度 */ overflow-y: auto !important; overscroll-behavior: contain !important; /* 防止滚动穿透父容器 */ } /* 搜索结果项悬停效果 */ .search-item { transition: opacity 0.08s ease-out, background-color 0.08s ease-out, transform 0.08s ease-out !important; position: relative !important; overflow: hidden !important; max-width: 100% !important; opacity: 0.8 !important; will-change: opacity, background-color, transform !important; contain: layout style !important; } /* 搜索结果项悬停时的效果 */ .search-item:hover { opacity: 1 !important; background-color: rgba(236, 72, 153, 0.05) !important; transform: translate3d(2px, 0, 0) !important; } /* 控制面板展开状态的视觉指示 - 内嵌搜索结果显示框 */ #douban-tmdb-panel.expanded { box-shadow: 0 4px 20px rgba(236, 72, 153, 0.15) !important; border-color: rgba(236, 72, 153, 0.3) !important; overflow: visible !important; } box-sizing: border-box !important; min-height: 70px !important; align-items: flex-start !important; } /* 窄屏幕下的搜索结果项优化 */ @media screen and (max-width: 500px) { .search-item { padding: 8px !important; gap: 8px !important; min-height: 60px !important; } .poster-placeholder { width: 28px !important; height: 42px !important; flex-shrink: 0 !important; } .search-item strong { font-size: 11px !important; line-height: 1.2 !important; } .search-item div[style*="color:#6b7280"] { font-size: 10px !important; } } @media screen and (max-width: 400px) { .search-item { padding: 6px !important; gap: 6px !important; min-height: 50px !important; } .poster-placeholder { width: 24px !important; height: 36px !important; } .search-item strong { font-size: 10px !important; } .search-item div[style*="color:#6b7280"] { font-size: 9px !important; } } .search-item:hover { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; transform: translateX(4px) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; border-left: 3px solid #ec4899 !important; } /* 海报图片悬停效果 */ .search-item:hover .poster-placeholder { transform: scale(1.05) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; } /* 标题文字样式 - 允许换行 */ .search-item strong { white-space: normal !important; overflow: visible !important; text-overflow: unset !important; max-width: 100% !important; display: block !important; line-height: 1.3 !important; word-wrap: break-word !important; word-break: break-word !important; } /* 标题文字悬停效果 */ .search-item:hover strong { color: #ec4899 !important; font-weight: 600 !important; } /* 元数据文字样式 */ .search-item div[style*="color:#6b7280"] { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 100% !important; } /* 元数据悬停效果 */ .search-item:hover div[style*="color:#6b7280"] { color: #4b5563 !important; } /* 加载状态动画 */ .search-item.loading { opacity: 0.7 !important; pointer-events: none !important; } .search-item.loading::after { content: '' !important; position: absolute !important; top: 0 !important; left: -100% !important; width: 100% !important; height: 100% !important; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent) !important; animation: shimmer 1.5s infinite !important; } @keyframes shimmer { 0% { left: -100%; } 100% { left: 100%; } } /* 滚动条美化 */ #search-results::-webkit-scrollbar { width: 6px !important; } #search-results::-webkit-scrollbar-track { background: #f1f5f9 !important; border-radius: 3px !important; } #search-results::-webkit-scrollbar-thumb { background: #cbd5e1 !important; border-radius: 3px !important; } #search-results::-webkit-scrollbar-thumb:hover { background: #94a3b8 !important; } /* 防止横向滚动条 */ #search-results { overflow-x: hidden !important; word-wrap: break-word !important; word-break: break-word !important; } /* 搜索结果容器宽度控制 */ .results-container { max-width: 100% !important; overflow-x: hidden !important; } /* 搜索中指示器基础样式已在上方统一定义 */ /* 父容器定位强化(解决定位基准缺失问题) */ /* 强制搜索输入框的父容器为定位基准,确保#search-loading不跑偏 */ #search-movie.parentNode, div[style*="position: relative"]:has(#search-movie) { position: relative !important; overflow: visible !important; /* 允许提示框正常显示,不被父容器裁剪 */ z-index: 1 !important; /* 确保父容器z-index较低,不遮挡导航栏 */ } /* 响应式样式:搜索中指示器自适应 */ @media screen and (max-width: 800px) { #search-loading { right: 6px !important; font-size: 11px !important; padding: 1px 4px !important; /* 移除极端断点宽度限制,避免过度压缩 */ max-width: 100% !important; top: calc(35% + 15px) !important; /* 上下布局时向下偏移 */ } #search-results { max-height: 350px !important; } } @media screen and (max-width: 650px) { #search-loading { right: 4px !important; font-size: 10px !important; padding: 1px 3px !important; max-width: 45px !important; top: calc(35% + 12px) !important; } #search-results { max-height: 300px !important; } } @media screen and (max-width: 400px) { #search-loading { right: 2px !important; font-size: 9px !important; padding: 1px 2px !important; max-width: 40px !important; background: rgba(255, 255, 255, 0.98) !important; } #search-results { max-height: 250px !important; } } @media screen and (max-width: 300px) { #search-loading { max-width: 30px !important; font-size: 8px !important; } #search-loading i::after { content: "搜索..." !important; font-style: normal !important; } #search-loading i { font-size: 0 !important; } } /* 空状态提示 */ .search-results-empty { text-align: center !important; padding: 20px !important; color: #9ca3af !important; font-style: italic !important; } /* 搜索结果项点击反馈 */ .search-item:active { transform: translateX(2px) scale(0.98) !important; background: #e2e8f0 !important; } </style> <!-- 防止密码管理器干扰的隐藏字段 --> <input type="password" style="display:none !important; position:absolute; left:-9999px;" aria-hidden="true" aria-label="ignore" autocomplete="new-password"> <input type="password" style="display:none !important; position:absolute; left:-9999px;" aria-hidden="true" aria-label="ignore" autocomplete="new-password"> <input type="password" style="display:none !important; position:absolute; left:-9999px;" aria-hidden="true" aria-label="ignore" autocomplete="new-password"> <!-- 额外的分散注意力字段 --> <input type="text" style="display:none !important; position:absolute; left:-9999px;" name="dummy-username" value="" aria-hidden="true"> <input type="text" style="display:none !important; position:absolute; left:-9999px;" name="dummy-email" value="" aria-hidden="true"> <textarea id="backup-html" style="display:none;"></textarea> <style> #fetch-btn:hover { background:#db2777; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3); } #paste-btn:hover { background:#be185d; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(219, 39, 119, 0.3); } #load-more-posters:hover { background:#ec4899; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(244, 114, 182, 0.3); } #load-more-stills:hover { background:#ec4899; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(244, 114, 182, 0.3); } #clear-btn:hover { background:#ef4444; border:1px solid #f472b6; } #emerald-city-format:hover { background:linear-gradient(135deg, #db2777 0%, #9d174d 100%); } #confirm-images-btn:hover { background:linear-gradient(135deg, #be185d 0%, #831843 100%); } </style> <script> (function(){ try { const panelEl = document.getElementById('douban-tmdb-panel'); if (!panelEl) return; const inputContainerEl = document.getElementById('input-container'); function log(message){ const time = new Date().toLocaleTimeString(); const line = document.createElement('div'); line.textContent = '[' + time + '] ' + message; logBox.appendChild(line); logBox.scrollTop = logBox.scrollHeight; } // 预览模式已移除 let resizeTimer; function applyDirectionalTransition(){ if (!inputContainerEl) return; // 根据当前布局状态决定是否添加过渡 var dir = (function(){ var w = window.innerWidth; if (w <= 800) return 'vertical'; return 'horizontal'; })(); var was = inputContainerEl.getAttribute('data-layout') || ''; if (dir === 'vertical' && was !== 'vertical') { // 横排 -> 竖排:添加过渡 inputContainerEl.classList.add('stacked-animate'); inputContainerEl.classList.remove('no-transition'); } else if (dir === 'horizontal' && was !== 'horizontal') { // 竖排 -> 横排:禁止过渡,直接闪切 inputContainerEl.classList.add('no-transition'); inputContainerEl.classList.remove('stacked-animate'); // 临时全局禁用过渡,彻底清空一帧动画 try { document.body.classList.add('no-transition-global'); } catch (e) {} // 同步禁用外层面板的过渡,避免高度/内边距的缓动引发"漂移感" try { var panelEl = document.getElementById('douban-tmdb-panel'); if (panelEl) panelEl.style.setProperty('transition', 'none', 'important'); } catch (e) {} // 小延时后移除 no-transition,避免后续动画被禁 setTimeout(function(){ inputContainerEl.classList.remove('no-transition'); try { var panelEl2 = document.getElementById('douban-tmdb-panel'); if (panelEl2) panelEl2.style.removeProperty('transition'); } catch (e) {} try { document.body.classList.remove('no-transition-global'); } catch (e) {} }, 160); } inputContainerEl.setAttribute('data-layout', dir); } // 初始化记录一次 applyDirectionalTransition(); window.addEventListener('resize', () => { if (!toggle.checked) return; // 800px阈值切换:横排→竖排无过渡且不延迟 log('窗口尺寸=' + window.innerWidth + 'x' + window.innerHeight); applyDirectionalTransition(); }); // 首次也执行一次,确保无预览下仍能按规则切换 window.addEventListener('resize', applyDirectionalTransition); } catch (e) { /* 静默失败以避免影响主流程 */ } })(); </script> </div> <!-- 选择海报区域 --> <div id="image-selection" style="margin-top:0; display:none;"> <div style="margin-bottom:15px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <h4 style="color:#db2777; margin:0; font-size:14px; font-weight:600; display:flex; align-items:center;">🖼️ 海报选择(点击选中)</h4> </div> <div id="poster-candidates" style="display: grid; grid-template-columns: repeat(5, 1fr); gap:14px; padding:8px; margin-bottom:8px; border:1px solid #f3d5d9; border-radius:8px; min-height:200px; max-height: clamp(320px, 60vh, 800px); overflow-y:auto; background:#fff;"></div> <button id="load-more-posters" style="display: none; background:#f472b6; color:white; border:none; padding:6px 14px; border-radius:8px; cursor:pointer; font-size:12px; margin-bottom:5px; transition: background 0.2s ease, transform 0.2s ease;">加载更多海报</button> </div> <div style="margin-bottom:15px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <h4 style="color:#db2777; margin:0; font-size:14px; font-weight:600; display:flex; align-items:center;">🎬 剧照选择(点击选中)</h4> </div> <div id="still-candidates" style="display: none; grid-template-columns: repeat(5, 1fr); gap:14px; padding:8px; margin-bottom:8px; border:1px solid #f3d5d9; border-radius:8px; min-height:200px; max-height: clamp(320px, 60vh, 800px); overflow-y:auto; background:#fff;"></div> <button id="load-more-stills" style="display: none; background:#f472b6; color:white; border:none; padding:6px 14px; border-radius:8px; cursor:pointer; font-size:12px; margin-bottom:5px; transition: background 0.2s ease, transform 0.2s ease;">加载更多剧照</button> </div> <!-- 核心功能按钮组 --> <div style="display:flex; justify-content:flex-end; margin-top:15px; flex-wrap:wrap; gap:12px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <button id="clear-btn" style="background:#f87171; color:white; border:none; padding:8px 16px; border-radius:8px; cursor:pointer; font-size:13px; transition: all 0.3s ease;">清除</button> <button id="emerald-city-format" style="background:linear-gradient(135deg, #ec4899 0%, #be185d 100%); color:white; border:none; padding:8px 18px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:bold; transition: all 0.3s ease;"> <i class="fa fa-magic" style="margin-right:5px;"></i>粉黛仙境排版美化 </button> <button id="confirm-images-btn" style="background:linear-gradient(135deg, #db2777 0%, #9d174d 100%); color:white; border:none; padding:8px 18px; border-radius:8px; cursor:pointer; font-size:13px; transition: all 0.3s ease;"> <i class="fa fa-heart" style="margin-right:5px;"></i>确认选择并填充(自动保存) </button> </div> </div> <div id="status" style="margin-top:15px; padding:8px; border-radius:8px; font-size:12px; display:none; transition: all 0.3s ease;"></div> </div> <!-- AI功能区域 --> <div id="ai-content-area" style="display:none;"> <div style="display:flex; justify-content:space-between; align-items:center; margin:0 0 15px 0; font-size:14px; font-weight:600; color:#be185d; border-bottom:1px solid #f3d5d9; padding-bottom:8px;"> 🤖 AI文字生成工具 <!-- 已按需移除“设置智能体”入口,避免冗余功能按钮 --> <style> /* 已移除设置智能体按钮,无需hover样式 */ </style> </div> <div id="ai-panel-container"></div> <div id="status" style="margin-top:12px; padding:8px; border-radius:8px; font-size:12px; display:none;"></div> </div> <!-- 设置功能区域 --> <div id="settings-content-area" style="display:none;"> <div style="display:flex; justify-content:space-between; align-items:center; margin:0 0 15px 0; font-size:14px; font-weight:600; color:#8b5cf6; border-bottom:1px solid #f3d5d9; padding-bottom:8px;"> ⚙️ 脚本设置 </div> <!-- TMDB API 配置 --> <div style="margin-bottom:20px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <h4 style="margin:0 0 12px 0; color:#8b5cf6; font-size:14px; font-weight:600;">🔑 TMDB API 配置</h4> <div style="display:flex; flex-wrap:wrap; gap:15px;"> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">API Key:</label> <input type="text" id="tmdb-api-key" placeholder="输入您的TMDB API Key" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none;"> </div> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">Access Token:</label> <input type="text" id="tmdb-base-url" placeholder="输入您的TMDB Access Token" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none;"> </div> </div> <div style="margin-top:10px; padding:10px; background:#f9fafb; border-radius:6px; border-left:3px solid #f59e0b;"> <p style="margin:0; font-size:12px; color:#6b7280;"> <strong>获取方法:</strong><br> 1. 登录(不可用)/注册(不可用)TMDB账号:访问 <a href="https://www.themoviedb.org/" target="_blank" style="color: #2563eb;">TMDB官网</a> ,登录(不可用)或注册(不可用)账号。<br> 2. 进入API设置:点击右上角头像→Settings→左侧API选项。<br> 3. 获取v3 API Key:在"API Keys (v3 auth)"区域,创建并复制Key。<br> 4. 获取v4 Access Token:在"Access Tokens (v4 auth)"区域,生成并复制Token。<br> <br> 注意:遵守TMDB服务条款,界面可能微调。 </p> </div> </div> <!-- AI API 配置 --> <div style="margin-bottom:20px; padding:15px; background:#fff; border-radius:8px; border:1px solid #f3d5d9;"> <h4 style="margin:0 0 12px 0; color:#8b5cf6; font-size:14px; font-weight:600;">🤖 AI API 配置</h4> <div style="display:flex; flex-wrap:wrap; gap:15px;"> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">API Key:</label> <input type="text" id="ai-api-key" placeholder="输入您的AI API Key" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none;"> </div> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">API 端点:</label> <input type="text" id="ai-api-endpoint" placeholder="AI API 端点 URL" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none;"> </div> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">AI 提供商:</label> <select id="ai-provider" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none; background-color: #fff;"> <option value="openai">OpenAI</option> <option value="anthropic">Anthropic</option> <option value="google">Google Gemini</option> <option value="doubao">豆包</option> <option value="tongyi">通义千问</option> <option value="glm">智谱AI (GLM)</option> <option value="spark">讯飞星火</option> <option value="qwen">通义千问 (Qwen)</option> <option value="zhipu">智谱AI</option> <option value="custom">自定义</option> </select> <div style="color: #6b7280; font-size: 12px; margin-top: 5px;">选择您使用的AI服务提供商</div> </div> <div style="flex:1; min-width:250px;"> <label style="display:block; margin-bottom:5px; font-weight:500; color:#6b7280; font-size:13px;">AI 模型:</label> <input type="text" id="ai-model" placeholder="AI 模型名称" style="width:100%; padding:8px; border:1px solid #f3d5d9; border-radius:8px; font-size:13px; transition: border-color 0.2s ease; outline:none;"> </div> </div> <div style="margin-top:10px; padding:10px; background:#f9fafb; border-radius:6px; border-left:3px solid #8b5cf6;"> <p style="margin:0; font-size:12px; color:#6b7280;"> <strong>推荐服务商:</strong><br> • <strong>OpenAI:</strong> <a href="https://platform.openai.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>Claude:</strong> <a href="https://console.anthropic.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>Gemini:</strong> <a href="https://makersuite.google.com/app/apikey" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>豆包:</strong> <a href="https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>通义千问:</strong> <a href="https://dashscope.console.aliyun.com/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>智谱AI (GLM):</strong> <a href="https://open.bigmodel.cn/" target="_blank" style="color:#2563eb;">获取API Key</a><br> • <strong>讯飞星火:</strong> <a href="https://console.xfyun.cn/services/bm3" target="_blank" style="color:#2563eb;">获取API Key</a> </p> </div> </div> <!-- 功能按钮 --> <div style="display:flex; justify-content:flex-end; margin-top:20px; gap:12px;"> <button id="reset-config-btn" style="background:#f59e0b; color:white; border:none; padding:8px 16px; border-radius:8px; cursor:pointer; font-size:13px; transition: all 0.3s ease;">重置为默认设置</button> <button id="save-config-btn" style="background:#a855f7; color:white; border:none; padding:8px 18px; border-radius:8px; cursor:pointer; font-size:13px; transition: all 0.3s ease;">保存设置</button> </div> <style> #reset-config-btn:hover { background:#d97706; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(245, 158, 11, 0.3); } #save-config-btn:hover { background:#9333ea; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(168, 85, 247, 0.3); } #tmdb-api-key:focus, #tmdb-base-url:focus, #ai-api-key:focus, #ai-api-endpoint:focus, #ai-model:focus, #ai-provider:focus { border-color: #f472b6; box-shadow: 0 0 0 2px rgba(244, 114, 182, 0.1); } </style> <div id="settings-status" style="margin-top:15px; padding:8px; border-radius:8px; font-size:12px; display:none; transition: all 0.3s ease;"></div> </div> `; return panel; } // 插入面板到标题输入框下方(精准定位+空值修复) function insertPanelInMarkedPosition() { if (isPanelInitialized) return; panel = createPanel(); let targetContainer = document.body; // 默认值 // 优先尝试在标题输入框下方插入 const subjectInput = document.getElementById('subject'); if (subjectInput && subjectInput.offsetParent !== null) { // 移除旧面板 const oldPanel = document.getElementById('douban-tmdb-panel'); const oldAIPanel = document.getElementById('ai-text-generation-panel'); if (oldPanel) oldPanel.remove(); if (oldAIPanel) oldAIPanel.remove(); // 将新面板插入到标题输入框之后 if (subjectInput.nextSibling) { subjectInput.parentNode.insertBefore(panel, subjectInput.nextSibling); } else { subjectInput.parentNode.appendChild(panel); } // 设置目标容器为标题输入框的父元素 targetContainer = subjectInput.parentNode; } else { // 如果标题输入框不存在,使用原有的回退逻辑 const targetContainers = [ document.querySelector('.main-content'), (document.querySelector('#thread-create-form') || {}).parentElement, (document.querySelector('.post-form') || {}).parentElement, document.querySelector('.panel-default'), document.body ]; for (const container of targetContainers) { if (container && container.offsetParent !== null) { targetContainer = container; break; } } if (!targetContainer) { targetContainer = document.body; console.warn('无法找到目标容器,已 fallback 到 document.body'); } // 移除旧面板 const oldPanel = targetContainer.querySelector('#douban-tmdb-panel'); const oldAIPanel = targetContainer.querySelector('#ai-text-generation-panel'); if (oldPanel) targetContainer.removeChild(oldPanel); if (oldAIPanel) targetContainer.removeChild(oldAIPanel); // 插入新面板 targetContainer.appendChild(panel); } posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); initFormatTools(); bindEventListeners(); bindAIEventListeners(); // 绑定AI相关事件监听器 setupMutationObserver(targetContainer); isPanelInitialized = true; showStatus('控制面板已放置在可用位置', false); return true; } insertPanelInMarkedPosition(); // 监听父容器变化,确保面板位置不移动 function setupMutationObserver(parent) { if (panelObserver) { panelObserver.disconnect(); } panelObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { const panel = document.getElementById('douban-tmdb-panel'); if (!panel || !parent.contains(panel)) { parent.appendChild(panel || createPanel()); posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); initFormatTools(); bindEventListeners(); bindAIEventListeners(); // 重新绑定AI相关事件监听器 } }); }); panelObserver.observe(parent, { childList: true, attributes: true, subtree: true, characterData: true }); } // 确保switchTab函数在全局作用域可访问 window.switchTab = switchTab; // 工具函数 - 支持向不同面板显示状态消息 function showStatus(text, isError = false, target = 'main') { // 检查当前激活的标签页 let activeTab = 'main'; if (document.getElementById('main-tab').style.background.includes('linear-gradient')) { activeTab = 'main'; } else if (document.getElementById('ai-tab').style.background.includes('linear-gradient')) { activeTab = 'ai'; } else if (document.getElementById('settings-tab') && document.getElementById('settings-tab').style.background.includes('linear-gradient')) { activeTab = 'settings'; } // 如果指定了目标,则使用指定的目标;否则使用当前激活的标签页 const targetTab = target === 'all' ? activeTab : target; // 获取对应的状态元素 let statusElement = null; if (targetTab === 'main') { statusElement = document.querySelector('#main-content-area #status'); } else if (targetTab === 'ai') { statusElement = document.querySelector('#ai-content-area #status'); } else if (targetTab === 'settings') { statusElement = document.querySelector('#settings-content-area #settings-status'); } if (!statusElement) return; // 设置状态消息(避免强制重排:先写样式,再写内容) statusElement.style.willChange = 'opacity, transform'; statusElement.style.transform = 'translateY(0)'; statusElement.style.opacity = '1'; statusElement.style.display = 'block'; statusElement.style.cssText = ` margin-top:10px; padding:8px; border-radius:6px; font-size:12px; transition: opacity .2s ease, transform .2s ease; ${isError ? 'background:#fee2e2; color:#b91c1c; border:1px solid #fecaca;' : 'background:#fef2f2; color:#be185d; border:1px solid #fecdd3;'} box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); `; requestAnimationFrame(()=>{ statusElement.textContent = text; }); // 2秒后自动隐藏(使用过渡,避免卡顿) setTimeout(() => { if (statusElement && statusElement.textContent === text) { statusElement.style.transform = 'translateY(-4px)'; statusElement.style.opacity = '0'; setTimeout(() => { if (!statusElement || statusElement.textContent !== text) return; statusElement.style.display = 'none'; statusElement.style.opacity = '1'; statusElement.style.transform = 'translateY(0)'; }, 220); } }, 2000); } function debounce(func, wait) { let timeout; return function () { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } function safeGet(obj, path, defaultValue = '') { try { return path.split('.').reduce((o, k) => o[k], obj) || defaultValue; } catch (e) { return defaultValue; } } // 图片获取函数 const IMG_CACHE = new Map(); const IMG_QUEUE = []; let IMG_ACTIVE = 0; const IMG_LIMIT = 8; // 图片请求并发上限(加速首屏) function pumpImageQueue() { try { while (IMG_ACTIVE < IMG_LIMIT && IMG_QUEUE.length > 0) { const { task, resolve, reject } = IMG_QUEUE.shift(); IMG_ACTIVE++; Promise.resolve() .then(task) .then(result => { IMG_ACTIVE--; resolve(result); pumpImageQueue(); }) .catch(err => { IMG_ACTIVE--; reject(err); pumpImageQueue(); }); } } catch (e) { IMG_ACTIVE = Math.max(0, IMG_ACTIVE - 1); } } function scheduleImageTask(task) { return new Promise((resolve, reject) => { IMG_QUEUE.push({ task, resolve, reject }); pumpImageQueue(); }); } function normalizeImageUrl(url) { try { if (!url) return ''; let u = String(url).trim(); if (u.startsWith('//')) u = 'https:' + u; if (u.includes('doubanio.com') && !/^https?:/.test(u)) u = 'https:' + u; return u; } catch (e) { return url; } } // 控制豆瓣图片下载并发(防止瞬时并发触发风控,同时保持速度) const D_IMG_CONCURRENCY = 5; let dImgActive = 0; const dImgQueue = []; function runDoubanImageTask(task){ return new Promise((resolve)=>{ const exec = ()=>{ dImgActive++; task().then(resolve).finally(()=>{ dImgActive--; const next = dImgQueue.shift(); if (next) next(); }); }; if (dImgActive < D_IMG_CONCURRENCY) exec(); else dImgQueue.push(exec); }); } function getImageDataURLWithQuality(url) { return scheduleImageTask(() => runDoubanImageTask(() => new Promise((resolve) => { if (!url) { resolve('https://picsum.photos/800/450?default-still'); return; } let baseUrl = normalizeImageUrl(url); // 内存级缓存,命中直接返回 if (IMG_CACHE.has(baseUrl)) { resolve(IMG_CACHE.get(baseUrl)); return; } if (baseUrl.includes('doubanio.com') && baseUrl.includes('/m/')) { const config = getConfig(); const qualityUrls = config.TMDB.DOUBAN_QUALITY.PRIORITY.map(quality => baseUrl.replace('/m/', `/${quality}/`) ); const tryQuality = (index, retryCount = 0) => { if (index >= qualityUrls.length) { getFallbackImageDataURL(baseUrl).then((d)=>{ IMG_CACHE.set(baseUrl, d); resolve(d); }); return; } const currentUrl = qualityUrls[index]; doubanRequest({ method: 'GET', url: currentUrl, headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/' }, responseType: 'blob', timeout: config.TMDB.DOUBAN_QUALITY.TIMEOUT }).then((res)=>{ if (res.status === 200 && res.response) { const reader = new FileReader(); reader.onload = (e) => { IMG_CACHE.set(baseUrl, e.target.result); resolve(e.target.result); }; reader.readAsDataURL(res.response); } else if (retryCount < config.TMDB.DOUBAN_QUALITY.RETRY) { setTimeout(() => tryQuality(index, retryCount + 1), 600); } else { tryQuality(index + 1); } }).catch(()=>{ if (retryCount < config.TMDB.DOUBAN_QUALITY.RETRY) { setTimeout(() => tryQuality(index, retryCount + 1), 600); } else { tryQuality(index + 1); } }); }; tryQuality(0); return; } getFallbackImageDataURL(baseUrl).then((d)=>{ IMG_CACHE.set(baseUrl, d); resolve(d); }); }))); } function shouldConvertToDataURL(url) { try { const u = normalizeImageUrl(url); // 仅对豆瓣域名转DataURL;TMDB直接走CDN缩略图,避免慢 return /doubanio\.com/.test(u); } catch (e) { return false; } } // 将豆瓣大图转为缩略图路径(m尺寸) function toDoubanThumb(url) { try { let u = normalizeImageUrl(url); u = u.replace(/\/raw\//, '/m/').replace(/\/l\//, '/m/'); if (!/\/m\//.test(u)) u = u.replace(/\/s\//, '/m/'); return u; } catch (e) { return url; } } // 获取用于展示的缩略图(豆瓣转为dataURL避免防盗链;TMDB返回CDN小图) function getThumbnailForDisplay(url) { try { const base = normalizeImageUrl(url); if (shouldConvertToDataURL(base)) { const m = toDoubanThumb(base); // 使用带Referer的请求转为dataURL,避免豆瓣直链防盗链导致空白 return getFallbackImageDataURL(m); } const tm = toTMDBThumb(base); if (tm && tm.src) return Promise.resolve(tm.src); return Promise.resolve(base); } catch (e) { return Promise.resolve(url); } } // 懒加载占位图(1x1透明gif) const LAZY_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; // 候选图片懒加载观察器 let posterObserver = null; let stillObserver = null; function initCandidateObservers() { try { const posterRoot = document.getElementById('poster-candidates') || null; const stillRoot = document.getElementById('still-candidates') || null; if (!posterObserver && posterRoot) { posterObserver = new IntersectionObserver(async (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const card = entry.target; if (!card || card.dataset.loaded === '1') continue; const rawUrl = card.dataset.rawUrl || card.dataset.url; const img = card.querySelector('img'); if (!rawUrl || !img) { posterObserver.unobserve(card); continue; } try { if (shouldConvertToDataURL(rawUrl)) { const du = await getImageDataURLWithQuality(rawUrl); img.src = du; card.dataset.url = du; if (card === selectedPosterEl && selectedPosterUrl === rawUrl) { selectedPosterUrl = du; } } else { // 列表小图,选中或升级时再换大图 img.src = rawUrl; card.dataset.url = rawUrl; } card.dataset.loaded = '1'; } catch (e) {} finally { posterObserver.unobserve(card); } } } }, { root: posterRoot, rootMargin: '200px', threshold: 0.05 }); } if (!stillObserver && stillRoot) { stillObserver = new IntersectionObserver(async (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const card = entry.target; if (!card || card.dataset.loaded === '1') continue; const rawUrl = card.dataset.rawUrl || card.dataset.url; const img = card.querySelector('img'); if (!rawUrl || !img) { stillObserver.unobserve(card); continue; } try { if (shouldConvertToDataURL(rawUrl)) { const du = await getImageDataURLWithQuality(rawUrl); img.src = du; card.dataset.url = du; if (card === selectedStillEl && selectedStillUrl === rawUrl) { selectedStillUrl = du; } } else { img.src = rawUrl; card.dataset.url = rawUrl; } card.dataset.loaded = '1'; } catch (e) {} finally { stillObserver.unobserve(card); } } } }, { root: stillRoot, rootMargin: '200px', threshold: 0.05 }); } } catch (e) {} } function observeCandidateCard(card, type) { try { if (type === 'poster' && posterObserver) posterObserver.observe(card); if (type === 'still' && stillObserver) stillObserver.observe(card); } catch (e) {} } function disconnectCandidateObservers() { try { if (posterObserver) { posterObserver.disconnect(); posterObserver = null; } } catch (e) {} try { if (stillObserver) { stillObserver.disconnect(); stillObserver = null; } } catch (e) {} } async function primeFirstCandidates() { try { // 立即加载首图,确保不点击也能拿到有效 dataURL if (selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.rawUrl && selectedPosterEl.dataset.loaded !== '1') { const rawUrl = selectedPosterEl.dataset.rawUrl; const img = selectedPosterEl.querySelector('img'); if (img) { try { const src = shouldConvertToDataURL(rawUrl) ? await getImageDataURLWithQuality(rawUrl) : rawUrl.replace(`/${config.TMDB.LIST_POSTER_SIZE}/`, `/${config.TMDB.SELECTED_POSTER_SIZE}/`); img.src = src; selectedPosterEl.dataset.url = src; selectedPosterEl.dataset.loaded = '1'; if (selectedPosterUrl === rawUrl) selectedPosterUrl = src; } catch (e) {} } } if (selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.rawUrl && selectedStillEl.dataset.loaded !== '1') { const rawUrl = selectedStillEl.dataset.rawUrl; const img = selectedStillEl.querySelector('img'); if (img) { try { const src = shouldConvertToDataURL(rawUrl) ? await getImageDataURLWithQuality(rawUrl) : rawUrl.replace(`/${config.TMDB.LIST_STILL_SIZE}/`, `/${config.TMDB.SELECTED_STILL_SIZE}/`); img.src = src; selectedStillEl.dataset.url = src; selectedStillEl.dataset.loaded = '1'; if (selectedStillUrl === rawUrl) selectedStillUrl = src; } catch (e) {} } } } catch (e) {} } // TMDB缩略图:w92 + 2x(w154) function toTMDBThumb(url) { try { if (!url) return null; const u = normalizeImageUrl(url); const m = u.match(/\/t\/p\/(?:original|w\d+|h\d+)\/([^\?\s]+)$/); if (!m) return null; const tail = m[1]; const base = 'https://image.tmdb.org/t/p'; // 提升首屏清晰度:列表小图用w154,2x用w342(仍远小于w780),性能与质量平衡 return { src: `${base}/w154/${tail}`, srcset: `${base}/w342/${tail} 2x` }; } catch (e) { return null; } } // 将TMDB图片URL统一升级为original(用于最终排版,避免放大模糊) function toTMDBOriginal(url) { try { if (!url) return url; const u = normalizeImageUrl(url); if (!/image\.tmdb\.org\/t\/p\//.test(u)) return url; return u.replace(/\/t\/p\/(?:w\d+|h\d+|original)\//, '/t/p/original/'); } catch (e) { return url; } } // TMDB结果缓存(热评/列表等) const TMDB_REVIEW_CACHE = new Map(); // 图片有效性验证函数(减少豆瓣请求,避免检测) function validateImageUrl(url) { return new Promise((resolve) => { if (!url || url.includes('picsum.photos')) { resolve(false); return; } // 对豆瓣图片采用保守策略,减少HEAD请求 if (url.includes('doubanio.com')) { // 豆瓣图片:仅基于URL格式判断,不发送验证请求 const isValidFormat = /doubanio\.com.*\.(jpg|jpeg|png|webp)/i.test(url) && !url.includes('default') && !url.includes('error'); console.log('豆瓣图片保守验证:', url, isValidFormat); resolve(isValidFormat); return; } // 非豆瓣图片才进行HEAD请求验证 GM_xmlhttpRequest({ method: 'HEAD', url: url, headers: { ...COMMON_HEADERS, 'Referer': url.includes('themoviedb.org') ? 'https://www.themoviedb.org/' : '' }, timeout: 3000, onload: (res) => { const isValid = res.status === 200 && res.responseHeaders.toLowerCase().includes('image/'); resolve(isValid); }, onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } function getFallbackImageDataURL(url) { return new Promise(async (resolve) => { // 豆瓣图片跳过预验证,直接尝试下载(避免触发检测) if (url.includes('doubanio.com')) { console.log('豆瓣图片跳过预验证,直接下载:', url); } else { // 非豆瓣图片才进行预验证 const isValid = await validateImageUrl(url); if (!isValid) { console.log('图片URL无效,跳过下载:', url); resolve('https://picsum.photos/800/450?error-still'); return; } } GM_xmlhttpRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Referer': url.includes('doubanio.com') ? 'https://movie.douban.com/' : url.includes('themoviedb.org') ? 'https://www.themoviedb.org/' : '' }, responseType: 'blob', timeout: 5000, // 增加到5秒 onload: (res) => { if (res.status === 200 && res.response && res.response.size > 0) { // 验证blob是否为有效图片 if (res.response.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (e) => { // 进一步验证DataURL const dataUrl = e.target.result; if (dataUrl && dataUrl.length > 100) { // 最小有效长度检查 resolve(dataUrl); } else { console.log('生成的DataURL无效:', url); resolve('https://picsum.photos/800/450?error-still'); } }; reader.onerror = () => { console.log('FileReader读取失败:', url); resolve('https://picsum.photos/800/450?error-still'); }; reader.readAsDataURL(res.response); } else { console.log('响应不是图片类型:', res.response.type, url); resolve('https://picsum.photos/800/450?error-still'); } } else { console.log('图片请求失败或空响应:', res.status, url); resolve('https://picsum.photos/800/450?error-still'); } }, onerror: (e) => { console.log('图片请求网络错误:', e, url); resolve('https://picsum.photos/800/450?error-still'); }, ontimeout: () => { console.log('图片请求超时:', url); resolve('https://picsum.photos/800/450?error-still'); } }); }); } // 【修复1】海报加载逻辑:按页截取(每次5张,不重复) async function getDoubanOfficialPosters(subjectUrl, page = 1) { return new Promise(resolve => { try { const config = getConfig(); const posterPage = page; // 使用传入的页码 const urlObj = new URL(subjectUrl); const photosUrl = subjectUrl.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=R'); // 豆瓣海报请求URL(走防爬封装) doubanRequest({ method: 'GET', url: photosUrl, headers: { ...COMMON_HEADERS, 'Referer': subjectUrl, 'Host': urlObj.hostname }, timeout: 8000 }).then((res)=>{ try { // 豆瓣海报页面请求成功 const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); // 尝试多种选择器 let posterImgs = []; // 尝试1: 原有选择器 let imgs = Array.from(doc.querySelectorAll('.poster-col3 li img, .article img')); console.log('选择器1找到图片数量:', imgs.length); if (imgs.length === 0) { // 尝试2: 更通用的选择器 imgs = Array.from(doc.querySelectorAll('img[src*="doubanio.com"], img[data-src*="doubanio.com"], img[srcset*="doubanio.com"]')); console.log('选择器2找到图片数量:', imgs.length); } if (imgs.length === 0) { // 尝试3: 查找所有img标签 imgs = Array.from(doc.querySelectorAll('img, source')); console.log('选择器3找到所有图片数量:', imgs.length); // 过滤出豆瓣图片 imgs = imgs.map(el => { if (el.tagName.toLowerCase() === 'source') return el.srcset || ''; return el.getAttribute('data-src') || el.getAttribute('data-origin') || el.src || ''; }).filter(u => u.includes('doubanio.com')); console.log('过滤后的豆瓣图片数量:', imgs.length); } // 尝试4: 如果还是没有,从页面主体区域查找 if (imgs.length === 0) { console.log('前面方法都失败,尝试从主内容区域查找'); const mainContent = doc.querySelector('#wrapper') || doc.querySelector('.main') || doc.body; if (mainContent) { imgs = Array.from(mainContent.querySelectorAll('img')).filter(img => img.src && img.src.includes('doubanio.com') && (img.src.includes('/movie_poster_') || img.src.includes('/public/p')) ); console.log('主内容区域找到的图片数量:', imgs.length); } } posterImgs = imgs .map(el => { let src = (typeof el === 'string') ? el : (el.getAttribute?.('data-src') || el.getAttribute?.('data-origin') || el.src || ''); if (src && src.includes(' ')) { // 从srcset提取最清晰一项 const parts = src.split(',').map(s => s.trim()); src = parts[parts.length - 1].split(' ')[0]; } if (src.includes('/m/')) src = src.replace('/m/', '/l/'); return src; }) .filter(Boolean) // 按当前页截取:(页号-1)*5 到 页号*5 .slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT); // 最终海报数据 console.log('最终海报数据:', posterImgs); resolve(posterImgs.length ? posterImgs : []); } catch (e) { console.error('解析豆瓣海报页面失败:', e); resolve([]); } }).catch((error)=>{ console.error('获取豆瓣海报请求错误:', error); resolve([]); }); } catch (e) { console.error('无效的豆瓣主题URL:', subjectUrl, e); resolve([]); } }); } // 【修复2】剧照加载逻辑:按页截取(每次5张,不重复)+ 宽高适配 function getDoubanStillsList(url, page = 1) { return new Promise(resolve => { try { const urlObj = new URL(url); const stillsUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=still'); // 豆瓣剧照请求URL(走防爬封装) doubanRequest({ method: 'GET', url: stillsUrl, headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': urlObj.hostname }, timeout: 8000 }).then((res)=>{ try { // 豆瓣剧照页面请求成功 const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); let finalStills = []; // 尝试1: 查找带剧照标记的图片 let labeledStills = Array.from(doc.querySelectorAll('.poster-col3 li img[data-title*="剧照"], .article img[data-title*="剧照"]')) .map(img => img.getAttribute('data-src') || img.getAttribute('data-origin') || img.src) .filter(Boolean); console.log('标记为剧照的图片数量:', labeledStills.length); if (labeledStills.length > 0) { finalStills = labeledStills; } else { // 尝试2: 所有poster-col3下的图片 let allImgs = Array.from(doc.querySelectorAll('.poster-col3 li img, .article img')) .map(img => img.getAttribute('data-src') || img.getAttribute('data-origin') || img.src) .filter(Boolean); console.log('poster-col3下的所有图片数量:', allImgs.length); if (allImgs.length === 0) { // 尝试3: 查找所有豆瓣图片 allImgs = Array.from(doc.querySelectorAll('img[src*="doubanio.com"], img[data-src*="doubanio.com"], img[srcset*="doubanio.com"]')) .map(el => el.getAttribute('data-src') || el.getAttribute('data-origin') || el.src || el.srcset || '') .map(s => (s && s.includes(' ')) ? s.split(',').map(p=>p.trim()).pop().split(' ')[0] : s) .filter(Boolean); console.log('所有豆瓣图片数量:', allImgs.length); } finalStills = allImgs; } // 按当前页截取:(页号-1)*5 到 页号*5 const config = getConfig(); const stillPage = page; // 使用传入的页码 const pageStills = finalStills.slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT); // 最终剧照数据 console.log('最终剧照数据:', pageStills); resolve(pageStills); } catch (e) { console.error('解析豆瓣剧照页面失败:', e); resolve([]); } }).catch((error)=>{ console.error('获取豆瓣剧照请求错误:', error); resolve([]); }); } catch (e) { console.error('无效的豆瓣URL:', url, e); resolve([]); } }); } // 【修复3】TMDB剧照加载逻辑:按页截取(统一分页逻辑) function getTMDBStillsList(mediaType, id, page = 1) { return new Promise(resolve => { const config = getConfig(); const stillPage = page; // 使用传入的页码 const stillCutsUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=still_cuts&sort_by=primary`; GM_xmlhttpRequest({ method: 'GET', url: stillCutsUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, timeout: 10000, onload: (res) => { try { const data = JSON.parse(res.responseText); const stillCuts = safeGet(data, 'still_cuts', []); // 按当前页截取剧照(每次5张) let currentPageStills = stillCuts .map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`) .filter(Boolean) .slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT); if (currentPageStills.length >= config.TMDB.IMAGE_CANDIDATES_COUNT) { resolve(currentPageStills); return; } // 补充backdrops(按当前页截取) const backdropsUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=backdrop&sort_by=vote_average.desc`; GM_xmlhttpRequest({ method: 'GET', url: backdropsUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const data = JSON.parse(res.responseText); const backdrops = safeGet(data, 'backdrops', []); const backdropPageStills = backdrops .map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`) .filter(Boolean) .slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT); currentPageStills = [...currentPageStills, ...backdropPageStills].slice(0, config.TMDB.IMAGE_CANDIDATES_COUNT); if (currentPageStills.length >= config.TMDB.IMAGE_CANDIDATES_COUNT) { resolve(currentPageStills); return; } // 补充posters(按当前页截取) const postersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`; GM_xmlhttpRequest({ method: 'GET', url: postersUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const data = JSON.parse(res.responseText); const posters = safeGet(data, 'posters', []); const posterPageStills = posters.slice(1) .map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_STILL_SIZE}/${img.file_path}`) .filter(Boolean) .slice((stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, stillPage * config.TMDB.IMAGE_CANDIDATES_COUNT); currentPageStills = [...currentPageStills, ...posterPageStills].slice(0, config.TMDB.IMAGE_CANDIDATES_COUNT); resolve(currentPageStills); } catch (e) { resolve(currentPageStills); } }, onerror: () => resolve(currentPageStills), ontimeout: () => resolve(currentPageStills) }); } catch (e) { resolve(currentPageStills); } }, onerror: () => resolve(currentPageStills), ontimeout: () => resolve(currentPageStills) }); } catch (e) { resolve([]); } }, onerror: () => resolve([]), ontimeout: () => resolve([]) }); }); } // 搜索相关函数 // 搜索结果缓存 const searchCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 // 全局搜索状态变量 let lastSearchResults = []; let lastSearchQuery = ''; // 统一的层级管理常量 const Z_INDEX_LAYERS = { SEARCH_RESULTS: 2147483647, // 搜索结果框 - 最高层级 SEARCH_CONTAINER: 2147483646, // 搜索容器 SEARCH_LOADING: 2147483645 // 搜索中指示器 - 在搜索框内显示 }; // 优化的搜索中指示器控制函数 function setSearchLoading(show) { const loadingIndicator = document.getElementById('search-loading'); if (loadingIndicator) { if (show) { // 记录显示时间 loadingIndicator.dataset.showTime = Date.now().toString(); // 立即显示并设置正确的层级 loadingIndicator.style.setProperty('display', 'block', 'important'); loadingIndicator.style.setProperty('visibility', 'visible', 'important'); loadingIndicator.style.setProperty('opacity', '1', 'important'); loadingIndicator.style.setProperty('z-index', Z_INDEX_LAYERS.SEARCH_LOADING, 'important'); // 立即校准位置,确保显示在搜索框内 resetSearchLoadingPosition(); // 搜索中指示器已显示 } else { // 清除显示时间记录 delete loadingIndicator.dataset.showTime; loadingIndicator.style.setProperty('display', 'none', 'important'); // 搜索中指示器已隐藏 } } else { console.warn('搜索中指示器元素未找到'); } } // 统一的搜索结果框显示控制函数 - 优化版,确保输入框水平对齐 function showSearchResults() { const resultsContainer = document.getElementById('search-results'); const controlPanel = document.getElementById('douban-tmdb-panel'); const inputContainer = document.getElementById('input-container'); if (resultsContainer && lastSearchResults.length > 0) { // 只有在内容已准备好时才显示 if (resultsContainer.innerHTML.trim() !== '') { // 优化控制面板展开机制 - 确保输入框保持水平对齐 if (controlPanel) { // 添加展开状态的视觉指示 controlPanel.classList.add('expanded'); // 确保控制面板有足够的内部空间显示搜索结果 controlPanel.style.setProperty('overflow', 'visible', 'important'); controlPanel.style.setProperty('transition', 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 'important'); } // 确保输入框容器保持水平对齐 if (inputContainer) { inputContainer.style.setProperty('align-items', 'flex-start', 'important'); inputContainer.style.setProperty('display', 'flex', 'important'); } // 设置显示状态:通过类控制展开,避免display切换导致空白 resultsContainer.style.setProperty('display', 'block', 'important'); resultsContainer.style.setProperty('visibility', 'visible', 'important'); resultsContainer.style.setProperty('opacity', '1', 'important'); resultsContainer.style.setProperty('position', 'relative', 'important'); resultsContainer.style.setProperty('z-index', '1', 'important'); resultsContainer.style.setProperty('margin-top', '8px', 'important'); // 添加显示动画 resultsContainer.classList.add('show'); return true; } } return false; } // 简化的显示动画函数 function animateShow(element, className = 'show') { if (element && !element.classList.contains(className)) { element.classList.add(className); } } // 将搜索结果容器的可视高度限制为约6条影片的高度,更多通过滚动查看 function adjustSearchResultsHeightToSix(container) { try { if (!container) return; const items = container.querySelectorAll('.search-item'); if (!items || items.length === 0) return; if (items.length <= 6) { // 少于等于6条不限制高度 container.style.removeProperty('max-height'); return; } // 计算前6条底部相对容器的像素高度 const sixth = items[5]; const targetBottom = sixth.offsetTop + sixth.offsetHeight + 6; // 6px缓冲 container.style.setProperty('max-height', targetBottom + 'px', 'important'); container.style.setProperty('overflow-y', 'auto', 'important'); container.style.setProperty('overscroll-behavior', 'contain', 'important'); } catch (_) {} } // 轻量隐藏:只做淡出与关闭交互,避免 display 切换引发布局重排 function fadeOutSearchResultsNoLayout() { const resultsContainer = document.getElementById('search-results'); const controlPanel = document.getElementById('douban-tmdb-panel'); const inputContainer = document.getElementById('input-container'); if (resultsContainer) { // 先移除动画类,触发隐藏动画 resultsContainer.classList.remove('show'); resultsContainer.style.willChange = 'opacity, transform, max-height'; resultsContainer.style.visibility = 'hidden'; resultsContainer.style.opacity = '0'; resultsContainer.style.pointerEvents = 'none'; resultsContainer.style.maxHeight = '0'; resultsContainer.style.marginTop = '0'; // 过渡结束后彻底移除占位,避免窄屏不回弹 const endHide = () => { resultsContainer.style.setProperty('display', 'none', 'important'); resultsContainer.removeEventListener('transitionend', endHide); }; resultsContainer.addEventListener('transitionend', endHide); setTimeout(endHide, 250); // 恢复控制面板的原始状态 if (controlPanel) { // 移除展开状态的视觉指示 controlPanel.classList.remove('expanded'); // 恢复控制面板的原始样式 controlPanel.style.setProperty('overflow', 'visible', 'important'); controlPanel.style.setProperty('transition', 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 'important'); } // 确保输入框容器保持水平对齐 if (inputContainer) { inputContainer.style.setProperty('align-items', 'flex-start', 'important'); inputContainer.style.setProperty('display', 'flex', 'important'); } } // 隐藏搜索中指示器 setSearchLoading(false); } // 兼容旧调用名 function hideSearchResults() { fadeOutSearchResultsNoLayout(); } // 检测当前布局状态 function detectLayoutState() { const searchInput = document.getElementById('search-movie'); const mediaUrlLabel = document.querySelector('label[for="media-url"], label[style*="width:70px"]'); if (!searchInput || !mediaUrlLabel) return 'horizontal'; const inputRect = searchInput.getBoundingClientRect(); const labelRect = mediaUrlLabel.getBoundingClientRect(); // 如果标签在搜索框下方,说明是垂直布局 const isVertical = labelRect.top > inputRect.bottom + 10; // 10px容差 return isVertical ? 'vertical' : 'horizontal'; } // 动态调整输入框对齐 function adjustInputAlignment() { const inputContainer = document.getElementById('input-container'); const searchContainer = document.querySelector('div[style*="flex: 1 1 250px"][style*="min-width: 200px"][style*="position: relative"]'); const linkContainer = document.getElementById('media-url-container'); if (!inputContainer || !searchContainer || !linkContainer) return; const windowWidth = window.innerWidth; const isMobile = windowWidth < 800; if (isMobile) { // 移动端:垂直布局 inputContainer.style.setProperty('flex-direction', 'column', 'important'); inputContainer.style.setProperty('align-items', 'stretch', 'important'); inputContainer.style.setProperty('gap', '12px', 'important'); // 竖排时子块恢复自适应,避免横排规则残留 try { searchContainer.style.removeProperty('display'); searchContainer.style.removeProperty('align-items'); searchContainer.style.removeProperty('min-height'); linkContainer.style.removeProperty('display'); linkContainer.style.removeProperty('align-items'); linkContainer.style.removeProperty('min-height'); } catch (e) {} } else { // 桌面端:水平布局 inputContainer.style.setProperty('flex-direction', 'row', 'important'); inputContainer.style.setProperty('align-items', 'flex-start', 'important'); inputContainer.style.setProperty('gap', '15px', 'important'); // 两个子容器统一为顶部对齐,避免切换瞬间出现"先居中"的视觉跳动 try { searchContainer.style.setProperty('display', 'flex', 'important'); searchContainer.style.setProperty('align-items', 'flex-start', 'important'); searchContainer.style.setProperty('min-width', '200px', 'important'); linkContainer.style.setProperty('display', 'flex', 'important'); linkContainer.style.setProperty('align-items', 'flex-start', 'important'); linkContainer.style.setProperty('min-width', '200px', 'important'); } catch (e) {} } // 确保两个输入框容器高度一致 const searchRect = searchContainer.getBoundingClientRect(); const linkRect = linkContainer.getBoundingClientRect(); if (!isMobile && Math.abs(searchRect.height - linkRect.height) > 5) { const maxHeight = Math.max(searchRect.height, linkRect.height); searchContainer.style.setProperty('min-height', maxHeight + 'px', 'important'); linkContainer.style.setProperty('min-height', maxHeight + 'px', 'important'); } } // 优化的位置校准函数 function resetSearchLoadingPosition() { const loadingIndicator = document.getElementById('search-loading'); const searchInput = document.getElementById('search-movie'); if (!loadingIndicator || !searchInput) return; const windowWidth = window.innerWidth; const isVerticalLayout = windowWidth < 800; // 简化的位置计算 const rightOffset = windowWidth > 900 ? 8 : windowWidth > 650 ? 6 : windowWidth > 400 ? 4 : windowWidth > 300 ? 2 : 1; const topPosition = isVerticalLayout ? `calc(45% + ${windowWidth > 650 ? 2 : 1}px)` : '50%'; // 强制设置所有样式,确保在缩放时完全不会消失 loadingIndicator.style.setProperty('position', 'absolute', 'important'); loadingIndicator.style.setProperty('right', `${rightOffset}px`, 'important'); loadingIndicator.style.setProperty('top', topPosition, 'important'); loadingIndicator.style.setProperty('transform', 'translateY(-50%)', 'important'); loadingIndicator.style.setProperty('display', 'block', 'important'); loadingIndicator.style.setProperty('visibility', 'visible', 'important'); loadingIndicator.style.setProperty('opacity', '1', 'important'); loadingIndicator.style.setProperty('z-index', Z_INDEX_LAYERS.SEARCH_LOADING, 'important'); loadingIndicator.style.setProperty('color', '#6b7280', 'important'); loadingIndicator.style.setProperty('font-size', '11px', 'important'); loadingIndicator.style.setProperty('font-weight', '500', 'important'); loadingIndicator.style.setProperty('background', 'rgba(255, 255, 255, 0.9)', 'important'); loadingIndicator.style.setProperty('padding', '2px 6px', 'important'); loadingIndicator.style.setProperty('border-radius', '4px', 'important'); } // 强制注入CSS确保搜索结果框显示(优化版 - 内嵌到控制面板中) function injectSearchResultsCSS() { // 避免重复注入CSS if (document.getElementById('search-results-force-style')) { return; } const style = document.createElement('style'); style.id = 'search-results-force-style'; style.textContent = ` /* 强制降低遮挡元素的层级 */ div.tox-editor-header { z-index: 1 !important; } /* 独立容器的搜索结果框样式 - 确保输入框水平对齐 */ #search-results { position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; z-index: 1 !important; background: #fff !important; border: 1px solid #f3d5d9 !important; border-radius: 6px !important; margin-top: 8px !important; margin-left: 0 !important; margin-right: 0 !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; width: 100% !important; max-height: 400px !important; overflow-y: auto !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; transform: none !important; } /* 搜索结果容器优化样式 - 已内嵌到控制面板中,无需绝对定位 */ /* 搜索中指示器基础样式 - 默认隐藏,由JavaScript控制显示 */ #search-loading { position: absolute !important; right: 10px !important; top: 35% !important; transform: translateY(-50%) !important; color: #6b7280 !important; font-size: 11px !important; font-weight: 500 !important; background: rgba(255, 255, 255, 0.9) !important; padding: 2px 6px !important; border-radius: 4px !important; z-index: 1001 !important; /* 搜索中指示器 - 最高层级 */ pointer-events: none !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 60px !important; box-sizing: border-box !important; display: none !important; /* 默认隐藏,由JavaScript控制 */ visibility: visible !important; /* 确保可见性 */ opacity: 1 !important; /* 确保透明度 */ } #search-results * { pointer-events: auto !important; } /* 确保搜索结果框的父容器也有足够高的z-index */ div[style*="position: relative"]:has(#search-results) { z-index: 999999 !important; } /* 搜索中指示器保护样式已在上方统一定义 */ /* 重复的媒体查询已在上方统一定义 */ /* 在垂直布局时调整搜索中指示器位置 */ @media screen and (max-width: 800px) { /* 确保在垂直布局时,指示器相对于输入框而不是标签定位 */ div[style*="position: relative"]:has(#search-movie) { position: relative !important; } /* 当标签和输入框变为上下布局时,调整搜索中指示器位置 */ #search-loading { position: absolute !important; right: 10px !important; /* 在垂直布局时,现在相对于搜索框容器定位,不需要额外偏移 */ top: 35% !important; transform: translateY(-50%) !important; color: #6b7280 !important; font-size: 11px !important; font-weight: 500 !important; background: rgba(255, 255, 255, 0.9) !important; padding: 2px 6px !important; border-radius: 4px !important; z-index: 1001 !important; /* 搜索中指示器 - 最高层级 */ pointer-events: none !important; /* 强制重置其他可能影响的属性 */ left: auto !important; bottom: auto !important; margin: 0 !important; width: auto !important; height: auto !important; max-width: none !important; min-width: 0 !important; } } @media screen and (max-width: 650px) { /* 更窄屏幕下的微调 */ #search-loading { position: absolute !important; right: 3px !important; top: 35% !important; transform: translateY(-50%) !important; font-size: 8px !important; padding: 1px 2px !important; max-width: 35px !important; width: auto !important; height: auto !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; box-sizing: border-box !important; } } /* 470像素以下优化 - 防止搜索中指示器超出搜索框 */ @media screen and (max-width: 470px) { #search-loading { position: absolute !important; right: 8px !important; top: 35% !important; transform: translateY(-50%) !important; font-size: 7px !important; padding: 1px 2px !important; max-width: 30px !important; width: auto !important; height: auto !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; box-sizing: border-box !important; } /* 如果空间太小,只显示点号 */ #search-loading i { font-style: normal !important; } } /* 极限窄屏幕下的搜索中指示器优化 */ @media screen and (max-width: 400px) { #search-loading { position: absolute !important; right: 6px !important; top: 35% !important; transform: translateY(-50%) !important; font-size: 6px !important; padding: 1px !important; max-width: 25px !important; width: auto !important; height: auto !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; box-sizing: border-box !important; } /* 极限窄屏幕下简化文本 */ #search-loading i::after { content: "..." !important; } #search-loading i { font-size: 0 !important; } } `; document.head.appendChild(style); } // 显示搜索结果的函数 - 终极版本,彻底解决空白问题 function displaySearchResults(results, container) { // 隐藏搜索中指示器 setSearchLoading(false); // 使用DocumentFragment和createElement优化DOM创建 const fragment = document.createDocumentFragment(); // 显示全部结果,滚动在容器内进行 const doubanResults = results.filter(r => r.source === '豆瓣'); const tmdbResults = results.filter(r => r.source === 'TMDB'); const orderedResults = [...doubanResults, ...tmdbResults]; lastSearchResults = orderedResults; if (results.length === 0) { const emptyDiv = document.createElement('div'); emptyDiv.style.cssText = 'padding:12px; color:#6b7280; text-align:center; background:#f9fafb; border-radius:6px; margin:4px;'; emptyDiv.textContent = '未找到结果,请尝试其他关键词'; fragment.appendChild(emptyDiv); } else { const makeGroup = (titleText, list, offsetBase) => { const title = document.createElement('div'); // 简单分组:仅标题 + 细分隔线,不使用背景块 title.style.cssText = 'margin:8px 4px 4px; padding:0 2px 4px; font-size:12px; color:#6b7280; font-weight:600; border-bottom:1px dashed #e5e7eb;'; title.textContent = titleText; fragment.appendChild(title); const box = document.createElement('div'); box.className = 'results-container'; list.forEach((item, idx) => { const searchItem = createSearchResultItem(item, offsetBase + idx); box.appendChild(searchItem); }); fragment.appendChild(box); return offsetBase + list.length; }; let offset = 0; if (doubanResults.length) offset = makeGroup('豆瓣', doubanResults, offset); if (tmdbResults.length) offset = makeGroup('TMDB', tmdbResults, offset); } // 先隐藏容器,避免显示空白 container.style.display = 'none'; container.style.visibility = 'hidden'; container.style.opacity = '0'; // 一次性设置内容,减少重绘 container.innerHTML = ''; container.appendChild(fragment); // 设置内嵌显示状态 container.style.setProperty('display', 'block', 'important'); container.style.setProperty('visibility', 'visible', 'important'); container.style.setProperty('opacity', '1', 'important'); container.style.setProperty('position', 'relative', 'important'); container.style.setProperty('z-index', '1', 'important'); // 调用统一的显示函数,确保控制面板自动展开和动画效果 showSearchResults(); // 将高度锁定在约6条的高度,可滚动查看更多 adjustSearchResultsHeightToSix(container); // 延迟加载豆瓣图片,避免阻塞显示框动画 if (orderedResults.length > 0) { // 立即设置懒加载(不再延迟与替换容器,避免首屏转圈) setupLazyImageLoading(orderedResults, container); } } // 创建单个搜索结果项的优化函数 function createSearchResultItem(item, index) { const resultItem = document.createElement('div'); resultItem.className = 'search-item'; resultItem.setAttribute('data-url', item.url || ''); resultItem.setAttribute('data-type', item.type || ''); resultItem.setAttribute('data-index', index); const isDouban = item.source === '豆瓣'; const isTMDB = item.source === 'TMDB'; const bgColor = isDouban ? '#eff6ff' : isTMDB ? '#e0f2fe' : ''; resultItem.style.cssText = `padding:6px; cursor:pointer; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:6px; background:${bgColor};`; // 创建海报占位符 const posterDiv = document.createElement('div'); posterDiv.className = 'poster-placeholder'; posterDiv.style.cssText = 'width:36px; height:54px; background:#f3f4f6; border-radius:3px; display:flex; align-items:center; justify-content:center; color:#9ca3af; transition: all 0.2s ease;'; // 处理图片 if (item.poster) { let imageUrl = item.poster; if (imageUrl.includes('doubanio.com') && !imageUrl.includes('https:')) { imageUrl = 'https:' + imageUrl; } if (imageUrl.includes('doubanio.com')) { // 豆瓣图片:显示加载中图标 const spinner = document.createElement('i'); spinner.className = 'fa fa-spinner fa-spin'; spinner.style.cssText = 'font-size:14px; color:#f59e0b;'; posterDiv.appendChild(spinner); } else { // 其他图片:直接显示 const img = document.createElement('img'); img.src = imageUrl; img.alt = item.title; img.style.cssText = 'width:36px; height:54px; object-fit:cover; border-radius:3px;'; img.setAttribute('referrerpolicy', 'no-referrer'); img.onerror = function() { this.style.display = 'none'; const fallback = document.createElement('i'); fallback.className = 'fa fa-film'; fallback.style.cssText = 'font-size:14px;'; posterDiv.appendChild(fallback); }; img.onload = () => {}; // 图片加载成功 posterDiv.appendChild(img); } } else { const fallback = document.createElement('i'); fallback.className = 'fa fa-film'; fallback.style.cssText = 'font-size:14px;'; posterDiv.appendChild(fallback); } // 创建内容区域 const contentDiv = document.createElement('div'); const title = document.createElement('strong'); title.textContent = item.title; title.style.cssText = 'color:#374151; font-size:12px; line-height:1.3; transition: all 0.2s ease;'; const meta = document.createElement('div'); meta.textContent = `${item.type} • ${item.year} • ${item.source}`; meta.style.cssText = 'color:#6b7280; font-size:11px; margin-top:2px; transition: all 0.2s ease;'; contentDiv.appendChild(title); contentDiv.appendChild(meta); resultItem.appendChild(posterDiv); resultItem.appendChild(contentDiv); return resultItem; } // 分批加载豆瓣图片,避免同时发起过多请求 function loadDoubanImagesBatch(results, container) { const doubanItems = results .map((item, index) => ({ item, index })) .filter(({ item }) => item.poster && item.poster.includes('doubanio.com')); if (doubanItems.length === 0) return; // 分批加载(每批2个),首屏更快 const batchSize = 2; let currentBatch = 0; function loadNextBatch() { const startIndex = currentBatch * batchSize; const endIndex = Math.min(startIndex + batchSize, doubanItems.length); const batch = doubanItems.slice(startIndex, endIndex); batch.forEach(({ item, index }) => { loadSingleDoubanImage(item, index, container); }); currentBatch++; // 如果还有更多批次,延迟加载下一批(加入抖动,避免固定间隔触发风控) if (endIndex < doubanItems.length) { const jitter = 350 + Math.floor(Math.random()*250); setTimeout(loadNextBatch, jitter); } } loadNextBatch(); } // 加载单个豆瓣图片 function loadSingleDoubanImage(item, index, container) { const searchItem = container.querySelector(`[data-index="${index}"]`); if (!searchItem) return; const placeholder = searchItem.querySelector('.poster-placeholder'); if (!placeholder) return; let imageUrl = item.poster; if (!imageUrl.includes('https:')) { imageUrl = 'https:' + imageUrl; } // 先尝试直链缩略图,失败再退到带Referer下载和条目页 const tryVariants = async () => { let thumb = imageUrl; try { thumb = toDoubanThumb(imageUrl); } catch(_) {} // 先快速插入直链缩略 try { const fastImg = document.createElement('img'); fastImg.src = thumb; fastImg.style.cssText = 'width:36px; height:54px; object-fit:cover; border-radius:3px;'; fastImg.alt = item.title; fastImg.referrerPolicy = 'no-referrer'; fastImg.onerror = () => fallbackFetch(); placeholder.innerHTML = ''; placeholder.appendChild(fastImg); } catch(_) {} const fallbackFetch = async () => { try { const res = await doubanRequest({ method:'GET', url: thumb, headers:{ 'Referer':'https://movie.douban.com/' }, responseType:'blob', timeout: 6500 }); const blob = res.response; const hdr = (res.responseHeaders||'').toLowerCase(); const invalid = !blob || (blob.size && blob.size < 1500) || hdr.includes('text/html'); if (!invalid) { const du = URL.createObjectURL(blob); placeholder.innerHTML = `<img src="${du}" style="width:36px; height:54px; object-fit:cover; border-radius:3px;" alt="${item.title}">`; return; } } catch(_) {} try { const real = await tryResolvePosterFromSubject(item.url); if (real) { const du = await getFallbackImageDataURL(real); if (du && !/picsum\.photos\/.+error/.test(du)) { placeholder.innerHTML = `<img src="${du}" style="width:36px; height:54px; object-fit:cover; border-radius:3px;" alt="${item.title}">`; return; } } } catch(_) {} placeholder.innerHTML = '<i class="fa fa-film" style="font-size:14px; color:#9ca3af;"></i>'; }; // 保险:2.2s内仍未替换成功,主动触发一次fallback setTimeout(()=>{ const im = placeholder.querySelector('img'); if (!im || !im.complete) fallbackFetch(); }, 2200); }; setTimeout(tryVariants, 80 + Math.floor(Math.random()*180)); } // 从豆瓣条目页解析海报URL(用于搜索列表占位图“new.gif”情况) function tryResolvePosterFromSubject(subjectUrl) { return new Promise((resolve) => { if (!subjectUrl) { resolve(''); return; } setTimeout(() => doubanRequest({ method: 'GET', url: subjectUrl, headers: { 'Referer': 'https://movie.douban.com/' }, timeout: 5000 }) .then((res) => { try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const og = doc.querySelector('meta[property="og:image"]'); let u = (og && og.content) || ''; if (!u) { const img = doc.querySelector('#mainpic img, .subject .pic img'); u = img ? (img.getAttribute('src') || '') : ''; } if (u && u.startsWith('//')) u = 'https:' + u; resolve(u); } catch (_) { resolve(''); } }) .catch(() => resolve('')), 200 + Math.floor(Math.random() * 300) ); }); } // 设置懒加载图片,只加载可见区域的图片 function setupLazyImageLoading(results, container) { const doubanItems = results .map((item, index) => ({ item, index })) .filter(({ item }) => item.poster && item.poster.includes('doubanio.com')); if (doubanItems.length === 0) return; // 创建IntersectionObserver进行懒加载 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const searchItem = entry.target; const index = parseInt(searchItem.getAttribute('data-index')); const item = results[index]; if (item && item.poster && item.poster.includes('doubanio.com')) { loadSingleDoubanImage(item, index, container); observer.unobserve(searchItem); // 加载后停止观察 } } }); }, { root: container, rootMargin: '80px', threshold: 0.01 }); // 观察所有搜索结果项;若选不到节点,延迟一帧重试一次,避免组标题插入后索引错位 const attachObservers = () => { doubanItems.forEach(({ index }) => { const searchItem = container.querySelector(`[data-index="${index}"]`); if (searchItem) { observer.observe(searchItem); } }); }; attachObservers(); if (container.querySelectorAll('.search-item').length < doubanItems.length) { requestAnimationFrame(attachObservers); } // 兜底:立即加载首屏可见项,避免IntersectionObserver在嵌套滚动容器内不触发 try { const rect = container.getBoundingClientRect(); container.querySelectorAll('.search-item').forEach(item => { const r = item.getBoundingClientRect(); if (r.top < rect.bottom + 40 && r.bottom > rect.top - 40) { const idx = parseInt(item.getAttribute('data-index')); const it = results[idx]; if (it && it.poster && it.poster.includes('doubanio.com')) { loadSingleDoubanImage(it, idx, container); } } }); } catch (_) {} } // 图片懒加载设置函数 - 简化版 function setupImageLazyLoading(results, container) { // 创建新的懒加载控制器 if (window.currentLazyLoadController) { window.currentLazyLoadController.abort(); } window.currentLazyLoadController = new AbortController(); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const item = entry.target; const index = parseInt(item.getAttribute('data-index')); const resultItem = results[index]; if (resultItem && resultItem.poster && !window.currentLazyLoadController.signal.aborted) { const placeholder = item.querySelector('.poster-placeholder'); if (placeholder) { // 尝试加载图片 // 直接使用原始URL,避免复杂的转换 let imageUrl = resultItem.poster; // 处理豆瓣图片URL if (imageUrl.includes('doubanio.com') && !imageUrl.includes('https:')) { imageUrl = 'https:' + imageUrl; } // 处理后的图片URL // 立即显示图片,不等待加载完成(TMDB使用小缩略图w92 + 2x) const tmdbThumb = toTMDBThumb(imageUrl); const attr = tmdbThumb ? `src='${tmdbThumb.src}' srcset='${tmdbThumb.srcset}'` : `src='${imageUrl}'`; placeholder.innerHTML = ` <img ${attr} style="width:36px; height:54px; object-fit:cover; border-radius:3px; background:#f3f4f6;" alt='${resultItem.title}' loading='lazy' onerror="this.style.display='none'; this.parentElement.innerHTML='<i class=\\"fa fa-film\\" style=\\"font-size:14px; color:#9ca3af;\\"></i>';" onload=''> `; } } observer.unobserve(item); } }); }, { rootMargin: '0px 0px 100px 0px' }); // 观察所有搜索项 container.querySelectorAll('.search-item').forEach(item => { observer.observe(item); }); } function searchDouban(query) { // 检查缓存 const cacheKey = `douban_${query}`; const cached = searchCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { // 使用豆瓣搜索缓存 return Promise.resolve(cached.data); } return new Promise((resolve) => { // 三通道:suggest(快)+ HTML(全)+ Rexxar(稳)并行合并 const suggestP = searchDoubanSuggest(query).catch(() => []); const rexxarP = searchDoubanRexxar(query).catch(() => []); const url = `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(query)}&cat=1002`; doubanRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Host': 'search.douban.com', 'Sec-Fetch-Dest': 'document' }, timeout: 3000 }) .then((res) => { try { if (res.status !== 200) throw new Error('bad-status'); const html = res.responseText; const dataMatch = html.match(/window\.__DATA__\s*=\s*({.*?});/s); if (!dataMatch || !dataMatch[1]) throw new Error('parse-fail'); const cleanData = dataMatch[1].replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))); const doubanData = JSON.parse(cleanData); const items = safeGet(doubanData, 'items', []); let results = items.map(item => ({ title: safeGet(item, 'title', '未知作品'), type: safeGet(item, 'labels', []).some(l => l.text === '剧集') ? '电视剧' : '电影', year: safeGet(item, 'title', '').match(/\((\d{4})\)/)?.[1] || '未知', source: '豆瓣', id: safeGet(item, 'id', ''), url: safeGet(item, 'url', ''), poster: safeGet(item, 'cover_url', '') })).filter(item => item.url); if (!results.length) { return Promise.all([suggestP, rexxarP]).then(([sug, rex]) => { const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=>t.title===it.title && t.year===it.year)); searchCache.set(cacheKey, { data: merged, timestamp: Date.now() }); resolve(merged); }); } return Promise.all([suggestP, rexxarP]).then(([sug, rex]) => { const merged = [...results, ...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year)); searchCache.set(cacheKey, { data: merged, timestamp: Date.now() }); resolve(merged); }).catch(() => { searchCache.set(cacheKey, { data: results, timestamp: Date.now() }); resolve(results); }); } catch (e) { Promise.all([suggestP, rexxarP]).then(([sug, rex]) => { const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year)); searchCache.set(cacheKey, { data: merged, timestamp: Date.now() }); resolve(merged); }).catch(() => resolve([])); } }) .catch(() => { Promise.all([suggestP, rexxarP]).then(([sug, rex]) => { const merged = [...(Array.isArray(sug)?sug:[]), ...(Array.isArray(rex)?rex:[])].filter((it,idx,arr)=> idx===arr.findIndex(t=> (t.title===it.title || normalizeTextForCompare(t.title)===normalizeTextForCompare(it.title)) && t.year===it.year)); searchCache.set(cacheKey, { data: merged, timestamp: Date.now() }); resolve(merged); }).catch(() => resolve([])); }); }); } // Rexxar 移动端搜索作为补充,提升 AI 面板豆瓣覆盖度 function searchDoubanRexxar(query) { return new Promise((resolve) => { const url = `https://m.douban.com/rexxar/api/v2/search?q=${encodeURIComponent(query)}&start=0&count=60&cat=1002`; doubanRequest({ method: 'GET', url, headers: { ...COMMON_HEADERS, 'Host': 'm.douban.com', 'Referer': `https://m.douban.com/search/?q=${encodeURIComponent(query)}`, 'X-Requested-With': 'XMLHttpRequest' }, timeout: 4000 }) .then((res) => { try { const data = JSON.parse(res.responseText || '{}'); const items = data.subjects || data.items || []; const list = items.map(it => { const id = it.id || (it.target && it.target.id); const title = it.title || (it.target && it.target.title) || (it.display && it.display.split('\n')[0]) || '未知作品'; const year = (it.year || (it.card_subtitle && (it.card_subtitle.match(/(\d{4})/)||[])[1])) || '未知'; const poster = (it.cover_url || (it.pic && it.pic.normal)) || ''; return id ? { title, type: '电影', year, source: '豆瓣', id, url: `https://movie.douban.com/subject/${id}/`, poster } : null; }).filter(Boolean); resolve(list); } catch (_) { resolve([]); } }) .catch(() => resolve([])); }); } // 豆瓣搜索回退:使用 subject_suggest 接口(JSON,速度更快,适合首次搜索) function searchDoubanSuggest(query) { return new Promise((resolve) => { const url = `https://movie.douban.com/j/subject_suggest?q=${encodeURIComponent(query)}`; doubanRequest({ method: 'GET', url: url, headers: { ...COMMON_HEADERS, 'Host': 'movie.douban.com', 'X-Requested-With': 'XMLHttpRequest' }, timeout: 4000 }) .then((res) => { try { if (res.status !== 200) { resolve([]); return; } const arr = JSON.parse(res.responseText); const results = (Array.isArray(arr) ? arr : []).map(item => ({ title: item.title || item.sub_title || '未知作品', type: item.type === 'movie' ? '电影' : (item.type === 'tv' ? '电视剧' : '电影'), year: item.year || '未知', source: '豆瓣', id: item.id || '', url: item.id ? `https://movie.douban.com/subject/${item.id}/` : (item.url || ''), poster: item.cover_url || '' })).filter(it => it.url); resolve(results); } catch (e) { resolve([]); } }) .catch(() => resolve([])); }); } function normalizeTextForCompare(text) { try { let s = (text || '').toString().toLowerCase(); // 全角转半角 s = s.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0)); // 去除符号与空白 s = s.replace(/[\p{P}\p{S}\s]+/gu, ''); return s; } catch (_) { return (text || '').toString().toLowerCase(); } } function parseQueryParts(q) { const m = (q || '').match(/(.+?)(?:[\((\[]?(\d{4})[\))\]]?)?$/); return { title: (m && m[1] ? m[1] : q).trim(), year: (m && m[2]) ? m[2] : '' }; } function rankAndDedupResults(results, query) { const { title, year } = parseQueryParts(query); const normQ = normalizeTextForCompare(title); const targetYear = parseInt(year || '0', 10); const seen = new Set(); const ranked = (results || []).map(it => { const t = (it.title || '').toString(); const y = parseInt((it.year || '').toString().slice(0,4) || '0', 10); const normT = normalizeTextForCompare(t); const titleScore = normT.includes(normQ) ? 5 : 0; const yearScore = (targetYear && y) ? (5 - Math.min(5, Math.abs(targetYear - y))) : 0; const sourceScore = it.source === '豆瓣' ? 2 : 1; return { ...it, _score: titleScore + yearScore + sourceScore }; }).sort((a,b)=> b._score - a._score); const dedup = []; for (const it of ranked) { const key = `${normalizeTextForCompare(it.title)}_${(it.year||'').slice(0,4)}`; if (!seen.has(key)) { seen.add(key); dedup.push(it); } } return dedup; } function searchTMDB(query) { // 检查缓存 const cacheKey = `tmdb_${query}`; const cached = searchCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { console.log('使用TMDB搜索缓存'); return Promise.resolve(cached.data); } return new Promise((resolve) => { const config = getConfig(); const { title, year } = parseQueryParts(query); const langs = ['zh-CN','en-US']; const reqs = langs.map(lang => `${config.TMDB.BASE_URL}/search/multi?api_key=${config.TMDB.API_KEY}&query=${encodeURIComponent(title)}&language=${lang}${year?`&year=${year}`:''}`); const keywordUrl = `${config.TMDB.BASE_URL}/search/keyword?api_key=${config.TMDB.API_KEY}&query=${encodeURIComponent(title)}`; const send = (url) => new Promise(resv => { GM_xmlhttpRequest({ method: 'GET', url, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, timeout: 5000, onload: r => { try { resv(JSON.parse(r.responseText)); } catch(_) { resv({}); } }, onerror: () => resv({}), ontimeout: () => resv({}) }); }); Promise.all([ ...reqs.map(send), send(keywordUrl) ]).then(async (arr) => { try { const merge = []; const pushItem = (item) => { if (!item || !item.id) return; const media_type = item.media_type || (item.name ? 'tv' : 'movie'); const url = media_type === 'movie' ? `https://www.themoviedb.org/movie/${item.id}` : `https://www.themoviedb.org/tv/${item.id}`; merge.push({ title: item.title || item.name || '未知作品', type: media_type === 'movie' ? '电影' : media_type === 'tv' ? '电视剧' : '未知', year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || '未知', source: 'TMDB', id: item.id, url, poster: item.poster_path ? `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${item.poster_path}` : '' }); }; // 多语言 multi 结果 for (const a of arr.slice(0, reqs.length)) { const results = safeGet(a, 'results', []); results.forEach(pushItem); } // 关键词 → 再拉一轮 movie/tv 搜索(更全) const kw = safeGet(arr[reqs.length], 'results', []); const kwId = (kw && kw[0] && kw[0].id) ? kw[0].id : null; if (kwId) { const byKw = await Promise.all([ send(`${config.TMDB.BASE_URL}/discover/movie?api_key=${config.TMDB.API_KEY}&with_keywords=${kwId}&language=zh-CN`), send(`${config.TMDB.BASE_URL}/discover/tv?api_key=${config.TMDB.API_KEY}&with_keywords=${kwId}&language=zh-CN`) ]); (safeGet(byKw[0],'results',[])||[]).forEach(pushItem); (safeGet(byKw[1],'results',[])||[]).forEach(pushItem); } const ranked = rankAndDedupResults(merge, query).slice(0, 30); searchCache.set(cacheKey, { data: ranked, timestamp: Date.now() }); console.log('TMDB合并结果:', ranked.length); resolve(ranked); } catch (e) { console.error(e); resolve([]); } }); }); } // 搜索结果交互 function setupSearchInteractions() { const searchInput = document.getElementById('search-movie'); const resultsContainer = document.getElementById('search-results'); const loadingIndicator = document.getElementById('search-loading'); const mediaUrlInput = document.getElementById('media-url'); let lazyLoadController = new AbortController(); if (!searchInput || !resultsContainer || !loadingIndicator || !mediaUrlInput) return; searchInput.addEventListener('input', debounce(async function () { const query = this.value.trim(); lastSearchQuery = query; if (!query) { hideSearchResults(); lastSearchResults = []; abortLazyLoad(); return; } // 立即显示搜索中指示器 setSearchLoading(true); console.log('开始搜索:', query); // 重置搜索结果状态,确保第二次搜索能正常显示 lastSearchResults = []; lastSearchQuery = query; // 搜索开始时只淡出,不折叠,待结果到达后再展开 resultsContainer.classList.remove('show'); resultsContainer.style.visibility = 'hidden'; resultsContainer.style.opacity = '0'; abortLazyLoad(); lazyLoadController = new AbortController(); try { // 优先搜索豆瓣(通常更快),然后搜索TMDB const doubanPromise = searchDouban(query).catch(e => { console.warn('豆瓣搜索失败:', e); return []; }); const tmdbPromise = searchTMDB(query).catch(e => { console.warn('TMDB搜索失败:', e); return []; }); // 等待所有搜索结果完成,避免显示旧内容 const [doubanResult, tmdbResult] = await Promise.all([ doubanPromise, tmdbPromise ]); setSearchLoading(false); // 合并、排序、去重,并扩大上限到60条 const uniqueResults = rankAndDedupResults([...(doubanResult||[]), ...(tmdbResult||[])], query).slice(0, 60); // 确保显示最新搜索结果 displaySearchResults(uniqueResults, resultsContainer); } catch (e) { console.error('搜索出错:', e); setSearchLoading(false); resultsContainer.innerHTML = '<div style="padding:12px; color:#ef4444; text-align:center; background:#fef2f2; border-radius:6px; margin:4px; border:1px solid #fecaca;">搜索出错,请检查网络连接后重试</div>'; // 使用统一的动画函数 animateShow(resultsContainer); } }, 500)); function abortLazyLoad() { if (lazyLoadController) { lazyLoadController.abort(); } } // 不再因输入框失焦自动隐藏结果,交由外部点击空白处或主动选择项来隐藏 searchInput.addEventListener('focus', function () { const query = this.value.trim(); if (query && lastSearchResults.length > 0) { // 如果有搜索结果,使用统一的显示函数 showSearchResults(); } else if (query && (lastSearchResults.length === 0 || lastSearchQuery !== query)) { // 如果有查询但没有结果或查询不同,触发搜索 const inputEvent = new Event('input', { bubbles: true }); this.dispatchEvent(inputEvent); } }); resultsContainer.addEventListener('click', async function (event) { // 确保容器有内容且可见时才响应点击 if (resultsContainer.innerHTML.trim() === '' || resultsContainer.style.display === 'none' || resultsContainer.style.visibility === 'hidden') { return; } const targetItem = event.target.closest('.search-item'); if (targetItem) { const url = targetItem.getAttribute('data-url'); const type = targetItem.getAttribute('data-type'); const title = targetItem.querySelector('strong').textContent; if (url) { // 添加加载状态 targetItem.classList.add('loading'); mediaUrlInput.value = url; hideSearchResults(); searchInput.blur(); const fetchBtn = document.getElementById('fetch-btn'); if (fetchBtn) fetchBtn.classList.remove('active'); // 使用active类而不是display属性 showStatus(`正在加载【${type}】${title}的信息...`, false); try { currentMovieInfo = await getBasicInfo(url); currentComments = await getHotComments(url); showStatus('信息加载完成,请选择海报和剧照', false); await showImageSelection(currentMovieInfo); } catch (err) { showStatus(`加载失败:${err.message}`, true); if (mediaUrlInput.value.trim() && fetchBtn) { fetchBtn.classList.add('active'); // 使用active类而不是display属性 } } } } }); // 点击面板外部区域才隐藏;点击面板内部的其它控件不自动隐藏 document.addEventListener('click', function (event) { const panel = document.getElementById('douban-tmdb-panel'); if (panel && panel.contains(event.target)) return; // 面板内部不隐藏 if (!searchInput.contains(event.target) && !resultsContainer.contains(event.target)) { hideSearchResults(); } }); // 智能保持搜索结果框显示的函数 function forceKeepSearchResultsVisible() { const query = searchInput.value.trim(); // 只有在有查询内容、有搜索结果、且搜索框有焦点时才显示 if (query && lastSearchResults.length > 0 && (document.activeElement === searchInput || resultsContainer.style.display === 'block')) { showSearchResults(); // 搜索结果已内嵌到控制面板中,无需位置调整 } } // 专门处理窗口缩放时的显示逻辑 - 只在搜索框有焦点时保持显示 function handleResizeDisplay() { const query = searchInput.value.trim(); const resultsContainer = document.getElementById('search-results'); const loadingIndicator = document.getElementById('search-loading'); // 强制保持搜索中指示器显示 if (loadingIndicator && (loadingIndicator.style.display === 'block' || loadingIndicator.style.display === '')) { resetSearchLoadingPosition(); } // 只在搜索框有焦点时保持搜索结果框显示 if (query && lastSearchResults.length > 0 && resultsContainer && document.activeElement === searchInput) { // 使用强制样式设置,确保不会消失 resultsContainer.style.setProperty('display', 'block', 'important'); resultsContainer.style.setProperty('visibility', 'visible', 'important'); resultsContainer.style.setProperty('opacity', '1', 'important'); resultsContainer.style.setProperty('z-index', '800', 'important'); resultsContainer.classList.add('show'); // 搜索结果已内嵌到控制面板中,无需位置调整 } } // 强制保持搜索结果框显示的函数 - 修复版本 function forceKeepSearchResultsVisible() { const query = searchInput.value.trim(); const resultsContainer = document.getElementById('search-results'); // 只有在有查询内容、有搜索结果、且搜索框有焦点时才保持显示 if (query && lastSearchResults.length > 0 && resultsContainer && (document.activeElement === searchInput || resultsContainer.style.display === 'block')) { // 强制保持显示状态 if (resultsContainer.style.display !== 'block') { resultsContainer.style.setProperty('display', 'block', 'important'); } if (resultsContainer.style.visibility !== 'visible') { resultsContainer.style.setProperty('visibility', 'visible', 'important'); } if (resultsContainer.style.opacity !== '1') { resultsContainer.style.setProperty('opacity', '1', 'important'); } } else if (!query || lastSearchResults.length === 0) { // 如果没有查询内容或没有搜索结果,确保隐藏 if (resultsContainer && resultsContainer.style.display !== 'none') { resultsContainer.style.setProperty('display', 'none', 'important'); resultsContainer.style.setProperty('visibility', 'hidden', 'important'); resultsContainer.style.setProperty('opacity', '0', 'important'); } } } // 移除定期检查,避免阻止正常的失焦隐藏 // setInterval(forceKeepSearchResultsVisible, 1000); // 智能检查机制:只在窗口大小变化时触发,避免阻止失焦隐藏 let lastWindowSize = { width: window.innerWidth, height: window.innerHeight }; let resizeCheckInterval = setInterval(() => { const currentSize = { width: window.innerWidth, height: window.innerHeight }; // 只有在窗口大小真正发生变化时才检查显示 if (currentSize.width !== lastWindowSize.width || currentSize.height !== lastWindowSize.height) { lastWindowSize = currentSize; handleResizeDisplay(); } }, 500); // 每500ms检查一次窗口大小变化,减少性能影响 // 超级窄窗口过渡效果增强 let resizeTimeout; let isUltraNarrow = false; function handleUltraNarrowTransition() { const panel = document.getElementById('douban-tmdb-panel'); if (!panel) return; const windowWidth = window.innerWidth; const wasUltraNarrow = isUltraNarrow; isUltraNarrow = windowWidth <= 10; if (isUltraNarrow && !wasUltraNarrow) { // 进入超级窄模式 panel.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)'; panel.style.transform = 'scale(0.1)'; panel.style.transformOrigin = 'top left'; panel.style.overflow = 'hidden'; // 添加特殊类名用于CSS选择器 panel.classList.add('ultra-narrow'); // 显示极简指示器 const indicator = document.createElement('div'); indicator.id = 'ultra-narrow-indicator'; indicator.style.cssText = ` position: fixed; top: 0; left: 0; width: 10px; height: 10px; background: linear-gradient(135deg, #ec4899 0%, #be185d 100%); color: white; font-size: 8px; display: flex; align-items: center; justify-content: center; border-radius: 2px; z-index: 1001; /* 搜索中指示器 - 最高层级 */ transition: all 0.3s ease; `; indicator.textContent = '🎬'; document.body.appendChild(indicator); } else if (!isUltraNarrow && wasUltraNarrow) { // 退出超级窄模式 panel.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)'; panel.style.transform = 'scale(1)'; panel.style.transformOrigin = 'top left'; panel.style.overflow = 'visible'; // 移除特殊类名 panel.classList.remove('ultra-narrow'); // 移除极简指示器 const indicator = document.getElementById('ultra-narrow-indicator'); if (indicator) { indicator.remove(); } } } // 监听窗口大小变化,同时处理搜索结果框和超级窄窗口过渡 window.addEventListener('resize', function() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { // 处理搜索结果框位置 - 只在有焦点时保持显示 const searchInput = document.getElementById('search-movie'); const resultsContainer = document.getElementById('search-results'); if (searchInput && resultsContainer && document.activeElement === searchInput) { const query = searchInput.value.trim(); if (query && lastSearchResults.length > 0) { showSearchResults(); } } // 处理窗口缩放时的显示 handleResizeDisplay(); // 处理超级窄窗口过渡 handleUltraNarrowTransition(); // 搜索中提示位置已在setSearchLoading中处理 }, 50); // 统一延迟时间 }); // 初始化检查 handleUltraNarrowTransition(); // 初始化时搜索中提示位置将在需要时自动校准 } // 影视信息提取(增强版) function getBasicInfo(url) { return new Promise((resolve, reject) => { // Douban API 回退:使用移动端 rexxar 接口规避“请证明你是人类” async function fetchDoubanDetailViaApi(id, isTv) { try { const apiUrl = `https://m.douban.com/rexxar/api/v2/${isTv ? 'tv' : 'movie'}/${id}?for_mobile=1`; const json = await new Promise((resv, rej)=>{ GM_xmlhttpRequest({ method: 'GET', url: apiUrl, headers: { ...COMMON_HEADERS, 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Referer': `https://m.douban.com/${isTv ? 'tv' : 'movie'}/subject/${id}/` }, timeout: 8000, onload: (r)=>{ try { resv(JSON.parse(r.responseText)); } catch(e){ rej(e);} }, onerror: ()=>rej(new Error('豆瓣API请求失败')), ontimeout: ()=>rej(new Error('豆瓣API请求超时')) }); }); // 字段映射 const title = json.title || ''; const originalTitle = json.original_title || ''; const genreTags = (json.genres || []).slice(0, 8); const year = (json.year || '').toString() || '未知'; const alsoKnown = (json.aka || []).join(' / '); const director = (json.directors || []).map(d=>d.name).join(' / ') || '未知'; const writer = (json.writers || []).map(w=>w.name).join(' / ') || '未知'; const actor = (json.actors || []).map(a=>a.name).join(',') || '未知'; const region = (json.countries || []).join('、') || '未知'; const lang = (json.languages || []).join('、') || '未知'; const release = (json.pubdate || json.release_date || '') || (isTv ? '未知首播时间' : '未知上映时间'); const rating = (json.rating && (json.rating.value || json.rating.average)) ? (json.rating.value || json.rating.average).toFixed ? (json.rating.value || json.rating.average).toFixed(1) : (json.rating.value || json.rating.average) : '暂无'; const voteCount = (json.rating && (json.rating.count || json.rating.votes)) || '0'; const doubanId = id; const imdbId = (json.imdb || (json.extra && json.extra.imdb)) || '暂无'; const runtime = (json.durations && json.durations[0]) || (json.duration ? `${json.duration}分钟` : (isTv ? '未知集数' : '未知片长')); const intro = json.intro || json.card_subtitle || ''; return { mediaType: isTv ? 'tv' : 'movie', source: '豆瓣', title, originalTitle, genreTags, year, alsoKnown, director, writer, actor, region, release, lang, rating, voteCount, doubanId, imdbId, runtime, intro, awards: [], posterUrls: [], stillUrls: [], url }; } catch (e) { throw e; } } // 重试函数,带指数退避 const retryRequest = async (requestFn, maxRetries = 2, delay = 1000) => { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt - 1))); } return await requestFn(); } catch (error) { lastError = error; console.warn(`请求失败,尝试 ${attempt + 1}/${maxRetries + 1}:`, error); } } throw lastError || new Error('所有重试均失败'); }; const urlObj = new URL(url); if (url.includes('douban.com')) { let isTv = url.includes('tv.douban.com'); const headers = { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/', 'Host': urlObj.hostname, 'Sec-Fetch-Dest': 'document' }; retryRequest(() => { return new Promise((innerResolve, innerReject) => { doubanRequest({ method: 'GET', url: url, headers: headers, timeout: 10000 }).then(async (res) => { try { // 若被重定向到登录(不可用)/检测页,或页面出现“请证明你是人类/嗯...”,则回退API if ((res.finalUrl && /\/accounts\.|\/j\/app\/user\/check/.test(res.finalUrl)) || /请证明你是人类|你的访问豆瓣的方式有点像机器人|嗯\s*\.\.\./.test(res.responseText)) { const fallback = await (async ()=>{ try { const list = await searchDoubanSuggest((currentMovieInfo && currentMovieInfo.title) || '') .catch(()=>[]); return Array.isArray(list) && list.length ? list[0] : null; } catch(e){ return null; } })(); if (fallback && (fallback.id || fallback.url)) { const id = fallback.id || (fallback.url.match(/subject\/(\d+)/)||[])[1]; if (id) { try { const viaApi = await fetchDoubanDetailViaApi(id, isTv); innerResolve(viaApi); return; } catch(_){} } innerResolve({ mediaType: isTv ? 'tv' : 'movie', source: '豆瓣', title: fallback.title, originalTitle: '', genreTags: [], year: fallback.year || '未知', alsoKnown: '', director: '未知', writer: '未知', actor: '未知', region: '未知', release: '未知', lang: '', rating: '暂无', voteCount: '0', doubanId: id || '', imdbId: '暂无', runtime: isTv ? '未知集数' : '未知片长', intro: '', awards: [], posterUrls: [], stillUrls: [], url }); } else { throw new Error('豆瓣需要登录(不可用),且回退失败'); } return; } const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const titleElem = doc.querySelector('h1 span[property="v:itemreviewed"], h1 span[itemprop="name"]'); const title = titleElem ? titleElem.textContent.trim() : (isTv ? '未知电视剧' : '未知电影'); const originalTitle = doc.querySelector('h1 .year')?.previousSibling?.textContent?.trim()?.replace(/\s+/g, ' ') || ''; const genreTags = Array.from(doc.querySelectorAll('span[property="v:genre"]')).map(g => g.textContent.trim()).filter(Boolean); let year = '未知'; const yearElem = doc.querySelector('span[property="v:initialReleaseDate"]'); if (yearElem) { const yearMatch = yearElem.textContent.trim().match(/\d{4}/); year = yearMatch ? yearMatch[0] : '未知'; } // 提取更多信息的函数 const extractInfo = (label) => { const infoText = Array.from(doc.querySelectorAll('#info')).find(info => info.textContent.includes(label) )?.textContent || ''; const match = infoText.match(new RegExp(`${label}[::]\s*(.+?)\n`)); return match ? match[1].replace(/\s+/g, ' ').trim() : '未知'; }; const alsoKnown = doc.querySelector('span[property="v:alternative"]')?.textContent.trim() || ''; const director = Array.from(doc.querySelectorAll('a[rel="v:directedBy"]')).map(d => d.textContent.trim()).join(' / ') || '未知'; const writer = extractInfo('编剧'); const actor = Array.from(doc.querySelectorAll('a[rel="v:starring"]')).map(a => a.textContent.trim()).join(',') || '未知'; const region = extractInfo('制片国家/地区'); const lang = extractInfo('语言'); const release = yearElem?.textContent.trim() || (isTv ? '未知首播时间' : '未知上映时间'); const rating = doc.querySelector('strong[property="v:average"]')?.textContent || '暂无'; const voteCount = doc.querySelector('span[property="v:votes"]')?.textContent || '0'; const doubanId = url.match(/subject\/(\d+)/)?.[1] || '未知'; const imdbId = doc.querySelector('a[href*="imdb.com/title/"]')?.href?.match(/tt\d+/)?.[0] || '暂无'; const runtime = isTv ? doc.querySelector('span[property="v:episodeCount"]') ? `共${doc.querySelector('span[property="v:episodeCount"]').textContent}集` : '未知集数' : doc.querySelector('span[property="v:runtime"]')?.textContent.trim() || '未知片长'; // 提取更完整的简介 let intro = ''; const summaryElem = doc.querySelector('span[property="v:summary"]'); if (summaryElem) { intro = summaryElem.textContent.trim().replace(/\s+/g, ' '); } if (!intro) { // 尝试其他可能的简介位置 const otherIntroElem = doc.querySelector('.all.hidden'); if (otherIntroElem) { intro = otherIntroElem.textContent.trim().replace(/\s+/g, ' '); } } if (!intro) { intro = isTv ? '暂无电视剧简介' : '暂无电影简介'; } // 尝试提取奖项信息 let awards = []; const awardsElem = doc.querySelector('#celebrities'); if (awardsElem && awardsElem.textContent.includes('获奖')) { const awardTexts = Array.from(awardsElem.querySelectorAll('.award')).map(a => a.textContent.trim()); if (awardTexts.length > 0) { awards = awardTexts.slice(0, 3); // 最多取3个主要奖项 } } innerResolve({ mediaType: isTv ? 'tv' : 'movie', source: '豆瓣', title, originalTitle: originalTitle || '', genreTags, year, alsoKnown, director, writer, actor, region, release, lang, rating, voteCount, doubanId, imdbId, runtime, intro, awards, // 延迟加载:图片在选择面板内部异步加载,提升首屏弹出速度 posterUrls: [], stillUrls: [], url // 保存原始URL用于后续加载更多 }); } catch (e) { innerReject(new Error(`豆瓣解析失败:${e.message}`)); } }).catch(() => innerReject(new Error('豆瓣请求失败'))); }); }).then(result => resolve(result)).catch(error => reject(error)); } else if (url.includes('themoviedb.org')) { const isMovie = url.includes('/movie/'); const isTv = url.includes('/tv/'); let mediaType = isMovie ? 'movie' : (isTv ? 'tv' : 'movie'); const idMatch = url.match(/\/(movie|tv)\/(\d+)/); if (!idMatch) { reject(new Error('TMDB链接格式错误(需包含/movie/或/tv/及数字ID)')); return; } const [, type, id] = idMatch; mediaType = type; // 请求更多信息,包含额外数据 const config = getConfig(); const tmdbDetailUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}?api_key=${config.TMDB.API_KEY}&language=zh-CN&append_to_response=credits,keywords,release_dates,external_ids`; retryRequest(() => { return new Promise((innerResolve, innerReject) => { GM_xmlhttpRequest({ method: 'GET', url: tmdbDetailUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: async (res) => { try { const data = JSON.parse(res.responseText); const title = mediaType === 'movie' ? data.title : data.name; const originalTitle = mediaType === 'movie' ? data.original_title : data.original_name; const genreTags = data.genres.map(g => g.name); const year = mediaType === 'movie' ? data.release_date?.split('-')[0] : data.first_air_date?.split('-')[0]; const alsoKnown = data.also_known_as?.join(' / ') || ''; const director = mediaType === 'movie' ? (data.credits?.crew?.filter(c => c.job === 'Director').map(d => d.name).join(' / ') || '未知') : (data.credits?.crew?.filter(c => c.job === 'Director' || c.job === 'Executive Producer').map(d => d.name).join(' / ') || '未知'); const writer = data.credits?.crew?.filter(c => c.job === 'Writer' || c.job === 'Screenplay').map(w => w.name).join(' / ') || '未知'; const actor = (data.credits?.cast || []).map(a => a.name).join(',') || '未知'; const region = (data.production_countries || []).map(c => c.name).join('、') || '未知'; const release = mediaType === 'movie' ? data.release_date : data.first_air_date; const lang = (data.spoken_languages || []).map(l => l.name).join('、') || '未知'; const rating = data.vote_average ? data.vote_average.toFixed(1) : '暂无'; const voteCount = data.vote_count || 0; const tmdbId = id; const imdbId = data.imdb_id || '暂无'; const runtime = mediaType === 'movie' ? `${data.runtime}分钟` : `${data.number_of_episodes || '未知'}集(共${data.number_of_seasons || '未知'}季)`; const intro = data.overview || (mediaType === 'tv' ? '暂无电视剧简介' : '暂无电影简介'); // 获取关键字 const keywords = data.keywords?.keywords?.map(k => k.name).join('、') || ''; // 获取预算和票房(电影) let budget = '未知'; let revenue = '未知'; if (mediaType === 'movie') { budget = data.budget > 0 ? `$${(data.budget / 1000000).toFixed(1)}M` : '未知'; revenue = data.revenue > 0 ? `$${(data.revenue / 1000000).toFixed(1)}M` : '未知'; } // 获取流媒体平台信息 let streamingPlatforms = []; const watchProvidersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/watch/providers?api_key=${config.TMDB.API_KEY}`; try { const providersData = await new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: watchProvidersUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => resolve(JSON.parse(res.responseText)), onerror: () => resolve({}), ontimeout: () => resolve({}) }); }); // 获取中国或美国的流媒体平台 const watchProviders = providersData.results?.CN || providersData.results?.US || {}; streamingPlatforms = [ ...(watchProviders.flatrate || []).map(p => p.provider_name), ...(watchProviders.buy || []).map(p => p.provider_name) ].slice(0, 5); // 最多取5个平台 } catch (e) { console.log('获取流媒体平台失败:', e); } // 初始加载海报和剧照 posterPage = 1; let posterUrls = []; if (data.poster_path) { const postersUrl = `${config.TMDB.BASE_URL}/${mediaType}/${id}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`; try { const posterData = await new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: postersUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => resolve(JSON.parse(res.responseText)), onerror: () => resolve({}), ontimeout: () => resolve({}) }); }); // 按第1页截取(5张) const pagePosters = safeGet(posterData, 'posters', []) .slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT) .map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${img.file_path}`) .filter(Boolean); posterUrls = pagePosters; } catch (e) { console.log('获取更多海报失败:', e); } } // 【统一逻辑】初始加载第1页剧照(5张) stillPage = 1; const stillUrls = await retryRequest(() => getTMDBStillsList(mediaType, id)); innerResolve({ mediaType, source: 'TMDB', title, originalTitle, genreTags, year, alsoKnown, director, writer, actor, region, release, lang, rating, voteCount, tmdbId, imdbId, runtime, intro, keywords, budget, revenue, streamingPlatforms, posterUrls: posterUrls.length > 0 ? posterUrls : [], stillUrls: stillUrls.length > 0 ? stillUrls : [], url // 保存原始URL用于后续加载更多 }); } catch (e) { innerReject(new Error(`TMDB解析失败:${e.message}`)); } }, onerror: () => innerReject(new Error('TMDB请求失败')), ontimeout: () => innerReject(new Error('TMDB请求超时')) }); }); }).then(result => resolve(result)).catch(error => reject(error)); } else { reject(new Error('不支持的链接类型(仅支持豆瓣、TMDB)')); } }); } function getHotComments(url) { return new Promise(async resolve => { // TMDB来源:改为走豆瓣短评页 if (url.includes('themoviedb.org')) { try { // 从currentMovieInfo获取豆瓣subject链接;若无,则以片名搜索豆瓣匹配 const info = currentMovieInfo || {}; let doubanId = info.doubanId; if (!doubanId) { try { const title = info.title || info.originalTitle || ''; if (title) { const list = await searchDouban(title).catch(() => []); if (Array.isArray(list) && list.length) { // 按年份或相似度粗匹配 const year = (info.release || '').slice(0,4); const picked = list.find(r => (r.year && year && String(r.year) === String(year))) || list[0]; const idMatch = picked && picked.url ? picked.url.match(/subject\/(\d+)/) : null; if (idMatch) doubanId = idMatch[1]; } } } catch (e) {} } const doubanLink = doubanId ? `https://movie.douban.com/subject/${doubanId}/comments?sort=new_score&status=P` : ''; if (!doubanLink) { resolve([]); return; } doubanRequest({ method: 'GET', url: doubanLink, headers: { ...COMMON_HEADERS, 'Referer': `https://movie.douban.com/subject/${doubanId}/`, 'Host': 'movie.douban.com' }, timeout: 8000 }).then((res)=>{ try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const items = Array.from(doc.querySelectorAll('.comment-item')).slice(0, 5); const comments = items.map(node => { const short = node.querySelector('span.short'); const infoA = node.querySelector('.comment-info a'); const content = short ? short.textContent.trim() : ''; const author = infoA ? infoA.textContent.trim() : ''; return content ? { content, author } : null; }).filter(Boolean).slice(0, 3); resolve(comments.length ? comments : []); } catch (e) { resolve([]); } }).catch(()=>resolve([])); } catch (e) { resolve([]); } return; } // 优先“热门短评”页,其次默认短评页 const base = url.replace(/\/$/, ''); const commentUrl = `${base}/comments?sort=new_score&status=P`; doubanRequest({ method: 'GET', url: commentUrl, headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': new URL(url).hostname }, timeout: 8000 }).then((res)=>{ try { const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); // 兼容新旧结构:.comment-item > .comment/.comment-info + span.short const items = Array.from(doc.querySelectorAll('.comment-item')).slice(0, 5); const comments = items.map(node => { const short = node.querySelector('span.short'); const info = node.querySelector('.comment-info a'); const content = short ? short.textContent.trim() : ''; const author = info ? info.textContent.trim() : ''; return content ? { content, author } : null; }).filter(Boolean).slice(0, 3); resolve(comments.length ? comments : []); } catch (e) { resolve([]); } }).catch(()=>resolve([])); }); } // 加载更多海报功能(修复点击无反应+统一分页逻辑) // 存储已加载的海报和剧照的唯一标识符,用于去重 const loadedPosterIds = new Set(); const loadedStillIds = new Set(); // 获取图片的唯一标识符(基于URL的哈希值) function getImageUniqueId(url) { try { // 从URL中提取有辨识度的部分用于生成唯一ID let id = url; // 移除查询参数 const urlObj = new URL(url); id = urlObj.origin + urlObj.pathname; // 移除可能变化的尺寸部分(针对TMDB和豆瓣) id = id.replace(/w\d+/g, '').replace(/h\d+/g, '').replace(/\/m\//g, '/').replace(/\/l\//g, '/').replace(/\/s\//g, '/'); // 使用简单的哈希算法生成短ID let hash = 0; for (let i = 0; i < id.length; i++) { const char = id.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // 转换为32位整数 } return Math.abs(hash).toString(36); } catch (e) { console.warn('生成图片唯一ID失败:', e); // 回退方案:返回URL的基本部分 return url.split('?')[0].split('#')[0]; } } async function loadMorePosters() { if (isLoadingPosters) { showStatus('正在加载海报,请稍候...', false); return; } if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } isLoadingPosters = true; const loadMoreBtn = document.getElementById('load-more-posters'); if (loadMoreBtn) { loadMoreBtn.textContent = '加载中...'; loadMoreBtn.disabled = true; } try { const prevPage = posterPage; posterPage++; // 页码递增(加载下一页) let morePosters = []; if (currentMovieInfo.source === '豆瓣') { morePosters = await getDoubanOfficialPosters(currentMovieInfo.url, posterPage); } else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) { const config = getConfig(); const postersUrl = `${config.TMDB.BASE_URL}/${currentMovieInfo.mediaType}/${currentMovieInfo.tmdbId}/images?api_key=${config.TMDB.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`; await new Promise(resolvePosters => { GM_xmlhttpRequest({ method: 'GET', url: postersUrl, headers: { 'Authorization': `Bearer ${config.TMDB.ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, onload: (res) => { try { const posterData = JSON.parse(res.responseText); // 按当前页截取(5张),避免重复 morePosters = safeGet(posterData, 'posters', []) .slice((posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT, posterPage * config.TMDB.IMAGE_CANDIDATES_COUNT) .map(img => `${config.TMDB.IMAGE_BASE_URL}${config.TMDB.LIST_POSTER_SIZE}/${img.file_path}`) .filter(Boolean); } catch (e) { console.log('获取更多海报失败:', e); } resolvePosters(); }, onerror: () => resolvePosters(), ontimeout: () => resolvePosters() }); }); } // 验证是否加载到新数据(避免重复加载空数据) if (morePosters.length > 0) { posterContainer.style.display = 'grid'; // 过滤掉已加载的海报 const uniquePosters = morePosters.filter(posterUrl => { const posterId = getImageUniqueId(posterUrl); if (loadedPosterIds.has(posterId)) { console.log('跳过重复海报:', posterUrl); return false; } return true; }); if (uniquePosters.length === 0) { // 没有新的唯一海报 posterPage = prevPage; showStatus('没有更多新的海报了', false); if (loadMoreBtn) { loadMoreBtn.textContent = '没有更多海报了'; loadMoreBtn.disabled = true; loadMoreBtn.style.opacity = '0.6'; } return; } let addedCount = 0; for (let i = 0; i < uniquePosters.length; i++) { try { const dataUrl = await getImageDataURLWithQuality(uniquePosters[i]); const posterImg = document.createElement('div'); // 统一海报样式:适应grid布局,避免超出,添加hover效果 posterImg.style.cssText = ` width: 100%; height: 200px; object-fit: contain; border: 1px solid #f3d5d9; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; `; // 添加hover效果的样式 posterImg.onmouseenter = function() { this.style.transform = 'scale(1.02)'; this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; }; posterImg.onmouseleave = function() { this.style.transform = 'scale(1)'; this.style.boxShadow = 'none'; }; const config = getConfig(); posterImg.innerHTML = `<img src="${dataUrl}" style="max-width:100%; max-height:100%; object-fit: contain;" alt="海报 ${i + 1 + (posterPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT}">`; posterImg.dataset.url = dataUrl; // 标记此海报已加载 const posterId = getImageUniqueId(uniquePosters[i]); loadedPosterIds.add(posterId); posterImg.dataset.posterId = posterId; posterContainer.appendChild(posterImg); addedCount++; } catch (e) { console.log(`加载海报 ${i + 1} 失败:`, e); } } if (addedCount === 0) { // 虽然有唯一海报,但加载全部失败 posterPage = prevPage; showStatus('加载海报失败,请稍后重试', true); } else { // 滚动到底部,显示新加载的海报 if (posterContainer) { posterContainer.scrollTop = posterContainer.scrollHeight; } showStatus(`已加载第${posterPage}页海报(新增${addedCount}张)`, false); if (loadMoreBtn) { loadMoreBtn.textContent = '加载更多海报'; loadMoreBtn.disabled = false; } } } else { // 无新数据,恢复页码并禁用按钮 posterPage = prevPage; posterContainer.style.display = 'grid'; if (loadMoreBtn) { loadMoreBtn.textContent = '没有更多海报了'; loadMoreBtn.disabled = true; loadMoreBtn.style.opacity = '0.6'; } showStatus('已加载全部海报', false); } } catch (e) { // 加载失败,恢复页码 posterPage--; console.error('加载更多海报出错:', e); showStatus('加载更多海报失败,请稍后重试', true); if (loadMoreBtn) { loadMoreBtn.textContent = '加载失败,重试'; loadMoreBtn.disabled = false; } } finally { isLoadingPosters = false; } } // 加载更多剧照功能(修复点击无反应+统一分页逻辑+样式适配) async function loadMoreStills() { if (isLoadingStills) { showStatus('正在加载剧照,请稍候...', false); return; } if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } isLoadingStills = true; const loadMoreBtn = document.getElementById('load-more-stills'); if (loadMoreBtn) { loadMoreBtn.textContent = '加载中...'; loadMoreBtn.disabled = true; } try { const prevPage = stillPage; stillPage++; // 页码递增(加载下一页) let moreStills = []; if (currentMovieInfo.source === '豆瓣') { moreStills = await getDoubanStillsList(currentMovieInfo.url, stillPage); } else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) { moreStills = await getTMDBStillsList(currentMovieInfo.mediaType, currentMovieInfo.tmdbId, stillPage); } // 验证是否加载到新数据(避免重复加载空数据) if (moreStills.length > 0) { stillContainer.style.display = 'grid'; // 过滤掉已加载的剧照 const uniqueStills = moreStills.filter(stillUrl => { const stillId = getImageUniqueId(stillUrl); if (loadedStillIds.has(stillId)) { console.log('跳过重复剧照:', stillUrl); return false; } return true; }); if (uniqueStills.length === 0) { // 没有新的唯一剧照 stillPage = prevPage; showStatus('没有更多新的剧照了', false); if (loadMoreBtn) { loadMoreBtn.textContent = '没有更多剧照了'; loadMoreBtn.disabled = true; loadMoreBtn.style.opacity = '0.6'; } return; } let addedCount = 0; for (let i = 0; i < uniqueStills.length; i++) { try { const dataUrl = await getImageDataURLWithQuality(uniqueStills[i]); const stillImg = document.createElement('div'); // 【修复剧照超出】统一剧照样式:适应grid布局,宽高比例协调,添加hover效果 stillImg.style.cssText = ` width: 100%; height: 120px; object-fit: contain; border: 1px solid #f3d5d9; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; `; // 添加hover效果的样式 stillImg.onmouseenter = function() { this.style.transform = 'scale(1.02)'; this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; }; stillImg.onmouseleave = function() { this.style.transform = 'scale(1)'; this.style.boxShadow = 'none'; }; const config = getConfig(); stillImg.innerHTML = `<img src="${dataUrl}" style="max-width:100%; max-height:100%; object-fit: contain;" alt="剧照 ${i + 1 + (stillPage - 1) * config.TMDB.IMAGE_CANDIDATES_COUNT}">`; stillImg.dataset.url = dataUrl; // 标记此剧照已加载 const stillId = getImageUniqueId(uniqueStills[i]); loadedStillIds.add(stillId); stillImg.dataset.stillId = stillId; stillContainer.appendChild(stillImg); addedCount++; } catch (e) { console.log(`加载剧照 ${i + 1} 失败:`, e); } } if (addedCount === 0) { // 虽然有唯一剧照,但加载全部失败 stillPage = prevPage; showStatus('加载剧照失败,请稍后重试', true); } else { // 滚动到底部,显示新加载的剧照 if (stillContainer) { stillContainer.scrollTop = stillContainer.scrollHeight; } showStatus(`已加载第${stillPage}页剧照(新增${addedCount}张)`, false); if (loadMoreBtn) { loadMoreBtn.textContent = '加载更多剧照'; loadMoreBtn.disabled = false; } } } else { // 无新数据,恢复页码并禁用按钮 stillPage = prevPage; stillContainer.style.display = 'grid'; if (loadMoreBtn) { loadMoreBtn.textContent = '没有更多剧照了'; loadMoreBtn.disabled = true; loadMoreBtn.style.opacity = '0.6'; } showStatus('已加载全部剧照', false); } } catch (e) { // 加载失败,恢复页码 stillPage--; console.error('加载更多剧照出错:', e); showStatus('加载更多剧照失败,请稍后重试', true); if (loadMoreBtn) { loadMoreBtn.textContent = '加载失败,重试'; loadMoreBtn.disabled = false; } } finally { isLoadingStills = false; } } async function showImageSelection(movieInfo) { return new Promise(async (resolve) => { console.log('showImageSelection 被调用,movieInfo:', movieInfo); console.log('posterUrls:', movieInfo.posterUrls); console.log('stillUrls:', movieInfo.stillUrls); if (!posterContainer || !stillContainer) { posterContainer = document.getElementById('poster-candidates'); stillContainer = document.getElementById('still-candidates'); } setupImageSelectionDelegates(); const imageSelection = document.getElementById('image-selection'); const loadMorePostersBtn = document.getElementById('load-more-posters'); const loadMoreStillsBtn = document.getElementById('load-more-stills'); if (!posterContainer || !stillContainer || !imageSelection) { resolve(); return; } // 重置去重集合 loadedPosterIds.clear(); loadedStillIds.clear(); // 初始加载时显示"加载中" posterContainer.style.display = 'grid'; posterContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">加载海报中...</div>'; // 确保剧照容器首屏可见(修复首次不显示) stillContainer.style.display = 'grid'; stillContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">加载剧照中...</div>'; imageSelection.style.display = 'block'; loadMorePostersBtn.style.display = 'none'; loadMoreStillsBtn.style.display = 'none'; // 重建观察器,避免旧实例导致乱加载 disconnectCandidateObservers(); initCandidateObservers(); // 处理海报(统一样式+初始第1页) if (movieInfo.posterUrls && movieInfo.posterUrls.length > 0) { stillContainer.style.display = 'grid'; posterContainer.innerHTML = ''; // 首张不等待转码,先用直链,极大缩短首屏时间 selectedPosterUrl = normalizeImageUrl(movieInfo.posterUrls[0]); // 批量创建DOM,减少重排 const posterFrag = document.createDocumentFragment(); for (let i = 0; i < movieInfo.posterUrls.length; i++) { try { // 异步并发处理:先创建卡片,图片加载完成后替换src,避免阻塞渲染 const rawUrl = normalizeImageUrl(movieInfo.posterUrls[i]); const posterImg = document.createElement('div'); // 统一海报样式:适应grid布局,添加hover效果 posterImg.style.cssText = ` width: 100%; height: 200px; object-fit: contain; border: ${i === 0 ? '3px solid #ec4899' : '1px solid #f3d5d9'}; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; `; // 添加hover效果的样式 posterImg.onmouseenter = function() { this.style.transform = 'scale(1.02)'; this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; }; posterImg.onmouseleave = function() { this.style.transform = 'scale(1)'; this.style.boxShadow = 'none'; }; // 创建图片元素(先用缩略图展示,提升首屏速度) const img = document.createElement('img'); img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;'; img.alt = `海报 ${i + 1}`; try { getThumbnailForDisplay(rawUrl).then((thumb)=>{ const im = posterImg.querySelector('img'); if (im) im.src = thumb; }); } catch(e) { img.src = LAZY_PLACEHOLDER; } posterImg.appendChild(img); posterImg.dataset.rawUrl = rawUrl; posterImg.dataset.url = rawUrl; // 设置点击选中行为(同步变量与高亮) posterImg.addEventListener('click', function() { try { selectedPosterUrl = this.dataset.url || rawUrl; selectedPosterEl = this; document.querySelectorAll('#poster-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; }); } catch (e) {} }); // 标记此海报已加载 const posterId = getImageUniqueId(movieInfo.posterUrls[i]); loadedPosterIds.add(posterId); posterImg.dataset.posterId = posterId; if (i === 0) { selectedPosterEl = posterImg; } posterFrag.appendChild(posterImg); // 懒加载观察 observeCandidateCard(posterImg, 'poster'); } catch (e) { console.log(`加载海报 ${i + 1} 失败:`, e); } } posterContainer.appendChild(posterFrag); loadMorePostersBtn.style.display = 'inline-block'; loadMorePostersBtn.disabled = false; } else { // 海报保底机制:如果有剧照,使用第一张剧照作为海报 if (movieInfo.source === '豆瓣' && movieInfo.url) { // 异步抓取首批海报,不阻塞面板弹出 posterContainer.innerHTML = '<div style="color:#9ca3af; grid-column: 1 / -1; text-align:center; padding:12px;">加载海报中…</div>'; (async () => { try { posterPage = 1; const urls = await getDoubanOfficialPosters(movieInfo.url, 1); if (Array.isArray(urls) && urls.length) { movieInfo.posterUrls = urls; posterContainer.innerHTML = ''; const frag = document.createDocumentFragment(); for (let i = 0; i < urls.length; i++) { const rawUrl = normalizeImageUrl(urls[i]); const card = document.createElement('div'); card.style.cssText = 'width: 100%; height: 200px; object-fit: contain; border: '+(i===0?'3px solid #ec4899':'1px solid #f3d5d9')+'; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative;'; const img = document.createElement('img'); img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;'; img.alt = `海报 ${i + 1}`; img.src = LAZY_PLACEHOLDER; card.appendChild(img); card.dataset.rawUrl = rawUrl; card.dataset.url = rawUrl; if (i === 0) { selectedPosterEl = card; selectedPosterUrl = rawUrl; } card.addEventListener('click', function(){ try { selectedPosterUrl = this.dataset.url || rawUrl; selectedPosterEl = this; document.querySelectorAll('#poster-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; }); } catch(e) {} }); frag.appendChild(card); observeCandidateCard(card, 'poster'); } posterContainer.appendChild(frag); loadMorePostersBtn.style.display = 'inline-block'; loadMorePostersBtn.disabled = false; primeFirstCandidates(); } else { posterContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">未找到海报</div>'; } } catch (e) { posterContainer.innerHTML = '<div style="color:#ef4444; grid-column: 1 / -1; text-align:center; padding:20px;">海报加载失败</div>'; } })(); } else if (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) { console.log('未找到海报,使用第一张剧照作为保底海报'); stillContainer.style.display = 'grid'; posterContainer.innerHTML = ''; const fallbackUrl = normalizeImageUrl(movieInfo.stillUrls[0]); selectedPosterUrl = fallbackUrl; // 创建保底海报显示 const fallbackPosterImg = document.createElement('div'); fallbackPosterImg.style.cssText = ` width: 100%; height: 200px; border: 2px solid #f59e0b; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fffbeb; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; `; // 添加保底标识 const badge = document.createElement('div'); badge.style.cssText = ` position: absolute; top: 5px; right: 5px; background: #f59e0b; color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; z-index: 1; font-weight: 500; `; badge.textContent = '首图'; const img = document.createElement('img'); img.src = fallbackUrl; img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;'; img.alt = '保底海报'; img.onerror = function() { this.src = 'https://picsum.photos/200/300?default-poster'; }; fallbackPosterImg.appendChild(img); fallbackPosterImg.appendChild(badge); fallbackPosterImg.dataset.url = fallbackUrl; // 添加点击选中效果 fallbackPosterImg.addEventListener('click', function() { selectedPosterUrl = fallbackUrl; document.querySelectorAll('#poster-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #f59e0b' : '1px solid #f3d5d9'; }); }); posterContainer.appendChild(fallbackPosterImg); loadMorePostersBtn.style.display = 'none'; } else { posterContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">未找到海报</div>'; selectedPosterUrl = 'https://picsum.photos/200/300?default-poster'; } } // 处理剧照(统一样式+初始第1页+修复超出) if (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) { stillContainer.innerHTML = ''; selectedStillUrl = normalizeImageUrl(movieInfo.stillUrls[0]); const stillFrag = document.createDocumentFragment(); for (let i = 0; i < movieInfo.stillUrls.length; i++) { try { const rawUrl = normalizeImageUrl(movieInfo.stillUrls[i]); const stillImg = document.createElement('div'); // 【修复剧照超出】统一剧照样式:适应grid布局,宽高比例协调,添加hover效果 stillImg.style.cssText = ` width: 100%; height: 120px; object-fit: contain; border: ${i === 0 ? '3px solid #ec4899' : '1px solid #f3d5d9'}; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; `; // 添加hover效果的样式 stillImg.onmouseenter = function() { this.style.transform = 'scale(1.02)'; this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; }; stillImg.onmouseleave = function() { this.style.transform = 'scale(1)'; this.style.boxShadow = 'none'; }; // 创建图片元素(先用缩略图展示,提升首屏速度) const img = document.createElement('img'); img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;'; img.alt = `剧照 ${i + 1}`; try { getThumbnailForDisplay(rawUrl).then((thumb)=>{ const im = stillImg.querySelector('img'); if (im) im.src = thumb; }); } catch(e) { img.src = LAZY_PLACEHOLDER; } stillImg.appendChild(img); stillImg.dataset.rawUrl = rawUrl; stillImg.dataset.url = rawUrl; // 设置点击选中行为(同步变量与高亮) stillImg.addEventListener('click', function() { try { selectedStillUrl = this.dataset.url || rawUrl; selectedStillEl = this; document.querySelectorAll('#still-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; }); } catch (e) {} }); // 标记此剧照已加载 const stillId = getImageUniqueId(movieInfo.stillUrls[i]); loadedStillIds.add(stillId); stillImg.dataset.stillId = stillId; if (i === 0) { selectedStillEl = stillImg; } stillFrag.appendChild(stillImg); // 懒加载观察 observeCandidateCard(stillImg, 'still'); // 保障首图立刻显示:若是首图则立即触发加载 if (i === 0) { try { const raw = stillImg.dataset.rawUrl || rawUrl; // 先展示缩略图,后台升级为原图dataURL getThumbnailForDisplay(raw).then((thumb)=>{ const im = stillImg.querySelector('img'); if (im) im.src = thumb; }); const upgrade = shouldConvertToDataURL(raw) ? getImageDataURLWithQuality(raw) : Promise.resolve(raw.replace(`/${getConfig().TMDB.LIST_STILL_SIZE}/`, `/${getConfig().TMDB.SELECTED_STILL_SIZE}/`)); upgrade.then((du)=>{ const im = stillImg.querySelector('img'); if (im) im.src = du; stillImg.dataset.url = du; stillImg.dataset.loaded = '1'; if (selectedStillUrl === raw) selectedStillUrl = du; }); } catch (e) {} } } catch (e) { console.log(`加载剧照 ${i + 1} 失败:`, e); } } stillContainer.appendChild(stillFrag); loadMoreStillsBtn.style.display = 'inline-block'; loadMoreStillsBtn.disabled = false; } else { if (movieInfo.source === '豆瓣' && movieInfo.url) { stillContainer.innerHTML = '<div style="color:#9ca3af; grid-column: 1 / -1; text-align:center; padding:12px;">加载剧照中…</div>'; (async () => { try { stillPage = 1; const urls = await getDoubanStillsList(movieInfo.url, 1); if (Array.isArray(urls) && urls.length) { movieInfo.stillUrls = urls; stillContainer.innerHTML = ''; const frag = document.createDocumentFragment(); for (let i = 0; i < urls.length; i++) { const rawUrl = normalizeImageUrl(urls[i]); const card = document.createElement('div'); card.style.cssText = 'width: 100%; height: 120px; object-fit: contain; border: '+(i===0?'3px solid #ec4899':'1px solid #f3d5d9')+'; border-radius: 8px; cursor: pointer; overflow: hidden; background: #fff5f7; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative;'; const img = document.createElement('img'); img.style.cssText = 'max-width:100%; max-height:100%; object-fit: contain;'; img.alt = `剧照 ${i + 1}`; img.src = LAZY_PLACEHOLDER; card.appendChild(img); card.dataset.rawUrl = rawUrl; card.dataset.url = rawUrl; if (i === 0) { selectedStillEl = card; selectedStillUrl = rawUrl; } card.addEventListener('click', function(){ try { selectedStillUrl = this.dataset.url || rawUrl; selectedStillEl = this; document.querySelectorAll('#still-candidates > div').forEach(el => { el.style.border = el === this ? '3px solid #ec4899' : '1px solid #f3d5d9'; }); } catch(e) {} }); frag.appendChild(card); observeCandidateCard(card, 'still'); // 保障首图立刻显示 if (i === 0) { try { if (shouldConvertToDataURL(rawUrl)) { getImageDataURLWithQuality(rawUrl).then((du)=>{ const im = card.querySelector('img'); if (im) im.src = du; card.dataset.url = du; card.dataset.loaded = '1'; if (selectedStillUrl === rawUrl) selectedStillUrl = du; }); } else { const im = card.querySelector('img'); if (im) im.src = rawUrl; card.dataset.url = rawUrl; card.dataset.loaded = '1'; } } catch (e) {} } } stillContainer.appendChild(frag); loadMoreStillsBtn.style.display = 'inline-block'; loadMoreStillsBtn.disabled = false; primeFirstCandidates(); } else { stillContainer.style.display = 'grid'; stillContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">未找到剧照</div>'; selectedStillUrl = 'https://picsum.photos/300/180?default-still'; loadMoreStillsBtn.style.display = 'none'; } } catch (e) { stillContainer.innerHTML = '<div style="color:#ef4444; grid-column: 1 / -1; text-align:center; padding:20px;">剧照加载失败</div>'; } })(); } else { stillContainer.style.display = 'grid'; stillContainer.innerHTML = '<div style="color:#6b7280; grid-column: 1 / -1; text-align:center; padding:20px;">未找到剧照</div>'; selectedStillUrl = 'https://picsum.photos/300/180?default-still'; loadMoreStillsBtn.style.display = 'none'; } } // 图片区域已完成:预热首图并延迟初始化模板工具栏,避免首屏阻塞 try { primeFirstCandidates(); setTimeout(() => { try { initTemplateToolbar(); } catch (e) {} }, 300); } catch (e) {} resolve(); }); } // 初始化美化工具 function initFormatTools() { const buttonContainer = document.getElementById('format-buttons'); const categoryContainer = document.getElementById('format-categories'); const previewContainer = document.getElementById('format-preview'); const previewToggle = document.getElementById('format-preview-toggle'); if (!buttonContainer || !categoryContainer || !previewContainer || !previewToggle) return; // 默认隐藏预览,避免初始化时渲染预览造成卡顿 try { previewContainer.style.display = 'none'; previewToggle.textContent = '显示预览'; previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:12px;">预览已关闭,点击“显示预览”后再加载</div>'; } catch (e) {} // 清空容器 buttonContainer.innerHTML = ''; categoryContainer.innerHTML = ''; // 获取所有唯一分类(稳定排序,避免reflow震荡) const categories = [...new Set(FORMAT_STYLES.map(style => style.category))]; // 创建分类标签(但不触发样式渲染) categories.forEach(category => { const catBtn = document.createElement('div'); catBtn.textContent = category; catBtn.style.cssText = ` padding:4px 10px; background:#fce7f3; color:#be185d; border-radius:6px; font-size:12px; cursor:pointer; white-space:nowrap; transition: all 0.3s ease; `; // 添加hover效果 catBtn.addEventListener('mouseenter', () => { if (catBtn.style.background !== 'rgb(236, 72, 153)') { catBtn.style.background = '#fbcfe8'; catBtn.style.transform = 'translateY(-1px)'; } }); catBtn.addEventListener('mouseleave', () => { if (catBtn.style.background !== 'rgb(236, 72, 153)') { catBtn.style.background = '#fce7f3'; catBtn.style.transform = 'translateY(0)'; } }); // 默认选中第一个分类 if (category === categories[0]) { catBtn.style.background = '#ec4899'; catBtn.style.color = 'white'; catBtn.style.fontWeight = '500'; } // 点击分类标签过滤样式(若尚未渲染按钮,则延迟到下一帧再过滤) catBtn.addEventListener('click', () => { // 更新分类按钮样式 document.querySelectorAll('#format-categories > div').forEach(btn => { btn.style.background = '#fce7f3'; btn.style.color = '#be185d'; btn.style.fontWeight = 'normal'; }); catBtn.style.background = '#ec4899'; catBtn.style.color = 'white'; catBtn.style.fontWeight = '500'; // 显示选中分类的样式按钮 const filterButtons = () => { document.querySelectorAll('#format-buttons > button').forEach(btn => { const btnCategory = btn.getAttribute('data-category'); btn.style.display = btnCategory === category ? 'inline-flex' : 'none'; }); }; if (buttonContainer.children.length === 0) { setTimeout(filterButtons, 0); } else { filterButtons(); } // 清空预览 previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>'; }); categoryContainer.appendChild(catBtn); }); // 创建样式按钮(极限斩断:初始不渲染任何按钮,首次点击分类时才懒加载) const CHUNK = 25; let index = 0; function renderChunk() { const end = Math.min(index + CHUNK, FORMAT_STYLES.length); for (let i = index; i < end; i++) { const style = FORMAT_STYLES[i]; const btn = document.createElement('button'); const iconHtml = style.icon ? `<i class="fa ${style.icon}" style="margin-right:4px;"></i>` : ''; btn.innerHTML = `${iconHtml}${style.name}`; btn.setAttribute('data-category', style.category); btn.style.cssText = ` background: #f472b6; color: white; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; margin: 2px; display: ${style.category === categories[0] ? 'inline-flex' : 'none'}; align-items: center; transition: all 0.3s ease; box-shadow: 0 1px 3px rgba(244, 114, 182, 0.2); `; // 样式预览功能 if (style.preview) { btn.addEventListener('mouseenter', () => { // 仅在“显示预览”开启后,才按需渲染预览内容 if (previewContainer.style.display === 'block') { previewContainer.innerHTML = ` <div style="margin-bottom:5px; font-size:13px; color:#4b5563; font-weight:500;"> ${style.name} 预览: </div> <div class="style-preview-content"> ${style.apply()} </div> `; } }); } // 样式应用功能 btn.addEventListener('click', async (e) => { e.stopPropagation(); e.preventDefault(); // 添加点击动画反馈 btn.style.background = '#db2777'; btn.style.transform = 'scale(0.98)'; setTimeout(() => { btn.style.background = '#f472b6'; btn.style.transform = 'scale(1)'; }, 200); await autoClickSourceBtn(); const editor = getCurrentEditor(); if (!editor) { showStatus('未找到编辑框,请先切换到源代码模式', true); return; } let selectedText = ''; if (editor.type === 'codemirror') { selectedText = editor.instance.getSelection(); } else { selectedText = editor.instance.value.substring( editor.instance.selectionStart, editor.instance.selectionEnd ); } let styledHtml = style.apply(selectedText); // 如果选择了模板,使用模板替换编辑器内容 try { const REG = window.__TEMPLATE__; if (REG && REG.CURRENT_ID) { const chosen = REG.REGISTRY.find(t => t.id === REG.CURRENT_ID); if (chosen) { const html = compileTemplate(chosen.content, buildTemplateVars()); // 阀门:选择模板时,直接用模板替换编辑器全文 styledHtml = html; // 清除按钮选中高亮 document.querySelectorAll('#template-toolbar button[data-tpl-id]').forEach(b => { b.style.background = '#f472b6'; b.style.color = '#fff'; b.style.border = '1px solid transparent'; b.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)'; }); REG.CURRENT_ID = null; } } } catch (e) {} if (editor.type === 'codemirror') { editor.instance.replaceSelection(styledHtml); } else { const start = editor.instance.selectionStart; const end = editor.instance.selectionEnd; editor.instance.value = editor.instance.value.substring(0, start) + styledHtml + editor.instance.value.substring(end); editor.instance.dispatchEvent(new Event('input', { bubbles: true })); editor.instance.focus(); editor.instance.setSelectionRange(start + styledHtml.length, start + styledHtml.length); } const saved = await autoClickSaveBtn(); if (saved) { showStatus(`已应用"${style.name}"并自动保存`, false); } else { showStatus(`已应用"${style.name}",请手动保存`, false); } }); buttonContainer.appendChild(btn); } index = end; if (index < FORMAT_STYLES.length) { const schedule = (cb) => { try { if (typeof window.requestIdleCallback === 'function') return window.requestIdleCallback(cb); if (typeof window.requestAnimationFrame === 'function') return window.requestAnimationFrame(() => cb()); } catch (e) {} return setTimeout(cb, 0); }; schedule(renderChunk); } } let stylesRendered = false; const ensureRender = () => { if (!stylesRendered) { stylesRendered = true; renderChunk(); } }; // 首次用户与工具交互时再开始渲染,避免初始化阻塞 buttonContainer.addEventListener('pointerover', ensureRender, { once: true }); categoryContainer.addEventListener('click', ensureRender, { once: true }); previewToggle.addEventListener('click', ensureRender, { once: true }); // 预览区域切换功能(持久化开关到localStorage,避免每次初始化造成抖动) previewToggle.addEventListener('click', () => { if (previewContainer.style.display === 'none') { // 打开预览时仅显示占位,不立即渲染,等待用户悬停某个样式按钮 previewContainer.style.display = 'block'; previewToggle.textContent = '隐藏预览'; previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>'; try { localStorage.setItem('format_preview_open', '1'); } catch (e) {} } else { // 关闭预览并清空内容,释放DOM,阻止后续渲染 previewContainer.style.display = 'none'; previewToggle.textContent = '显示预览'; previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:12px;">预览已关闭,点击“显示预览”后再加载</div>'; try { localStorage.setItem('format_preview_open', '0'); } catch (e) {} } }); // 根据本地记忆恢复预览开关 try { const open = localStorage.getItem('format_preview_open') === '1'; if (open) { previewContainer.style.display = 'block'; previewToggle.textContent = '隐藏预览'; previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>'; } } catch (e) {} // 为所有按钮添加悬停效果 setTimeout(() => { document.querySelectorAll('#format-buttons button').forEach(btn => { btn.addEventListener('mouseenter', () => { btn.style.transform = 'translateY(-1px)'; btn.style.boxShadow = '0 2px 6px rgba(244, 114, 182, 0.3)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'translateY(0)'; btn.style.boxShadow = '0 1px 3px rgba(244, 114, 182, 0.2)'; }); }); }, 100); // ===== 模板工具条(延迟到图片区域渲染后再初始化,避免首屏阻塞) ===== // 移至 showImageSelection 完成后触发 } // 模板系统:注册(不可用)/预览/选择 function initTemplateToolbar() { try { const hostCard = document.querySelector('#format-buttons')?.parentElement; if (!hostCard) return; // 创建独立的模板选择界面 let tplToolbar = document.getElementById('template-toolbar'); if (!tplToolbar) { tplToolbar = document.createElement('div'); tplToolbar.id = 'template-toolbar'; tplToolbar.style.cssText = ` margin: 20px 0; padding: 15px; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); width: 100%; box-sizing: border-box; `; // 创建标题 const titleDiv = document.createElement('div'); titleDiv.style.cssText = 'text-align: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f3f4f6;'; const title = document.createElement('h3'); title.textContent = '📋 模板选择器'; title.style.cssText = 'margin: 0; font-size: 16px; font-weight: 600; color: #1f2937;'; const subtitle = document.createElement('p'); subtitle.textContent = '选择您喜欢的排版风格'; subtitle.style.cssText = 'margin: 4px 0 0 0; font-size: 12px; color: #6b7280;'; titleDiv.appendChild(title); titleDiv.appendChild(subtitle); tplToolbar.appendChild(titleDiv); // 创建按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.id = 'template-button-container'; buttonContainer.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(85px, 1fr)); gap: 8px; justify-items: center;'; tplToolbar.appendChild(buttonContainer); // 插入到合适位置(在输入区域下方,格式按钮上方) const formatSection = document.querySelector('#format-buttons')?.parentElement; if (formatSection && formatSection.parentElement) { // 插在格式按钮区域上方 formatSection.parentElement.insertBefore(tplToolbar, formatSection); } else { // 备用方案:插在输入区域下方 const inputSection = document.querySelector('div[style*="margin-bottom:20px"][style*="padding:15px"][style*="background:#fff"]'); if (inputSection && inputSection.parentElement) { inputSection.parentElement.insertBefore(tplToolbar, inputSection.nextSibling); } else { hostCard.appendChild(tplToolbar); } } } // 初始化模板注册(不可用)中心 if (!window.__TEMPLATE__) window.__TEMPLATE__ = { REGISTRY: [], CURRENT_ID: null }; const REG = window.__TEMPLATE__; // 读取外部模板(以常量内嵌,避免运行时IO) const TPL1 = `<div class="editor-wrap" style="max-width: 880px; margin: 40px auto; font-family: 'Helvetica Neue', 'Noto Sans SC', sans-serif; color: #1f2937; background: #ffffff; padding: 0 30px;"><!-- 主标题:玫瑰红渐变双线+对称符号,视觉焦点完全创新 --> <h2 style="text-align: center; font-size: 31px; color: #e11d48; border-bottom: 2px double; border-image: linear-gradient(to right, transparent, #e11d48, transparent) 1; padding: 22px 0; margin: 0 0 40px 0; letter-spacing: 1.5px; text-shadow: 0 1px 4px rgba(225,29,72,0.15);">✦ {{title}} ✦</h2> <!-- 新增:视觉主题解析(优先拆解核心立意,区别原帖模块顺序) --> <div style="background: #f0fdf4; border-radius: 14px; padding: 24px; margin: 0 0 40px 0; border-left: 5px solid #34d399;"> <h4 style="color: #166534; font-size: 20px; margin-top: 0; margin-bottom: 18px; font-weight: 600;">◇ 视觉主题:魔法与反战的双向奔赴</h4> <ul style="margin: 0; padding-left: 30px; line-height: 2.1; font-size: 17px;"> <li>❶ 移动城堡:钢铁与草木交织的造型,隐喻“战争机器”与“自然生机”的对抗</li> <li>❷ 苏菲的魔法:从白发老妪到少女的转变,不是“变美”,而是“接纳自我”——真正的魔法是内心的勇敢</li> <li>❸ 哈尔的头发:金色→黑色→红色,对应他从“伪装完美”到“直面真实”再到“为守护而战”的成长</li> <li>❹ 反战内核:硝烟弥漫的天空与鲜花盛开的山谷对比,宫崎骏用魔法故事呐喊“战争会吞噬一切美好”</li> </ul> </div> <!-- 海报+基础信息:海报hover效果创新,信息符号差异化 --> <div style="border: 2px solid #fecdd3; border-radius: 14px; padding: 28px; margin: 0 0 40px 0; background: #fffafb;"> <div style="text-align: center; margin-bottom: 28px;"><img style="max-width: 100%; border-radius: 8px; box-shadow: 0 5px 15px rgba(225,29,72,0.12), 0 8px 20px rgba(225,29,72,0.08); transition: all 0.4s ease;" src="{{posterUrl}}" alt="主海报"> <p style="color: #e11d48; font-size: 18px; font-weight: 600; margin-top: 20px;">主视觉海报(苏菲与哈尔站在移动城堡前,天空泛着反战的硝烟蓝)</p> </div> <!-- 基础信息:符号位置调整,新增“原著”项,区别原帖 --> <div class="grid-layout" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); margin-top: 15px;"> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">🎬 影片名称:</strong>{{title}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">🈶️ 原名:</strong>{{originalTitle}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">🎬 导演:</strong>{{director}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">📚 原著:</strong>{{originalAuthor}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">✨ 类型:</strong>{{genres}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">📅 上映时间:</strong>{{release}}</p> <p style="margin: 0; font-size: 17px;"><strong style="color: #be123c;">⭐ 豆瓣评分:</strong><span style="color: #fff; background: #e11d48; padding: 3px 12px; border-radius: 15px; font-weight: 500;">{{rating}}</span></p> </div> </div> <!-- 角色羁绊:上下渐变边框+错位排列,保留原模板内容(可手动编辑) --> <h3 style="font-size: 22px; color: #e11d48; font-weight: 600; text-decoration: underline solid #fecdd3; margin: 0 0 26px 0;">▹ 魔法羁绊双人组</h3> <div style="display: flex; flex-wrap: wrap; gap: 25px; margin: 0 0 40px 0;"><div class="role-card" style="flex: 1; min-width: 300px; background: #ffffff; padding: 24px; border-radius: 12px; border-top: 3px solid; border-image: linear-gradient(to right, #e11d48, #fecdd3) 1; box-shadow: 0 6px 16px rgba(225,29,72,0.09); transition: box-shadow 0.3s ease;"><p style="margin: 0 0 18px 0; font-size: 19px;"><strong style="color: #be123c;">● 苏菲</strong>(帽子店少女)</p><p style="margin: 0; font-size: 17px; line-height: 2;">被荒野女巫施咒变成老妪,却因祸得福逃离平庸生活。她用温柔与勇敢治愈哈尔的“逃避症”,从“自卑少女”到“城堡守护者”,最终明白:“年龄和外貌都不重要,内心的强大才是真正的魔法”。</p></div><div class="role-card" style="flex: 1; min-width: 300px; background: #ffffff; padding: 24px; border-radius: 12px; border-top: 3px solid; border-image: linear-gradient(to right, #34d399, #bbf7d0) 1; box-shadow: 0 6px 16px rgba(52,211,153,0.09); transition: box-shadow 0.3s ease;"><p style="margin: 0 0 18px 0; font-size: 19px;"><strong style="color: #166534;">● 哈尔</strong>(魔法少年)</p><p style="margin: 0; font-size: 17px; line-height: 2;">拥有强大魔法却害怕承担责任的“完美主义者”,为逃避国王的征兵而四处躲藏。遇到苏菲后,他逐渐学会直面内心:“我终于找到想守护的东西了”——这份守护让他从“华丽的逃兵”变成“勇敢的战士”。</p></div></div> <!-- 关键场景解析:使用可变量剧照 --> <h3 style="font-size: 22px; color: #e11d48; font-weight: 600; text-decoration: underline solid #fecdd3; margin: 0 0 26px 0;">▹ 魔法场景隐喻</h3> <div style="margin: 0 0 40px 0; text-align: center;"><div style="display: inline-block; position: relative; max-width: 100%;"><img style="max-width: 100%; border-radius: 8px; border: 2px solid #fecdd3; box-shadow: 0 6px 16px rgba(225,29,72,0.1);" src="{{sceneUrl}}" alt="苏菲与哈尔的魔法场景"><div class="scene-desc" style="position: absolute; bottom: -45px; left: 50%; transform: translateX(-50%); background: #fffafb; padding: 8px 16px; border-radius: 8px; border: 1px solid #fecdd3; font-size: 15px; color: #be123c; width: 92%; line-height: 1.8;">场景说明</div></div></div> <!-- 剧情脉络/热评/观影提示:保留原模板的详细写作,用户可手动修订 --> <h3 style="font-size: 22px; color: #e11d48; font-weight: 600; text-decoration: underline solid #fecdd3; margin: 0 0 26px 0;">▹ 剧情脉络</h3> <div style="background: #fffafb; border-left: 5px solid #e11d48; padding: 28px; border-radius: 12px; margin: 0 0 40px 0; line-height: 2.1; font-size: 17px;"> <p>1. ……(保持模板原文,便于“一比一”呈现,可手动修改)</p> </div> </div>`; const TPL2 = `<!-- 3. 标题区(后置,用分割线强化区分) --> <div style="margin: 0 0 30px 0; padding: 10px 0; border-top: 1px dashed #f5e0c8; border-bottom: 1px dashed #f5e0c8;"> <h2 style="text-align: center; font-size: 22px; color: #cd7f32; margin: 0; letter-spacing: 2px; font-weight: 600;">◇ {{title}} ◇</h2> </div> <div style="max-width: 880px; margin: 25px auto; font-family: 'Heiti SC', 'Microsoft Yahei', sans-serif; color: #332718; background: #fffbf5; padding: 22px; border-radius: 10px; box-shadow: 0 3px 10px rgba(205, 127, 50, 0.09);"><!-- 1. 核心信息卡(置顶,打破“标题→海报”的常规顺序) --> <div style="background: #fff; border-radius: 8px; padding: 18px; margin: 0 0 28px 0; border-top: 4px solid #cd7f32; box-shadow: 0 2px 6px rgba(205, 127, 50, 0.06);"> <div style="display: grid; grid-template-columns: auto 1fr; gap: 15px; align-items: center; margin-bottom: 15px;"> <!-- 左侧:评分醒目展示 --> <div style="text-align: center; padding: 10px 15px; background: #fff8f0; border-radius: 6px; border: 1px solid #f5e0c8;"> <span style="font-size: 28px; color: #cd7f32; font-weight: bold;">{{rating}}</span> <p style="margin: 5px 0 0; font-size: 12px; color: #8b5a2b;">{{ratingSource}}</p> </div> <!-- 右侧:关键标识+基础信息 --> <div style="display: flex; justify-content: space-between; align-items: center;"> <div> <p style="margin: 0; font-size: 14px; color: #665233;"><strong>● 标识:</strong>{{markerType}} <a style="color: #cd7f32; text-decoration: none; border-bottom: 1px dotted #f5e0c8;" href="{{markerLink}}" target="_blank" rel="noopener">{{markerId}}</a></p> <p style="margin: 8px 0 0; font-size: 14px; color: #665233;"><strong>● 上线:</strong>{{release}}</p> </div> <p style="margin: 0; font-size: 14px; color: #665233;"><strong>● 片长:</strong>{{runtime}}</p> </div> </div> <!-- 下方:多列信息网格(拆分为三列,区别于之前的两列) --> <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 14px; line-height: 1.6;"> <p><strong style="color: #cd7f32;">▶ 片名:</strong>{{title}}</p> <p><strong style="color: #cd7f32;">▶ 导演:</strong>{{directorDisplay}}</p> <p><strong style="color: #cd7f32;">▶ 编剧:</strong>{{writerDisplay}}</p> <p><strong style="color: #cd7f32;">▶ 类型:</strong>{{genresDisplay}}</p> <p><strong style="color: #cd7f32;">▶ 地区:</strong>{{regionDisplay}}</p> <p><strong style="color: #cd7f32;">▶ 语言:</strong>{{filmLanguageDisplay}}</p> <p style="grid-column: 1 / -1;"><strong style="color: #cd7f32;">▶ 主演:</strong><span style="display: inline-block; max-width: calc(100% - 60px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{actor}}">{{actorDisplay}}</span></p> </div> </div> <!-- 2. 海报区(放在信息卡之后,视觉重心后移) --> <div style="margin: 0 0 32px 0; border: 1px solid #f5e0c8; border-radius: 8px; padding: 12px; background: #fff;"> <img style="max-width: 100%; border-radius: 6px; display: block; margin: 0 auto;" src="{{posterUrl}}" alt="{{title}}海报"> <p style="text-align: center; color: #8b5a2b; font-size: 13px; font-weight: 500; margin: 12px 0 0; letter-spacing: 0.5px;">{{title}} · 官方海报</p> </div> <!-- 4. 剧情区(用色块+图标引导,区别于竖线) --> <div style="margin: 0 0 30px 0;"> <div style="display: flex; align-items: center; margin-bottom: 10px;"><span style="display: inline-block; width: 20px; height: 20px; background: #cd7f32; color: #fff; border-radius: 50%; text-align: center; line-height: 20px; font-size: 12px; margin-right: 8px;">●</span> <h3 style="font-size: 17px; color: #8b5a2b; font-weight: 600; margin: 0;">剧情简介</h3> </div> <div style="background: #fff; padding: 18px; border-radius: 8px; border: 1px solid #f5e0c8; line-height: 1.7; font-size: 15px; color: #332718;">{{introAuto}}</div> </div> <!-- 5. 热评区(左对齐来源,区别于右对齐;加边框装饰) --> <div style="margin: 0 0 30px 0;"> <div style="display: flex; align-items: center; margin-bottom: 10px;"><span style="display: inline-block; width: 20px; height: 20px; background: #cd7f32; color: #fff; border-radius: 50%; text-align: center; line-height: 20px; font-size: 12px; margin-right: 8px;">●</span> <h3 style="font-size: 17px; color: #8b5a2b; font-weight: 600; margin: 0;">观众热评</h3> </div> <div style="border: 1px dashed #f5e0c8; border-radius: 8px; padding: 18px; background: #fff8f0; line-height: 1.7; font-size: 15px;"> {{commentsAuto}} </div> </div> <!-- 6. 观影贴士(用序号+图标,区别于列表) --> <div style="background: #fff; border-radius: 8px; padding: 20px; border: 1px solid #f5e0c8; margin: 0 0 5px 0;"> <h3 style="font-size: 16px; color: #cd7f32; font-weight: 600; margin: 0 0 15px 0; display: flex; align-items: center;"><span style="display: inline-block; width: 18px; height: 18px; background: #cd7f32; color: #fff; border-radius: 3px; text-align: center; line-height: 18px; font-size: 12px; margin-right: 8px;">◆</span> 观影小贴士</h3> <div style="font-size: 15px; line-height: 2; color: #332718;"> <p style="margin: 0;"><strong>1. </strong>推荐在{{watchScene}}观看,减少外界干扰</p> <p style="margin: 0;"><strong>2. </strong>二刷可留意{{detailTip}},会有新发现</p> <p style="margin: 0;"><strong>3. </strong>适合和{{watchWith}}一起看,看完可交流不同视角</p> </div> </div> </div>`; const TPL3 = `<div style="max-width: 860px; margin: 25px auto; font-family: 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif; color: #2e7d32; background: #f9fdf9; padding: 26px; border-radius: 16px; box-shadow: 0 3px 12px rgba(46, 125, 50, 0.06);"><!-- 1. 标题区(樱花符号+居中柔和排版) --> <h2 style="text-align: center; font-size: 22px; color: #f48fb1; margin: 0 0 35px 0; font-weight: 500; letter-spacing: 1.2px;">⭐{{title}} ⭐</h2> <!-- 2. 海报区(纸质感边框+充足留白) --> <div style="text-align: center; margin: 0 0 35px 0; padding: 12px; background: #fff; border-radius: 12px; border: 1px solid #e0e0e0; box-shadow: 0 2px 8px rgba(46, 125, 50, 0.04);"><img style="max-width: 75%; border-radius: 8px; border: 1px solid #f0f0f0;" src="{{posterUrl}}" alt="{{title}}海报"> <p style="color: #66bb6a; font-size: 13px; margin: 14px 0 0; line-height: 1.4;">{{title}} · 官方海报</p> </div> <!-- 3. 核心信息区(纵向列表+图标引导) --> <div style="background: #fff; padding: 22px; border-radius: 12px; margin: 0 0 35px 0; border: 1px solid #e8f5e9;"> <ul style="margin: 0; padding: 0; list-style: none;"> <li style="display: flex; align-items: center; margin-bottom: 12px; font-size: 15px;"> <div> <p style="margin: 0 0 4px; font-size: 12px; color: #81c784;">作品名</p> <p style="margin: 0; font-weight: 500;">{{title}}</p> </div> </li> <li style="display: flex; align-items: center; margin-bottom: 12px; font-size: 15px;"> <div> <p style="margin: 0 0 4px; font-size: 12px; color: #81c784;">主创团队</p> <p style="margin: 0;">导演:{{directorDisplay}} / 编剧:{{writerDisplay}}</p> </div> </li> <li style="display: flex; align-items: center; margin-bottom: 12px; font-size: 15px;"> <div> <p style="margin: 0 0 4px; font-size: 12px; color: #81c784;">主演</p> <p style="margin: 0; max-width: 100%;"><span style="display: inline-block; max-width: calc(100% - 40px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{actor}}">{{actorDisplay}}</span></p> </div> </li> <li style="display: flex; align-items: center; margin: 0 0 16px 0; font-size: 15px;"> <div> <p style="margin: 0 0 4px; font-size: 12px; color: #81c784;">基础信息</p> <p style="margin: 0;">类型:{{genresDisplay}} / 地区:{{regionDisplay}} / 语言:{{filmLanguageDisplay}}</p> </div> </li> <li style="display: flex; align-items: center; margin: 0; font-size: 15px;"> <div> <p style="margin: 0 0 4px; font-size: 12px; color: #81c784;">评分&时长</p> <p style="margin: 0;">评分:<span style="color: #fff; background: #66bb6a; padding: 2px 8px; border-radius: 12px; font-weight: 500; font-size: 14px;">{{rating}}</span> / 片长:{{runtimeDisplay}} / 上线:{{releaseDisplay}}</p> <p style="margin: 8px 0 0; font-size: 14px;">标识:<a style="color: #66bb6a; text-decoration: none; border-bottom: 1px dashed #e8f5e9;" href="{{markerLink}}" target="_blank" rel="noopener">{{markerType}} {{markerId}}</a></p> </div> </li> </ul> </div> <!-- 4. 剧情简介区(樱花分割线+柔和背景) --> <div style="margin: 0 0 35px 0;"> <div style="text-align: center; margin-bottom: 15px;"><span style="color: #f48fb1; font-size: 16px;">⭐ 剧情简介 ⭐</span> <div style="height: 1px; background: linear-gradient(to right, transparent, #e8f5e9, transparent); margin-top: 8px;"> </div> </div> <div style="background: #fff; padding: 22px; border-radius: 12px; line-height: 1.8; font-size: 15px; color: #2e7d32; border: 1px solid #e8f5e9;">{{introAuto}}</div> </div> <!-- 5. 观众热评区(手写感引用框) --> <div style="margin: 0 0 35px 0;"> <div style="text-align: center; margin-bottom: 15px;"><span style="color: #f48fb1; font-size: 16px;">✍️ 观众热评 ✍️</span> <div style="height: 1px; background: linear-gradient(to right, transparent, #e8f5e9, transparent); margin-top: 8px;"> </div> </div> <div style="background: #fff; padding: 20px; border-radius: 12px; border: 1px solid #fce4ec; box-shadow: 0 2px 6px rgba(244, 143, 177, 0.05);"> <div style="font-size: 24px; color: #f48fb1; margin: -10px 0 0 -10px; opacity: 0.3;">"</div> {{commentsAuto}} </div> </div> <!-- 6. 观影贴士(植物符号+纵向卡片) --> <div style="margin: 0 0 10px 0;"> <div style="text-align: center; margin-bottom: 15px;"><span style="color: #f48fb1; font-size: 16px;">✨ 观影贴士 ✨</span><br> <div style="height: 1px; background: linear-gradient(to right, transparent, #e8f5e9, transparent); margin-top: 8px;"> </div> </div> <div style="display: grid; grid-template-columns: 1fr; gap: 12px;"> <div style="background: #fff; padding: 18px; border-radius: 10px; border-left: 4px solid #66bb6a; font-size: 15px; color: #2e7d32;"> <p style="margin: 0;">① 推荐在{{watchScene}}观看,搭配热茶更有氛围~</p> </div> <div style="background: #fff; padding: 18px; border-radius: 10px; border-left: 4px solid #66bb6a; font-size: 15px; color: #2e7d32;"> <p style="margin: 0;">② 二刷可留意{{detailTip}},细节里藏着小温柔</p> </div> <div style="background: #fff; padding: 18px; border-radius: 10px; border-left: 4px solid #66bb6a; font-size: 15px; color: #2e7d32;"> <p style="margin: 0;">③ 适合和{{watchWith}}一起看,看完可以聊聊剧中的温暖瞬间</p> </div> </div> </div>`; const TPL4 = `<div style="max-width: 880px; margin: 30px auto; font-family: 'SimHei', 'Microsoft Yahei', sans-serif; color: #fff; background: #1a140f; padding: 28px; border-radius: 8px; border: 1px solid #8b4513; box-shadow: 0 4px 15px rgba(139, 69, 19, 0.3);"><!-- 1. 海报+核心信息 左右分栏(港风不对称排版) --> <div style="display: grid; grid-template-columns: 1fr 1.2fr; gap: 18px 25px; align-items: start; margin: 0 0 28px 0;"><!-- 左侧:海报区(做旧胶片边框) --> <div style="position: relative; align-self: start; border: 8px solid #fff; border-radius: 2px; box-shadow: 0 0 0 2px #8b4513;"><img style="width: 100%; display: block; border: 1px solid #333;" src="{{posterUrl}}" alt="{{title}}海报"> <div style="position: absolute; bottom: -8px; left: 0; width: 100%; text-align: center; font-size: 12px; color: #f0c892; letter-spacing: 2px;">{{title}} · 原版海报</div> </div> <!-- 右侧:核心信息(港风粗体+红棕对比) --> <div style="background: #2d2013; padding: 20px; border-radius: 4px; border-left: 4px solid #cd5c5c;"> <h3 style="font-size: 18px; color: #f0c892; margin: 0 0 20px 0; font-weight: bold; border-bottom: 1px dashed #8b4513; padding-bottom: 10px;">♦ 影片信息 ♦</h3> <div style="display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; font-size: 14px; line-height: 1.65;"> <p style="margin: 0; color: #f0c892; font-weight: bold;">片名:</p> <p style="margin: 0; font-size: 16px;">{{title}}</p> <p style="margin: 0; color: #f0c892; font-weight: bold;">导演:</p> <p style="margin: 0;">{{directorDisplay}}</p> <p style="margin: 0; color: #f0c892; font-weight: bold;">主演:</p> <p style="margin: 0; max-width: 100%;"><span style="display: inline-block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{actor}}">{{actorDisplay}}</span></p> <p style="margin: 0; color: #f0c892; font-weight: bold;">类型:</p> <p style="margin: 0;">{{genresDisplay}}</p> <p style="margin: 0; color: #f0c892; font-weight: bold;">评分:</p> <p style="margin: 0; color: #ffd700; font-weight: bold;">{{rating}}({{ratingSource}})</p> <p style="margin: 0; color: #f0c892; font-weight: bold;">上线:</p> <p style="margin: 0;">{{releaseDisplay}} / {{runtimeDisplay}}</p> <p style="margin: 0; color: #f0c892; font-weight: bold;">标识:</p> <p style="margin: 0;"><a style="color: #f0c892; text-decoration: none; border-bottom: 1px solid #cd5c5c;" href="{{markerLink}}" target="_blank" rel="noopener"> {{markerType}} {{markerId}} </a></p> </div> </div> </div> <!-- 2. 标题区(港风大标题+复古分割) --> <div style="margin: 0 0 28px 0; text-align: center;"> <div style="display: flex; align-items: center; justify-content: center; gap: 15px;"> <div style="height: 1px; flex: 1; background: linear-gradient(to right, transparent, #8b4513, transparent);"> </div> <h2 style="font-size: 26px; color: #f0c892; margin: 0; font-weight: bold; letter-spacing: 3px;">{{title}}</h2> <div style="height: 1px; flex: 1; background: linear-gradient(to right, transparent, #8b4513, transparent);"> </div> </div> <p style="margin: 10px 0 0; color: #cd5c5c; font-size: 13px; letter-spacing: 1px;">{{regionDisplay}} · {{filmLanguageDisplay}} · {{genresDisplay}}</p> </div> <!-- 3. 剧情简介区(港风报纸栏样式) --> <div style="margin: 0 0 28px 0;"> <h3 style="font-size: 17px; color: #f0c892; margin: 0 0 15px 0; font-weight: bold; display: inline-block; background: #cd5c5c; padding: 3px 12px; border-radius: 3px;">▶ 剧情简介</h3> <div style="background: #2d2013; padding: 22px; border-radius: 4px; line-height: 1.8; font-size: 15px; border: 1px solid #8b4513;">{{introAuto}}</div> </div> <!-- 4. 观众热评区(港风胶片框+手写感) --> <div style="margin: 0 0 28px 0;"> <h3 style="font-size: 17px; color: #f0c892; margin: 0 0 15px 0; font-weight: bold; display: inline-block; background: #cd5c5c; padding: 3px 12px; border-radius: 3px;">▶ 观众热评</h3> <div style="position: relative; background: #2d2013; padding: 25px 20px; border-radius: 4px; border: 1px solid #8b4513; box-shadow: 0 2px 8px rgba(139, 69, 19, 0.2);"><!-- 胶片装饰角 --> <!-- 角饰移除 --> {{commentsAuto}} </div> </div> <!-- 5. 观影贴士(港风标签式) --> <div style="margin: 0 0 10px 0;"> <h3 style="font-size: 17px; color: #f0c892; margin: 0 0 15px 0; font-weight: bold; display: inline-block; background: #cd5c5c; padding: 3px 12px; border-radius: 3px;">▶ 观影贴士</h3> <div style="display: grid; grid-template-columns: 1fr; gap: 12px;"> <div style="background: #2d2013; padding: 15px 20px; border-radius: 4px; border-left: 3px solid #ffd700; font-size: 15px; line-height: 1.6;"><span style="color: #ffd700; font-weight: bold;">★ </span>推荐在{{watchScene}}观看,搭配汽水或啤酒更有港味~</div> <div style="background: #2d2013; padding: 15px 20px; border-radius: 4px; border-left: 3px solid #ffd700; font-size: 15px; line-height: 1.6;"><span style="color: #ffd700; font-weight: bold;">★ </span>二刷可重点关注{{detailTip}},港片的细节藏着江湖气</div> <div style="background: #2d2013; padding: 15px 20px; border-radius: 4px; border-left: 3px solid #ffd700; font-size: 15px; line-height: 1.6;"><span style="color: #ffd700; font-weight: bold;">★ </span>适合和{{watchWith}}一起看,看完能聊透片中的“江湖道义”</div> </div> </div> </div>`; const TPL5 = `<div style="max-width: 800px; margin: 25px auto; font-family: 'SimSun', 'Microsoft Yahei', serif; color: #232323; background: #F5F0E1; padding: 24px; border: 1px solid #D4C8B8;"> <div style="margin: 0 0 20px 0; text-align: center; padding: 10px 0;"> <h1 style="font-size: 26px; color: #8B0000; margin: 0; font-weight: bold; letter-spacing: 3px;">「{{title}}」</h1> <p style="margin: 12px 0 0; font-size: 14px; color: #666; letter-spacing: 1px;">{{genres}} · {{release}} · {{region}}</p> </div> <div style="text-align: center; margin: 0 0 20px 0; padding: 12px; background: #fff; border: 1px solid #D4C8B8;"> <img src="{{posterUrl}}" alt="{{title}}海报" style="max-width: 70%; border: 1px solid #D4C8B8;"> <p style="margin: 12px 0 0; font-size: 13px; color: #8B0000; font-weight: bold;">{{title}} · 官方海报</p> </div> <div style="display: flex; flex-direction: column; gap: 10px; margin: 0 0 20px 0;"> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">影片名称:</span>{{title}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">导演:</span>{{directorDisplay}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">编剧:</span>{{writerDisplay}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">主演:</span><span style="display: inline-block; max-width: calc(100% - 60px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="{{actor}}">{{actorDisplay}}</span></div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">影片类型:</span>{{genres}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">制片地区:</span>{{regionDisplay}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">语言:</span>{{filmLanguageDisplay}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">片长:</span>{{runtimeDisplay}}</div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">评分:</span><span style="background: #8B0000; color: #fff; padding: 2px 10px; font-size: 14px;">{{rating}}</span></div> <div style="background: #fff; padding: 10px; border-left: 3px solid #8B0000; line-height: 2; font-size: 15px;"><span style="color: #8B0000; font-weight: bold;">标识:</span><a href="{{markerLink}}" target="_blank" style="color: #8B0000; text-decoration: none; border-bottom: 1px dashed #D4C8B8;">{{markerType}} {{markerId}}</a></div> </div> <div style="margin: 0 0 20px 0;"> <div style="background: #8B0000; color: #fff; padding: 5px 15px; font-size: 16px; font-weight: bold;">【剧情简介】</div> <div style="background: #fff; padding: 15px; border: 1px solid #D4C8B8; line-height: 1.8; font-size: 15px;">{{introAuto}}</div> </div> <div style="margin: 0 0 20px 0;"> <div style="background: #8B0000; color: #fff; padding: 5px 15px; font-size: 16px; font-weight: bold;">【观众热评】</div> <div style="background: #fff; padding: 15px; border: 1px solid #D4C8B8; line-height: 1.8; font-size: 15px;">{{commentsAuto}}</div> </div> <div style="background: #fff; padding: 15px; border: 1px solid #D4C8B8; margin: 0 0 10px 0;"> <div style="color: #8B0000; font-size: 16px; font-weight: bold; margin: 0 0 15px 0; border-bottom: 1px solid #D4C8B8; padding-bottom: 5px;">【观影贴士】</div> <ul style="margin: 0; padding-left: 20px; line-height: 2; font-size: 15px;"> <li>推荐于{{watchScene}}观看,更能品悟影片韵味</li> <li>二刷可着重留意{{detailTip}},藏有中式巧思</li> <li>适合与{{watchWith}}共赏,观后可交流中式意趣</li> </ul> </div> </div>`; const TPL6 = `<div style="max-width: 760px; margin: 30px auto; font-family: '微软雅黑', '宋体', serif; color: #2d2d2d; background: #ffffff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(26,54,93,0.04);"> <div style="margin-bottom: 26px; padding: 14px 18px; background: #1a365d; color: #fff; border-radius: 6px;"> <h1 style="font-size: 24px; margin: 0; letter-spacing: 1px;">{{title}}</h1> <p style="font-size: 14px; margin: 8px 0 0; opacity: 0.9; line-height: 1.6;">● {{genres}} | {{releaseDisplay}} | {{regionDisplay}}</p> </div> <div style="margin-bottom: 26px; display: flex; flex-direction: column; gap: 12px;"> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 导演:</span>{{directorDisplay}}</div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 编剧:</span>{{writerDisplay}}</div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><span style="color: #1a365d; font-weight: bold;">● 主演:</span><span title="{{actor}}">{{actorDisplay}}</span></div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 语言:</span>{{filmLanguageDisplay}}</div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 片长:</span>{{runtimeDisplay}}</div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 评分:</span><span style="background: #1a365d; color: #fff; padding: 2px 8px; border-radius: 12px; font-size: 14px;">{{rating}}</span></div> <div style="padding: 12px 16px; background: #f5f5f5; border-radius: 6px; line-height: 1.8; font-size: 15px;"><span style="color: #1a365d; font-weight: bold;">● 官方标识:</span><a href="{{markerLink}}" target="_blank" style="color: #1a365d; text-decoration: none; border-bottom: 1px dashed #c4c9d0;">{{markerType}} {{markerId}}</a></div> </div> <div style="text-align: center; margin-bottom: 26px; padding: 14px; border: 1px solid #e5e7eb; border-radius: 6px;"> <img src="{{posterUrl}}" alt="{{title}}官方海报" style="max-width: 70%; border-radius: 4px; border: 1px solid #e5e7eb;"> <p style="font-size: 14px; color: #1a365d; font-weight: 600; margin-top: 12px;">{{title}} · 官方海报</p> </div> <div style="margin-bottom: 26px; padding: 16px; border-radius: 6px; border-top: 2px solid #1a365d;"> <h3 style="font-size: 16px; color: #1a365d; margin: 0 0 12px; font-weight: bold;">● 剧情简介</h3> <div style="font-size: 15px; line-height: 1.8; color: #2d2d2d; margin: 0; text-indent: 2em;">{{introAuto}}</div> </div> <div style="margin-bottom: 26px; padding: 16px; background: #f9f9f9; border-radius: 6px; border-left: 3px solid #1a365d;"> <h3 style="font-size: 16px; color: #1a365d; margin: 0 0 12px; font-weight: bold;">● 观众热评</h3> <div style="font-size: 15px; line-height: 1.8; color: #2d2d2d; margin: 0; padding: 10px; background: #fff; border-radius: 4px;">{{commentsAuto}}</div> </div> <div style="padding: 16px; background: #f9f9f9; border-radius: 6px;"> <h3 style="font-size: 16px; color: #1a365d; margin: 0 0 12px; font-weight: bold;">● 观影贴士</h3> <ul style="margin: 0; padding-left: 24px; font-size: 15px; line-height: 2; color: #2d2d2d;"> <li style="margin-bottom: 10px; list-style-type: disc;">推荐于{{watchScene}}观看,更能体会影片情感内核</li> <li style="margin-bottom: 10px; list-style-type: disc;">适合与{{watchWith}}共赏,观后可深入交流角色塑造与剧情逻辑</li> <li style="list-style-type: disc;">二刷可重点留意{{detailTip}},能挖掘更多创作巧思</li> </ul> </div> </div>`; const TPL7 = `<div class="post-wrap"><!-- 1. 主标题区(固定框架:★装饰+渐变边框) --> <h2 style="text-align: center; font-size: 26px; color: #22c55e; border-bottom: 2px solid; border-image: linear-gradient(to right, #f0fdf4, #22c55e, #f0fdf4) 1; padding: 15px 0; margin: 0 0 28px 0; letter-spacing: 0.5px;">★ {{title}}{{originalTitleParen}} ★</h2> <!-- 2. 核心基础信息(固定框架:网格布局+▷符号) --> <div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; padding: 20px; margin: 0 0 28px 0;"> <h3 style="color: #166534; font-size: 19px; margin: 0 0 16px 0; font-weight: 600;">▷ 影片基础信息</h3> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px 20px; font-size: 16px; line-height: 1.6;"> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 导演:</strong>{{director}}</p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 类型:</strong>{{genres}}</p> <p style="margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><strong style="color: #22c55e;">▷ 主演:</strong><span title="{{actor}}">{{actorDisplay}}</span></p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 编剧:</strong>{{writer}}</p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 上映:</strong>{{release}}</p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 地区:</strong>{{region}}</p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ 片长:</strong>{{runtime}}</p> <p style="margin: 0;"><strong style="color: #22c55e;">▷ {{ratingSource}}评分:</strong><span style="color: #fff; background: #22c55e; padding: 2px 10px; border-radius: 12px; font-weight: 500;">{{rating}}</span></p> <p style="margin: 0; grid-column: 1 / -1;"><strong style="color: #22c55e;">▷ 标识:</strong>{{idLineHtml}}</p> </div> </div> <!-- 3. 海报展示区(固定框架:居中+hover阴影) --> <div style="text-align: center; margin: 0 0 28px 0;"> <div style="display: inline-block; max-width: 100%;"><img style="max-width: 100%; border-radius: 10px; box-shadow: 0 4px 10px rgba(34, 197, 94, 0.1); transition: box-shadow 0.3s ease;" src="{{posterUrl}}" alt="{{title}}主海报"></div> <p style="color: #166534; font-size: 16px; font-weight: 500; margin-top: 12px;">{{title}}主视觉海报</p> </div> <!-- 4. 剧情简介(固定框架:左侧绿边框+浅绿背景)→ 提前至剧照前 --> <h3 style="color: #22c55e; font-size: 19px; font-weight: 600; margin: 0 0 14px 0; text-decoration: underline dotted #bbf7d0;">▷ 影视简介</h3> <div style="background: #f0fdf4; border-left: 4px solid #22c55e; padding: 18px; border-radius: 0 8px 8px 0; margin: 0 0 28px 0; line-height: 1.8; font-size: 16px; color: #1f2937;">{{introAuto}}</div> <!-- 5. 影片剧照(固定框架:单张居中+hover阴影,后置到剧情简介后) --> <h3 style="color: #22c55e; font-size: 19px; font-weight: 600; margin: 0 0 14px 0; text-decoration: underline dotted #bbf7d0;">▷ 影片剧照</h3> <div style="text-align: center; margin: 0 0 28px 0;"> <div style="display: inline-block; max-width: 100%;"><img style="max-width: 100%; border-radius: 8px; box-shadow: 0 3px 8px rgba(139, 92, 246, 0.08); transition: box-shadow 0.3s ease;" src="{{sceneUrl}}" alt="{{title}}核心剧照"> <p style="color: #6b21a8; font-size: 15px; margin: 10px 0 0 0; line-height: 1.5;">{{title}}经典镜头</p> </div> </div> <!-- 6. 观众热评(固定框架:双视角区分+顶部色边框) --> <h3 style="color: #22c55e; font-size: 19px; font-weight: 600; margin: 0 0 14px 0; text-decoration: underline dotted #bbf7d0;">▷ 观众热评</h3> <div style="background: #fff; padding: 20px; border-radius: 8px; margin: 0 0 28px 0; border: 1px solid #bbf7d0; border-top: 3px solid #22c55e;"> {{commentsAuto}} </div> <!-- 7. 观影小贴士(固定框架:◆符号+浅紫背景) --> <p style="font-size: 16px; color: #1f2937; background: #f5f3ff; padding: 18px 18px 18px 32px; border-radius: 8px; margin: 0; line-height: 1.8; border-left: 4px solid #8b5cf6; position: relative;"><span style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #8b5cf6; font-size: 20px;">◆</span> 【观影小贴士】① 结合剧情简介看剧照,更易理解情感张力;② 二刷可留意剧照中的细节,与剧情伏笔呼应;③ 适合朋友/家人共同观看,看完可结合剧照讨论场景氛围的营造~</p> </div>`; // 覆盖模板(1):采用用户提供的橙红风格一比一排版(变量占位已简化为可替换形式) const TPL1_ORANGE = `<div style="max-width: 820px; margin: 30px auto; font-family: 'Noto Sans SC', 'Microsoft Yahei', sans-serif; color: #333; background: #fff; padding: 0 20px;"> <h2 style="text-align: center; font-size: 27px; color: #e64a19; border-bottom: 2px double; border-image: linear-gradient(to right, transparent, #e64a19, transparent) 1; padding: 15px 0; margin: 0 0 25px 0; letter-spacing: 0.8px;">★ {{title}}{{originalTitleParen}} ★</h2> <div style="text-align: center; margin: 0 0 25px 0; padding: 12px; border: 2px solid #ffccbc; border-radius: 10px;"> <img src="{{posterUrl}}" alt="{{title}}海报" style="max-width: 100%; border-radius: 8px; box-shadow: 0 3px 8px rgba(230,74,25,0.15);"> <p style="color: #e64a19; font-size: 15px; font-weight: 600; margin-top: 10px;">{{title}}主视觉海报</p> </div> <div style="background: #fff8f5; border-radius: 10px; padding: 18px 20px; margin: 0 0 16px 0; border: 1px solid #ffccbc;"> <h4 style="color: #c2185b; font-size: 18px; margin-top: 0; margin-bottom: 12px; font-weight: 600;">▷ 基础信息</h4> <div style="display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 8px 16px; align-items: start; font-size: 15px; line-height: 1.6;"> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 导演:</strong>{{director}}</p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 类型:</strong>{{genres}}</p> <p style="margin: 0; min-width: 0;"><strong style="color: #e64a19;">▷ 主演:</strong><span style="display:inline-block; max-width: calc(100% - 70px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: top;" title="{{actor}}">{{actorShort}}</span></p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 编剧:</strong>{{writer}}</p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 上映:</strong>{{release}}</p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 地区:</strong>{{region}}</p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 片长:</strong>{{runtime}}</p> <p style="margin: 0;"><strong style="color: #e64a19;">▷ 评分:</strong><span style="color: #fff; background: #e64a19; padding: 2px 8px; border-radius: 15px; font-weight: 500;">{{rating}}</span></p> <p style="margin: 0; grid-column: 1 / -1;"><strong style="color: #e64a19;">▷ 标识:</strong>{{idLineHtml}}</p> </div> </div> <h3 style="font-size: 19px; color: #e64a19; font-weight: 600; text-decoration: underline solid #ffccbc; margin: 0 0 10px 0;">▷ 剧情简介</h3> <div style="background: #fff; border-left: 4px solid #e64a19; padding: 14px; border-radius: 8px; margin: 0 0 18px 0; line-height: 1.7; font-size: 15px;">{{introAuto}}</div> <h3 style="font-size: 19px; color: #e64a19; font-weight: 600; text-decoration: underline solid #ffccbc; margin: 0 0 10px 0;">▷ 观众热评</h3> <div style="background: #fff; padding: 14px; border-radius: 8px; margin: 0 0 18px 0; border: 1px solid #ffccbc; border-top: 3px solid #e64a19;"> {{commentsAuto}} </div> <p style="font-size: 16px; color: #333; background: #fff8f5; padding: 12px 14px 12px 26px; border-radius: 8px; margin: 0 0 12px 0; line-height: 1.7; border-left: 4px solid #e64a19; position: relative;"> <span style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #e64a19; font-size: 18px;">◆</span> 【观影提示】① 建议优先查看长评,深度解析更易理解剧情;② 二刷可关注{{detailTip}},可能发现隐藏伏笔;③ 适合{{watchWith}}共同观看,看完可交流观点~ </p> </div>`; function safePush(id, name, raw) { if (!raw) return; if (REG.REGISTRY.some(t => t.id === id)) return; REG.REGISTRY.push({ id, name, content: templatizeHtml(raw) }); } // 为每个模板赋予风格化名称(纯名字,不带备注) safePush('tpl1', '玫瑰幻境', TPL1_ORANGE); safePush('tpl2', '琥珀复古', TPL2); safePush('tpl3', '樱雾清新', TPL3); safePush('tpl4', '港风胶片', TPL4); safePush('tpl5', '国韵典藏', TPL5); safePush('tpl6', '理性简约', TPL6); safePush('tpl7', '森系活力', TPL7); // 渲染模板按钮 const buttonContainer = document.getElementById('template-button-container') || tplToolbar; buttonContainer.querySelectorAll('button[data-tpl-id]').forEach(b => b.remove()); const previewContainer = document.getElementById('format-preview'); const previewToggle = document.getElementById('format-preview-toggle'); REG.REGISTRY.forEach(tpl => { const btn = document.createElement('button'); btn.textContent = tpl.name; btn.setAttribute('data-tpl-id', tpl.id); btn.style.cssText = ` background: linear-gradient(135deg, #f472b6, #ec4899); color: #fff; border: 1px solid transparent; padding: 8px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; text-align: center; min-width: 70px; box-shadow: 0 1px 3px rgba(244, 114, 182, 0.3); `; btn.addEventListener('mouseenter', () => { if (REG.CURRENT_ID !== tpl.id) { btn.style.transform = 'translateY(-1px)'; btn.style.boxShadow = '0 2px 6px rgba(244,114,182,.35)'; } }); btn.addEventListener('mouseleave', () => { if (REG.CURRENT_ID !== tpl.id) { btn.style.transform = 'translateY(0)'; btn.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)'; } }); btn.addEventListener('click', () => { // 选中动画 btn.style.transform = 'scale(0.98)'; setTimeout(() => { btn.style.transform = 'scale(1)'; }, 120); // 若再次点击同一个按钮,则视为取消选择 if (REG.CURRENT_ID === tpl.id) { REG.CURRENT_ID = null; btn.style.background = '#f472b6'; btn.style.color = '#fff'; btn.style.border = '1px solid transparent'; btn.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)'; // 不强制关闭预览,保留上一次内容;如需关闭可开启: // previewContainer.style.display = 'none'; previewToggle.textContent = '显示预览'; return; } // 取消其他按钮的选中样式 tplToolbar.querySelectorAll('button[data-tpl-id]').forEach(b => { b.style.background = '#f472b6'; b.style.color = '#fff'; b.style.border = '1px solid transparent'; b.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)'; }); // 设置当前为选中(白色高亮) REG.CURRENT_ID = tpl.id; btn.style.background = '#fff'; btn.style.color = '#db2777'; btn.style.border = '1px solid #f472b6'; btn.style.boxShadow = '0 2px 8px rgba(236,72,153,.25)'; // 预览模板 const demoHtml = compileTemplate(tpl.content, buildTemplateVars()); previewContainer.style.display = 'block'; previewToggle.textContent = '隐藏预览'; previewContainer.innerHTML = `<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">当前模板:${tpl.name}</div>` + demoHtml; }); buttonContainer.appendChild(btn); }); } catch (e) { /* 忽略模板初始化异常,避免影响主流程 */ } } // 将外部模板自动打上占位符,便于替换 function templatizeHtml(html) { try { let h = String(html || ''); if (!/\{\{title\}\}/.test(h)) h = h.replace(/(<h[12][^>]*>)([\s\S]*?)(<\/h[12]>)/i, '$1{{title}}$3'); let imgIdx = 0; h = h.replace(/(<img[^>]*?src=")([^"]+)("[^>]*>)/ig, (m, p1, p2, p3) => { imgIdx += 1; if (imgIdx === 1) return `${p1}{{posterUrl}}${p3}`; if (imgIdx === 2) return `${p1}{{sceneUrl}}${p3}`; return m; }); h = h.replace(/(豆瓣评分[^<]*?<span[^>]*>)[^<]+(<\/span>)/i, '$1{{rating}}$2'); return h; } catch { return html; } } function compileTemplate(raw, vars) { return String(raw || '').replace(/\{\{(\w+)\}\}/g, (m, k) => (vars && (k in vars) ? (vars[k] ?? '') : '')); } function buildTemplateVars() { const info = currentMovieInfo || {}; const src = (info && info.source) || ''; const url = (info && info.url) || ''; const isDouban = src === '豆瓣' || (url && url.includes('douban.com')); const isTmdb = src === 'TMDB' || (url && url.includes('themoviedb.org')); const isImdb = src === 'IMDB' || (url && url.includes('imdb.com')); const idLineHtml = (function(){ if (isDouban) { const id = info.doubanId || '—'; const link = url || (info.doubanId ? `https://movie.douban.com/subject/${info.doubanId}/` : ''); return link ? `豆瓣 <a href="${link}" target="_blank" rel="noopener">${id}</a>` : `豆瓣 ${id}`; } if (isTmdb) { const id = info.tmdbId || '—'; const link = info.tmdbId ? `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}` : (url || ''); return link ? `TMDB <a href="${link}" target="_blank" rel="noopener">${id}</a>` : `TMDB ${id}`; } if (info.imdbId) { const link = `https://www.imdb.com/title/${info.imdbId}/`; return `IMDB <a href="${link}" target="_blank" rel="noopener">${info.imdbId}</a>`; } return '—'; })(); // 评分来源标签与标识三元组 const ratingSource = isDouban ? '豆瓣' : (isTmdb ? 'TMDB' : (isImdb ? 'IMDB' : '评分')); let markerType = '—', markerLink = '', markerId = '—'; if (isDouban && (info.doubanId || url)) { markerType = '豆瓣'; markerId = info.doubanId || '—'; markerLink = url || (info.doubanId ? `https://movie.douban.com/subject/${info.doubanId}/` : ''); } else if (isTmdb && info.tmdbId) { markerType = 'TMDB'; markerId = info.tmdbId; markerLink = `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}`; } else if (isImdb && info.imdbId) { markerType = 'IMDB'; markerId = info.imdbId; markerLink = `https://www.imdb.com/title/${info.imdbId}/`; } // 自动简介:优先 info.intro,否则简短占位 const introAuto = (info.intro && String(info.intro).trim()) ? info.intro : '暂无剧情简介,欢迎补充你的观影理解~'; // 自动热评:从 currentComments 取首条 let commentsAuto = '<p style="margin:0;color:#666;font-size:15px;">暂无热评,分享你的观影感受吧~</p>'; try { if (Array.isArray(currentComments) && currentComments.length > 0) { const first = currentComments[0]; const quote = (first && (first.content || first.text || first.comment || '')).toString().trim(); const author = (first && (first.author || first.user || first.nickname || '匿名')); if (quote) { commentsAuto = `<p style=\"margin:0 0 8px 0;font-size:16px;color:#c2185b;line-height:1.7;\">\"${quote}\"</p><p style=\"margin:0;text-align:right;color:#e64a19;font-style:italic;font-size:14px;\">—— ${isDouban ? '豆瓣用户' : '用户'} @${author}</p>`; } } else if (isTmdb) { // TMDB无热评:用剧情/标语摘录 + TMDB更多评价链接 const raw = (info.intro || info.tagline || '').toString().trim(); const snippet = raw ? (raw.length > 80 ? raw.slice(0, 78) + '…' : raw) : `类型:${(Array.isArray(info.genreTags) ? info.genreTags.join(' / ') : (info.genres || '影片'))}`; const rvLink = info.tmdbId ? `https://www.themoviedb.org/${info.mediaType || 'movie'}/${info.tmdbId}/reviews` : ''; const linkHtml = rvLink ? ` <a href=\"${rvLink}\" target=\"_blank\" rel=\"noopener\" style=\"color:#e64a19;text-decoration:none;border-bottom:1px dashed #ffccbc;\">更多评价</a>` : ''; commentsAuto = `<p style=\"margin:0 0 8px 0;font-size:16px;color:#c2185b;line-height:1.7;\">\"${snippet}\"</p><p style=\"margin:0;text-align:right;color:#e64a19;font-style:italic;font-size:14px;\">—— 看点摘录${linkHtml}</p>`; } } catch (e) {} // 热评首条提供给模板2的精简占位 let commentQuote = ''; let commentAuthor = ''; let commentSourceLabel = isDouban ? '豆瓣用户' : (isTmdb ? 'TMDB用户' : '用户'); try { if (Array.isArray(currentComments) && currentComments.length > 0) { const first = currentComments[0]; commentQuote = (first && (first.content || first.text || first.comment || '')).toString().trim() || ''; commentAuthor = (first && (first.author || first.user || first.nickname || '匿名')) || ''; } } catch (e) {} if (!commentQuote) { commentQuote = '暂无热评,分享你的观影感受吧~'; commentAuthor = '匿名'; } // 演员截断:避免撑开导致中间空隙,保留前若干字符与人名数量 const actorShort = (function(){ const raw = info.actor || ''; if (!raw) return '未知'; // 先按顿号/逗号/空格拆分,最多取前6个;若依旧很长,再做总长度截断 const names = String(raw).split(/[、,,\s]+/).filter(Boolean).slice(0, 6); let txt = names.join('、'); if (txt.length > 38) txt = txt.slice(0, 36) + '…'; return txt; })(); // 展示用兜底字段,避免模板出现未替换占位 const directorDisplay = info.director || '未知'; const writerDisplay = info.writer || '未知'; const genresDisplay = (Array.isArray(info.genreTags) ? info.genreTags.filter(Boolean).join(' / ') : (info.genres || '')) || '未知'; const regionDisplay = info.region || '未知'; const filmLanguage = info.language || info.lang || ''; const filmLanguageDisplay = filmLanguage || '未知'; const runtimeDisplay = info.runtime || '未知'; const releaseDisplay = (info.release || info.releaseDate || '') || '未知'; const actorDisplay = (actorShort || info.actor || '').trim() || '未知'; return { title: info.title || info.originalTitle || '标题', originalTitle: info.originalTitle || '', originalTitleSafe: info.originalTitle || '无原名', originalTitleParen: (info.originalTitle && info.originalTitle !== (info.title || '')) ? `(${info.originalTitle})` : '', director: info.director || '', writer: info.writer || '', actor: info.actor || '', actorShort, originalAuthor: info.originalAuthor || info.writer || '', genres: Array.isArray(info.genreTags) ? info.genreTags.join(' / ') : (info.genres || ''), genreTags: Array.isArray(info.genreTags) ? info.genreTags : [], release: info.release || info.releaseDate || '', region: info.region || '', runtime: info.runtime || '', filmLanguage, doubanId: info.doubanId || '', imdbId: info.imdbId || '', idLineHtml, posterUrl: selectedPosterUrl || 'https://via.placeholder.com/680x480/ff69b4/FFF?text=Poster', sceneUrl: selectedStillUrl || 'https://via.placeholder.com/640x340/ff69b4/FFF?text=Scene', rating: info.rating || '—', introAuto, commentsAuto, ratingSource, markerType, markerLink, markerId, commentQuote, commentAuthor, commentSourceLabel, detailTip: info.detailTip || '镜头语言设计', watchWith: info.watchWith || '家人/朋友', watchScene: info.watchScene || '周末午后', // 展示用(模板2使用) directorDisplay, writerDisplay, genresDisplay, regionDisplay, filmLanguageDisplay, runtimeDisplay, releaseDisplay, actorDisplay }; } function getCurrentEditor() { // 先检查已经缓存的sourceCodeElement if (sourceCodeElement && sourceCodeElement.offsetParent !== null) { console.log('Found cached sourceCodeElement:', sourceCodeElement); return { type: 'textarea', instance: sourceCodeElement }; } // 检查CodeMirror编辑器 const codeMirror = document.querySelector('.CodeMirror'); if (codeMirror && codeMirror.CodeMirror) { console.log('Found CodeMirror editor'); return { type: 'codemirror', instance: codeMirror.CodeMirror }; } // 扩展的编辑器选择器列表,覆盖更多可能的编辑器类型 const editorSelectors = [ '#myModal-code textarea', 'textarea.tox-textarea', 'textarea.mce-textbox', 'textarea.cke_source', 'textarea[name="message"]', '#editor_content', 'textarea[name="content"]', // 常见的内容输入框 '#post_content', // 论坛常见的内容输入框 'textarea#content', // ID为content的textarea '.editor-content textarea', // 带有editor-content类的容器内的textarea '#post_message', // 论坛发帖编辑器 '.article-editor textarea', // 文章编辑器 'div[contenteditable="true"]', // 富文本编辑区 '.prose-editor', // 专业编辑器 'textarea[id^="editor_"]', // ID以editor_开头的textarea 'textarea[id$="_editor"]', // ID以_editor结尾的textarea 'textarea.editor' ]; for (const selector of editorSelectors) { const elem = document.querySelector(selector); if (elem && elem.style.display !== 'none' && elem.offsetParent !== null) { console.log('Found editor with selector:', selector); sourceCodeElement = elem; return { type: 'textarea', instance: elem }; } } // 最后的尝试:查找页面上所有可见的textarea const allTextareas = document.querySelectorAll('textarea'); for (let i = 0; i < allTextareas.length; i++) { const textarea = allTextareas[i]; if (textarea && textarea.style.display !== 'none' && textarea.offsetParent !== null && textarea.offsetWidth > 100 && textarea.offsetHeight > 100) { console.log('Found visible textarea as fallback'); sourceCodeElement = textarea; return { type: 'textarea', instance: textarea }; } } if (!editorNotFoundLogged) { console.log('No editor found'); editorNotFoundLogged = true; } return null; } // 修改绑定按钮事件(确保加载更多功能正常) function bindEventListeners() { // 绑定AI相关事件监听器 bindAIEventListeners(); let fetchBtn = document.getElementById('fetch-btn'); const mediaUrlInput = document.getElementById('media-url'); const pasteBtn = document.getElementById('paste-btn'); const clearBtn = document.getElementById('clear-btn'); const confirmImagesBtn = document.getElementById('confirm-images-btn'); const loadMorePostersBtn = document.getElementById('load-more-posters'); const loadMoreStillsBtn = document.getElementById('load-more-stills'); // 调试代码:检查按钮状态 if (fetchBtn) { console.log('提取按钮已找到,初始状态:', { classList: fetchBtn.classList.toString(), style: { pointerEvents: fetchBtn.style.pointerEvents, cursor: fetchBtn.style.cursor, opacity: fetchBtn.style.opacity } }); // 添加状态变化监听 const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { console.log('按钮class变化:', fetchBtn.classList.toString()); } }); }); observer.observe(fetchBtn, { attributes: true }); } // 输入URL时激活提取按钮 - 已移至按钮初始化中,避免重复监听 // 提取影视信息按钮 if (fetchBtn) { // 移除所有可能的现有事件监听器 const newFetchBtn = fetchBtn.cloneNode(true); fetchBtn.parentNode.replaceChild(newFetchBtn, fetchBtn); // 重新获取按钮引用 const updatedFetchBtn = document.getElementById('fetch-btn'); // 根据输入框内容决定按钮状态 if (mediaUrlInput && mediaUrlInput.value.trim()) { updatedFetchBtn.classList.add('active'); updatedFetchBtn.style.pointerEvents = 'auto'; updatedFetchBtn.style.cursor = 'pointer'; updatedFetchBtn.style.opacity = '1'; updatedFetchBtn.style.display = 'block'; } else { updatedFetchBtn.classList.remove('active'); updatedFetchBtn.style.pointerEvents = 'none'; updatedFetchBtn.style.cursor = 'not-allowed'; updatedFetchBtn.style.opacity = '0.6'; updatedFetchBtn.style.display = 'none'; } // 确保按钮在页面加载时根据当前输入框内容正确激活 if (mediaUrlInput) { const checkButtonState = () => { if (mediaUrlInput.value.trim()) { updatedFetchBtn.classList.add('active'); updatedFetchBtn.style.pointerEvents = 'auto'; updatedFetchBtn.style.cursor = 'pointer'; updatedFetchBtn.style.opacity = '1'; updatedFetchBtn.style.display = 'block'; } else { updatedFetchBtn.classList.remove('active'); updatedFetchBtn.style.pointerEvents = 'none'; updatedFetchBtn.style.cursor = 'not-allowed'; updatedFetchBtn.style.opacity = '0.6'; updatedFetchBtn.style.display = 'none'; } }; // 立即检查一次 checkButtonState(); // 监听输入变化 mediaUrlInput.addEventListener('input', checkButtonState); } // 绑定新的点击事件处理函数 updatedFetchBtn.addEventListener('click', async function (e) { console.log('提取按钮被点击'); // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); const url = document.getElementById('media-url')?.value.trim(); if (!url) { showStatus('请输入影视链接', true); return; } showStatus('正在提取影视信息...', false); try { currentMovieInfo = await getBasicInfo(url); currentComments = await getHotComments(url); showStatus('信息提取完成,请选择海报和剧照', false); await showImageSelection(currentMovieInfo); } catch (err) { showStatus(`提取失败:${err.message || '未知错误'}`, true); console.error('提取错误:', err); } }); // 更新引用 fetchBtn = updatedFetchBtn; // 确保按钮状态与输入框内容同步 if (mediaUrlInput && mediaUrlInput.value.trim()) { updatedFetchBtn.classList.add('active'); updatedFetchBtn.style.pointerEvents = 'auto'; updatedFetchBtn.style.cursor = 'pointer'; updatedFetchBtn.style.opacity = '1'; updatedFetchBtn.style.display = 'block'; } else { updatedFetchBtn.classList.remove('active'); updatedFetchBtn.style.pointerEvents = 'none'; updatedFetchBtn.style.cursor = 'not-allowed'; updatedFetchBtn.style.opacity = '0.6'; updatedFetchBtn.style.display = 'none'; } } // 手动粘贴内容按钮 if (pasteBtn) { pasteBtn.addEventListener('click', async function (e) { // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); const backupHtml = document.getElementById('backup-html').value; if (backupHtml) { await autoClickSourceBtn(); const filled = await autoFillSourceBox(backupHtml); if (filled) { showStatus('内容已粘贴到编辑框', false); } else { showStatus('内容粘贴失败,请手动粘贴剪贴板内容', true); } } }); } // 清除所有内容按钮 if (clearBtn) { clearBtn.addEventListener('click', function (e) { // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); if (mediaUrlInput) mediaUrlInput.value = ''; const searchInput = document.getElementById('search-movie'); if (searchInput) searchInput.value = ''; const searchResults = document.getElementById('search-results'); if (searchResults) { searchResults.classList.remove('show'); searchResults.style.visibility='hidden'; searchResults.style.opacity='0'; searchResults.style.pointerEvents='none'; } const imageSelection = document.getElementById('image-selection'); if (imageSelection) imageSelection.style.display = 'none'; if (posterContainer) posterContainer.innerHTML = ''; if (stillContainer) stillContainer.innerHTML = ''; if (fetchBtn) { fetchBtn.classList.remove('active'); fetchBtn.style.display = 'none'; fetchBtn.style.pointerEvents = 'none'; fetchBtn.style.cursor = 'not-allowed'; fetchBtn.style.opacity = '0.6'; } selectedPosterUrl = ''; selectedStillUrl = ''; currentMovieInfo = null; currentComments = []; // 重置页码 posterPage = 1; stillPage = 1; showStatus('已清除所有内容', false); }); } // 确认选择并填充按钮 if (confirmImagesBtn) { confirmImagesBtn.addEventListener('click', async function (e) { // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); if (!currentMovieInfo) { showStatus('未找到影视信息,请重新加载', true); return; } // 兜底:优先读取已选元素的dataset.url,保证首次不点击也能拿到生效后的dataURL try { if ((!selectedPosterUrl || /doubanio\.com/.test(selectedPosterUrl)) && selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.url) { selectedPosterUrl = selectedPosterEl.dataset.url; } } catch (e) {} try { if ((!selectedStillUrl || /doubanio\.com/.test(selectedStillUrl)) && selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.url) { selectedStillUrl = selectedStillEl.dataset.url; } } catch (e) {} // 保障:控件里可显示缩略,进入排版确保原图 try { // Douban:若仍是直链且疑似 m/s 尺寸,则拉取 raw/l 为 DataURL if (/^https?:\/\/.*doubanio\.com\//.test(selectedPosterUrl)) { const rawBase = selectedPosterEl && selectedPosterEl.dataset && selectedPosterEl.dataset.rawUrl ? selectedPosterEl.dataset.rawUrl : selectedPosterUrl; selectedPosterUrl = await getImageDataURLWithQuality(rawBase); } } catch(_) {} try { if (/^https?:\/\/.*doubanio\.com\//.test(selectedStillUrl)) { const rawBase = selectedStillEl && selectedStillEl.dataset && selectedStillEl.dataset.rawUrl ? selectedStillEl.dataset.rawUrl : selectedStillUrl; selectedStillUrl = await getImageDataURLWithQuality(rawBase); } } catch(_) {} const finalPosterUrl = toTMDBOriginal(selectedPosterUrl || '') || 'https://picsum.photos/200/300?default-poster'; const finalStillUrl = toTMDBOriginal(selectedStillUrl || '') || 'https://picsum.photos/300/180?default-still'; // 模板阀门:优先使用已选模板 let useTemplate = false; let html; try { const REG = window.__TEMPLATE__; if (REG && REG.CURRENT_ID) { const chosen = REG.REGISTRY.find(t => t.id === REG.CURRENT_ID); if (chosen) { useTemplate = true; html = compileTemplate(chosen.content, buildTemplateVars()); // 应用后清空选中与高亮 document.querySelectorAll('#template-toolbar button[data-tpl-id]').forEach(b => { b.style.background = '#f472b6'; b.style.color = '#fff'; b.style.border = '1px solid transparent'; b.style.boxShadow = '0 1px 3px rgba(244,114,182,.2)'; }); REG.CURRENT_ID = null; } } } catch (e) { /* 忽略模板异常,回退到原排版 */ } if (!useTemplate) { showStatus('正在生成HTML内容...', false); html = generateHTML(currentMovieInfo, currentComments, finalPosterUrl, finalStillUrl); } else { showStatus('正在应用模板并生成内容...', false); } // 异常兜底:本地存储备份,防止填充失败 try { localStorage.setItem('backup-movie-html', html); } catch (localStorageError) { console.log('localStorage备份失败:', localStorageError); } const backupHtml = document.getElementById('backup-html'); if (backupHtml) backupHtml.value = html; // 统一逻辑:直接写入并自动保存 const success = await writeHtmlToAnyEditor(html); try { await autoClickSaveBtn(); } catch(_) {} showStatus(success ? (useTemplate ? '模板已写入并自动保存' : '内容已写入并自动保存') : '写入失败,已提供剪贴板备份', !success); }); } // 绑定加载更多海报按钮(确保点击有效) if (loadMorePostersBtn) { // 先移除旧事件(避免重复绑定) loadMorePostersBtn.removeEventListener('click', loadMorePosters); // 创建新的包装函数以添加事件阻止 const wrappedLoadMorePosters = function(e) { // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); loadMorePosters(); }; loadMorePostersBtn.addEventListener('click', wrappedLoadMorePosters); } // 绑定加载更多剧照按钮(确保点击有效) if (loadMoreStillsBtn) { // 先移除旧事件(避免重复绑定) loadMoreStillsBtn.removeEventListener('click', loadMoreStills); // 创建新的包装函数以添加事件阻止 const wrappedLoadMoreStills = function(e) { // 触发阻断:阻止事件冒泡,防止触发表单验证 e.stopPropagation(); e.preventDefault(); loadMoreStills(); }; loadMoreStillsBtn.addEventListener('click', wrappedLoadMoreStills); } // 初始化搜索交互 setupSearchInteractions(); // 设置海报/剧照容器的事件委托(避免为每个item单独绑定) setupImageSelectionDelegates(); // 确保移动端适配在绑定事件后应用 applyMobileStyles(); } function setupImageSelectionDelegates() { if (!posterContainer) posterContainer = document.getElementById('poster-candidates'); if (!stillContainer) stillContainer = document.getElementById('still-candidates'); // 保证默认选中元素引用与dataset同步(首次渲染后立即记录) try { const firstPoster = posterContainer && posterContainer.firstElementChild; if (firstPoster && !selectedPosterEl) selectedPosterEl = firstPoster; const firstStill = stillContainer && stillContainer.firstElementChild; if (firstStill && !selectedStillEl) selectedStillEl = firstStill; } catch (e) {} if (posterContainer && !posterContainer.dataset.delegateBound) { posterContainer.addEventListener('click', function (e) { const item = e.target.closest('div'); if (!item || !posterContainer.contains(item) || !item.dataset || !item.dataset.url) return; const url = item.dataset.url; selectedPosterUrl = url; if (selectedPosterEl && selectedPosterEl !== item) { selectedPosterEl.style.border = '1px solid #f3d5d9'; selectedPosterEl.style.boxShadow = 'none'; selectedPosterEl.style.padding = '0px'; } // 使用粉色外框+偏移留白作为选中状态 item.style.border = '3px solid #ec4899'; item.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; item.style.padding = '2px'; selectedPosterEl = item; }); posterContainer.dataset.delegateBound = '1'; } if (stillContainer && !stillContainer.dataset.delegateBound) { stillContainer.addEventListener('click', function (e) { const item = e.target.closest('div'); if (!item || !stillContainer.contains(item) || !item.dataset || !item.dataset.url) return; const url = item.dataset.url; selectedStillUrl = url; if (selectedStillEl && selectedStillEl !== item) { selectedStillEl.style.border = '1px solid #f3d5d9'; selectedStillEl.style.boxShadow = 'none'; selectedStillEl.style.padding = '0px'; } // 使用粉色外框+偏移留白作为选中状态 item.style.border = '3px solid #ec4899'; item.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; item.style.padding = '2px'; selectedStillEl = item; }); stillContainer.dataset.delegateBound = '1'; } } // 绑定AI相关事件监听器 function bindAIEventListeners() { if (aiEventsBound) return; // 避免重复绑定导致多次请求 // 先确保AI面板内容已正确生成并插入 const aiPanelContainer = document.getElementById('ai-panel-container'); if (aiPanelContainer && aiPanelContainer.innerHTML.trim() === '') { // 使用传统的DOM操作方式插入AI面板内容 aiPanelContainer.innerHTML = createAIPanelContent(); } // 重新排序:将“AI检索与图片选择”整体移动到“生成提示”上方 try { const promptDiv = document.getElementById('ai-prompt-input')?.parentElement; const searchWrap = document.getElementById('ai-search-input')?.closest('div[style*="border:1px solid"]'); if (promptDiv && searchWrap && promptDiv.previousSibling !== searchWrap) { promptDiv.parentElement.insertBefore(searchWrap, promptDiv); } } catch (_) {} // 获取所有需要的元素 const aiFunctionSelect = document.getElementById('ai-function-select'); const aiPromptInput = document.getElementById('ai-prompt-input'); const generateAiTextBtn = document.getElementById('generate-ai-text'); const abortAiBtn = document.getElementById('abort-ai-generate'); const aiResultArea = document.getElementById('ai-result-area'); const aiResultContent = document.getElementById('ai-result-content'); // 选择结果忙碌锁,防止重复点击 let aiResultsBusy = false; // 新增:AI 检索与图片区域元素 const aiSearchInput = document.getElementById('ai-search-input'); const aiSearchStatus = document.getElementById('ai-search-status'); const aiSearchResults = document.getElementById('ai-search-results'); const aiImageSelection = document.getElementById('ai-image-selection'); const aiPosterContainer = document.getElementById('ai-poster-candidates'); const aiStillContainer = document.getElementById('ai-still-candidates'); const aiLoadMorePosters = document.getElementById('ai-load-more-posters'); const aiLoadMoreStills = document.getElementById('ai-load-more-stills'); const aiAutoTitle = document.getElementById('ai-auto-title'); const aiDeepThink = document.getElementById('ai-deep-think'); const aiWebBrowse = document.getElementById('ai-web-browse'); const aiFeatureTip = document.getElementById('ai-feature-tip'); // 根据模型能力开启/禁用“深度思考/联网补充” (function initFeatureToggles(){ const cfg = getConfig(); const model = (cfg.AI.DEFAULT_MODEL||'').toLowerCase(); const provider = (cfg.AI.PROVIDER||'').toLowerCase(); let supportsDeep = /gpt-4|gpt-4o|gpt-4\.1|claude-3|sonnet|gemini|glm|qwen|deepseek|mixtral/.test(model) || /openai|anthropic|gemini|bigmodel|aliyuncs|ark|together/.test(provider); let supportsWeb = /gpt-4o|gpt-4\.1|gemini|qwen|glm|deepseek/.test(model) || /gemini|aliyuncs|bigmodel/.test(provider); if (aiDeepThink) { aiDeepThink.disabled = !supportsDeep; const wrap = document.getElementById('ai-deep-wrap'); if (wrap) wrap.style.opacity = supportsDeep? '1' : '0.5'; } if (aiWebBrowse) { aiWebBrowse.disabled = !supportsWeb; const wrap = document.getElementById('ai-web-wrap'); if (wrap) wrap.style.opacity = supportsWeb? '1' : '0.5'; } if (aiFeatureTip) { aiFeatureTip.textContent = (!supportsDeep || !supportsWeb) ? '当前模型部分功能不可用' : ''; } })(); // 已移除AI“搜索”按钮,以下为通用的状态展示工具 function setAiSearchLoading(show, text = '正在加载...') { if (!aiSearchStatus) return; if (show) { aiSearchStatus.style.display = 'block'; aiSearchStatus.textContent = text; } else { aiSearchStatus.style.display = 'none'; } } async function renderAiImageSelection(info){ aiImageSelection.style.display = 'block'; aiPosterContainer.innerHTML = ''; aiStillContainer.innerHTML = ''; // 切换影片时清空AI区域多选集合 try { aiSelectedPosterUrls.clear(); aiSelectedStillUrls.clear(); } catch (e) {} // 记录AI侧影片信息 aiCurrentMovieInfo = info || null; const posters = info.posterUrls || []; const stills = info.stillUrls || []; const buildCard = (rawUrl, type) => { const card = document.createElement('div'); card.style.cssText = 'width:100%;height:'+(type==='poster'?'180px':'120px')+';border:1px solid #f3d5d9;border-radius:6px;cursor:pointer;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#fff5f7;'; const img = document.createElement('img'); img.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;'; img.src = LAZY_PLACEHOLDER; card.appendChild(img); card.dataset.url = rawUrl; card.onclick = function(){ try { const chosen = this.dataset.url; const isSelected = this.style.border && this.style.border.indexOf('3px solid')!==-1; if (isSelected) { this.style.border = '1px solid #f3d5d9'; this.style.boxShadow = 'none'; this.style.padding = '0px'; if (type==='poster') { aiSelectedPosterUrls.delete(chosen); } else { aiSelectedStillUrls.delete(chosen); } } else { this.style.border = '3px solid #ec4899'; this.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.3)'; this.style.padding = '2px'; if (type==='poster') { aiSelectedPosterUrls.add(chosen); } else { aiSelectedStillUrls.add(chosen); } } } catch(e){} }; // 懒加载真实缩略图 getThumbnailForDisplay(rawUrl).then(src=>{ img.src = src; }); // 预升级为更高清尺寸或DataURL const upgrade = shouldConvertToDataURL(rawUrl) ? getImageDataURLWithQuality(rawUrl) : Promise.resolve( type === 'poster' ? rawUrl.replace(`/${getConfig().TMDB.LIST_POSTER_SIZE}/`, `/${getConfig().TMDB.SELECTED_POSTER_SIZE}/`) : rawUrl.replace(`/${getConfig().TMDB.LIST_STILL_SIZE}/`, `/${getConfig().TMDB.SELECTED_STILL_SIZE}/`) ); upgrade.then(du=>{ card.dataset.url = du; if (type==='poster') { if (aiSelectedPosterUrls.has(rawUrl)) { aiSelectedPosterUrls.delete(rawUrl); aiSelectedPosterUrls.add(du); } } else { if (aiSelectedStillUrls.has(rawUrl)) { aiSelectedStillUrls.delete(rawUrl); aiSelectedStillUrls.add(du); } } }); return card; }; // 如果接口暂时未返回图片,主动拉取首批 let posterList = posters; let stillList = stills; if ((!posterList || posterList.length===0) && info.url) { try { posterList = await getDoubanOfficialPosters(info.url, 1); } catch(e) {} } if ((!stillList || stillList.length===0) && info.url) { try { stillList = await getDoubanStillsList(info.url, 1); } catch(e) {} } // 批量DOM插入,降低重排 const pFrag = document.createDocumentFragment(); (posterList||[]).forEach(u=>{ pFrag.appendChild(buildCard(u, 'poster')); }); aiPosterContainer.appendChild(pFrag); const sFrag = document.createDocumentFragment(); (stillList||[]).forEach(u=>{ sFrag.appendChild(buildCard(u, 'still')); }); aiStillContainer.appendChild(sFrag); // 当列表条数>=首批数时显示“加载更多” const pageSize = getConfig().TMDB.IMAGE_CANDIDATES_COUNT || 5; const hasMorePosters = (posterList && posterList.length >= pageSize); const hasMoreStills = (stillList && stillList.length >= pageSize); if (aiLoadMorePosters) { aiLoadMorePosters.style.display = hasMorePosters ? 'inline-block' : 'none'; } if (aiLoadMoreStills) { aiLoadMoreStills.style.display = hasMoreStills ? 'inline-block' : 'none'; } } // 输入时自动弹出搜索结果列表(500ms防抖) if (aiSearchInput) { let debounceTimer; aiSearchInput.addEventListener('input', function(){ clearTimeout(debounceTimer); const v = this.value.trim(); if (!v) { if (aiSearchResults) aiSearchResults.style.display = 'none'; return; } debounceTimer = setTimeout(async ()=>{ if (isMainFlowActive) return; // 主流程激活时隔离AI检索 setAiSearchLoading(true, '搜索中...'); const [d, t] = await Promise.all([searchDouban(v).catch(()=>[]), searchTMDB(v).catch(()=>[])]); const unique = rankAndDedupResults([...(d||[]), ...(t||[])], v).slice(0,60); aiSearchResults.style.display = 'block'; aiSearchResults.innerHTML = ''; displaySearchResults(unique, aiSearchResults); // 重新绑定点击(避免多次搜索后因旧监听失效而无反应) aiSearchResults.onclick = null; aiResultsBusy = false; aiSearchResults.addEventListener('click', async (ev)=>{ const itemEl = ev.target.closest('.search-item'); if (!itemEl) return; const url = itemEl.getAttribute('data-url'); if (!url) return; if (aiResultsBusy) return; aiResultsBusy = true; setAiSearchLoading(true, '正在加载图片...'); const detail = await getBasicInfo(url); await renderAiImageSelection(detail); // 自动把选中的片名写入提示词(可开关,且会替换旧的“目标影视:”行) try { if (aiAutoTitle && aiAutoTitle.checked) { const p = document.getElementById('ai-prompt-input'); if (p) { const title = (detail && detail.title) || (itemEl.querySelector('strong')?.textContent)||''; if (title) { // 移除旧的“目标影视:xxx”行 let cleaned = (p.value || '').replace(/^\s*目标影视:.*$/gm, '').trim(); p.value = (cleaned ? cleaned + '\n' : '') + `目标影视:${title}`; } } } // 若关掉开关,则清理已有的“目标影视:”行 else { const p = document.getElementById('ai-prompt-input'); if (p) p.value = (p.value || '').replace(/^\s*目标影视:.*$/gm, '').trim(); } } catch(_) {} setAiSearchLoading(false); aiResultsBusy = false; }); setAiSearchLoading(false); }, 500); }); } const copyAiResultBtn = document.getElementById('copy-ai-result'); const insertAiResultBtn = document.getElementById('insert-ai-result'); // 绑定AI角色设置按钮点击事件 - 已移除入口,保留空函数避免引用错误 function setupAIRoleSettingsBtn() { return; } // 立即执行一次 setupAIRoleSettingsBtn(); // 已移除轮询与降级逻辑,避免无意义的DOM操作 // 切换AI功能类型时更新提示框占位符 if (aiFunctionSelect && aiPromptInput) { aiFunctionSelect.addEventListener('change', function() { const selectedId = this.value; const config = getConfig(); const selectedFeature = config.AI.FEATURES.find(f => f.id === selectedId); if (selectedFeature && selectedFeature.placeholder) { aiPromptInput.placeholder = selectedFeature.placeholder; } }); } // 生成AI文本 if (generateAiTextBtn) { // 防抖:避免重复触发导致多次请求 let isGenerating = false; generateAiTextBtn.addEventListener('click', async function(e) { e.stopPropagation(); e.preventDefault(); if (isGenerating) return; isGenerating = true; if (abortAiBtn) abortAiBtn.style.display = 'inline-block'; const functionType = aiFunctionSelect.value; const promptText = aiPromptInput.value.trim(); const styleSelectionEl = document.getElementById('ai-style-select'); const styleChoice = styleSelectionEl ? (styleSelectionEl.value || '') : ''; const config = getConfig(); const apiEndpoint = config.AI.API_ENDPOINT; const apiKey = config.AI.API_KEY; if (!promptText) { showStatus('请输入生成提示', true, 'ai'); return; } if (!apiEndpoint) { showStatus('请配置AI API端点', true, 'ai'); return; } // 显示加载状态 generateAiTextBtn.disabled = true; generateAiTextBtn.innerHTML = '<i class="fa fa-spinner fa-spin" style="margin-right:5px;"></i>生成中...'; aiResultArea.style.display = 'none'; aiResultContent.textContent = ''; try { // 获取AI侧影片信息(与主流程隔离) let movieContext = ''; const refInfo = aiCurrentMovieInfo || currentMovieInfo; if (refInfo) { movieContext = `\n\n参考影视信息:\n名称:${refInfo.title}\n类型:${refInfo.genreTags?.join('、') || '未知'}\n简介:${refInfo.intro?.substring(0, 300) || '暂无简介'}`; } // 获取编辑框内容作为上下文 let editorContext = ''; const editor = getCurrentEditor(); if (editor) { let editorContent = ''; if (editor.type === 'codemirror') { editorContent = editor.instance.getValue(); } else if (editor.instance) { // 处理普通textarea if (editor.instance.value) { editorContent = editor.instance.value; } // 处理contenteditable元素 else if (editor.instance.getAttribute('contenteditable') === 'true') { editorContent = editor.instance.textContent || editor.instance.innerText; } } if (editorContent && editorContent.trim()) { console.log('Successfully got editor content, length:', editorContent.length); // 只取前1000个字符,避免上下文过长 const trimmedContent = editorContent.substring(0, 1000); editorContext = `\n\n编辑框内容(作为参考):\n${trimmedContent}`; } else { console.log('Editor found but content is empty'); } } else { if (!editorNotFoundLogged) { console.log('No editor found'); editorNotFoundLogged = true; } } // 生成完整的提示词(附加固定框架提示+预设模板+已选图片URL) // 去掉“让AI学习排版文件”的旧提示拼接 const presetHead = ''; const presetText = ''; // 收集多选图片 // 仅使用AI面板选择的图片,绝不读取主工具选择 const posterListForAI = Array.from(aiSelectedPosterUrls || []); const stillListForAI = Array.from(aiSelectedStillUrls || []); // 向后兼容:若无多选,则回退到单选值 // 不回退到主工具选择:无选择则按“无图”策略 // 控制上下文体积,避免超长 const maxImagesForPrompt = 6; const posterForPrompt = posterListForAI.slice(0, maxImagesForPrompt); const stillForPrompt = stillListForAI.slice(0, maxImagesForPrompt); // 将长链接压缩为可识别的短标识(避免占用大量token) const toShortRef = (u)=>{ try { const s = String(u); if (/doubanio\.com/.test(s)) { const m = s.match(/\/p(\d+)/); // 取图片ID return m ? `db:p${m[1]}` : 'db:img'; } if (/image\.tmdb\.org\/t\/p\//.test(s)) { const m = s.match(/t\/p\/[^/]+\/([^/?#]+)/); return m ? `tmdb:${m[1].slice(0,16)}` : 'tmdb:img'; } // 其他来源:仅保留文件名 const tail = s.split('/')[(s.split('/').length-1)] || ''; return tail ? tail.slice(0,18) : 'img'; } catch(_) { return 'img'; } }; const posterRefs = posterForPrompt.map(toShortRef); const stillRefs = stillForPrompt.map(toShortRef); const imageContext = `${posterRefs.length ? `\n\n[海报参考ID] ${posterRefs.length} 张\n${posterRefs.join(' ')}` : '\n\n[海报参考ID] 0 张'}${stillRefs.length ? `\n\n[剧照参考ID] ${stillRefs.length} 张\n${stillRefs.join(' ')}` : '\n\n[剧照参考ID] 0 张'}`; // 影片基础信息上下文(确保标题/导演/主演/评分/海报/简介/热评) const base = aiCurrentMovieInfo || currentMovieInfo || {}; const safeIntro = (base.intro||'').replace(/\s+/g,' ').slice(0,800); const safeComments = (base.comments && base.comments.length)?`\n观众热评:${base.comments.slice(0,3).map(c=>c.content.slice(0,140)).join(' / ')}`:''; const baseInfo = `\n\n[影片信息]\n标题:${base.title||''}\n原名:${base.originalTitle||''}\n又名:${base.alsoKnown||''}\n类型:${(base.genreTags||[]).join('、')}\n地区:${base.region||''}\n语言:${base.lang||''}\n上映:${base.release||''}\n片长:${base.runtime||''}\n导演:${base.director||''}\n编剧:${base.writer||''}\n主演:${base.actor||''}\nIMDb:${base.imdbId||''}\n评分:${base.rating||''}\n关键词:${(base.keywords||'').toString().slice(0,120)}\n平台:${(base.streamingPlatforms||[]).join('、').slice(0,60)}\n获奖:${(base.awards||[]).join('、').slice(0,120)}\n海报参考:${posterRefs[0]||''}\n剧情简介:${safeIntro}${safeComments}`; const styleHint = styleChoice ? ( styleChoice === '万象合流' ? `\n\n[风格要求]\n采用“万象合流”混合风格:可跨文化、跨体裁自适应(专业严谨/简洁实用/活泼有趣/学术/幽默/文艺/复古/赛博/国潮等),按影片题材与情绪在段落级别灵活切换,但整体语气与视觉层级保持一致、自然、可读。` : `\n\n[风格要求]\n请以“${styleChoice}”风格组织语言与排版。` ) : ''; // 标题锁定,避免串片 const refTitle = (base && base.title) ? String(base.title) : ''; const lockTitle = refTitle ? `\n\n[标题锁定]\n仅针对《${refTitle}》生成内容,禁止替换为其他影片。若无足够信息请明确标注“未知”。` : ''; // 无图策略:明确指令不生成任何图片容器 const noImageRule = (!posterRefs.length && !stillRefs.length) ? '\n\n[无图模式]\n禁止输出任何与图片相关的标题、容器或占位(包括海报、剧照、图集等),按纯文本/信息卡排版。' : ''; const combinedPrompt = `${presetHead}\n\n${presetText}\n\n${promptText}${styleHint}${lockTitle}${imageContext}${noImageRule}${baseInfo}`; // 组合并裁剪提示,避免超长 const merged = getAIPromptByType(functionType, combinedPrompt, movieContext + editorContext); const MAX_PROMPT_CHARS = 60000; // 兼容多数提供商 const fullPrompt = merged.length > MAX_PROMPT_CHARS ? merged.slice(0, MAX_PROMPT_CHARS) : merged; // 调用AI API生成文本 const aiResult = await generateAIText(apiEndpoint, apiKey, fullPrompt); // 显示结果 aiResultContent.textContent = aiResult; aiResultArea.style.display = 'block'; showStatus('AI文本生成成功', false, 'ai'); // 确保搜索相关元素状态正常恢复 setTimeout(() => { const searchInput = document.getElementById('search-movie'); const fetchBtn = document.getElementById('fetch-btn'); const mediaUrlInput = document.getElementById('media-url'); if (mediaUrlInput && fetchBtn) { // 确保获取按钮状态正常 if (mediaUrlInput.value.trim()) { fetchBtn.classList.add('active'); // 使用active类而不是display属性 fetchBtn.style.display = 'block'; } else { fetchBtn.classList.remove('active'); fetchBtn.style.display = 'none'; } } // 重置搜索结果状态,确保搜索功能可以正常使用 if (mediaUrlInput) { // 不清除用户已经输入的URL,但确保交互功能可用 setupSearchInteractions(); } }, 500); } catch (error) { console.error('AI生成失败:', error); showStatus(`AI生成失败:${error.message || '未知错误'}`, true, 'ai'); } finally { // 恢复按钮状态 generateAiTextBtn.disabled = false; generateAiTextBtn.innerHTML = '<i class="fa fa-magic" style="margin-right:5px;"></i>生成AI文本'; isGenerating = false; if (abortAiBtn) abortAiBtn.style.display = 'none'; } }); aiEventsBound = true; } // 终止按钮 if (abortAiBtn) { abortAiBtn.addEventListener('click', function(e){ e.stopPropagation(); e.preventDefault(); if (aiCurrentRequest && typeof aiCurrentRequest.abort === 'function') { try { aiCurrentRequest.abort(); } catch (_) {} } if (typeof aiAbortReject === 'function') { try { aiAbortReject(new Error('已终止')); } catch (_) {} } abortAiBtn.style.display = 'none'; showStatus('已终止AI生成', true, 'ai'); }); } // 清理按钮:重置AI结果、候选与搜索状态 const aiClearBtn = document.getElementById('ai-clear'); if (aiClearBtn) { aiClearBtn.addEventListener('click', function(e){ e.stopPropagation(); e.preventDefault(); try { if (aiResultArea) { aiResultArea.style.display = 'none'; } if (aiResultContent) { aiResultContent.textContent = ''; } const poster = document.getElementById('ai-poster-candidates'); const still = document.getElementById('ai-still-candidates'); if (poster) poster.innerHTML = ''; if (still) still.innerHTML = ''; aiSelectedPosterUrls.clear(); aiSelectedStillUrls.clear(); const aiSearchInputEl = document.getElementById('ai-search-input'); const aiSearchResultsEl = document.getElementById('ai-search-results'); if (aiSearchInputEl) aiSearchInputEl.value = ''; if (aiSearchResultsEl) { aiSearchResultsEl.innerHTML=''; aiSearchResultsEl.style.display='none'; } selectedPosterUrl = ''; selectedStillUrl = ''; showStatus('已清理AI结果与候选区', false, 'ai'); } catch (_) {} }); } // 复制AI结果 if (copyAiResultBtn) { copyAiResultBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); const resultText = aiResultContent.textContent; if (resultText) { GM_setClipboard(resultText); showStatus('AI生成结果已复制到剪贴板', false, 'ai'); } }); } // 插入AI结果到编辑框 if (insertAiResultBtn) { insertAiResultBtn.addEventListener('click', async function(e) { e.stopPropagation(); e.preventDefault(); const resultText = aiResultContent.textContent; if (!resultText) { showStatus('没有可插入的AI生成结果', true, 'ai'); return; } const ok = await writeHtmlToAnyEditor(resultText); try { await autoClickSaveBtn(); } catch(_) {} showStatus(ok ? 'AI生成结果已插入并保存' : '插入失败,已提供剪贴板备份', !ok, 'ai'); }); } } // 用户AI预设模板(内置) try { if (!window.AI_PRESET_TEMPLATE) { window.AI_PRESET_TEMPLATE = `<div style="max-width: 820px; margin: 28px auto; font-family: 'Noto Sans SC', sans-serif; color: #1e293b; background: #ffffff; padding: 0 20px;"> <!-- 主标题:深松绿主色+双线边框,区别用户的单虚线/纯色边框 --> <h2 style=" text-align: center; font-size: 27px; color: #2c5f2d; border-bottom: 2px double #97bc62; padding: 14px 0; margin: 0 0 28px 0; letter-spacing: 1px; ">【新海诚治愈神作】你的名字。(君の名は。)</h2> <!-- 海报+信息卡容器:浅灰蓝背景+深松绿边框,质感柔和且无重复色 --> <div style=" border: 2px solid #2c5f2d; border-radius: 12px; padding: 18px; margin: 0 0 28px 0; background: #e6f4ea; "> <!-- 主海报:深松绿边框+轻微阴影,贴合电影自然氛围 --> <div style="text-align: center; margin-bottom: 18px;"> <img src="https://via.placeholder.com/620x420/2c5f2d/FFF?text=Your+Name" alt="你的名字。电影海报" style="max-width: 100%; border-radius: 8px; border: 2px solid #2c5f2d; box-shadow: 0 4px 10px rgba(44,95,45,0.15);" > <p style="color: #2c5f2d; font-size: 15px; font-weight: 600; margin-top: 10px; margin-bottom: 0;">你的名字。主视觉海报(彗星与系守镇)</p> </div> <!-- 信息卡:纯白底+浅松绿边框,信息层级清晰,文字色无重复 --> <div style="background: #ffffff; padding: 18px; border-radius: 10px; border: 1px solid #97bc62;"> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>电影名称:</strong>你的名字。(君の名は。)</p> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>导演:</strong>新海诚</p> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>类型:</strong>动画 · 爱情 · 奇幻</p> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>上映时间:</strong>2016-08-26(日本) / 2016-12-02(中国大陆)</p> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>豆瓣评分:</strong> <span style="color: #fff; background: #2c5f2d; padding: 3px 10px; border-radius: 16px; font-weight: 600;">8.5</span></p> <p style="font-size: 17px; color: #1e293b; margin: 10px 0;"><strong>核心元素:</strong>身体互换 · 时空交错 · 结绳传说</p> </div> </div> <!-- 电影剧情:浅灰蓝背景+深松绿左粗边,突出治愈感,标题下划线创新 --> <h3 style=" font-size: 19px; color: #2c5f2d; font-weight: 600; text-decoration: underline wavy #97bc62; margin: 28px 0 18px 0; ">【电影剧情】</h3> <div style=" background: #e6f4ea; border-left: 4px solid #2c5f2d; padding: 18px; border-radius: 8px; margin: 0 0 28px 0; "> <p style="margin: 12px 0; line-height: 1.8;">住在东京的少年泷,某天醒来发现自己变成了陌生少女的身体——她是住在深山小镇“系守镇”的三叶。与此同时,三叶也在泷的身体里醒来,面对繁华的东京街头手足无措。两人开始通过日记、便签交流,在彼此的生活里“扮演”对方:泷帮三叶拯救濒临废弃的神社,三叶帮泷赢得暗恋前辈的好感。</p> <p style="margin: 12px 0; line-height: 1.8;">随着了解加深,情愫渐生,可他们从未见过面。直到某天,身体互换突然停止,泷发现三叶所在的系守镇,竟在三年前的彗星撞击中消失。为了拯救三叶和小镇,他带着仅存的记忆踏上寻找之旅,在神社的“御神体”前,通过结绳的力量穿越时空,与三叶在黄昏之时(たそがれ)相遇——那是“非日非夜,非人非鬼”的特殊时刻,也是他们跨越时空的唯一交集。</p> </div> <!-- 经典画面:深松绿边框+圆角,图片加阴影更显质感,说明文字更细腻 --> <h3 style=" font-size: 19px; color: #2c5f2d; font-weight: 600; text-decoration: underline wavy #97bc62; margin: 28px 0 18px 0; ">【经典画面】</h3> <div style=" border: 2px solid #2c5f2d; border-radius: 12px; padding: 18px; margin: 0 0 28px 0; text-align: center; "> <img src="https://via.placeholder.com/620x320/2c5f2d/FFF?text=Kimi+no+Na+wa+Twilight" alt="黄昏之时相遇场景" style="max-width: 100%; border-radius: 8px; display: inline-block; box-shadow: 0 4px 10px rgba(44,95,45,0.15);" > <p style="color: #2c5f2d; font-size: 15px; font-weight: 600; margin-top: 10px;">“黄昏之时”——泷与三叶跨越时空触碰的瞬间(结绳缠绕的手是关键伏笔)</p> </div> <!-- 观众热评:纯白底+浅松绿边框,评论文字带情绪感,作者信息更有设计感 --> <h3 style=" font-size: 19px; color: #2c5f2d; font-weight: 600; text-decoration: underline wavy #97bc62; margin: 28px 0 18px 0; ">【观众热评】</h3> <div style=" background: #ffffff; padding: 18px; border-radius: 10px; border: 1px solid #97bc62; margin: 0 0 28px 0; "> <p style="margin: 0 0 18px 0; font-weight: 500; color: #2c5f2d; font-size: 17px; line-height: 1.6;">“第一次看时没懂结绳的意义,二刷才发现:它是时间的象征,是三叶和泷的羁绊,也是拯救小镇的关键。新海诚太会把细腻的情感藏在画面里了——最后两人在街头问‘你的名字是?’时,我哭了半小时。”</p> <p style="margin: 0; text-align: right; color: #0f5132; font-style: italic; font-size: 16px; border-top: 1px dashed #97bc62; padding-top: 10px;">—— 豆瓣用户 @星尘收集者</p> </div> <!-- 观影提示:浅灰蓝背景+居中排版,增加独家细节建议,提升实用性 --> <p style=" font-size: 17px; color: #1e293b; text-align: center; margin: 32px 0; padding: 14px; background-color: #e6f4ea; border-radius: 8px; line-height: 1.8; "> 【观影提示】① 建议留意“结绳”“口嚼酒”“黄昏之时”三个关键元素,它们是串联时空的伏笔;② 搭配RADWIMPS的原声《前前前世》观看,音乐响起时会更沉浸;③ 结尾的“名字”梗适合二刷回味——你会发现,两人的羁绊早有伏笔。 </p> </div>`; } } catch (e) {} // 根据类型获取AI提示词 function getAIPromptByType(type, userPrompt, movieContext) { // 获取详细排版指南 const { POST_FORMAT_GUIDELINES } = DEFAULT_CONFIG.AI; const prompts = { summary: `请根据用户需求和提供的影视信息,生成一个高质量的剧情简介。\n用户需求:${userPrompt}${movieContext}`, comment: `请根据用户需求和提供的影视信息,生成一个专业、有深度的评论摘要。\n用户需求:${userPrompt}${movieContext}`, tagline: `请根据用户需求和提供的影视信息,生成几个吸引人的宣传标语。\n用户需求:${userPrompt}${movieContext}`, analysis: `请根据用户需求和提供的影视信息,生成一个深入的分析。\n用户需求:${userPrompt}${movieContext}`, // 影视资源帖排版美化智能体框架功能提示词 - 增强版 post_format: `作为专业的影视资源帖排版美化智能体,你需要基于编辑器提供的源代码内容,结合用户需求和影视信息,生成一个高度精美的HTML排版方案。\n\n用户需求:${userPrompt}\n\n【强制要求】\n请严格按照以下要求输出:\n1. 只返回完整的HTML代码,不要包含任何额外的文字说明、解释或注释\n2. 确保HTML代码是格式正确、可直接使用的,不要返回源代码形式的HTML\n3. 所有样式必须使用内联CSS,不要使用外部样式表\n4. 生成的HTML代码应直接展示为格式化的内容,而不是显示HTML标签\n\n【内容处理优先级】\n1. 优先使用编辑器内容作为基础进行美化排版\n2. 当编辑器内容不足时,结合影视信息进行补充\n3. 最后根据用户需求进行调整\n\n【核心样式和结构要求】\n请确保最终生成的HTML代码包含以下关键设计元素:\n1. **整体容器**:使用max-width: 800px; margin: 25px auto;的居中容器\n2. **主题配色**:根据影片类型选择主题色(动画用活泼色如#a2d2ff、电影用质感色如#2a9d8f、剧集用柔和色)\n3. **标题样式**:使用大号字体、主色、下边框装饰,如:text-align: center; font-size: 26px; color: #2a9d8f; border-bottom: 2px dashed #e9c46a;\n4. **海报区域**:使用带边框的居中海报,如:border: 2px solid #e9c46a; border-radius: 10px; text-align: center;\n5. **信息卡**:使用白色背景、圆角边框的信息卡,如:background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0;\n6. **评分样式**:使用主色背景的标签式评分,如:color: #fff; background: #2a9d8f; padding: 2px 8px; border-radius: 15px;\n7. **内容分区**:使用下划线虚线的子标题区分不同内容模块,如:text-decoration: underline dotted #e9c46a;\n8. **影评区域**:官方影评使用主色左侧边框,用户评论使用次要色左侧边框,如:border-left: 4px solid #2a9d8f;\n9. **合规提示**:使用浅色背景的合规提示框,如:background-color: #f8f9fa; border-radius: 5px;\n\n【内容模块要求】\n请包含以下完整模块结构:\n1. **标题区**:精美装饰的主标题,包含影片名称和类型标签\n2. **海报展示**:高清海报展示,包含来源标注\n3. **信息卡**:完整的影视参数,包括导演、主演、类型、上映日期、评分等\n4. **剧情简介**:清晰的剧情描述,可选择性使用折叠面板\n5. **经典场景**:剧照展示区域\n6. **观众热评**:官方推荐和用户评论区,区分不同评论类型\n7. **观影提示**:观影建议和注意事项\n8. **合规声明**:支持正版影视的声明\n\n【技术实现要求】\n- 所有样式使用内联CSS,确保兼容性\n- 使用Microsoft Yahei等中文字体,保证可读性\n- 确保文本内容完整、准确、流畅\n- 使用适当的间距和留白,避免内容拥挤\n- 确保所有模块都有明确的视觉边界和层次感${movieContext}`, content_optimize: `作为专业的内容优化助手,你需要根据用户需求和影视信息,提供详细的内容优化建议。\n\n用户需求:${userPrompt}\n\n优化方向:\n1. **SEO优化**:合理融入影片关键词、类型词、导演演员名称等\n2. **流量优化**:\n - 标题使用吸引人的表述,包含影片亮点\n - 开头段落简明扼要,突出核心吸引力\n - 使用emoji或特殊标记提升内容可辨识度(如适用)\n3. **合规优化**:\n - 替换敏感表述:将"百度云"替换为"合规平台","免费观看"改为"正版渠道观看"\n - 检查影评内容,确保无侵权引用,保留必要的作者署名\n4. **结构优化**:\n - 调整信息呈现顺序,重要内容前置\n - 使用合适的标题层级和排版元素划分内容\n - 增加可读性:剧情文本行高设为1.7倍,段落间距20px\n\n多平台适配建议:\n- 掘金:保留代码高亮,优化图片懒加载\n- 微信公众号:简化样式,优化防盗链格式\n- 知乎:优化首图尺寸,调整段落间距${movieContext}`, format_check: `作为专业的排版合规检查工具,你需要根据用户需求和影视信息,对内容进行全面的合规性检查。\n\n用户需求:${userPrompt}\n\n检查重点:\n1. **版权检查**:\n - 海报是否为官方发布的宣传海报,避免使用影院偷拍的正片截图\n - 是否在图片说明中注明"用于影视推荐合理使用"\n - 用户影评是否注明来源,专业影评引用是否保留作者署名且不超过原文1/3\n - 资源链接是否仅推荐正规视频平台(如腾讯视频、爱奇艺等)\n2. **敏感词检查**:\n - 扫描是否包含"盗版"、"枪版"、"百度云"、"网盘"等风险词汇\n - 检查影评中是否有过激表述或不当言论\n3. **排版结构检查**:\n - 模块顺序是否合理,是否包含完整的头图、信息卡、剧情、影评等核心模块\n - 样式是否统一,间距、字体、颜色等是否符合规范\n - 响应式设计是否完善,在375px、768px、1200px断点下是否正常显示\n4. **平台规则检查**:\n - 是否避免使用特殊符号(如★、→等)\n - 评分是否使用文本或CSS实现而非emoji\n - 是否包含"支持正版影视"的合规声明\n\n请生成详细的检查报告,指出问题并提供修改建议${movieContext}`, modular_design: `作为专业的模块化排版设计师,你需要根据用户需求和影视信息,设计一个完整的模块化排版方案。\n\n用户需求:${userPrompt}\n\n设计原则:\n1. **分章节展示**:\n - 头图区:自适应海报容器,16:9比例,包含来源标注\n - 信息卡区:网格布局,展示导演、类型、上映日期、评分等核心参数\n - 剧情简介区:防剧透折叠面板,分安全版和完整版\n - 影评区:区分官方推荐和用户评论,使用不同样式标识\n - 资源提示区:合规声明和正规平台链接推荐\n2. **醒目重点**:\n - 使用主题色突出标题和重要信息\n - 关键内容使用加粗或背景高亮(background:#fff380;padding:0 3px;border-radius:2px)\n - 评分使用主题色背景的标签样式\n3. **便于阅读**:\n - 统一使用"Microsoft Yahei, sans-serif"字体\n - 正文行高1.7倍,段落间距20px\n - 使用列表和分隔线组织内容,提升可读性\n4. **视觉层次**:\n - 建立清晰的标题层级(h1>h2>h3)\n - 使用不同的边框、背景色区分不同类型的内容模块\n - 合理使用空白区域,避免内容过于拥挤\n\n请提供详细的HTML和CSS代码示例,包括各模块的具体实现${movieContext}`, free_text: `${userPrompt}${movieContext}` }; let base = prompts[type] || `请根据用户需求生成内容。\n用户需求:${userPrompt}${movieContext}`; // 统一追加图片占位说明,避免AI输出真实图片链接(支持精确放置) base += `\n\n[排版占位规则]\n请勿在内容中粘贴任何图片URL。可用占位符:\n- {#POSTER#}: 主海报(可选,仅1次)\n- {#IMG1#}..{#IMGN#}: 我会按你给出的编号依次替换为已选剧照(逐一放置,适合自定义布局)\n- {#STILLS#}: 若未使用逐一占位,使用此占位一次性放入所有剧照(我会做自适应流式布局)。`; return base; } // 调用AI API生成文本 async function generateAIText(apiEndpoint, apiKey, prompt) { return new Promise((resolve, reject) => { // 获取配置的模型名称和提供商 const config = getConfig(); const modelName = config.AI.DEFAULT_MODEL; const provider = config.AI.PROVIDER; // 判断是否为排版美化相关功能 const isFormatRelated = prompt.includes('排版美化') || prompt.includes('内容优化') || prompt.includes('排版合规') || prompt.includes('模块化排版'); // 根据功能类型设置不同的系统提示 const roleConfig = getAIRoleConfig(); let systemPrompt = generateSystemPrompt(roleConfig, isFormatRelated); // 深度思考/联网补充(仅在支持时生效) try { const deep = document.getElementById('ai-deep-think'); const web = document.getElementById('ai-web-browse'); if (deep && !deep.disabled && deep.checked) { systemPrompt += '\n请在生成前进行结构化深度思考与多步推理,先在脑内列出要点再输出结果。'; } if (web && !web.disabled && web.checked) { systemPrompt += '\n如信息明显缺失或不确定,可模拟联网补充(无需真实请求),在结果中补全缺失信息并标注来源类型。'; } } catch(_) {} let max_tokens = 1000; if (isFormatRelated) { // 排版相关功能通常需要更长的输出内容 max_tokens = 2000; } // 根据不同的AI提供商构建适当的请求体 let requestBody = {}; // 检查AI提供商类型,使用不同的请求格式 if (provider === 'gemini' || apiEndpoint.includes('gemini')) { // Gemini API格式 requestBody = { contents: [{ parts: [{ text: prompt }] }], generationConfig: { maxOutputTokens: max_tokens, temperature: 0.7 } }; } else if (provider === 'claude' || apiEndpoint.includes('claude')) { // Claude API格式 requestBody = { model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens_to_sample: max_tokens, temperature: 0.7 }; } else if (provider === 'glm4' || apiEndpoint.includes('bigmodel')) { // 智谱AI API格式 requestBody = { model: modelName, prompt: { text: prompt }, parameters: { max_tokens: max_tokens, temperature: 0.7 } }; } else if (provider === 'qwen' || apiEndpoint.includes('aliyuncs')) { // 通义千问API格式 requestBody = { model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: max_tokens, temperature: 0.7 }; } else if (provider === 'xunfei' || apiEndpoint.includes('xf-yun')) { // 讯飞星火API格式 requestBody = { header: { app_id: apiKey.split('.')[0], // 假设app_id在apiKey的第一部分 uid: 'user' }, parameter: { chat: { domain: 'general', temperature: 0.7, max_tokens: max_tokens } }, payload: { message: { text: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ] } } }; } else if (provider === 'ark' || apiEndpoint.includes('doubao')) { // 豆包API格式 requestBody = { model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: max_tokens, temperature: 0.7 }; } else if (provider === 'together' || apiEndpoint.includes('together')) { // Together AI格式 requestBody = { model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: max_tokens, temperature: 0.7 }; } else { // 默认使用OpenAI API格式 requestBody = { model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: max_tokens, temperature: 0.7 }; } // 构建请求头 - 根据不同提供商处理认证 const headers = { 'Content-Type': 'application/json' }; // 根据AI提供商设置正确的认证方式 if (apiKey) { // 提供商特定的认证方式 if (provider === 'ark' || apiEndpoint.includes('doubao')) { // 豆包API使用的认证方式 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'claude' || apiEndpoint.includes('claude')) { // Claude API认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'gemini' || apiEndpoint.includes('gemini')) { // Gemini API认证 headers['x-goog-api-key'] = apiKey; } else if (provider === 'github' || apiEndpoint.includes('github')) { // GitHub模型市场认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'xunfei' || apiEndpoint.includes('xf-yun')) { // 讯飞星火认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'qwen' || apiEndpoint.includes('aliyuncs')) { // 通义千问认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'glm4' || provider === 'legalglm' || apiEndpoint.includes('bigmodel')) { // 智谱AI认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'huggingface' || apiEndpoint.includes('huggingface')) { // Hugging Face认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'together' || apiEndpoint.includes('together')) { // Together AI认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'baishan' || apiEndpoint.includes('baishanai')) { // 白山云认证 headers['Authorization'] = `Bearer ${apiKey}`; } else if (provider === 'gpt4free' || apiEndpoint.includes('gpt4free')) { // 一些免费API可能不需要标准Bearer格式 headers['Authorization'] = apiKey; } else { // 默认认证方式(OpenAI兼容) headers['Authorization'] = `Bearer ${apiKey}`; } } // 发送请求 console.log(`AI API请求 - 端点: ${apiEndpoint}, 提供商: ${provider}, 模型: ${modelName}`); console.log(`请求头:`, headers); console.log(`请求体结构:`, JSON.stringify(requestBody).substring(0, 500) + (JSON.stringify(requestBody).length > 500 ? '...' : '')); // 验证API端点格式 try { new URL(apiEndpoint); } catch (e) { throw new Error(`API端点格式无效:${apiEndpoint}\n请检查配置的API端点是否为有效的URL格式`); } // 发起请求并保存控制句柄 aiAbortReject = reject; const req = GM_xmlhttpRequest({ method: 'POST', url: apiEndpoint, headers: headers, data: JSON.stringify(requestBody), onload: (response) => { try { console.log(`API响应状态码: ${response.status}`); console.log(`API响应头:`, response.responseHeaders); // 检查响应状态 if (response.status < 200 || response.status >= 400) { let errorMsg = `请求失败:HTTP ${response.status}`; // 特殊处理404错误 if (response.status === 404) { errorMsg = `请求失败:HTTP 404\nAPI端点不存在或无效: ${apiEndpoint}\n请检查配置的API端点是否正确,不同AI提供商的API端点格式可能不同`; // 根据提供商类型提供更具体的建议 if (provider === 'openai') { errorMsg += '\nOpenAI API端点格式应为: https://api.openai.com/v1/chat/completions'; } else if (provider === 'claude') { errorMsg += '\nClaude API端点格式应为: https://api.anthropic.com/v1/messages'; } else if (provider === 'gemini') { errorMsg += '\nGemini API端点格式应为: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent'; } } try { const errorData = JSON.parse(response.responseText); if (errorData.error && errorData.error.message) { errorMsg = errorData.error.message; // 特殊处理模型访问错误 if (errorMsg.includes('model does not exist') || errorMsg.includes('not have access')) { errorMsg = `模型访问失败:${errorMsg}\n请在AI配置中更换为您有权限访问的模型`; } // 特殊处理认证错误 if (errorMsg.includes('Invalid bearer token') || errorMsg.includes('API key format is incorrect') || errorMsg.includes('authentication')) { errorMsg = `认证失败:${errorMsg}\n请检查API密钥格式是否正确,不同AI提供商可能有不同的密钥格式要求`; } } } catch (e) { // 如果解析错误信息失败,使用原始错误 console.error('解析错误响应失败:', e); } throw new Error(errorMsg); } const data = JSON.parse(response.responseText); // 处理不同API的响应格式 let resultText = ''; if (data.choices && data.choices.length > 0) { // OpenAI、通义千问、讯飞星火等API格式 resultText = data.choices[0].message?.content || data.choices[0].text || ''; } else if (data.result) { // GLM-4、豆包等API格式 resultText = data.result; } else if (data.text) { // Together AI等API格式 resultText = data.text; } else if (data.candidates && data.candidates.length > 0) { // Gemini API格式 resultText = data.candidates[0].content?.parts?.[0]?.text || ''; } else if (Array.isArray(data) && data.length > 0 && data[0].generated_text) { // Hugging Face API格式 resultText = data[0].generated_text || ''; } else if (data.output?.choices && data.output.choices.length > 0) { // 其他API格式 resultText = data.output.choices[0].text || ''; } else { throw new Error('未找到有效结果'); } if (!resultText.trim()) { throw new Error('AI返回了空结果'); } // 去除三引号围栏 ```xxx 开头/结尾 let cleaned = resultText.trim(); cleaned = cleaned.replace(/^```[a-zA-Z]*\n/, '').replace(/\n```$/, ''); // 将占位符替换为真实图片HTML const pickPoster = () => { try { const list = Array.from(aiSelectedPosterUrls || []); let url = list[0] || ''; try { url = toTMDBOriginal(url); } catch(_) {} return url || ''; } catch(_) { return ''; } }; const pickStills = () => { try { const list = Array.from(aiSelectedStillUrls || []); return (list && list.length? list : []).slice(0,6); } catch(_) { return []; } }; if (cleaned.includes('{#POSTER#}')) { const p = pickPoster(); const posterHtml = p ? `<div style="text-align:center;margin:12px 0;"><img data-ai-img="1" src="${p}" alt="海报" style="max-width:100%;height:auto;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.08);display:block;"/></div>` : ''; // 只替换第一次,其他占位清空,避免重复 cleaned = cleaned.replace('{#POSTER#}', posterHtml); cleaned = cleaned.replace(/\{#POSTER#\}/g, ''); } if (cleaned.includes('{#STILLS#}') || /\{#IMG\d+#\}/.test(cleaned)) { const arr = pickStills(); let html = ''; const n = Array.isArray(arr) ? arr.length : 0; const toImg = (u, radius='10px') => { const src = toTMDBOriginal ? toTMDBOriginal(u) : u; return `<img data-ai-img="1" src="${src}" alt="剧照" style="width:100%;height:auto;border-radius:${radius};object-fit:cover;display:block;"/>`; }; // 若AI使用精确占位 {#IMGn#},则按编号逐张替换,给足自由编排空间 if (/\{#IMG\d+#\}/.test(cleaned) && n > 0) { let placed = cleaned; for (let i = 0; i < n; i++) { const tag = new RegExp(`\\{#IMG${i+1}#\\}`,'g'); placed = placed.replace(tag, toImg(arr[i])); } // 清理未用完的编号占位 placed = placed.replace(/\{#IMG\d+#\}/g, ''); cleaned = placed; } if (n === 1) { html = arr[0] ? `<div style="text-align:center;margin:16px 0;">${toImg(arr[0],'12px')}</div>` : ''; } else if (n === 2) { html = `<div style="display:grid;width:100%;grid-template-columns:repeat(2,1fr);gap:16px;margin:16px 0;">${toImg(arr[0])}${toImg(arr[1])}</div>`; } else if (n === 3) { // 上1下2,避免右侧空白 const top = arr[0] ? `<div style=\"margin:0 0 14px 0;\">${toImg(arr[0],'12px')}</div>` : ''; const bottom = `<div style=\"display:grid;width:100%;grid-template-columns:repeat(2,1fr);gap:16px;\">${toImg(arr[1])}${toImg(arr[2])}</div>`; html = `<div style="width:100%;margin:16px 0;">${top}${bottom}</div>`; } else if (n === 4) { html = `<div style="display:grid;width:100%;grid-template-columns:repeat(2,1fr);gap:16px;margin:16px 0;">${arr.map(u=>toImg(u)).join('')}</div>`; } else if (n === 5) { // 上1(大图)+ 下4(2列) const top = `<div style=\"margin:0 0 14px 0;\">${toImg(arr[0],'12px')}</div>`; const rest = `<div style=\"display:grid;width:100%;grid-template-columns:repeat(2,1fr);gap:16px;\">${arr.slice(1).map(u=>toImg(u)).join('')}</div>`; html = `<div style="width:100%;margin:16px 0;">${top}${rest}</div>`; } else if (n >= 6) { html = `<div style="display:grid;width:100%;grid-template-columns:repeat(3,1fr);gap:16px;margin:16px 0;">${arr.map(u=>toImg(u)).join('')}</div>`; } // 若未使用{#IMGn#},再处理{#STILLS#} 的一次性替换 if (cleaned.includes('{#STILLS#}')) { cleaned = cleaned.replace('{#STILLS#}', html); cleaned = cleaned.replace(/\{#STILLS#\}/g, ''); } } // 清理非占位符产生的多余<img>与孤立属性片段 try { cleaned = cleaned.replace(/<img(?![^>]*data-ai-img=\"1\")[^>]*>/gi, ''); cleaned = cleaned.replace(/\salt=\"[^\"]*\"[^>]*>/gi, '>'); } catch(_) {} resolve(cleaned.trim()); } catch (error) { console.error('解析AI响应失败:', error, response.responseText); reject(new Error(`解析响应失败:${error.message}`)); } }, onerror: (error) => { console.error('AI API请求失败:', error); reject(new Error(`网络请求失败:${error.message || '未知错误'}`)); }, ontimeout: () => { reject(new Error('AI API请求超时')); } }); aiCurrentRequest = req; }); } // 初始化页面 function init() { // 立即禁用所有表单验证 disableAllFormValidation(); // 加载Font Awesome图标(确保美化工具图标正常显示) const faLink = document.createElement('link'); faLink.rel = 'stylesheet'; faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'; faLink.crossOrigin = 'anonymous'; // 解决跨域加载问题 // 确保head存在才添加 if (document.head) { document.head.appendChild(faLink); } else { // 如果head还没加载,延迟添加 setTimeout(() => { if (document.head) { document.head.appendChild(faLink); } }, 100); } // 插入控制面板(精准位置+空值修复) insertPanelInMarkedPosition(); // 检查是否有默认URL(从URL参数中获取) const urlParams = new URLSearchParams(window.location.search); const mediaUrl = urlParams.get('mediaUrl'); if (mediaUrl && document.getElementById('media-url')) { document.getElementById('media-url').value = mediaUrl; const fetchBtn = document.getElementById('fetch-btn'); // 根据输入框内容设置提取按钮状态 if (fetchBtn) { const mediaUrlInput = document.getElementById('media-url'); if (mediaUrlInput && mediaUrlInput.value.trim()) { fetchBtn.classList.add('active'); } else { fetchBtn.classList.remove('active'); } } } } // 启动脚本 init(); // 额外的安全检查 - 确保switchTab函数在全局作用域可访问 setTimeout(function() { if (typeof window.switchTab !== 'function') { // 如果window上没有switchTab函数,尝试重新暴露 if (typeof unsafeWindow !== 'undefined') { window.switchTab = unsafeWindow.switchTab || switchTab; } else { window.switchTab = switchTab; } } // 修复HTML中的onclick属性调用 const tabButtons = document.querySelectorAll('[onclick^="switchTab("]'); tabButtons.forEach(button => { const originalOnclick = button.getAttribute('onclick'); if (originalOnclick) { // 提取tabId参数 const match = originalOnclick.match(/switchTab\('([^']+)'\)/); if (match && match[1]) { const tabId = match[1]; // 移除原始的onclick属性 button.removeAttribute('onclick'); // 使用addEventListener添加点击事件 button.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); if (typeof window.switchTab === 'function') { window.switchTab(tabId); } else if (typeof document.switchTab === 'function') { document.switchTab(tabId); } }); } } }); }, 100); // ========== AI角色配置系统(保留内部使用,移除UI入口) ========== // 影视类型色彩配置 const MOVIE_GENRE_COLORS = { horror: { name: '恐怖', primary: '#2d3142', secondary: '#ef8354', lightBg: '#f5f5f5', text: '#333333' }, romance: { name: '爱情', primary: '#f8b195', secondary: '#f8e1d1', lightBg: '#fff8f5', text: '#8b4513' }, action: { name: '动作', primary: '#335c67', secondary: '#e09f3e', lightBg: '#f9f7f1', text: '#3a3a3a' }, drama: { name: '剧情', primary: '#3a0ca3', secondary: '#4361ee', lightBg: '#f8f9ff', text: '#333333' }, comedy: { name: '喜剧', primary: '#ffb347', secondary: '#fdfd96', lightBg: '#fffef0', text: '#5d4037' }, sciFi: { name: '科幻', primary: '#4169e1', secondary: '#87cefa', lightBg: '#f0f8ff', text: '#333333' }, fantasy: { name: '奇幻', primary: '#9370db', secondary: '#e6e6fa', lightBg: '#f8f5ff', text: '#4a4a4a' }, anime: { name: '动漫', primary: '#ff69b4', secondary: '#ffb6c1', lightBg: '#fff0f5', text: '#8b4513' }, documentary: { name: '纪录片', primary: '#708090', secondary: '#d3d3d3', lightBg: '#f5f5f5', text: '#333333' }, thriller: { name: '惊悚', primary: '#264653', secondary: '#2a9d8f', lightBg: '#f1faee', text: '#333333' }, adventure: { name: '冒险', primary: '#e9c46a', secondary: '#f4a261', lightBg: '#fffbeb', text: '#5d4037' }, musical: { name: '歌舞', primary: '#e76f51', secondary: '#f4a261', lightBg: '#fff5f5', text: '#5d4037' } }; // AI角色配置默认值 const AI_ROLE_DEFAULTS = { role: '专注影视资源帖排版美化的创意助手', personality: '审美敏锐(懂排版视觉逻辑)、严谨合规(熟平台过滤规则)、高效实用(模块化出稿)、富有创意(适配不同影视风格)', style: '简洁直白,用“设计师视角”讲排版思路;重点突出,同时说明“美化效果+合规技巧”;专业术语+通俗表达结合', tone: '友好、专业、鼓励性', specialRequirements: '1.输出用基础HTML+内联CSS(无复杂框架,多平台可复制);\n2.自动规避特殊符号(如emoji→【】)、敏感词(如“网盘”→“合规路径”);\n3.固定模块:标题区→核心信息卡→影视简介→剧照展示→热评模块→合规提示,每个模块带视觉分层(边框/背景/间距);\n4.配色贴合类型:动画活泼、电影质感、剧集柔和,单帖主色≤3种。', // 影视类型偏好配置 preferredGenre: '', autoApplyColors: true, // 色彩主题配置 primaryColor: '#ff69b4', secondaryColor: '#ffb6c1', lightBgColor: '#fff0f5', textColor: '#8b4513', // 排版配置 titleFontSize: 26, bodyFontSize: 16, lineHeight: 1.8, paragraphMargin: 15 }; // AI角色模板系统 const AI_ROLE_TEMPLATES = { professional: { name: '专业影评人', role: '专业的影视内容创作助手和影评人', personality: '知识渊博、专业严谨、见解独到', style: '深入分析、逻辑清晰、细节丰富、引用专业术语', tone: '专业、客观、学术性', specialRequirements: '请结合影视理论进行分析,适当引用经典电影案例' }, casual: { name: '影视爱好者', role: '热情的影视爱好者和推荐人', personality: '活泼开朗、热情洋溢、善于表达', style: '口语化、亲切自然、情感丰富', tone: '友好、热情、有感染力', specialRequirements: '请使用轻松愉快的语言,避免过于专业的术语' }, humorous: { name: '幽默评论员', role: '幽默风趣的影视评论员', personality: '幽默风趣、机智活泼、善于调侃', style: '诙谐幽默、语言生动、比喻巧妙', tone: '轻松、搞笑、充满活力', specialRequirements: '请适当加入幽默元素,使用生动有趣的表达方式' }, academic: { name: '影视学者', role: '严谨的影视研究者和教育者', personality: '严谨治学、逻辑严密、学识渊博', style: '学术化、系统化、理论深厚', tone: '庄重、学术、教导性', specialRequirements: '请从学术角度分析,结合相关理论和研究成果' }, postFormatter: { name: '影视资源帖排版美化师', role: '专注影视资源帖排版美化的创意助手', personality: '审美敏锐(懂排版视觉逻辑)、严谨合规(熟悉平台过滤规则)、高效实用(模块化出稿)、富有创意(适配不同影视风格)', style: '简洁直白,用"设计师视角"讲排版思路;重点突出,同时说明"美化效果+合规技巧";专业术语+通俗表达结合', tone: '友好耐心,像专业排版师一样提供step-by-step指导;鼓励性强,降低用户操作压力', specialRequirements: '1.输出用基础HTML+内联CSS(无复杂框架,多平台可复制);\n2.自动规避特殊符号(如emoji→【】)、敏感词(如"网盘"→合规平台);\n3.固定模块:标题区→核心信息→影视简介→剧照展示→热评模块→合规提示,每个模块带视觉分层(边框/背景/间距);\n4.配色贴合类型:动画用活泼色、电影用质感色、剧集用柔和色,单帖主色≤3种。' } }; // 生成AI系统提示 function generateSystemPrompt(roleConfig, isFormatRelated = false) { const { role, personality, style, tone, specialRequirements } = roleConfig; let basePrompt = `你是${role}。`; basePrompt += `你的性格特点是:${personality}。`; basePrompt += `你的语言风格是:${style}。`; basePrompt += `你的语气是:${tone}。`; if (specialRequirements) { basePrompt += `特别要求:${specialRequirements}。`; } if (isFormatRelated) { // 平台过滤认知与排版原则(精简版,低Token) basePrompt += `\n\n平台过滤:强标准化+安全管控+样式阉割。仅保留基础原子样式;复杂布局/渐变/阴影/圆角/媒体查询等多会失效。结构以table/tr/td与简单div为白名单;外链资源受限,内联style优先。`; basePrompt += `\n\n排版原则(6步):\n1) 布局:优先表格;多列用内层table,两列50%均分。\n2) 色彩:单色系三层次(主色10-20%,辅助30-40%,正文#444)。\n3) 分层:统一符号+加粗+字号;正文行高1.8,必要处首行缩进。\n4) 图片:按平台规则统一尺寸,max-width:100%; height:auto。\n5) 间距:统一padding/margin;模块用浅色边框分隔。\n6) 细节:仅用基础元素做精致感(边框/前缀符号/按钮浅底主色字)。`; } return basePrompt; } // 获取保存的AI角色配置 function getAIRoleConfig() { try { const saved = GM_getValue('ai_role_config', null); return saved ? JSON.parse(saved) : { ...AI_ROLE_DEFAULTS }; } catch (e) { console.error('读取AI角色配置失败:', e); return { ...AI_ROLE_DEFAULTS }; } } // 保存AI角色配置 function saveAIRoleConfig(config) { try { GM_setValue('ai_role_config', JSON.stringify(config)); return true; } catch (e) { console.error('保存AI角色配置失败:', e); return false; } } // 打开AI角色配置界面(开发预览下可用) function openAIRoleConfigUI() { const config = getAIRoleConfig(); // 创建配置界面HTML const dialogHTML = ` <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 50;"> <div style="background-color: white; border-radius: 8px; padding: 20px; width: 90%; max-width: 700px; max-height: 90vh; overflow-y: auto;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> <h2 style="margin: 0;">设置智能体与影视配置</h2> <button id="close-dialog" style="background: none; border: none; font-size: 24px; cursor: pointer;">×</button> </div> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 10px;">选择智能体模板:</label> <select id="template-select" style="width: 100%; padding: 8px; margin-bottom: 15px;"> <option value="custom">自定义</option> <option value="professional">专业影评人</option> <option value="casual">影视爱好者</option> <option value="humorous">幽默评论员</option> <option value="academic">影视学者</option> <option value="postFormatter">影视资源帖排版美化师</option> </select> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">角色定位:</label> <input type="text" id="role-input" value="${config.role}" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">性格特点:</label> <input type="text" id="personality-input" value="${config.personality}" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">语言风格:</label> <input type="text" id="style-input" value="${config.style}" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">语气特点:</label> <input type="text" id="tone-input" value="${config.tone}" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 5px;">特别要求(可选):</label> <textarea id="requirements-input" rows="3" style="width: 100%; padding: 8px; box-sizing: border-box;">${config.specialRequirements}</textarea> </div> <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 25px 0;"> <!-- 影视类型偏好配置 --> <div style="margin-bottom: 20px;"> <h3 style="margin: 0 0 15px 0; color: #333;">影视类型偏好设置</h3> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 8px;">首选影视类型:</label> <select id="preferred-genre" style="width: 100%; padding: 8px; box-sizing: border-box;"> <option value="">不指定</option> ${Object.entries(MOVIE_GENRE_COLORS).map(([key, value]) => `<option value="${key}" ${config.preferredGenre === key ? 'selected' : ''}>${value.name}</option>` ).join('')} </select> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 8px;">自动应用类型主题色彩:</label> <label style="display: inline-block; margin-right: 20px;"> <input type="radio" name="auto-apply-colors" value="yes" ${config.autoApplyColors !== false ? 'checked' : ''}> 开启 </label> <label style="display: inline-block;"> <input type="radio" name="auto-apply-colors" value="no" ${config.autoApplyColors === false ? 'checked' : ''}> 关闭 </label> </div> </div> <!-- 色彩主题配置 --> <div style="margin-bottom: 20px;"> <h3 style="margin: 0 0 15px 0; color: #333;">色彩主题配置</h3> <div style="grid-template-columns: 1fr 1fr; gap: 15px; display: grid;"> <div> <label style="display: block; margin-bottom: 5px;">主色调:</label> <input type="color" id="primary-color" value="${config.primaryColor || '#ff69b4'}" style="width: 100%; height: 35px; padding: 0; cursor: pointer;"> </div> <div> <label style="display: block; margin-bottom: 5px;">辅助色:</label> <input type="color" id="secondary-color" value="${config.secondaryColor || '#ffb6c1'}" style="width: 100%; height: 35px; padding: 0; cursor: pointer;"> </div> <div> <label style="display: block; margin-bottom: 5px;">浅背景色:</label> <input type="color" id="light-bg-color" value="${config.lightBgColor || '#fff0f5'}" style="width: 100%; height: 35px; padding: 0; cursor: pointer;"> </div> <div> <label style="display: block; margin-bottom: 5px;">文本主色:</label> <input type="color" id="text-color" value="${config.textColor || '#8b4513'}" style="width: 100%; height: 35px; padding: 0; cursor: pointer;"> </div> </div> <div style="margin-top: 15px;"> <button id="apply-genre-colors" style="background-color: #2196F3; color: white; border: none; padding: 8px 15px; cursor: pointer; border-radius: 4px;">应用选中类型的配色</button> </div> </div> <!-- 排版配置 --> <div style="margin-bottom: 20px;"> <h3 style="margin: 0 0 15px 0; color: #333;">排版配置</h3> <div style="grid-template-columns: 1fr 1fr; gap: 15px; display: grid;"> <div> <label style="display: block; margin-bottom: 5px;">标题字号:</label> <input type="number" id="title-font-size" value="${config.titleFontSize || 26}" min="16" max="48" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div> <label style="display: block; margin-bottom: 5px;">正文字号:</label> <input type="number" id="body-font-size" value="${config.bodyFontSize || 16}" min="12" max="24" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div> <label style="display: block; margin-bottom: 5px;">行高:</label> <input type="number" step="0.1" id="line-height" value="${config.lineHeight || 1.8}" min="1.0" max="3.0" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> <div> <label style="display: block; margin-bottom: 5px;">段落间距:</label> <input type="number" id="paragraph-margin" value="${config.paragraphMargin || 15}" min="0" max="50" style="width: 100%; padding: 8px; box-sizing: border-box;"> </div> </div> </div> <div style="display: flex; justify-content: flex-end; margin-top: 30px;"> <button id="save-config" style="background-color: #4CAF50; color: white; border: none; padding: 10px 20px; margin-left: 10px; cursor: pointer; border-radius: 4px;">保存配置</button> <button id="reset-config" style="background-color: #f44336; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px;">重置为默认值</button> </div> </div> </div>`; // 关闭对话框 - 安全版本 function closeDialog() { try { const dialog = document.querySelector('div[style*="position: fixed"]'); if (dialog && document.body.contains(dialog)) { document.body.removeChild(dialog); } else if (dialog) { // 如果dialog不是body的直接子元素,尝试找到其实际的父元素 const parent = dialog.parentNode; if (parent) { parent.removeChild(dialog); } } } catch (e) { console.warn('关闭对话框时出现错误:', e); // 作为最后手段,尝试隐藏元素 const dialog = document.querySelector('div[style*="position: fixed"]'); if (dialog) { dialog.style.display = 'none'; } } } // 添加到页面 - 包含安全检查 const tempContainer = document.createElement('div'); tempContainer.innerHTML = dialogHTML; const dialogElement = tempContainer.firstElementChild; // 确保body存在才添加 if (document.body) { document.body.appendChild(dialogElement); } else { // 如果body还没加载,延迟添加 setTimeout(() => { if (document.body) { document.body.appendChild(dialogElement); } }, 100); } // 获取元素并添加事件监听 const dialog = dialogElement; const closeBtn = dialog.querySelector('#close-dialog'); const saveBtn = dialog.querySelector('#save-config'); const resetBtn = dialog.querySelector('#reset-config'); const templateSelect = dialog.querySelector('#template-select'); // 确保closeBtn存在再绑定事件 if (closeBtn) { closeBtn.addEventListener('click', closeDialog); } else { console.error('未找到关闭按钮'); } // 点击对话框外部(遮罩层)不关闭 - 根据用户需求修改 dialog.addEventListener('click', function(e) { // 空函数,不执行任何关闭操作 }); // 确保点击内容区域也不关闭对话框 const dialogContent = dialog.querySelector('div[style*="background-color: white"]'); if (dialogContent) { dialogContent.addEventListener('click', function(e) { e.stopPropagation(); // 阻止事件冒泡到遮罩层 }); } // 模板选择事件 templateSelect.addEventListener('change', function() { const templateId = this.value; if (templateId !== 'custom' && AI_ROLE_TEMPLATES[templateId]) { const template = AI_ROLE_TEMPLATES[templateId]; document.getElementById('role-input').value = template.role; document.getElementById('personality-input').value = template.personality; document.getElementById('style-input').value = template.style; document.getElementById('tone-input').value = template.tone; document.getElementById('requirements-input').value = template.specialRequirements; } }); // 应用选中类型的配色 const applyGenreColorsBtn = dialog.querySelector('#apply-genre-colors'); if (applyGenreColorsBtn) { applyGenreColorsBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); const selectedGenre = document.getElementById('preferred-genre').value; if (selectedGenre && MOVIE_GENRE_COLORS[selectedGenre]) { const colors = MOVIE_GENRE_COLORS[selectedGenre]; document.getElementById('primary-color').value = colors.primary; document.getElementById('secondary-color').value = colors.secondary; document.getElementById('light-bg-color').value = colors.lightBg; document.getElementById('text-color').value = colors.text; showStatus('已应用' + colors.name + '类型的配色方案', false, 'ai'); } else { showStatus('请先选择一个影视类型', true, 'ai'); } }); } // 保存配置 saveBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); const autoApplyColorsValue = document.querySelector('input[name="auto-apply-colors"][type="radio"]:checked').value; const newConfig = { role: document.getElementById('role-input').value, personality: document.getElementById('personality-input').value, style: document.getElementById('style-input').value, tone: document.getElementById('tone-input').value, specialRequirements: document.getElementById('requirements-input').value, // 影视类型偏好配置 preferredGenre: document.getElementById('preferred-genre').value, autoApplyColors: autoApplyColorsValue === 'yes', // 色彩主题配置 primaryColor: document.getElementById('primary-color').value, secondaryColor: document.getElementById('secondary-color').value, lightBgColor: document.getElementById('light-bg-color').value, textColor: document.getElementById('text-color').value, // 排版配置 titleFontSize: parseInt(document.getElementById('title-font-size').value), bodyFontSize: parseInt(document.getElementById('body-font-size').value), lineHeight: parseFloat(document.getElementById('line-height').value), paragraphMargin: parseInt(document.getElementById('paragraph-margin').value) }; if (saveAIRoleConfig(newConfig)) { alert('配置已保存!'); closeDialog(); } else { alert('保存失败,请重试!'); } }); // 重置配置 resetBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); if (confirm('确定要重置为默认配置吗?')) { // 重置AI角色配置 document.getElementById('role-input').value = AI_ROLE_DEFAULTS.role; document.getElementById('personality-input').value = AI_ROLE_DEFAULTS.personality; document.getElementById('style-input').value = AI_ROLE_DEFAULTS.style; document.getElementById('tone-input').value = AI_ROLE_DEFAULTS.tone; document.getElementById('requirements-input').value = AI_ROLE_DEFAULTS.specialRequirements; templateSelect.value = 'custom'; // 重置影视类型配置 document.getElementById('preferred-genre').value = ''; document.querySelector('input[name="auto-apply-colors"][value="yes"]').checked = true; // 重置色彩配置(默认粉黛仙境主题) document.getElementById('primary-color').value = '#ff69b4'; document.getElementById('secondary-color').value = '#ffb6c1'; document.getElementById('light-bg-color').value = '#fff0f5'; document.getElementById('text-color').value = '#8b4513'; // 重置排版配置 document.getElementById('title-font-size').value = 26; document.getElementById('body-font-size').value = 16; document.getElementById('line-height').value = 1.8; document.getElementById('paragraph-margin').value = 15; } }); } // 粉黛仙境主题式提取影片信息功能 - 专门用于提取豆瓣和TMDB影片资源并以特定排版粘贴到编辑框 const emeraldButton = document.getElementById('emerald-city-format'); if (emeraldButton) { emeraldButton.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); try { // 直接写入编辑器,无需切换源代码对话框 // 轻微延迟,确保编辑器可用 setTimeout(async function() { showStatus('开始应用粉黛仙境主题式影片资源提取...', false); // 使用全局变量中已从豆瓣和TMDB获取的电影信息 if (!currentMovieInfo) { showStatus('请先搜索影片并选择海报和剧照', true); return; } // 从currentMovieInfo中获取完整的影片信息 const title = currentMovieInfo.title || '电影名称'; const director = currentMovieInfo.director || '未知'; const writer = currentMovieInfo.writer || '未知'; const actor = currentMovieInfo.actor || '未知'; const genreTags = currentMovieInfo.genreTags || []; const type = genreTags.join('、') || '未知'; const releaseDate = currentMovieInfo.release || '未知'; const rating = currentMovieInfo.rating || '9.0'; const mainContent = currentMovieInfo.intro || '暂无剧情简介内容'; const originalTitle = currentMovieInfo.originalTitle || ''; const alsoKnown = currentMovieInfo.alsoKnown || ''; const region = currentMovieInfo.region || '未知'; const lang = currentMovieInfo.lang || '未知'; const runtime = currentMovieInfo.runtime || '未知'; const imdbId = currentMovieInfo.imdbId || '暂无'; const keywords = currentMovieInfo.keywords || ''; const budget = currentMovieInfo.budget || '未知'; const revenue = currentMovieInfo.revenue || '未知'; const streamingPlatforms = currentMovieInfo.streamingPlatforms || []; const awards = currentMovieInfo.awards || []; // 智能识别内容类型(电影、动漫、电视剧) let contentType = '电影'; const animeKeywords = ['动漫', '动画', '番剧', '卡通', '二次元']; const tvKeywords = ['电视剧', '剧集', '网剧', '韩剧', '日剧', '美剧', '英剧', '国产剧', '集数']; if (currentMovieInfo.genreTags && currentMovieInfo.genreTags.some(tag => animeKeywords.some(keyword => tag.includes(keyword)))) { contentType = '动漫'; } else if (currentMovieInfo.genreTags && currentMovieInfo.genreTags.some(tag => tvKeywords.some(keyword => tag.includes(keyword)))) { contentType = '电视剧'; } else if (currentMovieInfo.mediaType === 'tv') { contentType = '电视剧'; } else if (mainContent && animeKeywords.some(keyword => mainContent.includes(keyword))) { contentType = '动漫'; } else if (mainContent && tvKeywords.some(keyword => mainContent.includes(keyword))) { contentType = '电视剧'; } // 获取已选择的海报和剧照URL const posterUrl = selectedPosterUrl || `https://via.placeholder.com/600x400/ff69b4/FFF?text=${encodeURIComponent(title)}`; const stillUrl = selectedStillUrl || `https://via.placeholder.com/600x300/ff69b4/FFF?text=${encodeURIComponent(title)+'经典场景'}`; // 创建粉黛仙境风格排版,用于展示从豆瓣和TMDB提取的完整影片资源 // 根据内容类型自动适配色彩 let primaryColor = '#ff69b4'; // 默认主色调:粉色 let secondaryColor = '#ffb6c1'; // 默认辅助色:浅粉色 let lightBgColor = '#fff0f5'; // 默认浅背景色:淡粉色 let lightBorderColor = '#ffcce7'; // 默认浅边框色:超浅粉色 let accentColor = '#db7093'; // 默认强调色:深粉色 let textColor = '#8b4513'; // 默认文本主色 // 根据类型标签识别并应用对应的色彩系统 const config = getConfig(); if (config.AI && config.AI.POST_FORMAT_GUIDELINES && config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT && config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM && genreTags && genreTags.length > 0) { // 类型关键词映射 const typeKeywords = { 'horror': ['恐怖', '惊悚', '悬疑'], 'romance': ['爱情', '浪漫', '恋爱'], 'action': ['动作', '冒险', '武侠'], 'drama': ['剧情', '文艺', '传记'], 'comedy': ['喜剧', '搞笑', '幽默'], 'sciFi': ['科幻', '未来', '太空'], 'fantasy': ['奇幻', '魔幻', '玄幻'], 'anime': ['动漫', '动画', '番剧'], 'documentary': ['纪录片', '纪录'] }; // 查找匹配的类型 for (const [type, keywords] of Object.entries(typeKeywords)) { if (genreTags.some(tag => keywords.some(keyword => tag.includes(keyword)))) { if (config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type]) { primaryColor = config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type][0]; secondaryColor = config.AI.POST_FORMAT_GUIDELINES.VISUAL_ENHANCEMENT.COLOR_SYSTEM[type][1]; // 根据主色调生成其他相关色彩 switch(type) { case 'horror': lightBgColor = '#2d3142'; lightBorderColor = '#4f5d75'; accentColor = '#ef8354'; textColor = '#f9f7f3'; break; case 'romance': lightBgColor = '#fff9f4'; lightBorderColor = '#f8d8c6'; accentColor = '#f8b195'; textColor = '#7b5d4b'; break; case 'action': lightBgColor = '#f8f9fa'; lightBorderColor = '#e0e0e0'; accentColor = '#335c67'; textColor = '#333333'; break; case 'drama': lightBgColor = '#f9f7fd'; lightBorderColor = '#e8e2f9'; accentColor = '#3a0ca3'; textColor = '#4a4a4a'; break; case 'comedy': lightBgColor = '#fffdf5'; lightBorderColor = '#fff1cc'; accentColor = '#ffb347'; textColor = '#8b4513'; break; case 'sciFi': lightBgColor = '#f0f7ff'; lightBorderColor = '#c5e0ff'; accentColor = '#4169e1'; textColor = '#2c3e50'; break; case 'fantasy': lightBgColor = '#fcfaff'; lightBorderColor = '#e8e4ff'; accentColor = '#9370db'; textColor = '#4a4a4a'; break; case 'anime': lightBgColor = '#fff0f5'; lightBorderColor = '#ffcce7'; accentColor = '#ff69b4'; textColor = '#8b4513'; break; case 'documentary': lightBgColor = '#f5f5f5'; lightBorderColor = '#e0e0e0'; accentColor = '#708090'; textColor = '#333333'; break; } break; } } } } // 处理类型显示格式(用·分隔) let formattedType = type; if (genreTags && genreTags.length > 0) { formattedType = genreTags.join('·'); } // 创建新的排版模板 const emeraldStyledContent = ` <div style="max-width: 800px; margin: 25px auto; font-family: 'Microsoft Yahei', sans-serif; color: ${textColor}; background: #fff; padding: 0 15px;"> <!-- 标题区 --> <h2 style="text-align: center; font-size: 26px; color: ${primaryColor}; border-bottom: 2px dashed ${secondaryColor}; padding: 15px 0; margin: 0 0 30px 0;"> 【${contentType}】${title}${originalTitle && originalTitle !== title ? `(${originalTitle})` : ''} </h2> <!-- 核心信息卡 - 粉黛仙境风格 --> <div style="border: 2px solid ${primaryColor}; border-radius: 10px; padding: 20px; margin: 0 0 30px 0; background: ${lightBgColor}; box-shadow: 0 4px 12px rgba(255, 105, 180, 0.15);"> <div style="text-align: center; margin-bottom: 20px;"> <img style="max-width: 100%; border-radius: 8px; border: 2px solid ${secondaryColor}; box-shadow: 0 4px 8px rgba(255, 105, 180, 0.2);" src="${posterUrl}" alt="${title}海报" > <p style="color: ${primaryColor}; font-size: 14px; font-weight: bold; margin-top: 10px;">${title}${contentType === '电影' ? '电影' : ''}海报</p> </div> <div style="background: #ffffff; padding: 20px; border-radius: 8px; border: 1px solid ${lightBorderColor}; color:#1f2937 !important;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;"> <div> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">作品名称:</strong> ${title}${originalTitle && originalTitle !== title ? `(${originalTitle})` : ''}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">导演/主创:</strong> ${director || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">编剧:</strong> ${writer || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">主演:</strong> ${actor.length > 30 ? actor.substring(0, 30) + '...' : actor || '暂无信息'}</p> </div> <div> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">类型:</strong> ${formattedType || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">制片地区:</strong> ${region || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">语言:</strong> ${lang || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">片长:</strong> ${runtime || '暂无信息'}</p> <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">上线时间:</strong> ${releaseDate || '暂无信息'}</p> </div> </div> ${alsoKnown ? `<p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style=\"color:#111827\">又名:</strong> ${alsoKnown}</p>` : ''} ${imdbId && imdbId !== '暂无' ? `<p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style=\"color:#111827\">IMDb ID:</strong> ${imdbId}</p>` : ''} <p style="font-size: 16px; color: #1f2937; margin: 12px 0; line-height: 1.6;"><strong style="color:#111827">评分:</strong> <span style="color: #fff; background: ${primaryColor}; padding: 3px 10px; border-radius: 15px; font-weight: 700;">${rating || '暂无'}</span></p> </div> </div> <!-- 内容简介区 --> <h3 style="font-size: 18px; color: ${primaryColor}; font-weight: bold; text-decoration: underline dotted ${secondaryColor}; margin: 30px 0 20px 0;"> 【${contentType === '电影' ? '电影' : contentType === '动漫' ? '动漫' : '电视剧'}故事】 </h3> <div style="background: ${lightBgColor}; border-left: 4px solid ${primaryColor}; padding: 20px; border-radius: 5px; margin: 0 0 30px 0;"> ${mainContent ? mainContent.split('。').filter(s => s.trim()).map(paragraph => ` <p style="margin: 15px 0; line-height: 1.8;">${paragraph.trim()}。</p>`).join('') : ` <p style="margin: 15px 0; line-height: 1.8;">暂无剧情简介内容。</p>`} </div> <!-- 精彩画面区 --> <h3 style="font-size: 18px; color: ${primaryColor}; font-weight: bold; text-decoration: underline dotted ${secondaryColor}; margin: 30px 0 20px 0;"> 【精彩画面】 </h3> <div style="border: 2px solid ${primaryColor}; border-radius: 10px; padding: 20px; margin: 0 0 30px 0; text-align: center; box-shadow: 0 4px 12px rgba(255, 105, 180, 0.15);"> <img style="max-width: 100%; border-radius: 8px; display: inline-block; box-shadow: 0 4px 8px rgba(255, 105, 180, 0.2);" src="${stillUrl}" alt="${title}经典画面" > <p style="color: ${primaryColor}; font-size: 14px; font-weight: bold; margin-top: 12px;">${title}经典场景</p> </div> <!-- 热评区 --> <h3 style="font-size: 18px; color: ${primaryColor}; font-weight: bold; text-decoration: underline dotted ${secondaryColor}; margin: 30px 0 20px 0;"> 【观众热评】 </h3> <div style="background: #fff; padding: 20px; border-radius: 8px; border: 1px solid ${lightBorderColor}; margin: 0 0 30px 0; line-height: 1.7;"> <p style="margin: 0 0 15px 0; font-weight: 500; color: ${primaryColor}; font-size: 16px;">“每次看${title}都会有不同的感受,故事情节引人入胜,人物形象鲜明,是一部值得反复观看的经典作品。”</p> <p style="margin: 0; text-align: right; color: ${textColor}; font-style: italic;">—— 豆瓣用户 @影视爱好者</p> </div> <!-- 提示区 --> <p style="font-size: 16px; color: ${textColor}; text-align: center; margin: 40px 0; padding: 15px; background-color: ${lightBgColor}; border-radius: 5px; line-height: 1.7;"> 【${contentType === '电影' ? '观影' : '追剧'}提示】${title}值得反复看,每次都有新感悟。推荐找个安静下午,沉浸式感受这部作品的魅力。 </p> </div>`; // 预览与归档 try { updateLivePreview(emeraldStyledContent, { step: '粉黛仙境预览' }); } catch(_) {} // 直接写入编辑器(统一助手,覆盖TinyMCE/CodeMirror/textarea/iframe/contenteditable) const ok = await writeHtmlToAnyEditor(emeraldStyledContent); // 保存(按钮自动点击,失败不抛错) try { await autoClickSaveBtn(); } catch(_) {} if (ok) { showStatus('粉黛仙境内容已直接写入编辑器并保存', false); try { logPreviewStep('写入编辑器并保存', true); } catch(_) {} } else { showStatus('写入失败,已提供剪贴板备份', true); try { logPreviewStep('写入失败,已回退到剪贴板', false); } catch(_) {} } }, 1000); // 1秒延迟 } catch (error) { showStatus('绿野仙踪主题式影片资源提取失败,请检查是否已选择影片', true); console.error('绿野仙踪主题式影片资源提取错误:', error); } }); } // 注册(不可用)油猴菜单项 try { GM_registerMenuCommand('🔧 脚本配置', createConfigDialog); // 移除外部菜单入口:仅在预览模式下保留,避免打扰实际使用 try { const isPreview = /[?&]preview=1\b/.test(location.search) || localStorage.getItem('script_preview') === '1'; if (isPreview) GM_registerMenuCommand('设置AI角色', openAIRoleConfigUI); } catch (e) {} } catch (e) { console.error('注册(不可用)菜单项失败:', e); // 降级方案:在控制台提供信息 console.log('=== 油猴菜单注册(不可用)失败,您可以通过控制台执行 createConfigDialog() 打开配置界面 ==='); } // 修改generateAIText函数以使用用户配置的角色信息 // 由于无法直接修改已定义的函数,我们需要在脚本中找到该函数并修改 // 以下是修改后的函数核心部分,用户可以手动替换原函数 /* 修改建议:在generateAIText函数中,将设置systemPrompt的部分替换为: const roleConfig = getAIRoleConfig(); let systemPrompt = generateSystemPrompt(roleConfig, isFormatRelated); */ // 豆瓣模拟登录(不可用)函数 function performDoubanLogin(username, password, statusElement) { // 模拟登录(不可用)成功 setTimeout(() => { if (statusElement && statusElement.parentNode) { statusElement.textContent = '✅ 模拟登录(不可用)成功!'; statusElement.style.color = '#065f46'; statusElement.style.backgroundColor = '#d1fae5'; // 保存登录(不可用)信息 try { const savedConfig = localStorage.getItem('apiConfig'); const config = savedConfig ? JSON.parse(savedConfig) : {}; config.doubanUsername = username; config.doubanLoginTime = new Date().toISOString(); config.doubanLoginStatus = 'simulated'; localStorage.setItem('apiConfig', JSON.stringify(config)); console.log('豆瓣模拟登录(不可用)信息已保存'); } catch (storageError) { console.error('保存模拟登录(不可用)信息失败:', storageError); } // 显示成功通知 try { showNotification('豆瓣模拟登录(不可用)成功!', 'success'); } catch (notifyError) { console.warn('显示通知失败:', notifyError); } // 更新编辑器内容 ensureEditorHasContent(); } }, 1000); } // 豆瓣登录(不可用)函数 function isolatedDoubanLogin() { // 获取用户名和密码输入框 const usernameInput = document.querySelector('input[name="username"], input[type="text"]'); const passwordInput = document.querySelector('input[name="password"], input[type="password"]'); if (!usernameInput || !passwordInput) { console.error('未找到用户名或密码输入框'); return; } const username = usernameInput.value.trim(); const password = passwordInput.value.trim(); if (!username || !password) { console.error('用户名或密码不能为空'); return; } // 创建状态显示元素 let statusElement = document.getElementById('login-status'); if (!statusElement) { statusElement = document.createElement('div'); statusElement.id = 'login-status'; statusElement.style.cssText = 'position:fixed;top:20px;right:20px;padding:10px;border-radius:5px;z-index:50;font-size:14px;'; document.body.appendChild(statusElement); } statusElement.textContent = '🔄 正在登录(不可用)...'; statusElement.style.color = '#1e40af'; statusElement.style.backgroundColor = '#dbeafe'; // 由于跨域限制,直接使用模拟登录(不可用) console.log('由于浏览器安全限制,使用模拟登录(不可用)模式'); // 显示回退通知 try { showNotification('由于浏览器安全限制,正在使用模拟登录(不可用)模式...', 'info'); } catch (notifyError) { console.warn('显示通知失败:', notifyError); } // 延迟后执行模拟登录(不可用) setTimeout(() => { if (statusElement && statusElement.parentNode) { performDoubanLogin(username, password, statusElement); } }, 1000); } // 确保编辑器有完整内容 function ensureEditorHasContent() { try { // 首先调用确保标题非空的函数 ensureTitleIsNotEmpty(); const editor = window.editor || { instance: { value: '' } }; // 确保editor.instance存在 if (!editor.instance) { console.warn('编辑器实例不存在,无法确保内容'); return; } let currentContent = ''; // 安全获取当前编辑器内容 try { if (editor.type === 'codemirror' && typeof editor.instance.getValue === 'function') { currentContent = editor.instance.getValue() || ''; } else if (typeof editor.instance.value !== 'undefined') { currentContent = editor.instance.value || ''; } } catch (contentError) { console.warn('获取编辑器内容失败:', contentError); currentContent = ''; } // 如果编辑器内容仍然为空,尝试恢复备份 if (!currentContent.trim()) { const restored = restoreBackupHtml(); // 如果没有备份或恢复失败,创建更完整的默认内容 if (!restored) { try { // 创建安全的默认HTML内容 const defaultContent = `<h1 style="color:#1e40af;font-size:24px;font-weight:bold;margin:20px 0 15px 0;padding-bottom:8px;border-bottom:2px solid #dbeafe;">【豆瓣影视资源】</h1>\n<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">已完成豆瓣账号登录(不可用),您可以开始搜索和提取影视信息。</p>\n<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">使用提示:搜索影片名称,选择合适的海报和剧照,然后点击确认填充按钮。</p>\n<p style="color:#6b7280;font-size:12px;line-height:1.6;margin:20px 0 10px 0;">提示:如需手动复制内容,可使用页面中的复制功能按钮。</p>`; // 保存备份 saveBackupHtml(defaultContent); // 安全填充内容到编辑器 if (editor.type === 'codemirror' && typeof editor.instance.setValue === 'function') { editor.instance.setValue(defaultContent); } else if (typeof editor.instance.value !== 'undefined') { editor.instance.value = defaultContent; } } catch (defaultContentError) { console.error('创建和设置默认内容失败:', defaultContentError); } } } } catch (error) { console.error('确保编辑器有内容失败:', error); } } // 从Cookie字符串中提取重要的豆瓣Cookie function extractImportantCookies(cookiesStr) { const importantCookies = ['dbcl2', 'bid', 'ck', 'll']; const cookies = cookiesStr.split('; '); const result = []; for (const cookie of cookies) { const [name, value] = cookie.split('=', 2); if (importantCookies.includes(name)) { result.push(`${name}=${value}`); } } return result.join('; '); } // 获取TMDB API配置 function getTmdbConfig() { try { const savedConfig = localStorage.getItem('apiConfig'); return savedConfig ? JSON.parse(savedConfig) : {}; } catch (error) { console.error('获取TMDB配置失败:', error); return {}; } } // 获取豆瓣Cookie配置 function getDoubanCookie() { try { const savedConfig = localStorage.getItem('apiConfig'); return savedConfig ? JSON.parse(savedConfig).doubanCookie || '' : ''; } catch (error) { console.error('获取豆瓣Cookie失败:', error); return ''; } } // 加载已保存的账号信息(只加载用户名,不加载密码以保证安全) function loadSavedAccountInfo() { try { const savedConfig = localStorage.getItem('apiConfig'); if (savedConfig) { const config = JSON.parse(savedConfig); // 如果之前保存过用户名,则填充用户名输入框 if (config.doubanUsername) { document.getElementById('douban-username').value = config.doubanUsername; } } // 页面加载时确保编辑器有内容 setTimeout(() => { ensureEditorHasContent(); }, 500); // 延迟500ms,确保编辑器已经初始化 } catch (error) { console.error('加载账号信息失败:', error); // 即使加载失败,也要确保编辑器有内容 ensureEditorHasContent(); } } // 复制内容到剪贴板功能 function copyEditorContent() { try { const editor = window.editor || { instance: { value: '' } }; let content = ''; // 获取编辑器内容 if (editor.type === 'codemirror') { content = editor.instance.getValue(); } else if (editor.instance) { content = editor.instance.value || ''; } // 如果编辑器内容为空,尝试获取备份 if (!content.trim()) { content = localStorage.getItem('editorBackupHtml') || ''; } if (content.trim()) { // 使用Clipboard API复制内容 if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(content).then(() => { showNotification('✅ 内容已复制到剪贴板!', 'success'); }).catch(err => { console.error('复制失败:', err); showNotification('❌ 复制失败,请手动选择复制', 'error'); }); } else { // 降级方案:创建临时textarea元素 const textArea = document.createElement('textarea'); textArea.value = content; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; // 确保body存在才添加 if (document.body) { document.body.appendChild(textArea); } else { // 如果body还没加载,延迟添加 setTimeout(() => { if (document.body) { document.body.appendChild(textArea); } }, 100); } textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) { showNotification('✅ 内容已复制到剪贴板!', 'success'); } else { showNotification('❌ 复制失败,请手动选择复制', 'error'); } } catch (err) { console.error('复制失败:', err); showNotification('❌ 复制失败,请手动选择复制', 'error'); } document.body.removeChild(textArea); } } else { showNotification('⚠️ 没有可复制的内容', 'warning'); } } catch (error) { console.error('复制功能异常:', error); showNotification('❌ 复制功能异常', 'error'); } } // 显示通知消息 function showNotification(message, type = 'info', duration = 3000) { try { // 确保message是字符串类型 const safeMessage = String(message || '未知消息'); // 检查是否已存在通知元素 try { let notification = document.getElementById('copy-notification'); if (notification) { notification.remove(); } } catch (removeError) { console.warn('移除现有通知失败:', removeError); } // 创建新的通知元素 const notification = document.createElement('div'); notification.id = 'copy-notification'; notification.textContent = safeMessage; // 使用安全的消息内容 // 增强可访问性 notification.setAttribute('role', 'alert'); notification.setAttribute('aria-live', 'assertive'); // 设置样式 notification.style.position = 'fixed'; notification.style.top = '20px'; notification.style.right = '20px'; notification.style.padding = '12px 20px'; notification.style.borderRadius = '8px'; notification.style.fontSize = '14px'; notification.style.fontWeight = '500'; notification.style.zIndex = '99999'; // 增加z-index确保在最上层 notification.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; notification.style.transition = 'opacity 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease'; notification.style.pointerEvents = 'auto'; notification.style.cursor = 'pointer'; // 提示用户可以点击关闭 notification.style.minWidth = '250px'; notification.style.maxWidth = '400px'; notification.style.wordWrap = 'break-word'; // 初始状态(用于动画效果) notification.style.opacity = '0'; notification.style.transform = 'translateX(100%) translateY(-20px)'; // 根据类型设置不同颜色 if (type === 'success') { notification.style.backgroundColor = '#d1fae5'; notification.style.color = '#065f46'; notification.style.border = '1px solid #a7f3d0'; } else if (type === 'error') { notification.style.backgroundColor = '#fee2e2'; notification.style.color = '#991b1b'; notification.style.border = '1px solid #fecaca'; } else if (type === 'warning') { notification.style.backgroundColor = '#fef3c7'; notification.style.color = '#92400e'; notification.style.border = '1px solid #fde68a'; } else { notification.style.backgroundColor = '#dbeafe'; notification.style.color = '#1e40af'; notification.style.border = '1px solid #bfdbfe'; } // 添加点击关闭功能 notification.addEventListener('click', function() { try { if (notification && notification.parentNode) { notification.style.opacity = '0'; notification.style.transform = 'translateX(100%) translateY(-20px)'; setTimeout(() => { try { if (notification && notification.parentNode) { notification.parentNode.removeChild(notification); } } catch (removeError) { console.warn('移除通知失败:', removeError); } }, 300); } } catch (clickError) { console.warn('关闭通知失败:', clickError); } }); // 安全添加到文档中 function addNotificationToBody() { if (document.body && !document.getElementById('copy-notification')) { document.body.appendChild(notification); // 强制重排以确保动画效果正常 setTimeout(() => { try { if (notification) { notification.style.opacity = '1'; notification.style.transform = 'translateX(0) translateY(0)'; // 添加微妙的悬停效果 notification.addEventListener('mouseenter', function() { try { notification.style.transform = 'translateX(0) translateY(-2px)'; notification.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.2)'; } catch (hoverError) { console.warn('悬停效果设置失败:', hoverError); } }); notification.addEventListener('mouseleave', function() { try { notification.style.transform = 'translateX(0) translateY(0)'; notification.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; } catch (leaveError) { console.warn('离开效果设置失败:', leaveError); } }); } } catch (animationError) { console.warn('通知动画设置失败:', animationError); } }, 10); } } if (document.body) { addNotificationToBody(); } else { // 如果body还没加载,延迟添加,最多尝试3次 let attempts = 0; const maxAttempts = 3; const interval = setInterval(() => { attempts++; if (attempts >= maxAttempts) { clearInterval(interval); // 如果多次尝试仍失败,使用alert作为最后的备选 console.warn('多次尝试添加通知失败,使用alert'); alert(safeMessage); return; } if (document.body) { clearInterval(interval); addNotificationToBody(); } }, 100); } // 定时自动消失 setTimeout(() => { try { if (notification && notification.parentNode) { notification.style.opacity = '0'; notification.style.transform = 'translateX(100%) translateY(-20px)'; setTimeout(() => { try { if (notification && notification.parentNode) { notification.parentNode.removeChild(notification); } } catch (removeError) { console.warn('移除通知失败:', removeError); } }, 300); } } catch (timeoutError) { console.warn('通知超时处理失败:', timeoutError); } }, duration); } catch (error) { console.error('显示通知失败:', error); // 如果所有显示方式都失败,使用alert作为最后的备选 try { alert(String(message || '操作已完成')); } catch (alertError) { console.error('显示alert也失败:', alertError); } } } // 禁用所有表单验证,防止浏览器原生验证触发 function disableAllFormValidation() { try { // 查找所有表单 const forms = document.querySelectorAll('form'); forms.forEach(form => { form.setAttribute('novalidate', 'true'); form.addEventListener('submit', function(e) { e.preventDefault(); e.stopPropagation(); return false; }, true); }); // 查找所有required字段并移除required属性 const requiredFields = document.querySelectorAll('input[required], textarea[required], select[required]'); requiredFields.forEach(field => { field.removeAttribute('required'); field.setAttribute('data-was-required', 'true'); }); console.log('已禁用所有表单验证'); } catch (error) { console.error('禁用表单验证失败:', error); } } // 注册(不可用)事件监听器 document.addEventListener('DOMContentLoaded', function() { // 立即禁用所有表单验证 disableAllFormValidation(); // 立即测试豆瓣登录(不可用)按钮 setTimeout(() => { const testBtn = document.getElementById('test-douban-login'); // 静默检查按钮状态 if (testBtn) { // 静默添加测试功能 testBtn.onclick = function() { alert('按钮点击测试成功!'); }; } }, 1000); // 页面完全加载后再次尝试 window.addEventListener('load', function() { // 静默尝试绑定按钮事件 const loadButton = document.getElementById('test-douban-login'); if (loadButton) { // 静默找到按钮 loadButton.onclick = function() { alert('页面加载后绑定测试成功!'); // 静默处理点击事件 return false; }; } else { // 静默处理按钮未找到的情况 } }); // 静默检查按钮状态 // 使用MutationObserver监听按钮变化 const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(function(node) { if (node.nodeType === 1) { // Element node if (node.id === 'test-douban-login' || (node.querySelector && node.querySelector('#test-douban-login'))) { console.log('发现豆瓣登录(不可用)按钮被添加:', node); const button = node.id === 'test-douban-login' ? node : node.querySelector('#test-douban-login'); if (button) { button.onclick = function() { alert('MutationObserver捕获到按钮!'); return false; }; button.style.border = '2px solid blue'; } } } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); // 移除了全局点击事件监听器,因为它会中断所有按钮的正常点击流程 // 仅保留必要的按钮特定事件监听器 // 立即尝试绑定事件,不等待 const immediateButton = document.getElementById('test-douban-login'); if (immediateButton) { console.log('立即找到按钮,直接绑定事件'); console.log('按钮HTML:', immediateButton.outerHTML); // 添加最简单的测试 immediateButton.onclick = function() { alert('立即绑定测试成功!'); console.log('立即绑定onclick被触发'); return false; }; // 也尝试添加内联onclick immediateButton.setAttribute('onclick', 'alert("内联onclick测试成功!"); return false;'); // 添加测试属性 immediateButton.setAttribute('data-test', 'true'); immediateButton.style.border = '2px solid red'; // 添加红色边框来确认按钮 // 添加一个简单的测试函数 window.testButton = function() { alert('测试函数被调用!'); }; // 尝试直接调用测试函数 setTimeout(() => { console.log('尝试直接调用测试函数'); window.testButton(); }, 2000); } else { console.log('立即未找到按钮,等待DOM加载'); } // 立即绑定手动配置按钮事件 bindManualConfigButtons(); // 添加全局点击事件监听器 document.addEventListener('click', function(event) { console.log('全局点击事件被触发:', event.target); console.log('点击元素ID:', event.target.id); console.log('点击元素文本:', event.target.textContent); if (event.target.id === 'test-douban-login') { console.log('全局监听器捕获到豆瓣登录(不可用)按钮点击'); event.preventDefault(); event.stopPropagation(); alert('全局监听器:豆瓣登录(不可用)按钮被点击了!'); isolatedDoubanLogin(); return false; } }, true); // 使用捕获阶段 // 等待DOM加载完成后再绑定事件 setTimeout(() => { // 添加更灵活的按钮查找逻辑 const findAndBindButton = function(textPattern, callback) { const allButtons = document.querySelectorAll('button, input[type="button"], input[type="submit"]'); for (let button of allButtons) { const buttonText = button.textContent || button.value || ''; if (buttonText.includes(textPattern)) { button.addEventListener('click', callback); console.log(`已绑定包含"${textPattern}"的按钮点击事件`); return button; } } return null; }; // 尝试通过文本内容绑定按钮 findAndBindButton('登录(不可用)', function() { fillAllRequiredFields(); isolatedDoubanLogin(); }); // 测试豆瓣登录(不可用)按钮点击事件 - 完全隔离版本 const testButton = document.getElementById('test-douban-login'); console.log('查找豆瓣登录(不可用)按钮:', testButton); console.log('所有按钮元素:', document.querySelectorAll('button')); console.log('所有ID包含douban的元素:', document.querySelectorAll('[id*="douban"]')); if (testButton) { console.log('找到豆瓣登录(不可用)按钮,开始绑定事件'); // 移除旧的事件监听器,确保只使用新的增强版 const newTestButton = testButton.cloneNode(true); testButton.parentNode.replaceChild(newTestButton, testButton); // 重新获取引用并绑定增强版事件监听器 const enhancedTestButton = document.getElementById('test-douban-login'); console.log('重新获取按钮引用:', enhancedTestButton); enhancedTestButton.addEventListener('click', function(event) { console.log('豆瓣登录(不可用)按钮被点击!'); // 多重阻断:确保事件完全被阻止,不触发任何表单验证 event.stopPropagation(); event.preventDefault(); if (event.stopImmediatePropagation) { event.stopImmediatePropagation(); } // 立即填充所有required字段,防止浏览器验证触发 fillAllRequiredFields(); // 完全隔离的豆瓣登录(不可用)处理,不触发任何编辑器验证 isolatedDoubanLogin(); return false; }, true); // 添加CSS类以明确标识这是一个填充按钮而非提交按钮 enhancedTestButton.classList.add('fill-only-button'); // 设置类型为button,确保不是submit类型 enhancedTestButton.type = 'button'; // 移除所有可能的旧的onclick属性 enhancedTestButton.removeAttribute('onclick'); // 确保事件处理器正确绑定 enhancedTestButton.onclick = function() { console.log('豆瓣登录(不可用)按钮被点击,执行登录(不可用)逻辑'); // 立即填充所有required字段,防止浏览器验证触发 fillAllRequiredFields(); // 执行实际的豆瓣登录(不可用)逻辑 isolatedDoubanLogin(); return false; }; console.log('豆瓣登录(不可用)按钮事件绑定完成,已确保正确执行登录(不可用)逻辑'); } else { console.error('未找到豆瓣登录(不可用)按钮元素'); // 如果找不到按钮,尝试通过其他方式查找 const allButtons = document.querySelectorAll('button'); console.log('页面中所有按钮:', allButtons); // 查找包含"豆瓣"或"登录(不可用)"文字的按钮 const doubanButtons = Array.from(allButtons).filter(btn => btn.textContent.includes('豆瓣') || btn.textContent.includes('登录(不可用)') || btn.id.includes('douban') || btn.id.includes('login') ); console.log('找到的相关按钮:', doubanButtons); } // 绑定手动复制内容按钮事件 const copyBtn = document.getElementById('copy-content-btn'); if (copyBtn) { // 移除旧的事件监听器,确保只使用新的增强版 const newCopyBtn = copyBtn.cloneNode(true); copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn); // 重新获取引用并绑定增强版事件监听器 const enhancedCopyBtn = document.getElementById('copy-content-btn'); enhancedCopyBtn.addEventListener('click', function(event) { // 多重阻断:确保事件完全被阻止,不触发任何表单验证 event.stopPropagation(); event.preventDefault(); if (event.stopImmediatePropagation) { event.stopImmediatePropagation(); } // 执行复制功能 copyEditorContent(); return false; }, true); // 添加CSS类以明确标识这是一个填充按钮而非提交按钮 enhancedCopyBtn.classList.add('fill-only-button'); // 设置类型为button,确保不是submit类型 enhancedCopyBtn.type = 'button'; } // 加载已保存的配置 loadManualConfig(); // 加载已保存的账号信息 loadSavedAccountInfo(); }, 100); // 稍等一下确保DOM完全加载 }); // 立即填充所有required字段,防止浏览器表单验证触发 function fillAllRequiredFields() { try { // 查找所有带required属性的输入框 const requiredFields = document.querySelectorAll('input[required], textarea[required], select[required]'); requiredFields.forEach(field => { // 强制填充,不管当前是否有值 if (field.id === 'title' || field.name === 'title' || field.placeholder === '标题') { field.value = '【豆瓣影视资源】自动生成内容'; } else if (field.type === 'textarea' || field.tagName.toLowerCase() === 'textarea') { field.value = '正在通过豆瓣账号获取影视信息...'; } else if (field.type === 'text' || field.type === 'email' || field.type === 'tel') { field.value = '自动填充'; } else if (field.type === 'number') { field.value = '1'; } else if (field.tagName.toLowerCase() === 'select') { // 选择第一个非空选项 const options = field.querySelectorAll('option'); for (let option of options) { if (option.value && option.value.trim() !== '') { field.value = option.value; break; } } } // 触发所有相关事件,确保验证状态更新 field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true })); field.dispatchEvent(new Event('blur', { bubbles: true })); }); console.log('已填充所有required字段,防止表单验证触发'); } catch (error) { console.error('填充required字段失败:', error); } } // 确保所有required字段都不为空,防止浏览器表单验证触发 function ensureRequiredFieldsNotEmpty() { try { // 查找所有带required属性的输入框 const requiredFields = document.querySelectorAll('input[required], textarea[required], select[required]'); requiredFields.forEach(field => { if (!field.value || field.value.trim() === '') { // 根据字段类型设置默认值 if (field.id === 'title' || field.name === 'title' || field.placeholder === '标题') { field.value = '【豆瓣影视资源】自动生成内容'; } else if (field.type === 'textarea' || field.tagName.toLowerCase() === 'textarea') { field.value = '正在通过豆瓣账号获取影视信息...'; } else { field.value = '自动填充'; } // 触发input和change事件,确保验证状态更新 field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true })); } }); } catch (error) { console.error('确保required字段非空失败:', error); } } // 确保标题字段非空 function ensureTitleIsNotEmpty() { try { const editor = window.editor || { instance: { value: '' } }; let currentContent = ''; // 获取当前编辑器内容 if (editor.type === 'codemirror') { currentContent = editor.instance.getValue(); } else if (editor.instance) { currentContent = editor.instance.value || ''; } // 检查标题是否为空,如为空则注入默认标题 if (!currentContent.trim() || !hasValidTitle(currentContent)) { const defaultTitle = '【豆瓣影视资源】自动生成内容'; const defaultHtml = `<h1 style="color:#1e40af;font-size:24px;font-weight:bold;margin:20px 0 15px 0;padding-bottom:8px;border-bottom:2px solid #dbeafe;">${defaultTitle}</h1>\n<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">正在通过豆瓣账号获取影视信息...</p>`; // 保存备份内容 saveBackupHtml(defaultHtml); // 填充内容到编辑器 if (editor.type === 'codemirror') { editor.instance.setValue(defaultHtml); } else if (editor.instance) { editor.instance.value = defaultHtml; } } } catch (error) { console.error('确保标题非空失败:', error); } } // 检查内容是否包含有效的标题 function hasValidTitle(content) { // 检查是否包含h1-h6标签或明显的标题结构 return /<h[1-6][^>]*>.*?<\/h[1-6]>/.test(content) || /^#\s+.*$/m.test(content) || /^【.*?】$/.test(content); } // 保存HTML备份 function saveBackupHtml(html) { try { // 确保html是字符串类型 const safeHtml = String(html || ''); // 创建或更新备份元素 let backupElement = document.getElementById('backup-html'); if (!backupElement) { backupElement = document.createElement('input'); backupElement.type = 'hidden'; backupElement.id = 'backup-html'; // 确保在尝试appendChild前body已加载 if (document.body) { document.body.appendChild(backupElement); } else { // 如果body还没加载,延迟添加 setTimeout(() => { if (document.body && !document.getElementById('backup-html')) { document.body.appendChild(backupElement); backupElement.value = safeHtml; } }, 100); } } // 安全地设置值 if (backupElement) { backupElement.value = safeHtml; } // 同时保存到localStorage作为额外备份 try { localStorage.setItem('lastBackupHtml', safeHtml); } catch (storageError) { console.warn('无法保存到localStorage:', storageError); } } catch (error) { console.error('保存HTML备份失败:', error); } } // 恢复HTML备份 function restoreBackupHtml() { try { const editor = window.editor || { instance: { value: '' } }; let backupHtml = ''; // 优先从页面元素中获取 try { const backupElement = document.getElementById('backup-html'); if (backupElement && backupElement.value) { backupHtml = String(backupElement.value || ''); } } catch (elemError) { console.warn('从页面元素获取备份失败:', elemError); } // 如果没有从页面元素获取到,尝试从localStorage获取 if (!backupHtml.trim()) { try { const storedHtml = localStorage.getItem('lastBackupHtml'); backupHtml = String(storedHtml || ''); } catch (storageError) { console.warn('从localStorage获取备份失败:', storageError); } } // 安全验证和清理HTML内容 if (backupHtml && backupHtml.trim() && (!editor.instance.value || !editor.instance.value.trim())) { // 简单的HTML验证,确保内容包含有效标签 if (/<[a-z][\s\S]*>/i.test(backupHtml)) { // 只有当编辑器内容为空时才恢复 try { if (editor.type === 'codemirror' && editor.instance && typeof editor.instance.setValue === 'function') { editor.instance.setValue(backupHtml); } else if (editor.instance) { editor.instance.value = backupHtml; } return true; } catch (editorError) { console.error('设置编辑器内容失败:', editorError); } } else { console.warn('备份内容不包含有效的HTML标签'); } } return false; } catch (error) { console.error('恢复HTML备份失败:', error); return false; } } // 设置面板功能 - 加载配置到设置面板 function loadConfigToSettingsPanel() { const config = getConfig(); // 加载TMDB配置 const tmdbApiKeyInput = document.getElementById('tmdb-api-key'); const tmdbAccessTokenInput = document.getElementById('tmdb-access-token'); if (tmdbApiKeyInput) tmdbApiKeyInput.value = config.TMDB.API_KEY || ''; if (tmdbAccessTokenInput) tmdbAccessTokenInput.value = config.TMDB.ACCESS_TOKEN || ''; // 加载AI配置 const aiApiKeyInput = document.getElementById('ai-api-key'); const aiApiEndpointInput = document.getElementById('ai-api-endpoint'); const aiProviderSelect = document.getElementById('ai-provider'); const aiModelInput = document.getElementById('ai-model'); if (aiApiKeyInput) aiApiKeyInput.value = config.AI.API_KEY || ''; if (aiApiEndpointInput) aiApiEndpointInput.value = config.AI.API_ENDPOINT || DEFAULT_CONFIG.AI.API_ENDPOINT; if (aiProviderSelect) aiProviderSelect.value = config.AI.PROVIDER || DEFAULT_CONFIG.AI.PROVIDER; if (aiModelInput) aiModelInput.value = config.AI.DEFAULT_MODEL || DEFAULT_CONFIG.AI.DEFAULT_MODEL; // 绑定AI提供商变更事件 if (aiProviderSelect) { aiProviderSelect.addEventListener('change', function() { if (this.value !== 'custom') { let endpoint = DEFAULT_CONFIG.AI.API_ENDPOINT; let model = DEFAULT_CONFIG.AI.DEFAULT_MODEL; switch (this.value) { case 'openai': endpoint = 'https://api.openai.com/v1/chat/completions'; model = 'gpt-3.5-turbo'; break; case 'anthropic': endpoint = 'https://api.anthropic.com/v1/messages'; model = 'claude-3-opus-20240229'; break; case 'google': endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; model = 'gemini-pro'; break; } if (aiApiEndpointInput && !aiApiEndpointInput.dataset.userModified) { aiApiEndpointInput.value = endpoint; } if (aiModelInput && !aiModelInput.dataset.userModified) { aiModelInput.value = model; } } }); } // 标记输入框为用户修改过的 const inputsToWatch = [tmdbApiKeyInput, tmdbAccessTokenInput, aiApiKeyInput, aiApiEndpointInput, aiModelInput]; inputsToWatch.forEach(input => { if (input) { input.addEventListener('input', function() { this.dataset.userModified = 'true'; }); } }); } // 设置面板功能 - 保存配置 function saveSettingsFromPanel() { const config = getConfig(); // 获取TMDB配置 const tmdbApiKeyInput = document.getElementById('tmdb-api-key'); const tmdbAccessTokenInput = document.getElementById('tmdb-access-token'); if (tmdbApiKeyInput) config.TMDB.API_KEY = tmdbApiKeyInput.value.trim(); if (tmdbAccessTokenInput) config.TMDB.ACCESS_TOKEN = tmdbAccessTokenInput.value.trim(); // 保持基础URL使用默认值 config.TMDB.BASE_URL = DEFAULT_CONFIG.TMDB.BASE_URL; // 获取AI配置 const aiApiKeyInput = document.getElementById('ai-api-key'); const aiApiEndpointInput = document.getElementById('ai-api-endpoint'); const aiProviderSelect = document.getElementById('ai-provider'); const aiModelInput = document.getElementById('ai-model'); if (aiApiKeyInput) config.AI.API_KEY = aiApiKeyInput.value.trim(); if (aiApiEndpointInput) config.AI.API_ENDPOINT = aiApiEndpointInput.value.trim() || DEFAULT_CONFIG.AI.API_ENDPOINT; if (aiProviderSelect) config.AI.PROVIDER = aiProviderSelect.value; if (aiModelInput) config.AI.DEFAULT_MODEL = aiModelInput.value.trim() || DEFAULT_CONFIG.AI.DEFAULT_MODEL; // 保存配置 try { saveConfig(config); showStatus('设置已保存', false, 'settings'); return true; } catch (error) { showStatus('保存设置失败: ' + error.message, true, 'settings'); console.error('保存配置失败:', error); return false; } } // 设置面板功能 - 重置为默认配置 function resetSettingsToDefault() { if (confirm('确定要将所有设置重置为默认值吗?这将清除您的API密钥等配置。')) { // 重置TMDB配置 const tmdbApiKeyInput = document.getElementById('tmdb-api-key'); const tmdbAccessTokenInput = document.getElementById('tmdb-access-token'); if (tmdbApiKeyInput) { tmdbApiKeyInput.value = ''; delete tmdbApiKeyInput.dataset.userModified; } if (tmdbAccessTokenInput) { tmdbAccessTokenInput.value = ''; delete tmdbAccessTokenInput.dataset.userModified; } // 重置AI配置 const aiApiKeyInput = document.getElementById('ai-api-key'); const aiApiEndpointInput = document.getElementById('ai-api-endpoint'); const aiProviderSelect = document.getElementById('ai-provider'); const aiModelInput = document.getElementById('ai-model'); if (aiApiKeyInput) { aiApiKeyInput.value = ''; delete aiApiKeyInput.dataset.userModified; } if (aiApiEndpointInput) { aiApiEndpointInput.value = DEFAULT_CONFIG.AI.API_ENDPOINT; delete aiApiEndpointInput.dataset.userModified; } if (aiProviderSelect) aiProviderSelect.value = DEFAULT_CONFIG.AI.PROVIDER; if (aiModelInput) { aiModelInput.value = DEFAULT_CONFIG.AI.DEFAULT_MODEL; delete aiModelInput.dataset.userModified; } // 保存默认配置 const defaultConfig = { TMDB: { ...DEFAULT_CONFIG.TMDB, API_KEY: '', ACCESS_TOKEN: '' }, AI: { ...DEFAULT_CONFIG.AI, API_KEY: '' } }; try { saveConfig(defaultConfig); showStatus('已重置为默认设置', false, 'settings'); } catch (error) { showStatus('重置设置失败: ' + error.message, true, 'settings'); console.error('重置配置失败:', error); } } } // 确保在切换到设置标签页时加载配置 const originalSwitchTab = document.switchTab; document.switchTab = function(tabId) { originalSwitchTab(tabId); // 当切换到设置标签页时,加载配置 if (tabId === 'settings') { loadConfigToSettingsPanel(); } }; // 同步更新到window和unsafeWindow if (typeof unsafeWindow !== 'undefined') { unsafeWindow.switchTab = document.switchTab; } window.switchTab = document.switchTab; // 绑定设置面板的事件监听器 function bindSettingsEventListeners() { // 保存设置按钮 const saveConfigBtn = document.getElementById('save-config-btn'); if (saveConfigBtn) { saveConfigBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); saveSettingsFromPanel(); }); } // 重置设置按钮 const resetConfigBtn = document.getElementById('reset-config-btn'); if (resetConfigBtn) { resetConfigBtn.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); resetSettingsToDefault(); }); } } // 修改bindEventListeners函数,添加设置面板的事件监听器绑定 (function() { const originalBindEventListeners = bindEventListeners; const enhancedBindEventListeners = function() { originalBindEventListeners(); bindSettingsEventListeners(); }; // 替换全局bindEventListeners函数 window.bindEventListeners = enhancedBindEventListeners; // 确保内部调用也使用增强版本 Object.defineProperty(window, 'bindEventListeners', { value: enhancedBindEventListeners, writable: true, configurable: true }); })(); // 修改insertPanelInMarkedPosition函数,确保设置面板的事件监听器被正确绑定 (function() { const originalInsertPanelInMarkedPosition = insertPanelInMarkedPosition; const enhancedInsertPanelInMarkedPosition = function() { const result = originalInsertPanelInMarkedPosition(); bindSettingsEventListeners(); return result; }; // 替换全局insertPanelInMarkedPosition函数 window.insertPanelInMarkedPosition = enhancedInsertPanelInMarkedPosition; // 确保内部调用也使用增强版本 Object.defineProperty(window, 'insertPanelInMarkedPosition', { value: enhancedInsertPanelInMarkedPosition, writable: true, configurable: true }); })(); // ==================== 豆瓣登录(不可用)检测功能 ==================== // 豆瓣登录(不可用)检测相关变量 let isCheckingDoubanLogin = false; // 通用请求头配置(确保请求不被豆瓣拦截) const DOUBAN_COMMON_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'no-cache' }; /** * 安全获取DOM元素:多次重试直到元素存在或超时 */ function safeGetDoubanElement(id, maxRetries = 10, retryDelay = 100) { return new Promise((resolve) => { let retries = 0; const checkElement = () => { const element = document.getElementById(id); if (element) resolve(element); else if (retries < maxRetries) { retries++; setTimeout(checkElement, retryDelay); } else { GM_log(`[豆瓣检测] 未找到元素: ${id}`); resolve(null); } }; checkElement(); }); } /** * 安全绑定事件:确保元素存在后再绑定 */ function safeBindDoubanEvent(elementId, eventType, handler, maxRetries = 10) { return safeGetDoubanElement(elementId, maxRetries).then(element => { if (element) { element.addEventListener(eventType, handler); return true; } return false; }); } /** * 豆瓣登录(不可用)状态检测(主函数) * @returns {Promise} { isLoggedIn: 布尔值, error: 错误信息, details: 检测详情 } */ function checkDoubanLoginStatus() { return new Promise((resolve) => { // 1. 防抖保护:若检测已在进行,直接返回 if (isCheckingDoubanLogin) { GM_log('[豆瓣检测] 检测已在进行中,跳过重复请求'); resolve({ isLoggedIn: false, error: '检测正在进行中', details: {} }); return; } isCheckingDoubanLogin = true; GM_log('[豆瓣检测] 开始跨域检测...'); // 2. 总超时保护(12秒,避免无限等待) const totalTimeoutId = setTimeout(() => { isCheckingDoubanLogin = false; GM_log('[豆瓣检测] 总超时(12秒)'); resolve({ isLoggedIn: false, error: '检测超时(12秒)', details: { method: '总超时保护' } }); }, 12000); // -------------------------- // 主方法:调用豆瓣官方用户状态接口 // -------------------------- GM_xmlhttpRequest({ method: 'GET', url: 'https://www.douban.com/j/app/user/check', // 豆瓣官方状态接口 headers: { ...DOUBAN_COMMON_HEADERS, 'Referer': 'https://www.douban.com/', // 模拟从豆瓣主页发起请求 'X-Requested-With': 'XMLHttpRequest' }, timeout: 8000, // 主方法单独超时(8秒) withCredentials: true, // 关键:携带浏览器中存储的豆瓣Cookie onload: (res) => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; GM_log(`[豆瓣检测] 主方法响应:状态码=${res.status}, 最终URL=${res.finalUrl}`); // 主方法成功(200状态码):解析返回数据判断登录(不可用)状态 if (res.status === 200) { try { const data = JSON.parse(res.responseText); // 多字段判断登录(不可用)(豆瓣接口可能返回不同字段,需兼容) const isLoggedIn = data && ( data.user_id || data.uid || data.logged_in === true || (data.user && (data.user.id || data.user.uid)) ); if (isLoggedIn) { resolve({ isLoggedIn: true, error: '', details: { method: '主方法(豆瓣官方接口)', status: res.status, userData: { uid: data.user_id || data.uid } // 脱敏返回用户ID } }); } else { GM_log('[豆瓣检测] 主方法返回未登录(不可用),切换到备用方法'); tryBackupMethod(); // 主方法未登录(不可用),用备用方法确认 } } catch (e) { GM_log(`[豆瓣检测] 主方法JSON解析失败:${e.message}`); tryBackupMethod(); // 解析失败,切换备用方法 } } else { GM_log(`[豆瓣检测] 主方法状态码异常(${res.status}),切换备用方法`); tryBackupMethod(); // 状态码非200,切换备用方法 } }, onerror: (err) => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; GM_log(`[豆瓣检测] 主方法请求失败:${err.message}`); tryBackupMethod(); // 请求失败,切换备用方法 }, ontimeout: () => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; GM_log('[豆瓣检测] 主方法超时(8秒)'); tryBackupMethod(); // 超时,切换备用方法 } }); // -------------------------- // 备用方法:访问豆瓣个人主页,判断是否重定向到登录(不可用)页 // -------------------------- function tryBackupMethod() { GM_log('[豆瓣检测] 启动备用方法(访问个人主页)'); GM_xmlhttpRequest({ method: 'GET', url: 'https://www.douban.com/mine/', // 豆瓣个人主页 headers: { ...DOUBAN_COMMON_HEADERS, 'Referer': 'https://www.douban.com/' }, timeout: 6000, // 备用方法超时(6秒) withCredentials: true, onload: (res) => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; // 判断逻辑:200状态码 + 未重定向到登录(不可用)页 → 已登录(不可用) const isLoggedIn = res.status === 200 && !res.finalUrl?.includes('/login') && !res.finalUrl?.includes('/passport'); const reason = isLoggedIn ? '个人主页访问成功' : (res.finalUrl?.includes('/login') ? '重定向到登录(不可用)页' : `状态码=${res.status}`); resolve({ isLoggedIn: isLoggedIn, error: isLoggedIn ? '' : reason, details: { method: '备用方法(个人主页)', status: res.status, finalUrl: res.finalUrl } }); }, onerror: (err) => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; resolve({ isLoggedIn: false, error: '网络连接失败', details: { method: '备用方法(请求失败)', error: err.message } }); }, ontimeout: () => { clearTimeout(totalTimeoutId); isCheckingDoubanLogin = false; resolve({ isLoggedIn: false, error: '备用方法超时(6秒)', details: { method: '备用方法(超时)' } }); } }); } }); } /** * 打开豆瓣登录(不可用)页面(处理弹窗拦截) */ function openDoubanLoginPage() { const doubanHomeUrl = 'https://www.douban.com/'; // 豆瓣主页(自动判断是否需要登录(不可用)) try { // 尝试打开新标签页 const newWindow = window.open(doubanHomeUrl, '_blank', 'noopener,noreferrer'); if (newWindow && !newWindow.closed) { showStatus('已在新标签页打开豆瓣主页,请登录(不可用)后返回重试检测', false, 'settings'); return; } // 弹窗被拦截,处理备用方案 GM_log('[豆瓣登录(不可用)] 弹窗被拦截,尝试复制链接'); handleDoubanPopupBlocked(doubanHomeUrl); } catch (e) { GM_log(`[豆瓣登录(不可用)] 打开页面失败:${e.message}`); handleDoubanPopupBlocked(doubanHomeUrl); } } /** * 处理弹窗被拦截:复制链接到剪贴板+显示手动链接 */ function handleDoubanPopupBlocked(url) { // 尝试复制链接到剪贴板 if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(url).then(() => { showStatus('豆瓣链接已复制到剪贴板,请粘贴到新标签页登录(不可用)', false, 'settings'); displayDoubanManualLink(url); // 显示手动点击链接 }).catch(() => displayDoubanManualLink(url)); } else { displayDoubanManualLink(url); // 不支持剪贴板API,直接显示链接 } } /** * 显示手动登录(不可用)链接(弹窗拦截时兜底) */ function displayDoubanManualLink(url) { safeGetDoubanElement('douban-login-status').then(container => { if (!container) return; const manualLink = document.createElement('div'); manualLink.style.cssText = 'margin-top:8px; padding:8px; background:#f5f7fa; border-radius:4px; border:1px solid #e4e7ed;'; manualLink.innerHTML = ` <div style="font-size:12px; color:#606266;">🔗 手动登录(不可用)链接:</div> <a href="${url}" target="_blank" style="color:#409eff; text-decoration:underline; font-size:11px;">点击访问豆瓣主页 →</a> `; container.appendChild(manualLink); }); } /** * 显示内嵌诊断面板(分析Cookie、DOM、环境) */ function showDoubanDiagnosticPanel(details = {}) { // 移除已存在的面板(避免重复创建) const existingPanel = document.getElementById('douban-diagnostic-panel'); if (existingPanel) existingPanel.remove(); // 创建诊断面板 const panel = document.createElement('div'); panel.id = 'douban-diagnostic-panel'; panel.style.cssText = ` position:fixed; top:50px; right:20px; width:500px; max-height:600px; overflow-y:auto; background:white; border:1px solid #e4e7ed; border-radius:4px; padding:15px; z-index:50; font-family:Arial,sans-serif; font-size:12px; box-shadow:0 2px 12px 0 rgba(0,0,0,0.1); `; panel.innerHTML = ` <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #e4e7ed; padding-bottom:8px; margin-bottom:10px;"> <h3 style="margin:0; color:#303133; font-size:14px;">🔍 豆瓣登录(不可用)诊断报告</h3> <button id="close-douban-diagnostic" style="background:transparent; border:none; font-size:16px; cursor:pointer; color:#c0c4cc;">×</button> </div> <div id="douban-diagnostic-results">正在生成诊断报告...</div> <div style="text-align:center; margin-top:15px;"> <button id="copy-douban-diagnostic" style="background:#409eff; color:white; border:none; padding:6px 12px; border-radius:4px; cursor:pointer;">复制报告</button> </div> `; document.body.appendChild(panel); // 绑定"关闭"按钮 safeBindDoubanEvent('close-douban-diagnostic', 'click', () => panel.remove()); // 绑定"复制报告"按钮 safeBindDoubanEvent('copy-douban-diagnostic', 'click', () => { safeGetDoubanElement('douban-diagnostic-results').then(resultsDiv => { navigator.clipboard.writeText(resultsDiv.innerText).then(() => { showStatus('诊断报告已复制到剪贴板', false, 'settings'); }).catch(() => alert('诊断报告:\n' + resultsDiv.innerText)); }); }); // 生成诊断报告 generateDoubanDiagnosticReport(); /** * 生成详细诊断报告(Cookie+DOM+环境) */ function generateDoubanDiagnosticReport() { safeGetDoubanElement('douban-diagnostic-results').then(resultsDiv => { if (!resultsDiv) return; // 1. 环境信息 const envInfo = ` <div style="border:1px solid #ebeef5; border-radius:4px; padding:10px; margin-bottom:10px; background:#f5f7fa;"> <h4 style="margin:0 0 8px 0; color:#303133; font-size:13px;">🌐 环境信息</h4> <p><strong>当前域名:</strong>${window.location.hostname}</p> <p><strong>当前URL:</strong>${window.location.href}</p> <p><strong>GM_API支持:</strong>${typeof GM_xmlhttpRequest !== 'undefined' ? '✅ 支持' : '❌ 不支持'}</p> <p><strong>检测时间:</strong>${new Date().toLocaleString()}</p> </div> `; // 2. Cookie分析(关键登录(不可用)Cookie:dbcl2/bid/ll) const allCookies = document.cookie.split(';').filter(c => c.trim()); const doubanCookies = allCookies.filter(c => c.includes('douban') || c.startsWith('dbcl') || c.startsWith('bid')); const keyCookies = ['dbcl2', 'bid', 'll'].map(name => { const cookie = allCookies.find(c => c.trim().startsWith(`${name}=`)); return { name, exists: !!cookie, value: cookie ? cookie.trim().slice(0, 50) + '...' : '无' }; }); const cookieInfo = ` <div style="border:1px solid #ebeef5; border-radius:4px; padding:10px; margin-bottom:10px; background:#f5f7fa;"> <h4 style="margin:0 0 8px 0; color:#303133; font-size:13px;">🍪 Cookie分析</h4> <p><strong>总Cookie数:</strong>${allCookies.length} | <strong>豆瓣Cookie数:</strong>${doubanCookies.length}</p> <h5 style="margin:8px 0 5px 0; color:#303133; font-size:12px;">关键登录(不可用)Cookie:</h5> ${keyCookies.map(c => ` <div style="margin:3px 0; padding:3px; background:${c.exists ? '#f0f9eb' : '#fef0f0'}; border-radius:3px;"> <strong>${c.name}:</strong>${c.exists ? '✅ 存在' : '❌ 不存在'} ${c.exists ? `(值:${c.value})` : ''} </div> `).join('')} </div> `; // 3. 检测详情(来自之前的检测结果) const checkDetails = ` <div style="border:1px solid #ebeef5; border-radius:4px; padding:10px; margin-bottom:10px; background:#f5f7fa;"> <h4 style="margin:0 0 8px 0; color:#303133; font-size:13px;">🔍 检测详情</h4> <div style="margin-top:10px; padding:10px; background:#f9fafb; border-radius:6px; border-left:3px solid #f59e0b;"> <p style="margin:0; font-size:12px; color:#6b7280;"> <strong>检测说明:</strong><br> 未登录(不可用)豆瓣账号时,页面将提示“检测失败:状态码=403”。请点击页面中的“去豆瓣登录(不可用)”按钮,完成豆瓣账号登录(不可用)流程后返回当前网页。若网页显示“豆瓣已登录(不可用)”,即表示系统已成功检测到您的豆瓣登录(不可用)状态,此后豆瓣源可正常使用。 </p> </div> <p><strong>HTTP状态码:</strong>${details.status || '未知'}</p> <p><strong>最终URL:</strong>${details.finalUrl || '未知'}</p> <p><strong>错误信息:</strong>${details.error || '无'}</p> </div> `; // 4. 建议操作 const suggestions = keyCookies.some(c => c.name === 'dbcl2' && c.exists) ? '1. Cookie存在但未登录(不可用):可能是Cookie过期,建议重新登录(不可用)豆瓣<br>2. 清除浏览器缓存后重试' : '1. 未检测到关键Cookie:请先访问豆瓣并完成登录(不可用)<br>2. 检查浏览器是否禁用Cookie<br>3. 尝试无痕模式登录(不可用)(排除缓存干扰)'; const suggestionInfo = ` <div style="border:1px solid #ebeef5; border-radius:4px; padding:10px; background:#f5f7fa;"> <h4 style="margin:0 0 8px 0; color:#303133; font-size:13px;">💡 建议操作</h4> <p>${suggestions}</p> </div> `; // 拼接报告并渲染 resultsDiv.innerHTML = envInfo + cookieInfo + checkDetails + suggestionInfo; }); } } /** * 渲染豆瓣登录(不可用)状态到UI * @param {boolean} isLoggedIn - 是否已登录(不可用) * @param {string} error - 错误信息(空表示无错误) * @param {object} details - 检测详情 */ function renderDoubanLoginStatus(isLoggedIn, error = '', details = {}) { safeGetDoubanElement('douban-login-status').then(statusContainer => { if (!statusContainer) return; // 1. 处理"检测错误"状态(如超时、网络失败) if (error) { statusContainer.innerHTML = ` <div style="display: flex; align-items: center; margin-bottom: 12px;"> <span style="color:#e91e63; font-size: 14px; margin-right: 8px;">❌</span> <span style="color:#e91e63; font-weight: 500;">检测失败:${error}</span> </div> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <button id="go-douban-login" style="background: #4caf50; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);">去豆瓣登录(不可用)</button> <button id="retry-douban-check" style="background: #ff9800; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(255, 152, 0, 0.2);">重试检测</button> <button id="douban-diagnose" style="background: #409eff; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);">详细诊断</button> </div> `; // 绑定"去登录(不可用)"按钮事件 safeBindDoubanEvent('go-douban-login', 'click', openDoubanLoginPage); // 绑定"重试检测"按钮事件(防重复点击) safeBindDoubanEvent('retry-douban-check', 'click', async () => { if (isCheckingDoubanLogin) { showStatus('检测正在进行中,请稍候...', true, 'settings'); return; } const retryBtn = await safeGetDoubanElement('retry-douban-check'); if (retryBtn) { retryBtn.textContent = '检测中...'; retryBtn.disabled = true; retryBtn.style.background = '#ccc'; } showStatus('正在重新检测豆瓣登录(不可用)状态...', false, 'settings'); const result = await checkDoubanLoginStatus(); renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details); }); // 绑定"详细诊断"按钮事件 safeBindDoubanEvent('douban-diagnose', 'click', () => showDoubanDiagnosticPanel(details)); return; } // 2. 处理"已登录(不可用)"状态 if (isLoggedIn) { statusContainer.innerHTML = ` <div style="display: flex; align-items: center; margin-bottom: 12px;"> <span style="color:#4caf50; font-size: 14px; margin-right: 8px;">✅</span> <span style="color:#4caf50; font-weight: 500;">豆瓣已登录(不可用)</span> </div> <div style="display:flex; gap:8px; margin-bottom: 8px;"> <button id="refresh-douban-status" style="background: #4caf50; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);">刷新状态</button> <button id="go-douban-home" style="background: #409eff; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);">访问豆瓣</button> </div> <div style="margin-top:10px; padding:10px; background:#f9fafb; border-radius:6px; border-left:3px solid #f59e0b;"> <p style="margin:0; font-size:12px; color:#6b7280;"> <strong>检测说明:</strong><br> 未登录(不可用)豆瓣账号时,页面将提示“检测失败:状态码=403”。请点击页面中的“去豆瓣登录(不可用)”按钮,完成豆瓣账号登录(不可用)流程后返回当前网页。若网页显示“豆瓣已登录(不可用)”,即表示系统已成功检测到您的豆瓣登录(不可用)状态,此后豆瓣源可正常使用。 </p> </div> `; // 绑定"访问豆瓣"按钮 safeBindDoubanEvent('go-douban-home', 'click', () => window.open('https://www.douban.com/', '_blank')); // 绑定"刷新状态"按钮 safeBindDoubanEvent('refresh-douban-status', 'click', async () => { if (isCheckingDoubanLogin) { showStatus('检测正在进行中,请稍候...', true, 'settings'); return; } const refreshBtn = await safeGetDoubanElement('refresh-douban-status'); if (refreshBtn) { refreshBtn.textContent = '刷新中...'; refreshBtn.disabled = true; refreshBtn.style.background = '#ccc'; } showStatus('正在刷新豆瓣登录(不可用)状态...', false, 'settings'); const result = await checkDoubanLoginStatus(); renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details); }); return; } // 3. 处理"未登录(不可用)(无错误)"状态 statusContainer.innerHTML = ` <div style="display: flex; align-items: center; margin-bottom: 12px;"> <span style="color:#ff9800; font-size: 14px; margin-right: 8px;">⚠️</span> <span style="color:#ff9800; font-weight: 500;">豆瓣未登录(不可用)</span> </div> <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom: 8px;"> <button id="go-douban-login" style="background: #4caf50; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);">去豆瓣登录(不可用)</button> <button id="retry-douban-check" style="background: #ff9800; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(255, 152, 0, 0.2);">重试检测</button> <button id="douban-diagnose" style="background: #409eff; color:white; border:none; padding:8px 16px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);">详细诊断</button> </div> <div style="font-size:11px; color:#606266; background:#f5f7fa; padding:6px 8px; border-radius:4px; border-left:3px solid #ff9800;">请先在豆瓣登录(不可用),登录(不可用)后点击"重试检测"</div> `; // 绑定按钮事件(同"检测错误"状态) safeBindDoubanEvent('go-douban-login', 'click', openDoubanLoginPage); safeBindDoubanEvent('retry-douban-check', 'click', async () => { if (isCheckingDoubanLogin) { showStatus('检测正在进行中,请稍候...', true, 'settings'); return; } const retryBtn = await safeGetDoubanElement('retry-douban-check'); if (retryBtn) { retryBtn.textContent = '检测中...'; retryBtn.disabled = true; retryBtn.style.background = '#ccc'; } showStatus('正在重新检测豆瓣登录(不可用)状态...', false, 'settings'); const result = await checkDoubanLoginStatus(); renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details); }); safeBindDoubanEvent('douban-diagnose', 'click', () => showDoubanDiagnosticPanel(details)); }); } /** * 创建豆瓣登录(不可用)检测UI容器 */ function createDoubanLoginContainer() { const container = document.createElement('div'); container.id = 'douban-login-container'; container.style.cssText = ` background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; padding: 15px; margin: 15px 0; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); position: relative; `; container.innerHTML = ` <div style="display: flex; align-items: center; margin-bottom: 12px;"> <span style="font-size: 16px; margin-right: 8px;">🔐</span> <h4 style="margin: 0; color: #8b5cf6; font-size: 14px; font-weight: 600;">豆瓣登录(不可用)状态</h4> </div> <div id="douban-login-status" style="font-size: 13px; margin-bottom: 10px;"></div> <div id="douban-status-container"></div> <style> #douban-login-container button { transition: border-color 0.2s ease, box-shadow 0.2s ease; } #douban-login-container button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important; } #douban-login-container button:active { transform: translateY(0); } #douban-login-container button:disabled { opacity: 0.6; cursor: not-allowed; transform: none !important; } </style> `; return container; } /** * 初始化豆瓣登录(不可用)检测功能 */ function initDoubanLoginCheck() { // 1. 创建UI容器并插入到设置面板中的指定位置(AI API配置区域下方) const settingsContentArea = document.querySelector('#settings-content-area'); if (settingsContentArea) { const doubanContainer = createDoubanLoginContainer(); // 查找AI API配置区域,将豆瓣检测UI插入到其下方 const aiApiConfigSection = settingsContentArea.querySelector('div:nth-of-type(2)'); // AI API配置区域 if (aiApiConfigSection) { settingsContentArea.insertBefore(doubanContainer, aiApiConfigSection.nextSibling); } else { // 备用方案:如果找不到特定位置,仍插入到设置内容区域的顶部 settingsContentArea.insertBefore(doubanContainer, settingsContentArea.firstChild); } } // 2. 页面加载完成后执行首次检测 setTimeout(async () => { showStatus('正在初始化豆瓣登录(不可用)检测...', false, 'settings'); const result = await checkDoubanLoginStatus(); renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details); // 3. 监听页面可见性变化(用户从登录(不可用)页返回后自动重新检测) document.addEventListener('visibilitychange', async () => { if (!document.hidden && !isCheckingDoubanLogin) { showStatus('页面重新可见,正在更新豆瓣登录(不可用)状态...', false, 'settings'); const result = await checkDoubanLoginStatus(); renderDoubanLoginStatus(result.isLoggedIn, result.error, result.details); } }); }, 500); // 延迟500ms确保DOM加载完成 } // 在脚本初始化时启动豆瓣登录(不可用)检测 setTimeout(() => { initDoubanLoginCheck(); // 自动应用移动端适配 applyMobileStyles(); // 预览模式:?preview=1 或 localStorage.preview=1 try { const urlHasPreview = /[?&]preview=1\b/.test(location.search); const lsPreview = (localStorage.getItem('script_preview') === '1'); if (urlHasPreview || lsPreview) { localStorage.setItem('script_preview', '1'); const badge = document.createElement('div'); badge.textContent = '预览模式'; badge.style.cssText = 'position:fixed;right:10px;bottom:10px;background:#ec4899;color:#fff;padding:6px 10px;border-radius:8px;font-size:12px;z-index:2147483647;box-shadow:0 2px 8px rgba(0,0,0,0.15);'; document.body.appendChild(badge); } } catch (e) { /* 忽略预览装饰失败 */ } }, 1000); // 延迟1秒确保主面板已创建 // 移动端适配工具函数 function isMobileDevice() { return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } // 应用移动端样式 function applyMobileStyles() { if (!isMobileDevice()) return; // 先移除已存在的移动端样式,避免样式重复 const existingStyle = document.getElementById('mobile-adaptation-styles'); if (existingStyle) existingStyle.remove(); // 添加全局响应式样式 const style = document.createElement('style'); style.id = 'mobile-adaptation-styles'; style.textContent = ` /* 全局搜索结果框保护规则 - 保持样式一致性 */ #search-results { /* 确保搜索结果框样式一致,但允许JavaScript控制显示状态 */ /* visibility: visible !important; - 移除强制可见 */ /* opacity: 1 !important; - 移除强制不透明 */ position: relative !important; z-index: 999999 !important; } /* 确保tox-editor-header不会遮挡搜索结果 */ div.tox-editor-header { z-index: 1 !important; } /* 确保只在移动设备上应用这些样式 */ @media screen and (min-width: 769px) { /* 电脑端样式保护 - 确保原有布局不受影响 */ div[style*="display:flex"][style*="gap:20px"] { display: flex !important; flex-direction: row !important; gap: 20px !important; } #search-results { position: absolute !important; z-index: 999999 !important; /* 移除强制显示,让JavaScript控制显示/隐藏 */ background: #fff !important; border: 1px solid #f3d5d9 !important; border-radius: 8px !important; max-height: 300px !important; overflow-y: auto !important; width: calc(100% - 85px) !important; left: 75px !important; top: 32px !important; display: none !important; } /* 在电脑端也强制降低编辑器头部层级 */ div.tox-editor-header { z-index: 1 !important; } label[style*="display:inline-block"][style*="width:75px"] { display: inline-block !important; width: 75px !important; vertical-align: middle !important; } input[style*="width: calc(100% - 85px)"] { width: calc(100% - 85px) !important; vertical-align: middle !important; } /* 电脑端提取按钮 - 默认隐藏,由JavaScript控制显示 */ #fetch-btn { display: none !important; /* 默认隐藏 */ opacity: 0.6 !important; pointer-events: none !important; background: #ec4899 !important; color: white !important; border: none !important; padding: 8px 16px !important; border-radius: 0 8px 8px 0 !important; cursor: not-allowed !important; font-size: 13px !important; transition: all 0.3s ease !important; } #fetch-btn.active { opacity: 1 !important; pointer-events: auto !important; cursor: pointer !important; background: #db2777 !important; } /* 电脑端控制面板响应式优化 */ #douban-tmdb-panel { min-width: 320px !important; max-width: 1400px !important; /* 扩大宽屏最大宽度限制,让搜索结果框能更好铺满 */ width: 100% !important; overflow-x: auto !important; } /* 电脑端搜索区域响应式优化 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] { flex-wrap: wrap !important; min-width: 0 !important; align-items: flex-start !important; } /* 宽窗口优化 (宽度 < 800px) - 提前处理 */ @media screen and (max-width: 800px) { #douban-tmdb-panel { min-width: 400px !important; } #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: 1 1 48% !important; min-width: 200px !important; } } /* 较宽窄窗口优化 (宽度 < 700px) - 提前处理 */ @media screen and (max-width: 700px) { #douban-tmdb-panel { min-width: 350px !important; } #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: 1 1 45% !important; min-width: 180px !important; } #search-movie, #media-url { min-width: 100px !important; } } /* 中等窄窗口优化 (宽度 < 650px) - 开始垂直布局 */ @media screen and (max-width: 650px) { #douban-tmdb-panel { min-width: 340px !important; } /* 强制搜索区域垂直布局 - 使用更高优先级的选择器 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; display: flex !important; flex-direction: row !important; align-items: center !important; } /* 搜索输入框优化 */ #search-movie { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 搜索结果框位置调整 */ #search-results { position: relative !important; width: 100% !important; left: 0 !important; top: 0 !important; margin-top: 4px !important; z-index: 1 !important; } /* 标签页按钮优化 */ #main-tab, #ai-tab, #settings-tab { font-size: 11px !important; padding: 6px 12px !important; min-width: 70px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 65px !important; font-size: 11px !important; } /* 修复链接框显示问题 */ .link-container { display: flex; flex-direction: column; gap: 10px; } .link-item { width: 100%; word-break: break-all; } } /* 中等窄窗口优化 (宽度 < 600px) - 强制垂直布局 */ @media screen and (max-width: 600px) { #douban-tmdb-panel { min-width: 320px !important; } /* 强制搜索区域垂直布局 - 使用更高优先级的选择器 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 12px !important; align-items: stretch !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; display: flex !important; flex-direction: row !important; align-items: center !important; } /* 搜索输入框优化 */ #search-movie { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 搜索结果框位置调整 */ #search-results { position: relative !important; width: 100% !important; left: 0 !important; top: 0 !important; margin-top: 4px !important; z-index: 1 !important; } /* 标签页按钮优化 */ #main-tab, #ai-tab, #settings-tab { font-size: 11px !important; padding: 6px 12px !important; min-width: 70px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 65px !important; font-size: 11px !important; } } /* 较窄窗口优化 (宽度 < 500px) - 强制换行 */ @media screen and (max-width: 500px) { #douban-tmdb-panel { min-width: 300px !important; padding: 12px !important; } /* 强制搜索区域垂直布局 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] { flex-direction: column !important; gap: 12px !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 搜索输入框优化 */ #search-movie { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 影视链接输入框优化 */ #media-url { width: calc(100% - 75px) !important; min-width: 150px !important; font-size: 12px !important; padding: 6px 10px !important; } /* 搜索结果框位置调整 */ #search-results { position: relative !important; width: 100% !important; left: 0 !important; top: 0 !important; margin-top: 4px !important; z-index: 1 !important; } /* 标签页按钮优化 */ #main-tab, #ai-tab, #settings-tab { font-size: 11px !important; padding: 6px 12px !important; min-width: 70px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 65px !important; font-size: 11px !important; } } /* 电脑端输入框响应式优化 */ #search-movie, #media-url { min-width: 100px !important; } /* 电脑端标签响应式优化 */ label[style*="width:70px"] { flex-shrink: 0 !important; } /* 电脑端标签页按钮响应式优化 */ #main-tab, #ai-tab, #settings-tab { flex: 1 1 auto !important; min-width: 80px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* 极窄窗口优化 (宽度 < 400px) - 强制搜索区域垂直布局 */ @media screen and (max-width: 400px) { #douban-tmdb-panel { min-width: 280px !important; padding: 10px !important; } /* 强制搜索区域垂直布局 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"] { flex-direction: column !important; gap: 10px !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 搜索输入框优化 */ #search-movie { width: calc(100% - 65px) !important; min-width: 120px !important; font-size: 11px !important; padding: 5px 8px !important; } /* 影视链接输入框优化 */ #media-url { width: calc(100% - 65px) !important; min-width: 120px !important; font-size: 11px !important; padding: 5px 8px !important; } /* 搜索结果框位置调整 */ #search-results { position: relative !important; width: 100% !important; left: 0 !important; top: 0 !important; margin-top: 4px !important; z-index: 1 !important; } /* 标签页按钮优化 */ #main-tab, #ai-tab, #settings-tab { font-size: 11px !important; padding: 6px 12px !important; min-width: 70px !important; } /* 标签文字优化 */ label[style*="width:70px"] { width: 65px !important; font-size: 11px !important; } } } /* 移动端专用样式 - 只在移动设备上生效 */ @media screen and (max-width: 768px) { /* 全局移动端适配 */ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } /* 主要容器适配 */ #douban-tmdb-panel { width: 100% !important; margin: 0 !important; padding: 8px !important; font-size: 14px !important; overflow-x: hidden !important; min-width: auto !important; max-width: none !important; } /* 标题和文本适配 */ h2, h3, h4 { font-size: 18px !important; margin: 8px 0 !important; } p, span { font-size: 14px !important; line-height: 1.5 !important; } /* 海报和剧照容器响应式布局 */ #poster-candidates, #still-candidates { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)) !important; gap: 4px !important; padding: 6px !important; max-height: 280px !important; overflow-y: auto !important; } /* 图片尺寸控制 */ #poster-candidates img, #still-candidates img { max-width: 100% !important; height: auto !important; object-fit: cover !important; border-radius: 4px !important; } /* 按钮适配 */ button { font-size: 13px !important; padding: 8px 12px !important; margin: 3px !important; border-radius: 4px !important; min-height: 36px !important; touch-action: manipulation; } /* 选择图片弹窗适配 */ #image-selection { width: 98% !important; max-height: none !important; /* 避免与内部候选区形成双重高度上限 */ margin: 2.5vh auto !important; padding: 10px !important; border-radius: 8px !important; } /* 输入框适配 */ input, select, textarea { width: 100% !important; margin-bottom: 8px !important; padding: 10px !important; font-size: 14px !important; border-radius: 4px !important; height: auto !important; } /* 搜索结果影视框适配 */ .movie-item, .search-result-item { width: 100% !important; padding: 8px !important; margin-bottom: 10px !important; border-radius: 6px !important; overflow: hidden !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; } .movie-item img, .search-result-item img { max-width: 40% !important; height: auto !important; float: left !important; margin-right: 10px !important; border-radius: 4px !important; } .movie-info, .search-result-info { display: block !important; overflow: hidden !important; white-space: normal !important; font-size: 13px !important; line-height: 1.4 !important; } .movie-title, .search-result-title { font-size: 16px !important; font-weight: bold !important; margin-bottom: 5px !important; white-space: normal !important; word-wrap: break-word !important; } /* 标签和评分适配 */ .movie-tags, .movie-rating { font-size: 12px !important; margin: 3px 0 !important; } /* 分页控制适配 */ .pagination { display: flex !important; justify-content: center !important; flex-wrap: wrap !important; padding: 10px 0 !important; } .pagination button { min-width: 30px !important; height: 30px !important; line-height: 30px !important; padding: 0 8px !important; font-size: 12px !important; } /* 滚动条美化 */ ::-webkit-scrollbar { width: 6px !important; height: 6px !important; } ::-webkit-scrollbar-track { background: #f1f1f1 !important; border-radius: 3px !important; } ::-webkit-scrollbar-thumb { background: #888 !important; border-radius: 3px !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* 表单布局适配 */ form { display: flex !important; flex-direction: column !important; gap: 8px !important; } /* 网格布局适配 */ .grid-container { grid-template-columns: 1fr !important; gap: 10px !important; } /* 加载更多按钮 */ #load-more-posters, #load-more-stills { width: 100% !important; margin: 10px 0 !important; padding: 10px !important; font-size: 14px !important; } /* 搜索结果弹窗优化 - 移动设备专用 */ #search-results { position: relative !important; z-index: 1000 !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 8px !important; max-height: 50vh !important; overflow-y: auto !important; width: 100% !important; left: 0 !important; right: 0 !important; top: auto !important; bottom: auto !important; box-shadow: 0 4px 20px rgba(0,0,0,0.15) !important; transform: none !important; transition: none !important; margin-top: 2px !important; } /* 移动端搜索区域整体布局 - 垂直排列 */ div[style*="display:flex"][style*="gap:20px"] { display: flex !important; flex-direction: column !important; gap: 10px !important; } /* 搜索框和提取按钮布局优化 */ #search-movie { width: 100% !important; margin-bottom: 8px !important; padding: 10px !important; font-size: 14px !important; } #fetch-btn { width: 100% !important; margin-top: 5px !important; padding: 10px !important; font-size: 14px !important; min-height: 40px !important; display: none !important; /* 移动端默认隐藏 */ } /* 移动端:移除CSS强制显示规则,完全由JavaScript控制按钮显示 */ /* #media-url:not(:placeholder-shown) + #fetch-btn, #media-url[value]:not([value=""]) + #fetch-btn { display: block; } */ /* 搜索输入框的父容器布局优化 */ div[id^="search-section"], div[id*="search-section"] { position: relative !important; width: 100% !important; margin-bottom: 10px !important; } /* 移动端影视链接容器 - 确保垂直布局 */ div[style*="flex:1"][style*="display:flex"][style*="align-items:center"] { flex: none !important; width: 100% !important; display: flex !important; flex-direction: column !important; align-items: stretch !important; } /* 搜索加载指示器优化 */ #search-loading { position: absolute !important; right: 15px !important; top: 35% !important; transform: translateY(-50%) !important; color: #999 !important; font-size: 12px !important; } /* 影视链接输入框优化 */ #media-url { width: 100% !important; padding: 10px !important; font-size: 14px !important; margin-bottom: 10px !important; } /* 修复标签和输入框之间的布局问题 */ label[for="search-movie"], label[for="media-url"], label[style*="width:80px"], label[style*="width:70px"] { display: block !important; width: 100% !important; margin-bottom: 4px !important; font-size: 13px !important; } /* 移动端标签布局优化 - 确保标签在输入框上方 */ label[style*="display:inline-block"][style*="width:75px"] { display: block !important; width: 100% !important; margin-bottom: 6px !important; font-size: 14px !important; font-weight: 500 !important; color: #6b7280 !important; } /* 移动端输入框布局优化 */ input[style*="width: calc(100% - 85px)"] { width: 100% !important; margin-left: 0 !important; vertical-align: top !important; } /* 修复搜索结果项的样式 */ .search-item { padding: 10px !important; border-bottom: 1px solid #eee !important; display: flex !important; align-items: center !important; gap: 8px !important; font-size: 13px !important; } .search-item .poster-placeholder { width: 36px !important; height: 54px !important; background: #f5f5f5 !important; border-radius: 4px !important; } /* 修复AI功能面板样式 */ #ai-function-select, #ai-prompt-input { width: 100% !important; padding: 8px !important; font-size: 13px !important; } /* 确保按钮在移动设备上更易于点击 */ button, input[type="button"], input[type="submit"] { touch-action: manipulation; cursor: pointer; -webkit-appearance: none; border-radius: 4px; } /* 避免键盘弹出时布局错乱 */ body { overflow-x: hidden; } /* 修复链接框显示问题 */ .link-container { display: flex; flex-direction: column; gap: 10px; } .link-item { width: 100%; word-break: break-all; } } /* 闭合移动端媒体查询 */ /* 针对极窄屏幕的额外优化 */ @media screen and (max-width: 480px) { #douban-tmdb-panel { padding: 10px 8px !important; } /* 强制搜索区域垂直布局 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 10px !important; align-items: stretch !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 影视链接容器优化 */ #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; } /* 影视链接包装器优化 - 强制垂直布局 */ #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 6px !important; width: 100% !important; } #search-movie { width: 100% !important; font-size: 11px !important; padding: 5px 8px !important; min-width: 120px !important; } #media-url { width: 100% !important; font-size: 11px !important; padding: 5px 8px !important; min-width: 120px !important; border-radius: 6px !important; } #fetch-btn { width: 100% !important; padding: 5px 8px !important; font-size: 11px !important; margin-left: 0 !important; border-radius: 6px !important; flex-shrink: 0 !important; } label[style*="width:70px"] { font-size: 10px !important; width: 60px !important; } #search-results { width: calc(100% - 70px) !important; left: 60px !important; max-height: 200px !important; } /* 确保标签在极窄屏幕上不被挤压 */ .search-item { flex-wrap: wrap !important; } .search-item .poster-placeholder { width: 30px !important; height: 45px !important; } /* 修复链接框显示问题 */ .link-container { gap: 5px; } } /* 针对超窄屏幕的优化 */ @media screen and (max-width: 360px) { #douban-tmdb-panel { padding: 8px 6px !important; } /* 强制搜索区域垂直布局 */ #douban-tmdb-panel div[style*="display:flex"][style*="gap:15px"][style*="margin-bottom:12px"][style*="align-items:center"] { flex-direction: column !important; gap: 8px !important; align-items: stretch !important; } /* 搜索和链接输入容器全宽显示 */ #douban-tmdb-panel div[style*="flex: 1 1 250px"][style*="min-width: 200px"] { flex: none !important; width: 100% !important; min-width: auto !important; } /* 影视链接容器优化 */ #media-url-container { width: 100% !important; flex: none !important; min-width: auto !important; } /* 影视链接包装器优化 - 强制垂直布局 */ #media-url-wrapper { flex-direction: column !important; align-items: stretch !important; gap: 4px !important; width: 100% !important; } #search-movie { width: 100% !important; font-size: 10px !important; padding: 4px 6px !important; min-width: 100px !important; } #media-url { width: 100% !important; font-size: 10px !important; padding: 4px 6px !important; min-width: 100px !important; border-radius: 6px !important; } #fetch-btn { width: 100% !important; padding: 4px 6px !important; font-size: 10px !important; margin-left: 0 !important; border-radius: 6px !important; flex-shrink: 0 !important; } label[style*="width:70px"] { font-size: 9px !important; width: 50px !important; } #search-results { position: relative !important; width: 100% !important; left: 0 !important; top: 0 !important; max-height: 150px !important; margin-top: 4px !important; z-index: 1 !important; } /* 搜索结果项进一步压缩 */ .search-item { padding: 6px !important; gap: 5px !important; } .search-item .poster-placeholder { width: 25px !important; height: 38px !important; } /* 修复链接框显示问题 */ .link-container { gap: 3px; } } `; document.head.appendChild(style); // 提取按钮初始化 - 默认隐藏(所有设备) const fetchBtn = document.getElementById('fetch-btn'); if (fetchBtn) { fetchBtn.style.display = 'none'; } // 添加动态调整搜索结果位置的逻辑 const searchInput = document.getElementById('search-movie'); const resultsContainer = document.getElementById('search-results'); if (searchInput && resultsContainer) { // 调整搜索结果位置的函数 - 简化版本,因为显示框现在在控制面板内部 function adjustResultsPosition() { if (!searchInput || !resultsContainer) return; // 尝试查找并降低div.tox-editor-header的z-index const editorHeader = document.querySelector('div.tox-editor-header'); if (editorHeader) { editorHeader.style.zIndex = '1'; // 临时降低编辑器头部的z-index } // 显示框现在在控制面板内部,不需要复杂的位置计算 // 只需要确保显示框正确显示 if (resultsContainer.style.display === 'block') { resultsContainer.style.setProperty('position', 'relative', 'important'); resultsContainer.style.setProperty('top', '0', 'important'); resultsContainer.style.setProperty('left', '0', 'important'); resultsContainer.style.setProperty('right', '0', 'important'); resultsContainer.style.setProperty('z-index', '1000', 'important'); } } // 防抖函数,避免频繁调用 let positionTimeout; const debouncedAdjustPosition = () => { clearTimeout(positionTimeout); positionTimeout = setTimeout(adjustResultsPosition, 50); }; // 立即调整位置 setTimeout(adjustResultsPosition, 100); // 监听输入框聚焦事件 searchInput.addEventListener('focus', debouncedAdjustPosition); // 监听窗口大小变化 window.addEventListener('resize', debouncedAdjustPosition); // 监听页面滚动 window.addEventListener('scroll', debouncedAdjustPosition); // 监听搜索结果显示/隐藏状态变化 const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.attributeName === 'style') { if (resultsContainer.style.display !== 'none') { debouncedAdjustPosition(); } } }); }); observer.observe(resultsContainer, { attributes: true }); } } try { // 注册(不可用)油猴菜单项 GM_registerMenuCommand('📱 移动端适配', () => { applyMobileStyles(); showStatus('移动端适配样式已应用', false); }); } catch (e) { console.error('注册(不可用)移动端适配菜单项失败:', e); } // 立即注入搜索结果框CSS确保显示 setTimeout(() => { if (typeof injectSearchResultsCSS === 'function') { injectSearchResultsCSS(); console.log('已注入搜索结果框强制CSS样式'); } // 确保搜索中指示器初始状态为隐藏 setSearchLoading(false); // 定期检查并隐藏搜索中指示器(防止意外显示) setInterval(() => { const searchInput = document.getElementById('search-movie'); const resultsContainer = document.getElementById('search-results'); const loadingIndicator = document.getElementById('search-loading'); // 只有在搜索框为空、没有搜索结果、且搜索中指示器已经显示超过5秒时才隐藏 if (searchInput && !searchInput.value.trim() && resultsContainer && resultsContainer.style.display === 'none' && loadingIndicator && loadingIndicator.style.display === 'block') { // 检查指示器是否已经显示超过5秒 const now = Date.now(); if (!loadingIndicator.dataset.showTime) { loadingIndicator.dataset.showTime = now.toString(); } else { const showTime = parseInt(loadingIndicator.dataset.showTime); if (now - showTime > 5000) { setSearchLoading(false); // 静默隐藏超时的搜索中指示器 } } } }, 3000); }, 100); // 脚本功能检查机制 - 静默运行,不显示UI function initScriptChecks() { console.log('🎬 豆瓣+TMDB影视工具脚本已启动'); // 核心功能检查(静默运行) const coreChecks = { // 检查控制面板是否正确创建 checkPanelCreation: () => { const panel = document.getElementById('douban-tmdb-panel'); return panel ? true : false; }, // 检查搜索功能 checkSearchFunction: () => { const searchInput = document.getElementById('search-movie'); const results = document.getElementById('search-results'); return !!(searchInput && results); }, // 检查输入框水平对齐(更稳健:直接比较元素本身位置,移动端跳过) checkInputAlignment: () => { try { // 移动端/窄屏为垂直布局,不做水平对齐校验 if (window.innerWidth <= 800) return true; const searchEl = document.getElementById('search-movie'); const linkContainer = document.getElementById('media-url-container'); if (!searchEl || !linkContainer) return true; // 缺少元素时不报错 const searchRect = searchEl.getBoundingClientRect(); const linkRect = linkContainer.getBoundingClientRect(); const heightDiff = Math.abs(searchRect.top - linkRect.top); return heightDiff < 8; // 适度放宽容差,避免偶发1-2px误差 } catch (e) { return true; } } }; // 静默运行功能检查,不显示UI // 静默运行核心功能检查 setTimeout(() => { // 只检查关键功能,只在出错时输出警告 Object.entries(coreChecks).forEach(([key, check]) => { if (!check()) { console.warn('脚本功能检查失败:', key); } }); }, 2000); // 监听搜索功能(静默) const searchInput = document.getElementById('search-movie'); if (searchInput) { searchInput.addEventListener('input', (e) => { // 静默监听,不输出日志 }); } // 监听控制面板展开状态(静默) const panel = document.getElementById('douban-tmdb-panel'); if (panel) { const observer = new MutationObserver((mutations) => { // 静默监听,不输出日志 }); observer.observe(panel, { attributes: true }); } // 监听窗口大小变化,动态调整输入框对齐(横排即时,竖排轻微防抖) let resizeTimeout; window.addEventListener('resize', () => { const w = window.innerWidth; // 竖→横:立即执行,配合no-transition实现闪切 if (w > 800) { adjustInputAlignment(); return; } // 横→竖:仅做极短防抖,减小抖动 clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { adjustInputAlignment(); }, 50); }); // 初始化时调整对齐(缩短延迟,加快就绪速度) setTimeout(() => { adjustInputAlignment(); }, 120); } // 初始化脚本检查机制 setTimeout(initScriptChecks, 1000); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址