// ==UserScript==
// @name Bangumi Ultimate Enhancer
// @namespace https://tampermonkey.net/
// @version 2.5
// @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; /* 边框颜色,浅灰色 */
--background-light: #F9F9F9; /* 浅色背景,几乎白色 */
--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 {
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_header {
font-size: 17px; /* 文字大小17像素 */
margin: 14px 0 8px; /* 上边距14像素,左右0,下边距8像素 */
color: var(--text-primary); /* 使用主要文本颜色 */
font-weight: 600; /* 字体粗细程度,半粗体 */
}
.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,无偏移 */
}
/* 封面上传功能 - 定义封面上传相关元素样式 */
.cover-upload-button a {
color: var(--primary-color) !important; /* 链接颜色使用主要颜色,强制覆盖 */
font-weight: 600; /* 字体粗细程度,半粗体 */
transition: var(--transition-normal); /* 应用统一的过渡动画 */
text-decoration: none; /* 移除下划线 */
}
.cover-upload-button a:hover {
color: var(--primary-hover) !important; /* 鼠标悬停时链接颜色变深,强制覆盖 */
text-decoration: underline; /* 添加下划线 */
}
#coverUploadFormContainer {
background: white; /* 背景色为白色 */
border: 1px solid var(--border-color); /* 1像素实线边框,使用统一边框颜色 */
border-radius: var(--border-radius); /* 使用统一的圆角大小 */
box-shadow: var(--shadow-soft); /* 应用柔和阴影效果 */
padding: 15px; /* 四周内边距15像素 */
width: 240px; /* 宽度240像素 */
max-width: 100%; /* 确保不超过父容器宽度 */
transition: var(--transition-normal); /* 应用统一的过渡动画 */
}
#coverUploadFormContainer:hover {
box-shadow: var(--shadow-hover); /* 鼠标悬停时阴影效果更明显 */
}
#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 20px; /* 内边距:上下8px,左右20px */
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;
};
// 创建表单容器,保持原有 UI 设计不变
const createFormContainer = () => {
const formContainer = document.createElement("div");
formContainer.id = "coverUploadFormContainer";
formContainer.classList.add("cover-upload-modal");
// 保持原设计布局:URL 上传输入框、异步加载的本地上传表单、预览区域均按原有样式呈现
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>
`;
// 设置必要的定位和初始隐藏,保持原来的样式控制由外部CSS决定
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;
// 图片下载和转换函数(保持原有逻辑)
const downloadAndConvertImage = async (imageUrl) => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
// 创建 JPG 文件
const jpgBlob = new Blob([blob], { type: 'image/jpeg' });
const jpgFile = new File([jpgBlob], 'cover.jpg', { type: 'image/jpeg' });
// 显示预览
const previewContainer = formContainer.querySelector("#imagePreviewContainer");
const previewImage = formContainer.querySelector("#imagePreview");
previewImage.src = URL.createObjectURL(jpgBlob);
previewContainer.style.display = "block";
// 查找上传表单中的文件输入
const fileInput = document.querySelector("#coverUploadForm input[type='file']");
if (fileInput) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(jpgFile);
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';
}
} else {
alert('未找到文件上传输入框');
}
} catch (error) {
console.error('下载或转换图片时发生错误:', error);
alert('下载图片失败:' + error.message);
}
};
// 新增的全局点击事件,点击表单容器外区域关闭并取消表单容器
function setupGlobalClickHandler(container, trigger) {
document.addEventListener('click', function (event) {
if (!container.contains(event.target) && !trigger.contains(event.target)) {
container.style.display = "none";
}
});
}
// 预先加载本地上传表单,提升加载速度
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 fileInput = document.querySelector("#coverUploadForm input[type='file']");
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
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";
const submitButton = document.querySelector("#coverUploadForm input[type='submit']");
if (submitButton) {
submitButton.style.display = 'block';
}
};
reader.readAsDataURL(file);
}
});
formLoaded = true;
} else {
uploadFormContainer.innerHTML = "无法加载上传表单";
}
} catch (e) {
uploadFormContainer.innerHTML = "加载失败";
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 = () => {
const previewContainer = formContainer.querySelector("#imagePreviewContainer");
if (previewContainer.style.display === "block") return;
hideTimeout = setTimeout(() => {
if (!formContainer.matches(":hover")) {
formContainer.style.display = "none";
}
}, 200);
};
uploadLi.addEventListener("mouseenter", showForm);
uploadLi.addEventListener("mouseleave", hideForm);
formContainer.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
formContainer.addEventListener("mouseleave", 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 {
alert('请输入图片 URL');
}
});
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 BatchEpisodeEditor = {
// 常量配置
CHUNK_SIZE: 20,
BASE_URL: '',
CSRF_TOKEN: '',
// 初始化入口
init() {
// 非分集编辑页面直接返回
if (!this.isEpisodePage()) return;
// 获取基础URL和CSRF令牌
this.BASE_URL = location.pathname.replace(/\/edit_batch$/, '');
this.CSRF_TOKEN = $('[name=formhash]')?.value || '';
if (!this.CSRF_TOKEN) return;
// 绑定事件和增强功能
this.bindHashChange();
this.upgradeCheckboxes();
this.addEnhancerNotice();
},
// 添加功能增强提示
addEnhancerNotice() {
const header = document.querySelector('h2.subtitle');
if (header) {
const notice = document.createElement('div');
notice.className = 'bgm-enhancer-status';
notice.textContent = '已启用分批编辑功能,支持超过20集的批量编辑';
header.parentNode.insertBefore(notice, header.nextSibling);
}
},
// 检查是否为分集编辑页面
isEpisodePage: () => /^\/subject\/\d+\/ep(\/edit_batch)?$/.test(location.pathname),
// 监听hash变化处理批量编辑
bindHashChange() {
const processHash = () => {
const ids = this.getSelectedIdsFromHash();
if (ids.length > 0) this.handleBatchEdit(ids);
};
window.addEventListener('hashchange', processHash);
if (location.hash.includes('episodes=')) processHash();
},
// 增强复选框功能
upgradeCheckboxes() {
const updateFormAction = () => {
const ids = $$('[name="ep_mod[]"]:checked').map(el => el.value);
$('form[name="edit_ep_batch"]').action =
`${this.BASE_URL}/edit_batch#episodes=${ids.join(',')}`;
};
$$('[name="ep_mod[]"]').forEach(el =>
el.addEventListener('change', updateFormAction)
);
// 全选功能
$('[name=chkall]')?.addEventListener('click', () => {
$$('[name="ep_mod[]"]').forEach(el => el.checked = true);
updateFormAction();
});
},
// 从hash获取选中ID
getSelectedIdsFromHash() {
const match = location.hash.match(/episodes=([\d,]+)/);
return match ? match[1].split(',').filter(Boolean) : [];
},
// 批量编辑主逻辑
async handleBatchEdit(episodeIds) {
try {
const chunks = this.createChunks(episodeIds, this.CHUNK_SIZE);
const dataChunks = await this.loadChunkedData(chunks);
// 填充表单数据
$('#summary').value = dataChunks.flat().join('\n');
$('[name=ep_ids]').value = episodeIds.join(',');
this.upgradeFormSubmit(chunks, episodeIds);
window.chiiLib?.ukagaka?.presentSpeech('数据加载完成');
} catch (err) {
console.error('批量处理失败:', err);
alert('数据加载失败,请刷新重试');
}
},
// 分块加载数据
async loadChunkedData(chunks) {
window.chiiLib?.ukagaka?.presentSpeech('正在加载分集数据...');
return Promise.all(
chunks.map(chunk => this.fetchChunkData(chunk).then(data => data.split('\n')))
);
},
// 获取单块数据
async fetchChunkData(episodeIds) {
const params = new URLSearchParams({
chkall: 'on',
submit: '批量修改',
formhash: this.CSRF_TOKEN,
'ep_mod[]': episodeIds
});
const res = await fetch(`${this.BASE_URL}/edit_batch`, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params
});
const html = await res.text();
const match = html.match(/<textarea [^>]*name="ep_list"[^>]*>([\s\S]*?)<\/textarea>/i);
return match?.[1]?.trim() || '';
},
// 增强表单提交处理
upgradeFormSubmit(chunks, originalIds) {
const form = $('form[name="edit_ep_batch"]');
if (!form) return;
form.onsubmit = async (e) => {
e.preventDefault();
const inputData = $('#summary').value.trim().split('\n');
if (inputData.length !== originalIds.length) {
alert(`数据不匹配 (预期 ${originalIds.length} 行,实际 ${inputData.length} 行)`);
return;
}
try {
window.chiiLib?.ukagaka?.presentSpeech('正在提交数据...');
await this.saveChunkedData(chunks, inputData);
window.chiiLib?.ukagaka?.presentSpeech('保存成功');
location.href = this.BASE_URL;
} catch (err) {
console.error('保存失败:', err);
alert('保存过程中发生错误');
}
};
},
// 分块保存数据
async saveChunkedData(chunks, fullData) {
const dataChunks = this.createChunks(fullData, this.CHUNK_SIZE);
return Promise.all(
chunks.map((idChunk, index) => this.saveChunkData(idChunk, dataChunks[index]))
);
},
// 保存单块数据
async saveChunkData(episodeIds, chunkData) {
const params = new URLSearchParams({
formhash: this.CSRF_TOKEN,
rev_version: '0',
editSummary: $('#editSummary')?.value || '',
ep_ids: episodeIds.join(','),
ep_list: chunkData.join('\n'),
submit_eps: '改好了'
});
await fetch(`${this.BASE_URL}/edit_batch`, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params
});
},
// 通用分块方法
createChunks(array, size) {
return Array.from(
{ length: Math.ceil(array.length / size) },
(_, i) => array.slice(i * size, (i + 1) * size)
);
}
};
/* ====================
批量关联 - 用于关联条目
======================*/
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;
// 根据当前 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();
});
}
// 点击项目后利用 MutationObserver 监听新增条目,然后对该条目的下拉框设置类型
function processItem(element, item_type) {
return new Promise((resolve) => {
// 关联列表容器
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]; // 没有完全匹配,取第一个
}
resolve(await processItem(selectedElement, item_type));
});
}
// 处理下一个项目
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 {
$(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': '十',
'Ⅰ': '一', 'Ⅱ': '二', 'Ⅲ': '三', 'Ⅳ': '四', 'Ⅴ': '五',
'Ⅵ': '六', 'Ⅶ': '七', 'Ⅷ': '八', 'Ⅸ': '九', 'Ⅹ': '十'
};
function normalizeSeasonOrEpisode(text) {
// 移除可能的空格
text = text.replace(/\s+/g, '');
// 处理带数字的情况(包括直接的数字转换)
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) {
if (!input.trim()) {
alert("请输入ID或内容");
return [];
}
// 先检查是否以 bgm_id= 开头
if (input.startsWith("bgm_id=")) {
return input.substring(7)
.split(/[,\n\r,、\/|;。.()【】<>!?]+| +/)
.map(id => "bgm_id=" + id.trim())
.filter(id => id);
}
// 识别 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);
}
// 拆分并标准化每个条目
return input.split(/[,\n\r,、\/|;。.()【】<>!?]+/)
.map(part => part.trim())
.filter(part => part.length > 0)
.map(normalizeSeasonOrEpisode)
.map(part => {
// 处理纯数字ID
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_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');
}
// 根据页面类型设定 UI 标题
let uiTitle = '人物';
const pageType = getCurrentPageType();
if (pageType === 'character') {
uiTitle = '角色';
} else if (pageType === 'subject') {
uiTitle = '条目';
}
// 创建改进的UI界面
$('.subjectListWrapper').after(`
<div class="Relation_wrapper">
<h2>批量关联助手</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="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>
`);
// 添加关联类型选择器
$('.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();
}
})();