// ==UserScript== // @name Bangumi Ultimate Enhancer // @namespace https://tampermonkey.net/ // @version 3.0.2 // @description 集成Wiki按钮、关联按钮、封面上传、批量关联、批量分集编辑、内容快捷填充、单行本快捷创建、编辑预览功能 // @author Bios (improved by Claude) // @match *://bgm.tv/subject/* // @match *://chii.in/subject/* // @match *://bangumi.tv/subject/* // @match *://bgm.tv/character/* // @match *://chii.in/character/* // @match *://bangumi.tv/character/* // @match *://bgm.tv/person/* // @match *://chii.in/person/* // @match *://bangumi.tv/person/* // @match *://bgm.tv/new_subject/* // @match *://chii.in/new_subject/* // @match *://bangumi.tv/new_subject/* // @exclude */character/*/add_related/person/* // @exclude */person/*/add_related/character/* // @connect bgm.tv // @connect bangumi.tv // @connect chii.in // @icon https://lain.bgm.tv/pic/icon/l/000/00/01/128.jpg // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @license MIT // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @run-at document-idle // @downloadURL none // ==/UserScript== /* global $ jQuery */ (function() { "use strict"; // 样式注入 function injectStyles() { $('head').append(` `); } /* ============= Wiki、关联按钮 =============== */ function initNavButtons() { // 精确匹配仅以数字结尾的路径(不允许多余路径) const pathMatch = location.pathname.match(/^\/(subject|person|character)\/(\d+)$/); if (!pathMatch) return; const [, pageType, pageId] = pathMatch; const origin = location.origin; // 获取导航栏 const nav = document.querySelector(".subjectNav .navTabs, .navTabs"); if (!nav) return; // 按钮配置 const buttons = [ { className: "wiki-button", getText: () => "Wiki", getUrl: () => pageType === "subject" ? `${origin}/${pageType}/${pageId}/edit_detail` : `${origin}/${pageType}/${pageId}/edit` }, { className: "relate-button", getText: () => "关联", getUrl: () => pageType === "subject" ? `${origin}/${pageType}/${pageId}/add_related/subject/anime` : `${origin}/${pageType}/${pageId}/add_related/anime` } ]; // 添加按钮 buttons.forEach(button => { if (!nav.querySelector(`.${button.className}`)) { const li = document.createElement("li"); li.className = button.className; li.innerHTML = `${button.getText()}`; nav.appendChild(li); } }); } // 监听 URL 变化 function observeURLChanges() { let lastURL = location.href; new MutationObserver(() => { if (location.href !== lastURL) { lastURL = location.href; initNavButtons(); } }).observe(document, { subtree: true, childList: true }); } /* =========== 封面快捷上传 ============= */ async function initCoverUpload() { // 定义支持的域名(用于后续可能的URL匹配校验,当前未使用) const SUPPORTED_DOMAINS = ['bangumi\\.tv', 'bgm\\.tv', 'chii\\.in']; const url = window.location.href; // 当前页面完整 URL const pathname = location.pathname; // 当前页面路径部分(不含域名、参数) // 仅当路径完全以数字结尾才认为是有效的目标页面 const match = pathname.match(/^\/(subject|person|character)\/(\d+)$/); if (!match) return; // 如果不匹配,直接退出函数,不执行后续逻辑 // 解析页面类型(subject/person/character)和 ID const type = match[1]; const id = match[2]; const parsedInfo = { type, id }; // 封装为对象便于后续使用 // 如果上传按钮已经存在,则不再重复创建,避免重复注入 if (document.querySelector("#coverUploadButton")) return; // 获取导航栏 DOM 元素(兼容旧模板和新模板) const nav = document.querySelector(".subjectNav .navTabs") || document.querySelector(".navTabs"); if (!nav) return; // 如果找不到导航栏,则无法插入按钮,直接退出 // 创建上传按钮和表单容器(无论是否有图片) const createUploadButton = () => { const uploadLi = document.createElement("li"); uploadLi.id = "coverUploadButton"; uploadLi.className = "upload-button"; uploadLi.style.float = "right"; uploadLi.innerHTML = `上传封面`; return uploadLi; }; const createFormContainer = () => { const formContainer = document.createElement("div"); formContainer.id = "coverUploadFormContainer"; formContainer.classList.add("cover-upload-modal"); formContainer.innerHTML = `
图片预览
`; formContainer.style.position = "absolute"; formContainer.style.zIndex = "9999"; formContainer.style.display = "none"; return formContainer; }; const uploadLi = createUploadButton(); const formContainer = createFormContainer(); nav.appendChild(uploadLi); document.body.appendChild(formContainer); // 初始化图片相关变量 let imgIdx = 0, imgList = [], voteLinks = []; let imgEl = null, coverLnk = null, coverDiv = null, currImgSrc = null; // 尝试获取封面图片容器 coverDiv = document.querySelector('#bangumiInfo .infobox div[align="center"]'); if (coverDiv) { imgEl = coverDiv.querySelector('a.cover img'); coverLnk = coverDiv.querySelector('a.cover'); currImgSrc = imgEl ? imgEl.getAttribute('src') : null; } // 创建圆形箭头按钮 const createArrow = (cls) => { const arrow = document.createElement('div'); arrow.className = `cover-arrow ${cls}`; Object.assign(arrow.style, { position: 'absolute', top: '50%', transform: 'translateY(-50%)', [cls.includes('left') ? 'left' : 'right']: '10px', opacity: '0.8', cursor: 'pointer', background: 'rgba(0, 0, 0, 0.7)', color: '#fff', width: '30px', height: '30px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', fontWeight: 'bold', zIndex: '1000', boxShadow: '0 0 5px rgba(0, 0, 0, 0.5)', transition: 'background 0.2s ease, opacity 0.2s ease' }); arrow.innerHTML = cls.includes('left') ? '◄' : '►'; arrow.addEventListener('mouseenter', () => { arrow.style.background = 'rgba(0, 0, 0, 0.9)'; arrow.style.opacity = '1'; }); arrow.addEventListener('mouseleave', () => { arrow.style.background = 'rgba(0, 0, 0, 0.7)'; arrow.style.opacity = '0.8'; }); return arrow; }; // 创建投票按钮 const createVoteButton = () => { const voteButton = document.createElement('div'); voteButton.className = 'vote-button'; Object.assign(voteButton.style, { position: 'absolute', bottom: '15px', left: '50%', transform: 'translateX(-50%)', opacity: '0.9', cursor: 'pointer', background: 'rgba(240, 98, 146, 0.9)', color: '#fff', padding: '8px 20px', borderRadius: '20px', fontSize: '14px', fontWeight: 'bold', zIndex: '1000', boxShadow: '0 2px 5px rgba(0, 0, 0, 0.3)', transition: 'background 0.2s ease, transform 0.1s ease', }); voteButton.textContent = '投票'; voteButton.addEventListener('mouseenter', () => { voteButton.style.background = 'rgba(240, 98, 146, 1)'; voteButton.style.transform = 'translateX(-50%) scale(1.05)'; }); voteButton.addEventListener('mouseleave', () => { voteButton.style.background = 'rgba(240, 98, 146, 0.9)'; voteButton.style.transform = 'translateX(-50%) scale(1)'; }); return voteButton; }; // 图片切换逻辑(仅在有图片时启用) if (coverDiv && imgEl && coverLnk) { const updateImg = () => { const newImgSrc = imgList[imgIdx]; imgEl.setAttribute('src', newImgSrc); coverLnk.setAttribute('href', newImgSrc.replace('/r/400/', '/')); const voteButton = coverDiv.querySelector('.vote-button'); if (voteButton) voteButton.remove(); if (newImgSrc !== currImgSrc && voteLinks[imgIdx]) { const voteBtn = createVoteButton(); voteBtn.addEventListener('click', () => { const voteUrl = voteLinks[imgIdx]; if (!voteUrl) { showStatus('找不到有效的投票链接', true); showBrowserNotification("投票失败", "找不到有效的投票链接。"); return; } const fullVoteUrl = voteUrl.startsWith('http') ? voteUrl : `https://${window.location.host}${voteUrl.startsWith('/') ? '' : '/'}${voteUrl}`; showStatus('正在提交投票...'); const xhr = new XMLHttpRequest(); xhr.open('GET', fullVoteUrl, true); xhr.withCredentials = true; xhr.setRequestHeader('Accept', 'text/html'); xhr.setRequestHeader('Referer', window.location.href); xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { alert('投票成功!页面将在3秒后刷新...'); showBrowserNotification("投票成功", "封面投票成功,页面即将刷新。"); setTimeout(() => window.location.reload(), 3000); } else { const errorMsg = `投票失败(${xhr.status}),请手动投票`; showStatus(errorMsg, true); showBrowserNotification("投票失败", `投票遇到问题 (${xhr.status}),请尝试手动投票。`); console.error('投票请求失败:', xhr.statusText); } }; xhr.onerror = function() { alert('网络错误,投票失败,请手动投票!'); showBrowserNotification("投票失败", "网络错误导致投票失败,请尝试手动投票。"); console.error('投票XHR请求错误'); }; xhr.send(); }); coverDiv.appendChild(voteBtn); } }; const changeImg = (dir) => { if (imgList.length) { imgIdx = (imgIdx + dir + imgList.length) % imgList.length; updateImg(); } }; const arrows = [ createArrow('cover-arrow-left'), createArrow('cover-arrow-right') ]; coverDiv.style.position = 'relative'; coverDiv.append(...arrows); arrows.forEach(arrow => arrow.style.display = 'none'); coverDiv.addEventListener('mouseenter', () => { arrows.forEach(arrow => arrow.style.display = 'flex'); const voteBtn = coverDiv.querySelector('.vote-button'); if (voteBtn) voteBtn.style.display = 'block'; }); coverDiv.addEventListener('mouseleave', () => { arrows.forEach(arrow => arrow.style.display = 'none'); const voteBtn = coverDiv.querySelector('.vote-button'); if (voteBtn) voteBtn.style.display = 'none'; }); coverDiv.addEventListener('click', (e) => { if (e.target.classList.contains('cover-arrow-left')) changeImg(-1); if (e.target.classList.contains('cover-arrow-right')) changeImg(1); }); } // 获取图片列表 const fetchImgList = () => { fetch(`${window.location.pathname}/upload_img`) .then(res => res.text()) .then(html => { const $html = new DOMParser().parseFromString(html, 'text/html'); const imgs = Array.from($html.querySelectorAll('.photoList li a.grid img')).map(img => img.getAttribute('src')); voteLinks = Array.from($html.querySelectorAll('.photoList li a[href*="/vote/cover/"]')).map(a => a.getAttribute('href')); imgList = [...new Set(imgs)]; imgIdx = Math.max(0, imgList.indexOf(currImgSrc)); if (imgList.length > 1 && coverDiv && imgEl && coverLnk) { updateImg(); } }) }; fetchImgList(); let formLoaded = false; let hideTimeout = null; let isFormPinned = false; // 显示状态消息 function showStatus(message, isError = false) { const statusDiv = document.getElementById('statusMessage'); statusDiv.textContent = message; statusDiv.style.display = 'block'; statusDiv.style.backgroundColor = isError ? '#ffeeee' : '#eeffee'; statusDiv.style.color = isError ? '#cc0000' : '#007700'; statusDiv.style.border = `1px solid ${isError ? '#cc0000' : '#007700'}`; console.log(`[状态] ${message}`); } // 显示浏览器通知 function showBrowserNotification(title, body) { if (!("Notification" in window)) { console.log("此浏览器不支持桌面通知"); showStatus(body || title, title.includes("失败")); // Fallback to status message } else if (Notification.permission === "granted") { new Notification(title, { body: body }); } else if (Notification.permission !== "denied") { Notification.requestPermission().then(function (permission) { if (permission === "granted") { new Notification(title, { body: body }); } else { showStatus(body || title, title.includes("失败")); // Fallback if permission denied } }); } else { showStatus(body || title, title.includes("失败")); // Fallback if permission denied } } // 创建隐藏的iframe用于POST请求 function createHiddenIframe() { const existingIframe = document.getElementById('hiddenUploadFrame'); if (existingIframe) return existingIframe; const iframe = document.createElement('iframe'); iframe.id = 'hiddenUploadFrame'; iframe.name = 'hiddenUploadFrame'; iframe.style.display = 'none'; document.body.appendChild(iframe); return iframe; } // 从上传结果页面中提取投票链接并自动投票 function processUploadResult(iframe) { return new Promise((resolve, reject) => { iframe.onload = function() { try { const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; const allVoteLinks = iframeDocument.querySelectorAll('a[href*="/vote/cover/"]'); const voteLink = allVoteLinks.length > 0 ? allVoteLinks[allVoteLinks.length - 1] : null; if (voteLink) { const href = voteLink.getAttribute('href'); const host = window.location.host; const voteUrl = href.startsWith('http') ? href : `https://${host}${href.startsWith('/') ? '' : '/'}${href}`; showStatus('封面上传成功,正在投票...'); const xhr = new XMLHttpRequest(); xhr.open('GET', voteUrl, true); xhr.withCredentials = true; xhr.setRequestHeader('Accept', 'text/html'); xhr.setRequestHeader('Referer', window.location.href); xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { const successMsg = '投票成功!页面将在3秒后刷新...'; showStatus(successMsg); showBrowserNotification("投票成功", "封面上传后的自动投票已成功,页面即将刷新。"); setTimeout(() => window.location.reload(), 3000); resolve(true); } else { const errorMsg = '封面上传成功,但投票失败。3秒后跳转到手动投票页面...'; showStatus(errorMsg, true); showBrowserNotification("投票失败", "封面上传成功,但自动投票失败。将跳转到手动投票页面。"); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(new Error(`投票请求失败,状态 ${xhr.status}`)); } }; xhr.onerror = function() { const errorMsg = '封面上传成功,但投票失败。3秒后跳转到手动投票页面...'; showStatus(errorMsg, true); showBrowserNotification("投票失败", "封面上传成功,但网络错误导致自动投票失败。将跳转到手动投票页面。"); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(new Error('XHR请求错误')); }; xhr.send(); } else { const errorMsgEl = iframeDocument.querySelector('.error, .errorMessage, [class*="error"]'); if (errorMsgEl) { const errorText = `上传失败: ${errorMsgEl.textContent},3秒后跳转到手动上传页面...`; showStatus(errorText, true); showBrowserNotification("上传失败", `${errorMsgEl.textContent} 将跳转到手动上传页面。`); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(new Error(errorMsgEl.textContent)); } else { const warnMsg = '封面似乎已上传成功,但未找到投票链接。3秒后跳转到手动处理页面...'; showStatus(warnMsg, true); showBrowserNotification("操作提醒", "封面已上传,但未找到投票链接。将跳转到手动处理页面。"); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(new Error('未找到投票链接')); } } } catch (error) { const errorText = '处理上传结果时出错,3秒后跳转到手动上传页面...'; showStatus(errorText, true); showBrowserNotification("处理错误", "处理上传结果时出错,将跳转到手动上传页面。"); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(error); } }; iframe.onerror = function(error) { const errorText = '上传请求失败,3秒后跳转到手动上传页面...'; showStatus(errorText, true); showBrowserNotification("上传失败", "上传请求失败,将跳转到手动上传页面。"); setTimeout(() => window.location.href = `${window.location.href.split('?')[0]}/upload_img`, 3000); reject(new Error('上传请求失败')); }; }); } // 修改表单提交处理函数,使用iframe提交 function setupFormForIframeSubmission(form) { const iframe = createHiddenIframe(); form.target = 'hiddenUploadFrame'; form.addEventListener('submit', function(e) { showStatus('正在上传封面...'); processUploadResult(iframe).catch(error => console.error('处理上传结果失败:', error)); }); } // 优化的图片转换函数 async function convertImageFormat(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const fileType = file.type.toLowerCase(); let hasTransparency = false; let finalFormat; const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); try { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; for (let i = 3; i < pixels.length; i += 4) { if (pixels[i] < 255) { hasTransparency = true; break; } } } catch (e) { console.warn("无法检查透明度,默认使用原格式", e); } if (hasTransparency) { finalFormat = 'image/png'; } else if (fileType.includes('png') && !hasTransparency) { finalFormat = 'image/jpeg'; } else { finalFormat = fileType.includes('jpeg') || fileType.includes('jpg') ? 'image/jpeg' : fileType.includes('png') ? 'image/png' : 'image/jpeg'; } canvas.width = img.width; canvas.height = img.height; const newCtx = canvas.getContext('2d'); if (finalFormat === 'image/jpeg') { newCtx.fillStyle = '#FFFFFF'; newCtx.fillRect(0, 0, canvas.width, canvas.height); } newCtx.drawImage(img, 0, 0); const quality = finalFormat === 'image/jpeg' ? 0.92 : undefined; canvas.toBlob((blob) => { if (!blob) { reject(new Error('转换图片失败')); return; } const ext = finalFormat === 'image/png' ? 'png' : 'jpg'; const newFileName = file.name.split('.')[0] + '.' + ext; const convertedFile = new File([blob], newFileName, { type: finalFormat }); resolve({ file: convertedFile, dataURL: canvas.toDataURL(finalFormat, quality), format: finalFormat.split('/')[1] }); }, finalFormat, quality); }; img.onerror = () => reject(new Error('加载图片失败')); img.src = e.target.result; }; reader.onerror = () => reject(new Error('读取文件失败')); reader.readAsDataURL(file); }); } // 图片下载和转换函数 async function downloadAndConvertImage(imageUrl) { try { let actualImageUrl = imageUrl; if (imageUrl.includes('google.com/imgres')) { const urlParams = new URL(imageUrl).searchParams; actualImageUrl = urlParams.get('imgurl'); } if (!actualImageUrl) actualImageUrl = imageUrl; showStatus('正在下载图片...'); const response = await fetch(actualImageUrl); const blob = await response.blob(); const tempFileName = actualImageUrl.split('/').pop() || 'image'; const tempFile = new File([blob], tempFileName, { type: blob.type }); showStatus('正在优化图片格式...'); const convertedData = await convertImageFormat(tempFile); const previewContainer = document.querySelector("#imagePreviewContainer"); const previewImage = document.querySelector("#imagePreview"); previewImage.src = convertedData.dataURL; previewContainer.style.display = "block"; const fileInput = document.querySelector("#coverUploadForm input[type='file']"); if (fileInput) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(convertedData.file); fileInput.files = dataTransfer.files; const event = new Event('change', { bubbles: true }); fileInput.dispatchEvent(event); const submitButton = document.querySelector("#coverUploadForm input[type='submit']"); if (submitButton) submitButton.style.display = 'block'; showStatus(`图片已优化为 ${convertedData.format.toUpperCase()} 格式,点击提交按钮上传`); } else { showStatus('未找到文件上传输入框', true); } } catch (error) { console.error('下载或转换图片时发生错误:', error); showStatus(`下载图片失败:${error.message}`, true); } } // 全局点击事件 function setupGlobalClickHandler(container, trigger) { document.addEventListener('click', function (event) { if (!container.contains(event.target) && !trigger.contains(event.target)) { container.style.display = "none"; isFormPinned = false; } }); } // 预先加载本地上传表单 async function preloadLocalUpload() { if (formLoaded) return; const uploadFormContainer = formContainer.querySelector("#uploadFormContainer"); uploadFormContainer.innerHTML = "加载中..."; try { const uploadUrl = `https://${window.location.host}/${parsedInfo.type}/${parsedInfo.id}/upload_img`; const res = await fetch(uploadUrl); const doc = new DOMParser().parseFromString(await res.text(), "text/html"); const form = doc.querySelector("form[enctype='multipart/form-data']"); if (form) { form.id = "coverUploadForm"; form.style.margin = "0"; form.style.padding = "0"; uploadFormContainer.innerHTML = form.outerHTML; const insertedForm = document.getElementById("coverUploadForm"); setupFormForIframeSubmission(insertedForm); const fileInput = document.querySelector("#coverUploadForm input[type='file']"); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) { try { showStatus('正在处理图片...'); const convertedData = await convertImageFormat(file); const dataTransfer = new DataTransfer(); dataTransfer.items.add(convertedData.file); fileInput.files = dataTransfer.files; const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const previewImage = formContainer.querySelector("#imagePreview"); previewImage.src = convertedData.dataURL; previewContainer.style.display = "block"; const submitButton = document.querySelector("#coverUploadForm input[type='submit']"); if (submitButton) submitButton.style.display = 'block'; showStatus(`图片已优化为 ${convertedData.format.toUpperCase()} 格式,点击提交按钮上传`); } catch (error) { console.error('处理本地图片失败:', error); showStatus(`处理图片失败: ${error.message}`, true); const reader = new FileReader(); reader.onload = (ev) => { const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const previewImage = formContainer.querySelector("#imagePreview"); previewImage.src = ev.target.result; previewContainer.style.display = "block"; showStatus('使用原始格式,点击提交按钮上传'); }; reader.readAsDataURL(file); } } }); formLoaded = true; } else { uploadFormContainer.innerHTML = "无法加载上传表单"; showStatus("无法加载上传表单", true); } } catch (e) { uploadFormContainer.innerHTML = "加载失败"; showStatus("加载上传表单失败", true); console.error("上传模块加载失败:", e); } } // 事件绑定逻辑 const setupEventHandlers = () => { const urlInput = formContainer.querySelector("#imageUrlInput"); const downloadButton = formContainer.querySelector("#downloadUrlButton"); const showForm = () => { clearTimeout(hideTimeout); const buttonRect = uploadLi.getBoundingClientRect(); formContainer.style.top = `${buttonRect.bottom + window.scrollY + 5}px`; formContainer.style.left = `${buttonRect.left + window.scrollX - 180}px`; formContainer.style.display = "block"; }; const hideForm = () => { if (isFormPinned) return; const previewContainer = formContainer.querySelector("#imagePreviewContainer"); const statusMessage = formContainer.querySelector("#statusMessage"); if (previewContainer.style.display === "block" || statusMessage.style.display === "block") return; hideTimeout = setTimeout(() => { if (!formContainer.matches(":hover") && !isFormPinned) { formContainer.style.display = "none"; } }, 200); }; uploadLi.addEventListener("click", () => { showForm(); isFormPinned = true; console.log("表单已固定,鼠标移出不会自动关闭"); }); uploadLi.addEventListener("mouseenter", showForm); uploadLi.addEventListener("mouseleave", () => { if (!isFormPinned) hideForm(); }); formContainer.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); formContainer.addEventListener("mouseleave", () => { if (!isFormPinned) hideForm(); }); urlInput.addEventListener('focus', () => { urlInput.style.borderColor = '#F4C7CC'; urlInput.style.boxShadow = '0 0 5px rgba(244, 199, 204, 0.5)'; }); urlInput.addEventListener('blur', () => { urlInput.style.borderColor = '#ddd'; urlInput.style.boxShadow = 'none'; }); downloadButton.addEventListener('click', () => { const imageUrl = urlInput.value.trim(); if (imageUrl) downloadAndConvertImage(imageUrl); else showStatus('请输入图片 URL', true); }); urlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') downloadButton.click(); }); }; setupGlobalClickHandler(formContainer, uploadLi); preloadLocalUpload(); setupEventHandlers(); } // MutationObserver 保证上传按钮始终存在 const observer = new MutationObserver(() => { if (!document.querySelector("#coverUploadButton")) { initCoverUpload(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 确保在脚本开始时检查通知权限 if (Notification.permission === "default") { Notification.requestPermission(); } /* ============= 批量关联增强版 =============== */ async function initBatchRelation() { injectStyles(); // 参数配置 const DELAY_AFTER_CLICK = 150; const DELAY_BETWEEN_ITEMS = 300; const MAX_RETRY_ATTEMPTS = 10; const RETRY_INTERVAL = 100; // 全局变量 let globalItemType = '1'; let currentProcessingIndex = -1; // 添加全局设置变量 let enableExistingRelationCheck = false; // 根据当前 URL 判断页面类型(支持排除特定路径) function getCurrentPageType() { const path = window.location.pathname; // 调整正则表达式优先级,先检查更具体的路径 if (/^\/(?:subject\/\d+\/add_related\/character|character\/\d+\/add_related\/)/.test(path)) { return 'character'; } else if (/^\/subject\/\d+\/add_related\//.test(path)) { return 'subject'; } else { return 'person'; } } // 增强版下拉框生成 function generateTypeSelector() { const pageType = getCurrentPageType(); // 公共选项生成逻辑 const generateOptions = (types) => { return Object.entries(types) .map(([value, text]) => ``) .join(''); }; switch(pageType) { case 'character': return `类型: `; default: return `${ typeof genPrsnStaffList === "function" ? genPrsnStaffList(-1) : '' }`; } } // 针对传入的元素内的下拉框进行设置,并通过递归确保修改成功 function setRelationTypeWithElement($li, item_type) { return new Promise((resolve) => { let attempts = 0; function trySet() { // 确保我们获取的是当前元素内部的select,而不是全局的 let $select = $li.find('select').first(); if ($select.length > 0) { // 先确保下拉框可交互 if ($select.prop('disabled')) { setTimeout(trySet, RETRY_INTERVAL); return; } $select.val(item_type); // 触发 change 事件 const event = new Event('change', { bubbles: true }); $select[0].dispatchEvent(event); setTimeout(() => { if ($select.val() == item_type) { resolve(true); } else if (attempts < MAX_RETRY_ATTEMPTS) { attempts++; setTimeout(trySet, RETRY_INTERVAL); } else { resolve(false); } }, 200); } else if (attempts < MAX_RETRY_ATTEMPTS) { attempts++; setTimeout(trySet, RETRY_INTERVAL); } else { resolve(false); } } trySet(); }); } // 修改 checkAndHandleExistingRelation 函数 function checkAndHandleExistingRelation(search_name, item_id, item_type) { return new Promise(async (resolve) => { // 如果开关关闭,直接返回未关联状态 if (!enableExistingRelationCheck) { resolve({ exists: false }); return; } // 获取所有已关联条目的容器 const relatedContainer = document.querySelector('#crtRelateSubjects'); if (!relatedContainer) { resolve({ exists: false }); return; } // 原有的检查逻辑保持不变... const relatedItems = relatedContainer.querySelectorAll('li'); for (const item of relatedItems) { // 检查条目ID是否匹配 - 从URL中提取ID const itemLink = item.querySelector('a[href*="/subject/"], a[href*="/character/"], a[href*="/person/"]'); if (!itemLink) continue; const urlMatch = itemLink.href.match(/\/(subject|character|person)\/(\d+)/); if (!urlMatch || urlMatch[2] !== item_id.toString()) continue; // 找到匹配的已关联条目,检查并更新类型 const $select = $(item).find('select').first(); if ($select.length > 0) { const currentType = $select.val(); if (currentType !== item_type) { // 类型不同,需要更新 const success = await setRelationTypeWithElement($(item), item_type); if (success) { $('.Relation_item_type_changed').append(`${search_name} `); resolve({ exists: true, typeChanged: true }); return; } } else { // 类型相同,无需更新 $('.Relation_item_unchanged').append(`${search_name} `); resolve({ exists: true, typeChanged: false }); return; } } } // 未找到匹配的已关联条目 resolve({ exists: false }); }); } // 点击项目后利用 MutationObserver 监听新增条目,然后对该条目的下拉框设置类型 function processItem(element, item_type, item_id, search_name) { return new Promise(async (resolve) => { // 先检查条目是否已关联 const existingCheck = await checkAndHandleExistingRelation(search_name, item_id, item_type); if (existingCheck.exists) { return resolve(true); // 已处理完毕,无需继续 } // 条目未关联,进行新增操作 // 关联列表容器 const container = document.querySelector('#crtRelateSubjects'); if (!container) { return resolve(false); } // 保存处理前的条目列表 const initialItems = Array.from(container.children); // 绑定 MutationObserver 监听子节点变化 const observer = new MutationObserver((mutations) => { // 获取当前所有条目 const currentItems = Array.from(container.children); // 找出新增的条目(在当前列表中但不在初始列表中的元素) const newItems = currentItems.filter(item => !initialItems.includes(item)); if (newItems.length > 0) { observer.disconnect(); const newItem = newItems[0]; // 获取第一个新增条目 // 确保等待DOM完全渲染 setTimeout(async () => { // 使用新的条目元素直接查找其内部的select const $select = $(newItem).find('select'); if ($select.length > 0) { const success = await setRelationTypeWithElement($(newItem), item_type); resolve(success); } else { resolve(false); } }, DELAY_AFTER_CLICK); } }); observer.observe(container, { childList: true, subtree: true }); // 触发点击 $(element).click(); // 超时防护 setTimeout(() => { observer.disconnect(); resolve(false); }, MAX_RETRY_ATTEMPTS * RETRY_INTERVAL); }); } // 处若搜索结果不唯一且没有完全匹配项则自动选择第一个 function normalizeText(text) { return text.normalize("NFC").replace(/\s+/g, '').replace(/[\u200B-\u200D\uFEFF]/g, '').trim(); } function extractTextFromElement(el) { if (!el) return ''; let text = el.innerText || el.textContent || $(el).text(); // 尝试从 `iframe` 和 `shadowRoot` 获取文本 if (!text.trim()) { if (el.shadowRoot) { text = [...el.shadowRoot.querySelectorAll('*')].map(e => e.textContent).join(''); } let iframe = el.querySelector('iframe'); if (iframe && iframe.contentDocument) { text = iframe.contentDocument.body.textContent; } } return normalizeText(text); } async function processSingleItem(elements, item_type, search_name) { return new Promise(async (resolve) => { if (elements.length === 0) { $('.Relation_item_not_found').append(search_name + ' '); resolve(false); return; } let elementsArray = elements.toArray(); let normalizedSearchName = normalizeText(search_name); console.log("搜索名(规范化):", normalizedSearchName); // 等待元素加载,避免空文本 await new Promise(res => setTimeout(res, 500)); let selectedElement = elementsArray.find(el => { let normalizedElementText = extractTextFromElement(el); console.log("元素文本(规范化):", normalizedElementText); // 调试用 return normalizedElementText === normalizedSearchName; }); if (!selectedElement) { if (elements.length > 1) { $('.Relation_item_dupe').append(`${search_name} `); } selectedElement = elements[0]; // 没有完全匹配,取第一个 } // 提取条目ID let item_id = null; const itemHref = $(selectedElement).attr('href'); const idMatch = itemHref && itemHref.match(/\/(subject|character|person)\/(\d+)/); if (idMatch) { item_id = idMatch[2]; } resolve(await processItem(selectedElement, item_type, item_id, search_name)); }); } // 处理下一个项目 async function proceedToNextItem(idx, item_list, item_type, item_num) { if (idx < item_num - 1) { setTimeout(async () => { await ctd_findItemFunc(item_list, item_type, idx + 1); }, DELAY_BETWEEN_ITEMS); } else { setTimeout(() => { $('#subjectList').empty(); $('#subjectList').show(); alert('全部添加完成'); }, DELAY_BETWEEN_ITEMS); } } // 核心查找及处理函数:依次检索每个条目并处理 var ctd_findItemFunc = async function(item_list, item_type, idx) { currentProcessingIndex = idx; item_type = globalItemType; let search_name = item_list[idx].trim(); if (!search_name) { proceedToNextItem(idx, item_list, item_type, item_list.length); return; } var item_num = item_list.length; $('#subjectList').html('正在检索中...'); var search_mod = $('#sbjSearchMod').attr('value'); try { const response = await new Promise((resolve, reject) => { $.ajax({ type: "GET", url: '/json/search-' + search_mod + '/' + encodeURIComponent(search_name), dataType: 'json', success: resolve, error: reject }); }); var html = ''; if ($(response).length > 0) { subjectList = response; for (var i in response) { if ($.inArray(search_mod, enableStaffSbjType) != -1) { html += genSubjectList(response[i], i, 'submitForm'); } else { html += genSubjectList(response[i], i, 'searchResult'); } } $('#subjectList').html(html); $('.Relation_current_idx').text(idx + 1); $('.Relation_all_num').text(item_num); await new Promise(resolve => setTimeout(resolve, 400)); // 减少等待时间 var elements = $('#subjectList>li>a.avatar.h'); if (window.location.pathname.includes('/person/') && window.location.pathname.includes('/add_related/character/anime')) { if (elements.length === 0) { $('.Relation_item_not_found').append(search_name + ' '); } else { // 提取条目ID let item_id = null; const itemHref = $(elements[0]).attr('href'); const idMatch = itemHref && itemHref.match(/\/(subject|character|person)\/(\d+)/); if (idMatch) { item_id = idMatch[2]; } // 检查是否已关联 const existingCheck = await checkAndHandleExistingRelation(search_name, item_id, item_type); if (!existingCheck.exists) { $(elements[0]).click(); } if (elements.length > 1) { $('.Relation_item_dupe').append(`${search_name} `); } } $('.Relation_current_idx').text(idx + 1); if (idx < item_num - 1) { setTimeout(async () => { await ctd_findItemFunc(item_list, item_type, idx + 1); }, DELAY_BETWEEN_ITEMS); } else { setTimeout(() => { $('#subjectList').empty(); $('#subjectList').show(); alert('全部添加完成'); }, DELAY_BETWEEN_ITEMS); } } else { await processSingleItem(elements, item_type, search_name, idx, item_list, item_num); await proceedToNextItem(idx, item_list, item_type, item_num); } } else { $("#robot").fadeIn(300); $("#robot_balloon").html(`没有找到 ${search_name} 的相关结果`); $("#robot").animate({ opacity: 1 }, 500).fadeOut(500); // 减少动画时间 $('.Relation_item_not_found').append(search_name + ' '); $('#subjectList').html(html); $('.Relation_current_idx').text(idx + 1); $('.Relation_all_num').text(item_num); await proceedToNextItem(idx, item_list, item_type, item_num); } } catch (error) { console.error('查询出错:', error); $("#robot").fadeIn(300); $("#robot_balloon").html('通信错误,您是不是重复查询太快了?'); $("#robot").animate({ opacity: 1 }, 500).fadeOut(1000); // 减少动画时间 $('#subjectList').html(''); setTimeout(async () => { if (idx < item_list.length - 1) { await ctd_findItemFunc(item_list, item_type, idx + 1); } else { $('#subjectList').empty(); $('#subjectList').show(); alert('全部添加完成'); } }, 1500); // 减少等待时间 } }; // 增强的解析函数:支持多种ID分隔和准确搜索 function parsePersonInput(input) { input = input.trim(); // 支持URL格式 const urlMatch = input.match(/(?:bgm\.tv|bangumi\.tv|chii\.in)\/(?:person|character|subject)\/(\d+)/i); if (urlMatch) return urlMatch[1]; // 提取纯数字ID - 每次只返回一个ID const numberMatch = input.match(/^\d+$/); if (numberMatch) return numberMatch[0]; // 支持姓名直接搜索 if (/^[\u4e00-\u9fa5a-zA-Z0-9\s]+$/.test(input)) { return encodeURIComponent(input); } return input; // 如果无法识别,返回原始输入 } // 从ID范围中提取ID列表 function getIDsFromRange(start, end) { const startID = parseInt(start, 10); const endID = parseInt(end, 10); if (isNaN(startID) || isNaN(endID) || startID > endID) { alert("ID范围无效"); return []; } return Array.from({ length: endID - startID + 1 }, (_, i) => "bgm_id=" + (startID + i)); } const numberMap = { '0': '零', '1': '一', '2': '二', '3': '三', '4': '四', '5': '五', '6': '六', '7': '七', '8': '八', '9': '九', '10': '十', 'Ⅰ': '一', 'Ⅱ': '二', 'Ⅲ': '三', 'Ⅳ': '四', 'Ⅴ': '五', 'Ⅵ': '六', 'Ⅶ': '七', 'Ⅷ': '八', 'Ⅸ': '九', 'Ⅹ': '十' }; // 修改后的 normalizeSeasonOrEpisode 函数 function normalizeSeasonOrEpisode(text) { text = text.replace(/\s+/g, ''); // 如果完全由数字组成,则直接返回原文本 if (/^\d+$/.test(text)) return text; // 处理带数字的情况(包括直接的数字转换) const numberMatch = text.match(/(\d+)季$/); if (numberMatch) { const number = numberMatch[1]; const chineseNumber = numberMap[number] || number; return text.replace(/\d+季$/, `${chineseNumber}季`); } // 处理原有的罗马数字模式 const romanMatch = text.match(/[^\d]([ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ])$/); if (romanMatch) { const romanNumber = romanMatch[1]; const chineseNumber = numberMap[romanNumber]; return text.replace(/[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]$/, `${chineseNumber}季`); } // 新增:处理"标题 数字"格式 const simpleTitleNumberMatch = text.match(/(.+?)(\d+)$/); if (simpleTitleNumberMatch) { const title = simpleTitleNumberMatch[1]; const number = simpleTitleNumberMatch[2]; const chineseNumber = numberMap[number] || number; return `${title}第${chineseNumber}季`; } return text; } // 修改后的 getIDsFromText 函数 function getIDsFromText(input) { input = input.trim(); if (!input) { alert("请输入ID或内容"); return []; } // 先识别 URL 形式的 ID const urlPattern = /(bgm\.tv|bangumi\.tv|chii\.in)\/(subject|character|person)\/(\d+)/g; const urlMatches = [...input.matchAll(urlPattern)].map(m => m[3]); if (urlMatches.length > 0) { return urlMatches.map(id => "bgm_id=" + id); } // 如果以 "bgm_id=" 开头,则去掉前缀后进行分割,使用 /[^0-9]+/ 作为分隔符 if (input.startsWith("bgm_id=")) { return input.substring(7) .split(/[^0-9]+/) .filter(token => token) .map(token => "bgm_id=" + token); } // 否则先按标点和各种分隔符拆分,再进行标准化和数字提取 return input.split(/[,\n\r,、\/|;。.()【】<>!?]+/) .map(part => part.trim()) .filter(part => part.length > 0) .map(normalizeSeasonOrEpisode) .map(part => { // 处理纯数字ID(此时 normalizeSeasonOrEpisode 不会修改纯数字) const numberMatch = part.match(/\b\d+\b/); if (numberMatch) { return "bgm_id=" + numberMatch[0]; } return part; }) .filter(part => part); } // 批量查找入口函数 var Relation_MultiFindItemFunc = async function() { let item_type = '1'; let typeSelector = $('.Relation_item_type select'); if (typeSelector.length > 0) { item_type = typeSelector.val(); if (item_type == '-999') { alert('请先选择关联类型'); return false; } globalItemType = item_type; } let ctd_item_list = []; const activeTab = $('.tab-panel.active').attr('id'); if (activeTab === 'tab-text') { // 处理文本输入模式 const inputVal = $('#custom_ids').val().trim(); ctd_item_list = getIDsFromText(inputVal); } else if (activeTab === 'tab-range') { // 处理ID范围模式 const startID = $('#id_start').val().trim(); const endID = $('#id_end').val().trim(); ctd_item_list = getIDsFromRange(startID, endID); } if (ctd_item_list.length === 0) { return false; } $('#subjectList').hide(); $('.Relation_item_not_found').empty(); $('.Relation_item_dupe').empty(); $('.Relation_item_type_changed').empty(); $('.Relation_item_unchanged').empty(); $('.Relation_current_idx').text('0'); $('.Relation_all_num').text(ctd_item_list.length); currentProcessingIndex = -1; await ctd_findItemFunc(ctd_item_list, item_type, 0); }; // 切换标签页 function switchTab(tabId) { $('.tab-nav button').removeClass('active'); $(`.tab-nav button[data-tab="${tabId}"]`).addClass('active'); $('.tab-panel').removeClass('active'); $(`#${tabId}`).addClass('active'); } let uiTitle = '条目'; const pathname = window.location.pathname; if (pathname.includes('/subject/') && pathname.includes('/add_related/person')) { uiTitle = '人物'; } else if (pathname.includes('/subject/') && pathname.includes('/add_related/character')) { uiTitle = '角色'; } // 创建改进的UI界面 $('.subjectListWrapper').after(`

