- // ==UserScript==
- // @name Bangumi Ultimate Enhancer
- // @namespace https://tampermonkey.net/
- // @version 2.6.2
- // @description Bangumi 终极增强套件 - 集成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/*
- // @exclude */character/*/add_related/person*
- // @exclude */person/*/add_related/character*
- // @connect bgm.tv
- // @icon https://lain.bgm.tv/pic/icon/l/000/00/01/128.jpg
- // @grant GM_xmlhttpRequest
- // @license MIT
- // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
- // @run-at document-idle
- // ==/UserScript==
-
- (function() {
- "use strict";
-
- // 样式注入
- function injectStyles() {
- $('head').append(`
- <style>
- /* 统一色彩变量 - 定义整个样式表使用的全局CSS变量 */
- :root {
- --primary-color: #369CF8; /* 主要颜色,蓝色 */
- --primary-light: rgba(74, 144, 226, 0.1); /* 主要颜色的浅色版本,透明度10% */
- --primary-hover: #3A80D2; /* 鼠标悬停时的主要颜色,深蓝色 */
- --text-primary: #333333; /* 主要文本颜色,深灰色 */
- --text-secondary: #666666; /* 次要文本颜色,中灰色 */
- --border-color: #E0E0E0; /* 边框颜色,浅灰色 */
- --shadow-soft: 0 4px 10px rgba(0, 0, 0, 0.05); /* 柔和阴影效果 */
- --shadow-hover: 0 6px 14px rgba(0, 0, 0, 0.1); /* 鼠标悬停时的阴影效果,更明显 */
- --success-color: #42B983; /* 成功状态颜色,绿色 */
- --warning-color: #E6A23C; /* 警告状态颜色,橙色 */
- --danger-color: #F56C6C; /* 危险/错误状态颜色,红色 */
- --info-color: #4A90E2; /* 信息状态颜色,与主要颜色相同的蓝色 */
- --new-color: #F09199; /* 新增粉色变量 */
- --border-radius: 8px; /* 统一的边框圆角大小 */
- --transition-normal: all 0.1s cubic-bezier(0.4, 0, 0.2, 1); /* 标准过渡动画效果 */
- }
-
- /* 通用按钮美化 - 定义自定义按钮样式 */
- .btnCustom {
- margin: 5px 0; /* 上下外边距5px,左右0 */
- background-color: var(--primary-color) !important; /* 使用主要颜色作为背景,强制覆盖 */
- color: white !important; /* 文字颜色为白色,强制覆盖 */
- border-radius: var(--border-radius) !important; /* 使用统一的圆角大小,强制覆盖 */
- padding: 8px 16px !important; /* 内边距:上下8px,左右16px,强制覆盖 */
- border: none !important; /* 移除边框,强制覆盖 */
- cursor: pointer !important; /* 鼠标悬停时显示手指图标,强制覆盖 */
- font-size: 14px; /* 文字大小14像素 */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- text-align: center; /* 文字居中对齐 */
- display: flex; /* 使用弹性布局 */
- justify-content: center; /* 水平居中对齐 */
- align-items: center; /* 垂直居中对齐 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- box-shadow: var(--shadow-soft); /* 应用柔和阴影效果 */
- }
- .btnCustom:hover {
- background-color: var(--primary-hover) !important; /* 鼠标悬停时背景色变深,强制覆盖 */
- transform: translateY(-1px); /* 鼠标悬停时按钮上移1像素,产生悬浮效果 */
- box-shadow: var(--shadow-hover); /* 鼠标悬停时阴影效果更明显 */
- }
-
- /* 文本域美化 - 美化多行文本输入框 */
- .enhancer-textarea {
- width: 100%; /* 宽度占满父容器 */
- min-height: 80px; /* 最小高度80像素 */
- max-height: 300px; /* 最大高度300像素 */
- border: 1px solid var(--border-color); /* 1像素实线边框,使用统一边框颜色 */
- border-radius: var(--border-radius); /* 使用统一的圆角大小 */
- padding: 12px; /* 四周内边距12像素 */
- margin: 10px 0; /* 上下外边距10像素,左右0 */
- resize: vertical; /* 仅允许垂直方向调整大小 */
- font-size: 14px; /* 文字大小14像素 */
- box-sizing: border-box; /* 盒模型计算方式:边框和内边距包含在宽高中 */
- background: white; /* 背景色为白色 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- .enhancer-textarea:focus {
- border-color: var(--primary-color); /* 获得焦点时边框颜色变为主要颜色 */
- box-shadow: 0 0 0 3px var(--primary-light); /* 获得焦点时添加主要颜色的浅色阴影 */
- outline: none; /* 移除默认的焦点轮廓 */
- }
-
- /* 数字输入框 - 美化数字类型的输入框 */
- .input-number {
- width: 100px; /* 宽度100像素 */
- height: 10px; /* 高度10像素 */
- padding: 10px; /* 四周内边距10像素 */
- border-radius: var(--border-radius); /* 使用统一的圆角大小 */
- border: 1px solid var(--border-color); /* 1像素实线边框,使用统一边框颜色 */
- background: white; /* 背景色为白色 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- font-size: 14px; /* 文字大小14像素 */
- }
- .input-number:focus {
- border-color: var(--primary-color); /* 获得焦点时边框颜色变为主要颜色 */
- box-shadow: 0 0 0 3px var(--primary-light); /* 获得焦点时添加主要颜色的浅色阴影 */
- outline: none; /* 移除默认的焦点轮廓 */
- }
-
- /* Relation 相关样式 - 特定组件的样式定义 */
- .Relation_wrapper {
- width: 280px; /* 宽度280像素 */
- margin: 18px 0; /* 上下间距18px,左右0 */
- text-align: center; /* 文字居中对齐 */
- background: white; /* 背景色为白色 */
- padding: 18px; /* 四周内边距18像素 */
- border-radius: var(--border-radius); /* 使用统一的圆角大小 */
- border: 1px solid var(--border-color); /* 1像素实线边框,使用统一边框颜色 */
- box-shadow: var(--shadow-soft); /* 应用柔和阴影效果 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- .Relation_wrapper:hover {
- box-shadow: var(--shadow-hover); /* 鼠标悬停时阴影效果更明显 */
- }
- .select-label {
- display: flex; /* 使用弹性布局 */
- margin-right: auto; /* 右侧外边距自动(将内容推到左侧) */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- color: var(--text-secondary); /* 使用次要文本颜色 */
- }
- .Relation_item_type {
- display: flex; /* 使用弹性布局 */
- margin-right: auto; /* 右侧外边距自动(将内容推到左侧) */
- align-items: center; /* 垂直居中对齐 */
- }
- .Relation_progress {
- margin: 14px 0; /* 上下外边距14像素,左右0 */
- color: var(--primary-color); /* 使用主要颜色 */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- font-size: 18px; /* 文字大小18像素 */
- text-align: center; /* 文字居中对齐 */
- }
-
- /* 状态提示区域 - 用于显示不同状态的提示信息 */
- .Relation_item_not_found,
- .Relation_item_dupe,
- .Relation_item_type_changed,
- .Relation_item_unchanged{
- margin-top: 10px; /* 顶部外边距10像素 */
- padding: 12px; /* 四周内边距12像素 */
- min-height: 15px; /* 最小高度15像素 */
- border-radius: var(--border-radius); /* 使用统一的圆角大小 */
- font-size: 14px; /* 文字大小14像素 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- .Relation_item_not_found {
- color: var(--danger-color); /* 文字颜色使用危险/错误状态颜色 */
- background: rgba(245, 108, 108, 0.1); /* 浅红色背景,10%透明度 */
- border: 1px solid rgba(245, 108, 108, 0.2); /* 红色边框,20%透明度 */
- }
- .Relation_item_dupe {
- color: var(--info-color); /* 文字颜色使用信息状态颜色 */
- background: rgba(74, 144, 226, 0.1); /* 浅蓝色背景,10%透明度 */
- border: 1px solid rgba(74, 144, 226, 0.2); /* 蓝色边框,20%透明度 */
- }
- .Relation_item_type_changed {
- color: var(--success-color); /* 文字颜色使用成功状态颜色 */
- background: rgba(66, 185, 131, 0.1); /* 浅绿色背景,10%透明度 */
- border: 1px solid rgba(66, 185, 131, 0.2); /* 绿色边框,20%透明度 */
- }
-
- .Relation_item_unchanged {
- color: var(--warning-color); /* 文字颜色使用警告状态颜色 */
- background: rgba(230, 162, 60, 0.1); /* 浅橙色背景,10%透明度 */
- border: 1px solid rgba(230, 162, 60, 0.2); /* 橙色边框,20%透明度 */
- }
- .Relation_header {
- font-size: 17px; /* 文字大小17像素 */
- margin: 14px 0 8px; /* 上边距14像素,左右0,下边距8像素 */
- color: var(--text-primary); /* 使用主要文本颜色 */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- text-align: left; /* 内容靠左对齐 */
- }
- .Relation_controls {
- display: flex; /* 使用弹性布局 */
- justify-content: flex-end; /* 内容靠右对齐 */
- align-items: center; /* 垂直居中对齐 */
- gap: 10px; /* 元素之间的间距10像素 */
- }
-
- /* 布局辅助 - 提供辅助布局的通用类 */
- .flex-row {
- display: flex; /* 使用弹性布局 */
- gap: 14px; /* 元素之间的间距14像素 */
- align-items: center; /* 垂直居中对齐 */
- }
-
- /* 标签页导航 - 定义标签页导航栏样式 */
- .tab-nav {
- display: flex; /* 使用弹性布局 */
- justify-content: center; /* 水平居中对齐 */
- border-bottom: 2px solid var(--border-color); /* 底部2像素实线边框,使用统一边框颜色 */
- margin-bottom: 18px; /* 底部外边距18像素 */
- }
- .tab-nav button {
- background: none; /* 移除背景色 */
- border: none; /* 移除边框 */
- padding: 12px 24px; /* 内边距:上下12px,左右24px */
- cursor: pointer; /* 鼠标悬停时显示手指图标 */
- font-size: 14px; /* 文字大小14像素 */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- color: var(--text-secondary); /* 使用次要文本颜色 */
- border-bottom: 3px solid transparent; /* 底部3像素透明边框,为激活状态做准备 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- .tab-nav button.active {
- border-bottom: 3px solid var(--new-color); /* 当前活动标签底部边框使用主要颜色 */
- color: var(--new-color); /* 当前活动标签文字颜色使用主要颜色 */
- }
- .tab-nav button:hover {
- color: var(--primary-color); /* 鼠标悬停时文字颜色使用主要颜色 */
- border-bottom: 3px solid var(--primary-color); /* 底部2像素实线边框,使用统一边框颜色 */
- }
-
- /* 标签页内容 - 定义标签页内容区域样式 */
- .tab-panel {
- display: none; /* 默认隐藏所有标签页内容 */
- }
- .tab-panel.active {
- display: block; /* 显示当前活动的标签页内容 */
- animation: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(8px); } /* 动画起始状态:透明度0,向下偏移8像素 */
- to { opacity: 1; transform: translateY(0); } /* 动画结束状态:透明度1,无偏移 */
- }
-
- /* 封面上传功能 - 定义封面上传相关元素样式 */
- #coverUploadForm {
- display: flex; /* 使用弹性布局 */
- flex-direction: column; /* 垂直排列子元素 */
- align-items: center; /* 水平居中对齐 */
- }
- #coverUploadForm input[type="file"] {
- border: 2px dashed var(--border-color); /* 2像素虚线边框,使用统一边框颜色 */
- border-radius: var(--border-radius); /* 使用统一的圆角大小 */
- padding: 10px; /* 四周内边距10像素 */
- width: 100%; /* 宽度占满父容器 */
- background: var(--background-light); /* 使用浅色背景 */
- box-sizing: border-box; /* 确保padding不会增加实际宽度 */
- cursor: pointer; /* 鼠标悬停时显示手指图标 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- }
- #coverUploadForm input[type="file"]:hover {
- border-color: var(--primary-color); /* 鼠标悬停时边框颜色变为主要颜色 */
- background: var(--primary-light); /* 鼠标悬停时背景色变为主要颜色的浅色版本 */
- }
- #coverUploadForm input[type="submit"] {
- background: var(--primary-color); /* 背景色使用主要颜色 */
- color: white; /* 文字颜色为白色 */
- border: none; /* 移除边框 */
- border-radius: 20px; /* 边框圆角20像素,使按钮更圆润 */
- padding: 8px 16px; /* 内边距:上下8px,左右16px */
- margin-top: 8px; /* 顶部外边距8像素 */
- cursor: pointer; /* 鼠标悬停时显示手指图标 */
- font-weight: 600; /* 字体粗细程度,半粗体 */
- transition: var(--transition-normal); /* 应用统一的过渡动画 */
- box-shadow: var(--shadow-soft); /* 应用柔和阴影效果 */
- position: relative; /* 开启相对定位 */
- top: -15px; /* 向上移动15px */
- }
- #coverUploadForm input[type="submit"]:hover {
- background: var(--primary-hover); /* 鼠标悬停时背景色变深 */
- transform: translateY(-1px); /* 鼠标悬停时按钮上移1像素,产生悬浮效果 */
- box-shadow: var(--shadow-hover); /* 鼠标悬停时阴影效果更明显 */
- }
-
- /* 封面上传模态框的整体容器样式 */
- .cover-upload-modal {
- display: none; /* 默认隐藏模态框 */
- position: fixed; /* 固定定位,相对于视窗 */
- z-index: 1000; /* 设置高层级,确保在其他元素上方 */
- background-color: white; /* 使用白色背景 */
- border: 1px solid var(--border-color); /* 使用全局定义的边框颜色 */
- border-radius: var(--border-radius); /* 使用全局定义的圆角大小 */
- padding: 15px; /* 内部内容的内边距 */
- box-shadow: var(--shadow-soft); /* 使用全局定义的柔和阴影 */
- width: 240px; /* 固定宽度 */
- max-width: 100%; /* 最大宽度不超过父容器 */
- transition: var(--transition-normal); /* 使用全局定义的过渡动画 */
- }
-
- /* 鼠标悬停在模态框上时的阴影效果 */
- .cover-upload-modal:hover {
- box-shadow: var(--shadow-hover); /* 使用全局定义的悬停阴影效果 */
- }
-
- /* 上传区域的整体布局 */
- .upload-section {
- display: flex; /* 使用弹性布局 */
- flex-direction: column; /* 垂直方向排列子元素 */
- }
-
- /* URL输入框和按钮的容器 */
- .url-input-container {
- display: flex; /* 使用弹性布局 */
- margin-bottom: 10px; /* 底部外边距,与下一个元素间隔 */
- height: 30px; /* 固定高度 */
- }
-
- /* URL输入框样式 */
- .image-url-input {
- flex-grow: 1; /* 允许输入框自动填充剩余空间 */
- padding: 8px; /* 内部内边距 */
- margin-right: 10px; /* 右侧外边距,与按钮间隔 */
- border: 1px solid var(--border-color); /* 使用全局定义的边框颜色 */
- border-radius: var(--border-radius); /* 使用全局定义的圆角大小 */
- outline: none; /* 移除默认聚焦轮廓 */
- transition: var(--transition-normal); /* 使用全局定义的过渡动画 */
- font-size: 14px; /* 文字大小 */
- }
-
- /* URL输入框获得焦点时的样式 */
- .image-url-input:focus {
- border-color: var(--primary-color); /* 边框颜色变为主要颜色 */
- box-shadow: 0 0 0 3px var(--primary-light); /* 添加主要颜色的浅色阴影 */
- }
-
- /* 下载按钮样式 */
- .download-url-button {
- padding: 8px 16px; /* 内部内边距 */
- background-color: var(--new-color); /* 使用全局定义的新增粉色 */
- color: white; /* 白色文字 */
- border: none; /* 移除边框 */
- border-radius: var(--border-radius); /* 使用全局定义的圆角大小 */
- cursor: pointer; /* 鼠标悬停显示手型 */
- font-weight: 600; /* 半粗体文字 */
- transition: var(--transition-normal); /* 使用全局定义的过渡动画 */
- box-shadow: var(--shadow-soft); /* 使用全局定义的柔和阴影 */
- display: flex; /* 让按钮使用弹性布局 */
- align-items: center; /* 让文本垂直居中 */
- justify-content: center; /* 让文本水平居中 */
- height: 100%; /* 确保按钮内部填充满 */
- }
-
- /* 下载按钮鼠标悬停时的样式 */
- .download-url-button:hover {
- background-color: color-mix(in srgb, var(--new-color) 90%, black); /* 颜色略微变暗 */
- transform: translateY(-1px); /* 稍微上移,产生悬浮效果 */
- box-shadow: var(--shadow-hover); /* 使用全局定义的悬停阴影 */
- }
-
- /* 上传表单容器 */
- .upload-form-container {
- margin-top: 10px; /* 顶部外边距 */
- }
-
- /* 图片预览容器 */
- .image-preview-container {
- margin-top: 10px; /* 顶部外边距 */
- display: none; /* 默认隐藏 */
- text-align: center; /* 内容居中 */
- }
-
- /* 图片预览样式 */
- .image-preview {
- max-width: 100%; /* 最大宽度不超过容器 */
- max-height: 300px; /* 最大高度限制 */
- object-fit: contain; /* 保持图片比例 */
- border: 1px solid var(--border-color); /* 使用全局定义的边框颜色 */
- border-radius: var(--border-radius); /* 使用全局定义的圆角大小 */
- }
- </style>
- `);
- }
-
- /* ====================
- Wiki 按钮和关联按钮模块
- ======================*/
- function initNavButtons() {
- // 排除特定编辑页面
- const EXCLUDE_PATHS = /(edit_detail|edit|add_related|upload_img)/;
- if (EXCLUDE_PATHS.test(location.pathname)) return;
-
- // 获取导航栏
- const nav = document.querySelector(".subjectNav .navTabs, .navTabs");
- if (!nav) return;
-
- // 解析页面类型和ID
- const pathMatch = location.pathname.match(/\/(subject|person|character)\/(\d+)/);
- if (!pathMatch) return;
-
- const [, pageType, pageId] = pathMatch;
- const origin = location.origin;
-
- // 按钮配置
- 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 = `<a href="${button.getUrl()}" target="_blank">${button.getText()}</a>`;
- 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() {
- // 支持的域名和需要排除的路径
- const SUPPORTED_DOMAINS = ['bangumi\\.tv', 'bgm\\.tv', 'chii\\.in'];
- const EXCLUDE_PATHS = /\/(edit_detail|edit|add_related|upload_img)$/;
-
- // 如果是编辑页面,则不执行
- if (EXCLUDE_PATHS.test(location.pathname)) return;
-
- // 解析页面类型和 ID
- const url = window.location.href;
- const parseId = (path) => {
- const regex = new RegExp(`(${SUPPORTED_DOMAINS.join('|')})\\/${path}\\/(\\d+)`);
- const match = url.match(regex);
- return match ? { id: match[2], type: path } : null;
- };
-
- const typeMapping = ['subject', 'person', 'character'];
- const parsedInfo = typeMapping.reduce((result, type) => result || parseId(type), null);
- if (!parsedInfo) return;
-
- // 避免重复添加按钮
- if (document.querySelector("#coverUploadButton")) return;
-
- // 获取导航栏(兼容多个模板)
- const nav = document.querySelector(".subjectNav .navTabs") || document.querySelector(".navTabs");
- if (!nav) return;
-
- // 创建上传按钮(保持原有UI设计)
- const createUploadButton = () => {
- const uploadLi = document.createElement("li");
- uploadLi.id = "coverUploadButton";
- uploadLi.className = "upload-button";
- uploadLi.style.float = "right";
- uploadLi.innerHTML = `<a href="javascript:void(0);" style="padding: 10px 10px 9px;">上传封面</a>`;
- return uploadLi;
- };
-
- // 创建表单容器,移除格式选择部分
- const createFormContainer = () => {
- const formContainer = document.createElement("div");
- formContainer.id = "coverUploadFormContainer";
- formContainer.classList.add("cover-upload-modal");
- // 移除格式选择下拉框
- formContainer.innerHTML = `
- <div class="upload-section">
- <div class="url-input-container">
- <input type="text" id="imageUrlInput" class="image-url-input" placeholder="输入图片 URL">
- <button id="downloadUrlButton" class="download-url-button">下载</button>
- </div>
- <div id="uploadFormContainer" class="upload-form-container"></div>
- <div id="imagePreviewContainer" class="image-preview-container">
- <img id="imagePreview"class="image-preview" alt="图片预览">
- </div>
- <div id="statusMessage" class="status-message" style="display:none; margin-top:10px; padding:5px; text-align:center;"></div>
- </div>
- `;
- // 设置必要的定位和初始隐藏
- 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 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}`);
- }
-
- // 创建一个隐藏的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) {
- try {
- // 等待iframe加载完成
- return new Promise((resolve, reject) => {
- iframe.onload = function() {
- try {
- // 获取iframe中的文档内容
- const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
- console.log("Iframe加载完成,检查投票链接...");
-
- // 获取所有投票链接
- const allVoteLinks = iframeDocument.querySelectorAll('a[href*="/vote/cover/"]');
- console.log(`找到 ${allVoteLinks.length} 个投票链接`);
-
- // 选择最后一个投票链接
- const voteLink = allVoteLinks.length > 0 ? allVoteLinks[allVoteLinks.length - 1] : null;
-
- if (voteLink) {
- const href = voteLink.getAttribute('href');
- console.log("选择最后一个投票链接:", href);
-
- // 构建完整的投票URL
- const host = window.location.host;
- const voteUrl = href.startsWith('http') ? href : `https://${host}${href.startsWith('/') ? '' : '/'}${href}`;
- console.log("完整投票URL:", voteUrl);
-
- // 使用fetch API进行投票
- showStatus('封面上传成功,正在投票...');
-
- fetch(voteUrl, {
- method: 'GET',
- credentials: 'include', // 重要:包含cookies
- headers: {
- 'Accept': 'text/html,application/xhtml+xml,application/xml',
- 'Referer': window.location.href
- }
- })
- .then(response => {
- if (response.ok) {
- console.log("投票成功,状态:", response.status);
- showStatus('投票成功!页面将在3秒后刷新...');
- setTimeout(() => window.location.reload(), 3000);
- resolve(true);
- } else {
- console.error("投票失败,状态:", response.status);
- showStatus('封面上传成功,但投票失败。3秒后跳转到手动投票页面...', true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(new Error(`投票请求失败,状态 ${response.status}`));
- }
- })
- .catch(error => {
- console.error("投票请求错误:", error);
- showStatus('封面上传成功,但投票失败。3秒后跳转到手动投票页面...', true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(error);
- });
- } else {
- // 检查是否有错误信息
- console.log("未找到投票链接,检查是否有错误消息");
- const errorMsg = iframeDocument.querySelector('.error, .errorMessage, [class*="error"]');
- if (errorMsg) {
- console.error("上传错误:", errorMsg.textContent);
- showStatus(`上传失败: ${errorMsg.textContent},3秒后跳转到手动上传页面...`, true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(new Error(errorMsg.textContent));
- } else {
- console.log("未找到投票链接或错误消息");
- showStatus('封面似乎已上传成功,但未找到投票链接。3秒后跳转到手动处理页面...', true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(new Error('未找到投票链接'));
- }
- }
- } catch (error) {
- console.error('处理上传结果时出错:', error);
- showStatus('处理上传结果时出错,3秒后跳转到手动上传页面...', true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(error);
- }
- };
-
- // 处理iframe加载错误
- iframe.onerror = function(error) {
- console.error("Iframe加载错误:", error);
- showStatus('上传请求失败,3秒后跳转到手动上传页面...', true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- reject(new Error('上传请求失败'));
- };
- });
- } catch (error) {
- console.error('处理上传结果时出错:', error);
- showStatus(`处理出错: ${error.message},3秒后跳转到手动上传页面...`, true);
- setTimeout(() => {
- window.location.href = `${window.location.href.split('?')[0]}/upload_img`;
- }, 3000);
- return Promise.reject(error);
- }
- }
-
- // 修改表单提交处理函数,使用iframe提交
- function setupFormForIframeSubmission(form) {
- // 创建隐藏的iframe
- const iframe = createHiddenIframe();
-
- // 设置表单的target为iframe
- form.target = 'hiddenUploadFrame';
-
- // 监听表单提交事件
- form.addEventListener('submit', function(e) {
- // 不阻止默认提交行为,而是让它提交到iframe
- showStatus('正在上传封面...');
-
- // 处理iframe的响应
- processUploadResult(iframe).catch(error => {
- console.error('处理上传结果失败:', error);
- });
- });
- }
-
- // 优化的图片转换函数 - 自动选择最佳格式
- async function convertImageFormat(file) {
- return new Promise((resolve, reject) => {
- try {
- // 读取文件
- 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) {
- // 有透明度,使用PNG
- finalFormat = 'image/png';
- console.log("检测到透明像素,使用PNG格式");
- } else if (fileType.includes('png') && !hasTransparency) {
- // 无透明度的PNG图片转为JPEG以减小体积
- finalFormat = 'image/jpeg';
- console.log("PNG图片无透明像素,转换为JPEG格式以优化体积");
- } else {
- // 保持原格式
- finalFormat = fileType.includes('jpeg') || fileType.includes('jpg')
- ? 'image/jpeg'
- : fileType.includes('png')
- ? 'image/png'
- : 'image/jpeg'; // 默认JPEG
- console.log(`保持原始格式: ${finalFormat}`);
- }
-
- // 重新创建画布以适应格式
- canvas.width = img.width;
- canvas.height = img.height;
- const newCtx = canvas.getContext('2d');
-
- // 如果是JPEG,先填充白色背景
- 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; // JPEG质量设置
- 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 });
- console.log(`图片已转换为 ${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);
- } catch (error) {
- reject(error);
- }
- });
- }
-
- // 图片下载和转换函数 - 更新为自动转换格式
- async function downloadAndConvertImage(imageUrl) {
- try {
- // 尝试提取实际的图片 URL
- let actualImageUrl = imageUrl;
-
- // 检查是否是 Google 图片重定向链接
- if (imageUrl.includes('google.com/imgres')) {
- const urlParams = new URL(imageUrl).searchParams;
- actualImageUrl = urlParams.get('imgurl');
- }
-
- // 如果没有提取到图片 URL,则使用原始链接
- 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) {
- // 创建 DataTransfer 对象并填充文件
- const dataTransfer = new DataTransfer();
- dataTransfer.items.add(convertedData.file);
- fileInput.files = dataTransfer.files;
-
- // 触发文件上传输入框的 change 事件
- 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";
-
- // 配置表单使用iframe提交
- uploadFormContainer.innerHTML = form.outerHTML;
-
- // 获取插入到DOM中的表单元素并设置
- 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);
- }
- }
-
- // 事件绑定逻辑,保持原有UI展示行为但增加点击固定功能
- 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();
- }
- });
-
- // URL上传部分事件处理
- 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 });
-
- /* ===============
- 批量添加章节模板
- =================*/
- 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 = `
- <h2 class="batch_header" style="margin-bottom: 20px;">批量添加章节</h2>
- <div class="batch-controls">
- <div class="flex-row" style="margin-bottom: 14px; gap: 12px; flex-wrap: wrap;">
- <div class="flex-row" style="gap: 8px; align-items: center; width: 100%; justify-content: center;">
- <span class="select-label">范围:</span>
- <input type="number" id="batch-start" min="1" value="" class="input-number" style="width: 50px">
- <span> - </span>
- <input type="number" id="batch-end" min="1" value="" class="input-number" style="width: 50px">
- </div>
- <div class="flex-row" style="gap: 8px; align-items: center; width: 100%; justify-content: center; margin-top: 8px;">
- <span class="select-label">结束值:</span>
- <label style="display: inline-flex; align-items: center; margin-right: 10px;">
- <input type="radio" name="end-value-mode" value="auto" checked>
- <span style="margin-left: 5px">自动输入</span>
- </label>
- <label style="display: inline-flex; align-items: center;">
- <input type="radio" name="end-value-mode" value="manual">
- <span style="margin-left: 5px">手动输入</span>
- </label>
- </div>
- <label class="flex-row" style="gap: 6px; align-items: center; width: 100%; margin-top: 8px;">
- <input type="checkbox" id="empty-content">
- <span class="text-secondary">空内容模式</span>
- </label>
- </div>
- <button type="button" id="generate-episodes" class="btnCustom" style="display: block; margin: 0 auto 10px auto;">生成章节模板</button>
- </div>
- <div class="batch-preview">
- <textarea id="batch-result" rows="5" class="enhancer-textarea" placeholder="生成的章节将显示在这里"></textarea>
- <div class="batch_controls" style="margin-top: 8px; display: flex; justify-content: center;">
- <button type="button" id="apply-episodes" class="btnCustom">应用到表单</button>
- </div>
- </div>
- `;
-
- // 设置批量创建区域的样式
- 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 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]) => `<option value="${value}">${text}</option>`)
- .join('');
- };
- switch(pageType) {
- case 'character':
- return `<span class="select-label">类型: </span>
- <select>${generateOptions({
- '1': '主角',
- '2': '配角',
- '3': '客串'
- })}</select>`;
- default:
- return `<span class="select-label"></span>${
- 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('<tr><td>正在检索中...</td></tr>');
- 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(`
- <div class="Relation_wrapper">
- <h2 style="color: #333333;">批量关联助手</h2>
- <div class="tab-nav">
- <button data-tab="tab-text" class="active">自由文本输入</button>
- <button data-tab="tab-range">ID范围输入</button>
- </div>
-
- <div id="tab-text" class="tab-panel active">
- <textarea id="custom_ids" class="enhancer-textarea"
- placeholder="输入ID/网址/名称(支持多种格式:bgm_id=xx、数字、网址、文本,支持除空格外各类符号分隔)"></textarea>
- </div>
-
- <div id="tab-range" class="tab-panel">
- <div class="flex-row" style="justify-content: center">
- <input id="id_start" type="number" placeholder="起始ID" class="input-number">
- <span style="line-height: 30px">~</span>
- <input id="id_end" type="number" placeholder="结束ID" class="input-number">
- </div>
- </div>
-
- <div class="Relation_controls" style="margin-top: 10px">
- <span class="Relation_item_type"></span>
- <button id="btn_ctd_multi_search" class="btnCustom">批量关联</button>
- </div>
-
- <div class="toggle-container" style="margin-top: 10px; display: flex; align-items: center;">
- <input type="checkbox" id="toggle_existing_check">
- <label for="toggle_existing_check" style="margin-left: 8px;">已关联条目检查</label>
- <span class="toggle-description" style="margin-left: 8px; font-size: 12px; color: #666;">(取消勾选将不检查已关联项)</span>
- </div>
-
- <div class="Relation_progress">
- 添加进度:<span class="Relation_current_idx">0</span>/<span class="Relation_all_num">0</span>
- </div>
-
- <div class="Relation_header">未找到的${uiTitle}:</div>
- <div class="Relation_item_not_found"></div>
-
- <div class="Relation_header">存在多结果的${uiTitle}(无最佳匹配结果,将自动选择第一个):</div>
- <div class="Relation_item_dupe"></div>
-
- <div class="Relation_header">已修改类型的${uiTitle}:</div>
- <div class="Relation_item_type_changed"></div>
-
- <div class="Relation_header">无需修改类型的${uiTitle}:</div>
- <div class="Relation_item_unchanged"></div>
- </div>
- `);
-
- // 绑定开关事件
- $('#toggle_existing_check').on('change', function() {
- enableExistingRelationCheck = $(this).prop('checked');
- console.log("已关联条目检查功能:", enableExistingRelationCheck ? "已启用" : "已禁用");
- });
- // 添加关联类型选择器
- $('.Relation_item_type').append(generateTypeSelector());
- $('.Relation_item_type select').prepend('<option value="-999">请选择关联类型</option>').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'));
- });
- }
-
- // 启动所有功能
- function startEnhancer() {
- initNavButtons();
- observeURLChanges();
- initCoverUpload();
- initBatchRelation()
- BatchEpisodeEditor.init();
- console.log("Bangumi Ultimate Enhancer 已启动");
- }
- // 在DOM加载完成后启动脚本
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', startEnhancer);
- } else {
- startEnhancer();
- }
- })();