批量关联助手

(取消勾选将不检查已关联项)
添加进度:0/0
未找到的${uiTitle}:
存在多结果的${uiTitle}(无最佳匹配结果,将自动选择第一个):
已修改类型的${uiTitle}:
无需修改类型的${uiTitle}:
`); // 绑定开关事件 $('#toggle_existing_check').on('change', function() { enableExistingRelationCheck = $(this).prop('checked'); console.log("已关联条目检查功能:", enableExistingRelationCheck ? "已启用" : "已禁用"); }); // 添加关联类型选择器 $('.Relation_item_type').append(generateTypeSelector()); $('.Relation_item_type select').prepend('').val('-999'); // 绑定事件 $('#btn_ctd_multi_search').on('click', Relation_MultiFindItemFunc); $('.Relation_item_type select').on('change', function() { globalItemType = $(this).val(); }); $('.tab-nav button').on('click', function() { switchTab($(this).data('tab')); }); } /* =============== 批量添加章节模板 ================== */ const BatchEpisodeCreator = { // 初始化入口 init() { // 检查是否为添加章节页面 if (!this.isEpisodeCreatePage()) return; // 设置元素监听,等待页面 DOM 变化后执行 this.setupElementObserver(); // 检查目标元素 this.checkTargetElement(); }, // 判断当前页面是否为添加章节页面 isEpisodeCreatePage() { const pattern = /\/subject\/\d+\/ep\/create/; return pattern.test(window.location.href); }, // 设置监听器,观察页面 DOM 变化 setupElementObserver() { // 创建一个 MutationObserver,监听 DOM 变化 this.observer = new MutationObserver(() => this.checkTargetElement()); // 监听整个文档的变化,特别是子节点变化 this.observer.observe(document.body, { childList: true, subtree: true }); }, // 检查目标元素是否存在 checkTargetElement() { // 查找目标元素 div#batch,并检查其是否处于显示状态 const targetElement = document.querySelector('div#batch[style*="display: block"]'); // 如果目标元素存在,则添加批量创建界面 if (targetElement) { this.addBatchCreationInterface(); } else { // 如果目标元素不存在,移除已有的批量创建界面 const existing = document.querySelector('.batch-creator-area'); if (existing) existing.remove(); } }, // 添加批量创建界面 addBatchCreationInterface() { // 如果批量创建界面已经存在,则不再重复添加 if (document.querySelector('.batch-creator-area')) return; // 查找目标元素 div#batch,并确认其处于显示状态 const targetElement = document.querySelector('div#batch[style*="display: block"]'); if (!targetElement) return; // 创建批量添加区域的 HTML 内容 const batchArea = document.createElement('div'); batchArea.className = 'batch-creator-area Relation_wrapper'; batchArea.innerHTML = `

批量添加章节

-
`; // 设置批量创建区域的样式 batchArea.style.cssText = ` width: 200px; margin-bottom: var(--margin-medium); padding: 18px; background: white; `; // 在指定位置插入批量创建区域 const subjectInnerInfo = document.getElementById('subject_inner_info'); if (subjectInnerInfo) { subjectInnerInfo.insertAdjacentElement('afterend', batchArea); batchArea.style.marginTop = 'var(--margin-medium)'; batchArea.style.marginBottom = 'var(--margin-medium)'; } else { console.warn('未找到 subject_inner_info 元素,已附加到body末尾'); document.body.appendChild(batchArea); } // 绑定批量创建按钮事件 this.bindBatchEvents(); // 在输入框创建后绑定联动逻辑 this.setupEndValueMode(); }, // 绑定批量创建按钮事件 bindBatchEvents() { const generateBtn = document.getElementById('generate-episodes'); const applyBtn = document.getElementById('apply-episodes'); // 绑定生成按钮事件 if (generateBtn) { generateBtn.addEventListener('click', () => this.generateEpisodes()); } else { console.error('生成按钮未找到'); } // 绑定应用按钮事件 if (applyBtn) { applyBtn.addEventListener('click', () => this.applyToForm()); } else { console.error('应用按钮未找到'); } }, // 生成章节模板 generateEpisodes() { const start = parseInt(document.getElementById('batch-start').value) || 1; const end = parseInt(document.getElementById('batch-end').value) || 20; const isEmpty = document.getElementById('empty-content').checked; // 校验起始数字是否大于结束数字 if (start > end) { alert('起始数字不能大于结束数字'); return; } // 当生成章节数过多时进行确认 if (end - start >= 100) { if (!confirm(`您将生成 ${end - start + 1} 个章节,确定继续吗?`)) return; } // 生成章节内容 let result = ''; for (let i = start; i <= end; i++) { result += isEmpty ? `${i}| | | |\n` : `${i}| | | m| 0000-00-00\n`; } // 显示生成的章节内容 const resultArea = document.getElementById('batch-result'); if (resultArea) { resultArea.value = result.trim(); } else { console.error('结果区域未找到'); } }, // 应用生成的章节内容到表单 applyToForm() { const episodeText = document.getElementById('batch-result')?.value; const epTextarea = document.querySelector('textarea[name="eplist"]') || document.querySelector('textarea#eplist') || document.querySelector('form textarea'); if (epTextarea && episodeText) { epTextarea.value = epTextarea.value.trim() ? epTextarea.value + '\n' + episodeText : episodeText; alert('章节模板已应用到表单'); } else { console.error('未找到章节输入区域'); alert('未找到章节输入区域,请手动复制生成的内容'); } }, // 设置结束值输入模式 setupEndValueMode() { const startInput = document.getElementById('batch-start'); const endInput = document.getElementById('batch-end'); const radioButtons = document.querySelectorAll('input[name="end-value-mode"]'); if (startInput && endInput && radioButtons.length) { // 获取当前选中的模式 const getCurrentMode = () => { const checkedRadio = document.querySelector('input[name="end-value-mode"]:checked'); return checkedRadio ? checkedRadio.value : 'auto'; }; // 根据初始模式设置状态 this.updateEndInputState(getCurrentMode()); // 监听单选按钮变化 radioButtons.forEach(radio => { radio.addEventListener('change', () => { this.updateEndInputState(getCurrentMode()); }); }); // 监听 batch-start 输入框的变化 startInput.addEventListener('input', () => { if (getCurrentMode() === 'auto') { const val = parseInt(startInput.value); if (!isNaN(val)) { endInput.value = val + 19; // 将 batch-end 设置为 batch-start + 19 } else { endInput.value = ''; // 如果输入无效,清空 batch-end } } }); // 初始化时,如果 start 有值,自动设置 end if (startInput.value) { const val = parseInt(startInput.value); if (!isNaN(val) && getCurrentMode() === 'auto') { endInput.value = val + 19; } } } }, // 更新结束值输入框状态 updateEndInputState(mode) { const startInput = document.getElementById('batch-start'); const endInput = document.getElementById('batch-end'); if (mode === 'auto') { // 自动模式:更新结束值,并使其读取 start 值的变化 const val = parseInt(startInput.value); if (!isNaN(val)) { endInput.value = val + 19; } } } }; // 延迟初始化,防止 DOM 未加载完成 setTimeout(() => { BatchEpisodeCreator.init(); }, 1000); /* =========== 内容快捷填充 ============== */ function initBgmDropdownMenu() { const currentPath = window.location.pathname; // 排除包含 "/add_related/" 的路径 if (currentPath.includes('/add_related/')) { return; } // 正则匹配:路径是否以 "/数字" 结尾,且不以 "/new_subject" 开头 if (/^(?!\/new_subject).*\/\d+$/.test(currentPath)) { return; // 直接退出,不执行后续逻辑 } // 获取页面类型 const pageType = determinePageType(); // 为每种页面类型单独存储配置 // 根据页面类型获取配置存储键名 const configKey = `menuConfig_${pageType}`; // 根据页面类型获取相应默认配置 const defaultConfig = getDefaultConfig(pageType); // 从本地存储获取当前页面类型的配置或使用默认配置 let config = JSON.parse(localStorage.getItem(configKey)) || defaultConfig; // 保存配置到本地存储 const saveConfig = () => localStorage.setItem(configKey, JSON.stringify(config)); // 判断页面类型的函数 function determinePageType() { // 检查URL路径 if (document.querySelector('label[for="cat_comic"]')) { return 'book'; } else if (document.querySelector('label[for="cat_jp"]') || document.querySelector('label[for="cat_tv"]')) { return 'video'; } else if (document.querySelector('label[for="cat_games"]')) { return 'game'; } else if (document.querySelector('td[valign="top"][width="70"]')?.textContent.includes('唱片名')) { return 'music'; } else if (currentPath.includes('/person/')) { return 'person'; } return 'common'; } // 根据页面类型获取默认配置 function getDefaultConfig(pageType) { // 通用配置,所有页面(除person)都会包含 const commonConfig = { "价格, 定价, 售价": { options: ["¥", "¥(税込)", "¥(税抜)", "$", "NT$", "HK$", "₩", "€", "£", "฿"], mode: "append", display: "horizontal" }, "发售日, 发售日期, 发行日期, 放送开始, 播放结束, 连载开始, 连载结束, 开始, 结束": { options: [" 年 月 日", " - - "], mode: "replace", display: "horizontal" } }; // 根据页面类型添加特定配置 switch (pageType) { case 'game': return { ...commonConfig, "": { options: ["Android", "iOS", "PC", "macOS", "Linux", "PS4", "PS5", "Xbox 360", "Xbox One", "Xbox Series X/S", "Nintendo Switch", "Nintendo Switch 2", "SteamOS"], mode: "replace", display: "horizontal" }, "游戏类型": { options: ["AAVG", "ACT", "ADV", "ARPG", "AVG", "CCG", "CRPG", "DBG", "DRPG", "EDU", "FPS", "FTG", "Fly", "Horror", "JRPG", "MMORPG", "MOBA", "MUG", "PUZ", "Platform", "RAC", "RPG", "RTS", "RTT", "Rhythm", "Roguelike", "Roguelite", "SIM", "SLG", "SPG", "SRPG", "STG", "Sandbox", "Strategy", "Survival", "TAB", "TCG", "TPS", "VN", "动作", "动作冒险", "动作角色扮演", "动作类Rogue", "角色扮演", "日式角色扮演", "美式角色扮演", "战略角色扮演", "地下城角色扮演", "大型多人在线角色扮演", "第一人称射击", "第三人称射击", "弹幕射击", "策略", "即时战略", "即时战术", "冒险", "文字冒险", "视觉小说", "多人在线战术竞技", "模拟", "生存", "沙盒", "飞行模拟", "牌组构建", "收集式卡牌", "集换式卡牌", "桌面游戏", "体育游戏", "竞速", "益智", "解谜", "平台跳跃", "休闲", "格斗", "节奏", "恐怖", "音乐", "类Rogue", "教育", "游戏引擎"], mode: "replace", display: "horizontal" }, "游戏引擎": { "options": [ "Unity Engine", "Unreal Engine", "Godot Engine", "CryEngine", "Frostbite Engine", "Cocos2d-x Engine", "GameMaker Studio", "RPG Maker Engine", "Ren'Py Engine", "Source Engine", "Source 2 Engine", "Red Engine", "RAGE", "Anvil Engine", "Snowdrop Engine", "IW Engine", "Creation Engine", "DECIMA Engine", "Avalanche Engine", "Id Tech Engine", "ForzaTech Engine", "Naughty Dog Engine", "EpicOnlineServices SDK", "Divinity Engine", "KiriKiri Engine", "Live Maker Engine", "Microsoft XNA", "Adobe AIR", "Cocos Engine", "PyGame" ], mode: "replace", display: "horizontal" } }; case 'book': return { ...commonConfig, "册数": { options: ["册全", "卷全", "册既刊", "册中断", "卷既刊", "卷中断"], mode: "replace", display: "horizontal" }, "出版社, 其他出版社": { options: [ "一迅社", "芳文社", "KADOKAWA", "ホーム社", "講談社", "集英社", "小学館", "白泉社", "新潮社", "秋田書店", "双葉社", "竹書房", "幻冬舎", "徳間書店", "マッグガーデン", "エンターブレイン", "一水社", "リイド社", "少年画報社", "朝日新聞出版", "宝島社", "日経BP", "NHK出版", "河出書房新社", "太田出版", "メディアファクトリー", "スクウェア・エニックス", "祥伝社", "皇冠文化集團", "尖端出版", "東立出版社", "青文出版社", "台灣角川", "蓋亞文化", "四季出版", "漫遊者文化", "聯經出版事業公司", "三民書局", "遠流出版", "麥田出版", "大塊文化", "貓頭鷹出版社", "漫客文化", "原動力文化", "木馬文化", "小魯文化", "小宇宙出版", "開學文化", "親子天下", "安徽文艺出版社", "安徽美术出版社", "长春出版社", "长江出版社", "长江文艺出版社", "二十一世纪出版社", "广西美术出版社", "黑龙江美术出版社", "湖南文艺出版社", "湖南美术出版社", "吉林美术出版社", "江苏凤凰文艺出版社", "连环画出版社", "内蒙古人民出版社", "人民美术出版社", "人民邮电出版社", "上海人民美术出版社", "上海文艺出版社", "上海译文出版社", "四川文艺出版社", "四川美术出版社", "天津人民美术出版社", "新世纪出版社", "新星出版社", "云南美术出版社", "浙江人民美术出版社", "浙江文艺出版社", "中信出版社" ], mode: "append", display: "horizontal" }, "连载杂志": { "options": [ "爱奇艺叭嗒", "哔哩哔哩漫画", "大角虫漫画", "快看漫画", "MOJOIN", "日更计划", "腾讯漫画", "菠萝包轻小说", "长佩文学", "刺猬猫阅读", "番茄小说", "晋江文学城","起点中文网", "纵横中文网", "pixiv", "コミック", "ニコニコ静画", "マンガハック", "LINEマンガ", "comico PLUS", "Palcy", "GANMA!", "マガポケ", "コミック百合姫", "ガレット","カクヨム", "小説家になろう", "週刊少年ジャンプ", "週刊少年サンデー", "週刊少年マガジン", "月刊アフタヌーン", "ヤングジャンプ", "モーニング", "月刊少年ガンガン", "電撃大王", "COMICリュウ", "ウルトラジャンプ", "花とゆめ", "LaLa", "別冊マーガレット", "FEEL YOUNG", "鏡文學", "LINE Webtoon", "Comico","NAVER Webtoon", "Daum Webtoon", "Lezhin Comics", "TopToon", "Toomics", "Bomtoon", "Mr.Blue", "Peanutoon", "Kakao Page" ], mode: "append", display: "horizontal" }, "话数": { options: ["序章", "正篇", "后记", "番外", "外传", "尾声", "杂篇", "特别篇", "特典番外"], mode: "append", display: "horizontal" } }; case 'video': return { ...commonConfig, "语言": { options: ["简体中文", "繁体中文", "日文", "韩文", "英文", "法文", "德文"], mode: "replace", display: "horizontal" }, "国家/地区, 首播国家, 首播地区": { options: ["中国大陆", "香港", "澳门", "台湾", "日本", "韩国", "泰国", "美国", "英国", "法国", "德国", "加拿大", "意大利", "土耳其", "新西兰", "俄罗斯"], mode: "replace", display: "horizontal" }, "放送星期": { options: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"], mode: "append", display: "horizontal" }, "类型": { options: ["爱情", "布袋戏", "传记", "动作", "犯罪", "歌舞", "古装", "家庭", "纪录片", "惊悚", "剧情", "科幻", "恐怖", "历史", "冒险", "美食", "奇幻", "少儿", "同性", "特摄", "推理", "武侠", "喜剧", "校园", "悬疑", "玄幻", "西部", "音乐", "运动", "灾难", "战争", "职场"], mode: "append", display: "horizontal" }, "在线播放平台": { options: ["bilibili", "爱奇艺", "腾讯视频", "优酷视频", "芒果TV", "猫耳FM", "饭角", "AcFun", "蜻蜓FM", "漫播", "喜马拉雅", "咪咕视频", "搜狐视频", "PPTV", "可可FM", "央视网", "乐视视频", "西瓜视频", "土豆网", "风行网", "华数TV", "酷米网", "淘米视频"], mode: "append", display: "horizontal" }, "播放电视台, 其他电视台, 电视台, 频道": { options: ["ABC", "AMC", "BBC", "Cable TV", "CBS", "CNN", "CTiTV", "CTS", "CTV", "Fuji TV", "France TV", "FOX", "HBO", "JTBC", "KBS", "KBS2", "KTV", "MBC", "Mnet", "NBC", "NHK", "NTV", "OCN", "RTHK", "SETTV", "STV", "SBS", "TBS", "TV Tokyo", "TVB", "TVBS", "ViuTV", "ZDF", "安徽动漫频道", "安徽广播电视台农业·科教频道", "成都广播电视台少儿频道", "重庆电视台少儿频道", "动漫秀场", "福建电视台少儿频道", "福州广播电视台少儿频道", "甘肃广播电视台少儿频道", "广东广播电视台少儿频道", "广西广播电视台公共频道", "广州广播电视台少儿频道", "哈哈少儿", "哈哈炫动", "河北广播电视台少儿科教频道", "黑龙江广播电视台少儿频道", "湖北广播电视台少儿频道", "湖南金鹰卡通", "吉林教育电视台", "济南广播电视台少儿频道", "嘉佳卡通", "江西广播电视台少儿频道", "卡酷少儿", "辽宁广播电视台教育·青少频道", "辽宁广播电视台新动漫频道", "内蒙古广播电视台少儿频道", "陕西卫视", "山东广播电视台少儿频道", "武汉电视台少儿频道", "厦门电视台综合频道", "炫动卡通", "优漫卡通", "云南广播电视台少儿频道", "浙江电视台少儿频道", "中央电视台少儿频道"], mode: "append", display: "horizontal" } }; case 'music': return { ...commonConfig, "版本特性": { options: ["CD", "Digital", "Soundtrack", "Remix Album", "CD, Album"], mode: "replace", display: "vertical" } }; case 'person': return { "": { options: ["X/Twitter", "YouTube", "Instagram", "Soundcloud", "HP", "Pixiv", "bilibili", "微博"], mode: "replace", display: "horizontal" } }; default: return commonConfig; } } // 添加设置按钮到导航栏 const addSettingsButton = () => { const $ = window.jQuery || window.$; $('
  • 修改选单 |
  • ').insertBefore($('li.last a#showrobot').parent()); $(document).on('click', '#editMenu', showConfigModal); }; // 显示配置模态框 const showConfigModal = () => { const $ = window.jQuery || window.$; // 创建模态框容器 const $modal = $('
    ', { id: 'bgmConfigModal', class: 'bgm-config-modal' }).appendTo('body'); // 创建模态框内容 const $content = $('
    ', { class: 'bgm-config-content' }).appendTo($modal); // 关闭按钮 $('