', {
text: '没有匹配选项',
class: 'bgm-dropdown-item',
css: { color: '#999', cursor: 'default' }
}).appendTo($dropdownContent);
}
};
// 将选项值插入到输入框
function insertOptionValue(inputElem, value) {
const text = inputElem.value;
const pos = inputElem.selectionStart;
const mode = inputElem.dataset.dropdownMode || 'append'; // 获取插入模式
const conn = inputElem.dataset.dropdownConnector || '、';
// 根据不同的插入模式处理
if (mode === 'replace') {
// 直接替换
inputElem.value = value;
} else if (mode === 'append') {
// 自动补全逻辑改进
if (text) {
const currentWord = text.slice(0, pos).toLowerCase();
if (value.toLowerCase().startsWith(currentWord)) {
// 如果当前输入是目标值的开头,直接替换整个值
inputElem.value = value + text.slice(pos);
inputElem.setSelectionRange(value.length, value.length);
inputElem.focus();
return;
}
// 处理分隔符和空格
const lastChar = text.slice(-1);
if (['、',',',',','+',' '].includes(lastChar)) {
// 如果最后一个字符是分隔符,直接添加新值
inputElem.value = text.trimRight() + value;
} else {
// 否则添加分隔符和新值
inputElem.value = text + conn + value;
}
} else {
// 如果输入框为空,直接设置值
inputElem.value = value;
}
}
inputElem.focus();
}
// 处理所有字段
const processAllFields = () => {
const $ = window.jQuery || window.$;
// 处理ID字段和对应的属性字段
$('input.inputtext.id').each(function() {
const $idInput = $(this);
const idValue = $idInput.val().trim();
const $propInput = $idInput.next('.inputtext.prop');
// 为空ID字段添加下拉菜单(如果配置了)
if (idValue === '' && config[''] && !$idInput.data('dropdownAdded')) {
createDropdown(this, config[''].options, config[''].mode);
}
// 为其他配置的字段添加下拉菜单
if ($propInput.length) {
Object.entries(config).forEach(([key, options]) => {
const keys = key.split(',').map(k => k.trim());
if (keys.includes(idValue) && !$propInput.data('dropdownAdded')) {
createDropdown($propInput[0], options.options, options.mode, options.connector);
}
});
}
});
};
// 处理动态添加的字段
const processDynamicField = (element) => {
const $ = window.jQuery || window.$;
const $element = $(element);
// 如果元素本身是输入框
if ($element.is('input.inputtext.id')) {
const $idInput = $element;
const idValue = $idInput.val().trim();
const $propInput = $idInput.next('.inputtext.prop');
// 空ID字段处理
if (idValue === '' && config['']) {
createDropdown($idInput[0], config[''].options, config[''].mode);
}
// 属性字段处理
if ($propInput.length) {
Object.entries(config).forEach(([key, options]) => {
const keys = key.split(',').map(k => k.trim());
if (keys.includes(idValue)) {
createDropdown($propInput[0], options.options, options.mode, options.connector);
}
});
}
}
// 如果元素是容器
else {
// 处理常规ID字段
$element.find('input.inputtext.id').each(function() {
const $idInput = $(this);
const idValue = $idInput.val().trim();
const $propInput = $idInput.next('.inputtext.prop');
// 空ID字段处理
if (idValue === '' && config['']) {
createDropdown($idInput[0], config[''].options, config[''].mode);
}
// 属性字段处理
if ($propInput.length) {
Object.entries(config).forEach(([key, options]) => {
const keys = key.split(',').map(k => k.trim());
if (keys.includes(idValue)) {
createDropdown($propInput[0], options.options, options.mode, options.connector);
}
});
}
});
}
};
// 初始化函数
const init = () => {
const $ = window.jQuery || window.$;
// 添加设置按钮
addSettingsButton();
// 初始处理所有字段
$(document).ready(function() {
processAllFields();
// 监听动态添加的字段
$(document).on('DOMNodeInserted', function(e) {
const $target = $(e.target);
if ($target.find('input.inputtext.id').length || $target.is('input.inputtext.id')) {
setTimeout(() => processDynamicField(e.target), 100);
}
});
// 点击页面其他位置时隐藏所有下拉框
$(document).on('click', function(e) {
if (!$(e.target).closest('.bgm-input-wrapper').length && !$(e.target).closest('.bgm-config-modal').length) {
$('.bgm-dropdown-content').removeClass('bgm-dropdown-active');
}
});
});
// 监听添加新行的按钮点击事件
$(document).on('click', '.newbtn', function() {
setTimeout(processAllFields, 100);
});
// 处理现有字段ID值变化的情况
$(document).on('change', 'input.inputtext.id', function() {
const $idInput = $(this);
const idValue = $idInput.val().trim();
const $propInput = $idInput.next('.inputtext.prop');
// 若属性字段已有下拉菜单,需要移除
if ($propInput.data('dropdownAdded')) {
if ($propInput.next('.bgm-dropdown-content').length) {
$propInput.next('.bgm-dropdown-content').remove();
}
$propInput.data('dropdownAdded', false);
// 如果输入框被包装了,尝试解包
if ($propInput.parent().hasClass('bgm-input-wrapper')) {
$propInput.unwrap();
}
}
// 添加新的下拉菜单
if ($propInput.length) {
Object.entries(config).forEach(([key, options]) => {
const keys = key.split(',').map(k => k.trim());
if (keys.includes(idValue)) {
createDropdown($propInput[0], options.options, options.mode, options.connector);
}
});
}
});
};
// 执行初始化
init();
}
/* =============
单行本快捷创建
=============== */
function initBgmCreateSubject() {
// 调试日志函数
function log(message) {
console.log(`[BGM助手] ${message}`);
}
// 添加通知区域
function addNotificationArea() {
// 检查是否已存在通知区域
if (document.querySelector('#bgm-notification-area')) return;
// 创建通知区域
const notificationArea = document.createElement('div');
notificationArea.id = 'bgm-notification-area';
notificationArea.style.position = 'fixed';
notificationArea.style.top = '10px';
notificationArea.style.right = '10px';
notificationArea.style.padding = '8px 15px';
notificationArea.style.backgroundColor = '#369CF8';
notificationArea.style.color = 'white';
notificationArea.style.borderRadius = '5px';
notificationArea.style.zIndex = '9999';
notificationArea.style.display = 'none';
notificationArea.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
document.body.appendChild(notificationArea);
}
// 显示通知
function showNotification(message, isError = false) {
// 确保通知区域存在
if (!document.querySelector('#bgm-notification-area')) {
addNotificationArea();
}
const notificationArea = document.querySelector('#bgm-notification-area');
notificationArea.textContent = message;
notificationArea.style.backgroundColor = isError ? '#FF5151' : '#369CF8';
notificationArea.style.display = 'block';
// 3秒后隐藏通知
setTimeout(() => {
notificationArea.style.display = 'none';
}, 3000);
// 如果支持GM通知,也显示一个
if (typeof GM_notification !== 'undefined') {
GM_notification({
title: 'Bangumi 条目创建助手',
text: message,
timeout: 3000
});
}
// 同时在控制台输出
log(message);
}
// 解决跨域存储问题 - 使用GM储存而不是localStorage
function saveData(key, value) {
if (typeof GM_setValue !== 'undefined') {
GM_setValue(key, value);
log(`使用GM_setValue保存数据: ${key}`);
return true;
} else if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem(key, value);
log(`使用localStorage保存数据: ${key}`);
return true;
} catch (e) {
log(`localStorage保存失败: ${e.message}`);
return false;
}
}
return false;
}
function getData(key) {
if (typeof GM_getValue !== 'undefined') {
const value = GM_getValue(key);
log(`使用GM_getValue获取数据: ${key}=${value ? '有数据' : '无数据'}`);
return value;
} else if (typeof localStorage !== 'undefined') {
try {
const value = localStorage.getItem(key);
log(`使用localStorage获取数据: ${key}=${value ? '有数据' : '无数据'}`);
return value;
} catch (e) {
log(`localStorage获取失败: ${e.message}`);
return null;
}
}
return null;
}
function removeData(key) {
if (typeof GM_deleteValue !== 'undefined') {
GM_deleteValue(key);
log(`使用GM_deleteValue删除数据: ${key}`);
} else if (typeof localStorage !== 'undefined') {
try {
localStorage.removeItem(key);
log(`使用localStorage删除数据: ${key}`);
} catch (e) {
log(`localStorage删除失败: ${e.message}`);
}
}
}
// 在条目页面添加"新条目"按钮
function addCreateButton() {
const currentPath = location.pathname;
// 只有精确匹配 /subject/数字 时才继续
const matchSubject = location.pathname.match(/^\/subject\/(\d+)$/);
if (!matchSubject) {
console.log("路径不匹配 /subject/数字,跳过");
return;
}
// 确保作品类型是"小说"或"漫画"
const genreSmall = document.querySelector('small.grey');
if (!genreSmall) {
log("未找到类型标签");
return;
}
const genre = genreSmall.textContent.trim();
if (!(genre === '小说' || genre === '漫画')) {
log(`条目类型不是小说或漫画: ${genre}`);
return;
}
// 查找导航栏,并避免重复添加按钮
const nav = document.querySelector(".subjectNav .navTabs, .navTabs");
if (!nav) {
log("未找到导航栏");
return;
}
if (nav.querySelector(".create-button")) {
log("按钮已存在,不重复添加");
return;
}
// 添加通知区域,确保在点击按钮前已准备好
addNotificationArea();
// 创建"新条目"按钮并插入导航栏
const createLi = document.createElement("li");
createLi.className = "create-button";
const subjectId = matchSubject[1];
createLi.innerHTML = `
新条目`;
// 按钮点击事件:拉取编辑页信息并跳转到创建页
createLi.querySelector("a").addEventListener("click", () => {
// 先显示通知:正在准备打开新页面
showNotification("正在获取条目内容...");
// 检测当前域名,统一使用同一域名请求和跳转
const currentHost = window.location.hostname; // 获取当前域名
log(`当前域名: ${currentHost}`);
const editUrl = `//${currentHost}/subject/${subjectId}/edit`;
const title = document.querySelector('h1 a[property="v:itemreviewed"]')?.textContent?.trim() ||
document.querySelector('h1.nameSingle a')?.textContent?.trim() ||
'';
if (!title) {
log("未找到条目标题");
}
// 直接使用fetch API,如果可用
const fetchData = () => {
log(`使用fetch请求: ${editUrl}`);
fetch(editUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP状态码: ${response.status}`);
}
return response.text();
})
.then(html => processEditPage(html))
.catch(e => {
showNotification(`获取内容失败: ${e.message}`, true);
log(`Fetch错误: ${e.message}`);
// 如果fetch失败,尝试使用GM_xmlhttpRequest
if (typeof GM_xmlhttpRequest !== 'undefined') {
useGMRequest();
}
});
};
// 使用GM_xmlhttpRequest作为备选
const useGMRequest = () => {
if (typeof GM_xmlhttpRequest === 'undefined') {
showNotification("脚本缺少必要的GM_xmlhttpRequest权限,请检查脚本管理器设置", true);
return;
}
log(`使用GM_xmlhttpRequest请求: ${window.location.protocol}${editUrl}`);
GM_xmlhttpRequest({
method: "GET",
url: window.location.protocol + editUrl,
timeout: 10000, // 10秒超时
onload: function (response) {
if (response.status !== 200) {
showNotification(`请求失败,状态码: ${response.status}`, true);
return;
}
processEditPage(response.responseText);
},
onerror: function (error) {
showNotification("网络请求失败,请检查网络连接", true);
console.error("请求失败", error);
},
ontimeout: function () {
showNotification("请求超时,请稍后重试", true);
}
});
};
// 处理编辑页数据
const processEditPage = (html) => {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
// 检查是否需要登录
const loginForm = doc.querySelector('form[action="/FollowTheRabbit"]');
if (loginForm) {
showNotification("获取失败,请先登录Bangumi", true);
return;
}
const textarea = doc.querySelector('#subject_summary');
if (!textarea) {
showNotification("未找到条目内容,可能没有编辑权限", true);
return;
}
const content = textarea.value;
if (!content) {
showNotification("条目内容为空", true);
return;
}
// 识别条目类型
let type = "Unknown";
if (content.includes("Infobox animanga/Novel")) {
type = "Novel";
} else if (content.includes("Infobox animanga/Manga")) {
type = "Manga";
}
// 如果无法识别类型则提示
if (type === "Unknown") {
showNotification("未识别 Infobox 类型(支持 Novel 或 Manga)", true);
return;
}
// 使用全局唯一标识符,解决域名问题
const uniqueId = Date.now().toString();
const keyPrefix = `bgm_infobox_${uniqueId}`;
// 标记数据来源为原始条目页面
const sourceType = "original";
// 保存数据
const savedContent = saveData(`${keyPrefix}_content`, content);
const savedType = saveData(`${keyPrefix}_type`, type);
const savedTitle = saveData(`${keyPrefix}_title`, title);
const savedSource = saveData(`${keyPrefix}_source_type`, sourceType);
if (!savedContent || !savedType) {
showNotification("保存数据失败,请检查浏览器权限", true);
return;
}
// 显示成功通知
showNotification("条目内容获取成功,正在打开创建页面...");
// 调试输出所有存储数据
log(`保存的数据ID: ${uniqueId}`);
log(`保存的类型: ${type}`);
log(`保存的标题: ${title}`);
log(`保存的内容长度: ${content.length} 字符`);
log(`保存的来源类型: ${sourceType}`);
// 在新标签页打开新建条目页面
window.open(`//${currentHost}/new_subject/1?source=${uniqueId}`, "_blank");
} catch (e) {
showNotification(`解析页面失败: ${e.message}`, true);
console.error(e);
}
};
// 开始请求,优先使用fetch
try {
if (window.fetch) {
fetchData();
} else if (typeof GM_xmlhttpRequest !== 'undefined') {
useGMRequest();
} else {
showNotification("浏览器不支持所需的请求方法", true);
}
} catch (e) {
showNotification(`发起请求时出错: ${e.message}`, true);
console.error(e);
}
});
nav.appendChild(createLi); // 将按钮添加到导航栏
log("成功添加新条目按钮");
}
// 自动填充新建条目的表单内容
function fillNewSubjectForm() {
// 获取 URL 中传递的 source 参数
function getQueryParam(param) {
const url = new URL(window.location.href);
return url.searchParams.get(param);
}
const sourceId = getQueryParam("source");
if (!sourceId) {
log("没有找到source参数,不执行填充");
return;
}
log(`检测到source参数: ${sourceId}`);
// 确保通知区域已创建
addNotificationArea();
// 构建键名前缀
const keyPrefix = `bgm_infobox_${sourceId}`;
// 获取存储的数据
const content = getData(`${keyPrefix}_content`);
const type = getData(`${keyPrefix}_type`);
const originalTitle = getData(`${keyPrefix}_title`);
if (!content || !type) {
log(`未找到数据: ${keyPrefix}`);
// 尝试使用旧版键名
const oldKeyPrefix = `infobox_${sourceId}`;
const oldContent = getData(`${oldKeyPrefix}_content`);
const oldType = getData(`${oldKeyPrefix}_type`);
const oldTitle = getData(`${oldKeyPrefix}_title`);
if (oldContent && oldType) {
log(`找到旧版数据: ${oldKeyPrefix}`);
// 使用找到的旧数据
fillForm(oldContent, oldType, oldTitle);
return;
}
// 检查有没有任何bgm_infobox开头的键,可能是跨域问题
if (typeof localStorage !== 'undefined') {
log("检查localStorage中的所有键");
let foundKeys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('bgm_infobox_') || key.startsWith('infobox_')) {
foundKeys.push(key);
}
}
if (foundKeys.length > 0) {
log(`找到相关键: ${foundKeys.join(', ')}`);
// 尝试使用最近的一个键
foundKeys.sort();
const latestKey = foundKeys[foundKeys.length - 1];
const keyBase = latestKey.split('_').slice(0, -1).join('_');
log(`尝试使用最近的键前缀: ${keyBase}`);
const latestContent = getData(`${keyBase}_content`);
const latestType = getData(`${keyBase}_type`);
const latestTitle = getData(`${keyBase}_title`);
if (latestContent && latestType) {
log(`使用找到的最近数据`);
fillForm(latestContent, latestType, latestTitle);
return;
}
} else {
log("未找到任何相关键");
}
}
showNotification("未找到条目内容,请重试", true);
return;
}
fillForm(content, type, originalTitle);
function fillForm(content, type, title) {
// 定义类型与分类选项的映射
const typeMap = {
"Novel": { id: "cat_novel", tpl: "Novel" },
"Manga": { id: "cat_comic", tpl: "Manga" }
};
const category = typeMap[type];
if (!category) {
log(`未知条目类型: ${type}`);
return;
}
// 新页面显示填充通知
showNotification("正在填充条目信息...");
log(`准备填充 ${type} 类型的条目,标题: ${title || '无标题'}`);
// 当表单加载好后执行填充
waitForFormLoaded();
function waitForFormLoaded() {
// 添加一个检查函数,看看表格是否都加载出来了
const checkForm = () => {
const infoboxInput = document.querySelector("#subject_infobox");
const titleInput = document.querySelector('input[name="subject_title"]');
const typeRadio = document.getElementById(category.id);
return infoboxInput && titleInput && typeRadio;
};
// 检查表单是否已加载
if (checkForm()) {
doFillForm();
return;
}
// 观察DOM变化等待表单加载
log("等待表单加载...");
let attempts = 0;
const maxAttempts = 30; // 最多等待30*200ms = 6秒
const waitInterval = setInterval(() => {
attempts++;
if (checkForm()) {
clearInterval(waitInterval);
log("表单已加载,开始填充");
doFillForm();
return;
}
if (attempts >= maxAttempts) {
clearInterval(waitInterval);
log("表单加载超时");
showNotification("表单加载超时,请手动填写", true);
}
if (attempts % 5 === 0) {
log(`等待表单加载中... ${attempts}/${maxAttempts}`);
}
}, 200);
}
function doFillForm() {
try {
// 选择分类
const typeRadio = document.getElementById(category.id);
if (typeRadio) {
typeRadio.click();
log(`已选择类型: ${type}`);
} else {
log(`未找到类型选择框: ${category.id}`);
}
// 等待分类选择完成后,填充表单
setTimeout(() => {
try {
// 填充内容
const infobox = document.querySelector("#subject_infobox");
if (infobox) {
infobox.value = content;
log("已填充Infobox内容");
// 确保触发表单的change事件
const event = new Event('change', { bubbles: true });
infobox.dispatchEvent(event);
} else {
log("未找到Infobox输入框");
}
// 填充标题
const titleInput = document.querySelector('input[name="subject_title"]');
if (titleInput && title) {
let newTitle = title;
const match = title.match(/\((\d+)\)$/);
if (match) {
const num = parseInt(match[1], 10) + 1;
newTitle = title.replace(/\(\d+\)$/, `(${num})`);
} else {
newTitle = `${title} (1)`;
}
titleInput.value = newTitle;
log(`已填充标题: ${newTitle}`);
// 触发change事件
const event = new Event('change', { bubbles: true });
titleInput.dispatchEvent(event);
} else {
log("未找到标题输入框或没有标题");
}
// 尝试根据需要切换模式
const currentMode = document.querySelector('#header_infobox');
const wikiModeLink = document.querySelector('a[onclick*="NormaltoWCODE"]');
const simpleModeLink = document.querySelector('a[onclick*="WCODEtoNormal"]');
// 判断当前是否在wiki模式
const isInWikiMode = currentMode && currentMode.textContent.includes('WCODE');
// 如果用户想要wiki模式但当前不是wiki模式
if (!isInWikiMode && wikiModeLink) {
wikiModeLink.click();
log("已切换到wiki模式");
}
// 如果用户想要普通模式但当前是wiki模式
else if (isInWikiMode && simpleModeLink) {
simpleModeLink.click();
log("已切换到普通模式");
}
else {
log("未找到模式切换链接或已经是所需模式");
}
// 显示成功通知
showNotification("条目信息填充完成");
// 填充完毕后删除 localStorage 中的数据
removeData(`${keyPrefix}_content`);
removeData(`${keyPrefix}_type`);
removeData(`${keyPrefix}_title`);
removeData(`${keyPrefix}_source_type`);
log("已清理临时数据");
} catch (e) {
showNotification(`填充表单时出错: ${e.message}`, true);
console.error(e);
}
}, 500);
} catch (e) {
showNotification(`选择分类时出错: ${e.message}`, true);
console.error(e);
}
}
}
}
// 从当前新建条目页面添加"复制创建"功能
function addCloneButton() {
// 确保当前页面是新建条目页面
if (!location.pathname.includes("/new_subject/")) {
log("当前不是新建条目页面,不添加克隆按钮");
return;
}
// 查找标题元素 - 兼容多种可能的标题文本
const titleHeader = document.querySelector('h1');
if (!titleHeader) {
log("未找到标题元素");
return;
}
// 检查标题是否包含"添加"字样
if (!titleHeader.textContent.includes('添加')) {
log(`标题内容不匹配: ${titleHeader.textContent}`);
return;
}
// 确保按钮未重复添加
if (document.querySelector('#clone-entry-button')) {
log("克隆按钮已存在,不重复添加");
return;
}
// 添加通知区域
addNotificationArea();
// 创建按钮
const cloneButton = document.createElement('button');
cloneButton.id = 'clone-entry-button';
cloneButton.textContent = '新条目';
cloneButton.style.backgroundColor = '#F09199';
cloneButton.style.color = 'white';
cloneButton.style.border = 'none';
cloneButton.style.padding = '5px 10px';
cloneButton.style.borderRadius = '4px';
cloneButton.style.marginLeft = '10px';
cloneButton.style.cursor = 'pointer';
cloneButton.style.fontWeight = 'bold';
cloneButton.style.textAlign = 'center';
// 添加按钮到标题旁边
titleHeader.appendChild(cloneButton);
log("成功添加克隆按钮");
// 添加点击事件
cloneButton.addEventListener('click', () => {
// 获取当前表单内容
const infobox = document.querySelector("#subject_infobox");
const titleInput = document.querySelector('input[name="subject_title"]');
if (!infobox || !titleInput) {
showNotification("无法获取表单内容", true);
return;
}
const content = infobox.value;
const title = titleInput.value;
if (!content) {
showNotification("Infobox内容为空", true);
return;
}
// 识别类型
let type = "Unknown";
if (content.includes("Infobox animanga/Novel")) {
type = "Novel";
} else if (content.includes("Infobox animanga/Manga")) {
type = "Manga";
}
if (type === "Unknown") {
showNotification("未识别 Infobox 类型(支持 Novel 或 Manga)", true);
return;
}
// 生成唯一ID作为存储键
const cloneId = 'clone_' + Date.now();
// 标记数据来源为克隆页面
const sourceType = "cloned";
// 保存内容
const savedContent = saveData(`bgm_infobox_${cloneId}_content`, content);
const savedType = saveData(`bgm_infobox_${cloneId}_type`, type);
const savedTitle = saveData(`bgm_infobox_${cloneId}_title`, title);
const savedSource = saveData(`bgm_infobox_${cloneId}_source_type`, sourceType);
if (!savedContent || !savedType) {
showNotification("保存数据失败,请检查浏览器权限", true);
return;
}
log(`已复制当前页面条目内容, ID: ${cloneId}`);
// 显示通知
showNotification("已复制当前页面条目内容,正在打开新建页面...");
// 获取当前主机名用于跳转
const currentHost = window.location.hostname;
// 打开新的条目创建页面
window.open(`//${currentHost}/new_subject/1?source=${cloneId}`, "_blank");
});
}
// 初始化
function init() {
log("Bangumi条目创建助手初始化中...");
// 若当前页面是条目页面,添加"新条目"按钮
if (location.pathname.match(/^\/subject\/\d+$/)) {
log("检测到条目页面");
addCreateButton();
}
// 若当前页面是创建条目页,添加功能
if (location.pathname.includes("/new_subject/")) {
log("检测到新建条目页面");
addCloneButton();
// 如果有source参数,则填充表单
if (location.search.includes("source=")) {
log("检测到source参数,准备填充表单");
// 稍微延迟执行填充,确保页面已完全加载
setTimeout(fillNewSubjectForm, 500);
}
}
log("初始化完成");
}
// 页面加载完成后执行初始化
window.addEventListener("load", init);
// 也可以在DOM加载完成后就执行,提高响应速度
if (document.readyState === "interactive" || document.readyState === "complete") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
}
/* ========
编辑预览
========== */
function initBgmPreview() {
// 标记预览功能是否已禁用
let previewDisabled = false;
// 保存原始按钮的引用,防止重复创建
let originalButtons = new Map();
// 拦截提交按钮
function interceptSubmitButtons() {
// 查找所有匹配的按钮
const submitButtons = document.querySelectorAll('input.inputBtn[value="提交"][name="submit"][type="submit"]');
submitButtons.forEach(button => {
// 检查该按钮是否已经被处理过
if (originalButtons.has(button)) return;
// 备份原始的表单提交行为
const originalForm = button.form;
const originalSubmitEvent = originalForm ? originalForm.onsubmit : null;
// 保存原始按钮的引用和信息
originalButtons.set(button, {
originalOnClick: button.onclick,
originalForm: originalForm,
originalSubmitEvent: originalSubmitEvent,
handled: true
});
// 替换onclick处理函数
button.onclick = function(event) {
// 如果预览功能已禁用,直接提交表单
if (previewDisabled) {
return true;
}
event.preventDefault();
// 如果当前处于入门模式,先切换到Wiki模式获取数据
const wikiModeLink = document.querySelector('a.l[onclick="NormaltoWCODE()"]');
if (wikiModeLink) {
// 保存当前表单数据
const formDataBeforeSwitch = saveFormData();
// 切换到Wiki模式
NormaltoWCODE();
// 延迟获取Wiki模式下的数据
setTimeout(() => {
const formDataWiki = collectFormData();
// 切换回入门模式
WCODEtoNormal();
// 延迟恢复表单数据并显示预览
setTimeout(() => {
restoreFormData(formDataBeforeSwitch);
showPreview(button);
}, 100);
}, 100);
} else {
// 如果已经是Wiki模式,直接获取数据并显示预览
showPreview(button);
}
return false;
};
// 同时拦截表单的submit事件
if (originalForm) {
originalForm.onsubmit = function(event) {
// 修复:如果预览功能已禁用,允许正常提交
if (previewDisabled) {
return originalSubmitEvent ? originalSubmitEvent.call(this, event) : true;
}
// 如果是通过我们的确认按钮提交的,则允许继续
if (event && event.submittedViaPreview) {
return originalSubmitEvent ? originalSubmitEvent.call(this, event) : true;
}
// 否则拦截并显示预览
event.preventDefault();
showPreview(button);
return false;
};
}
});
}
// 保存表单数据
function saveFormData() {
const formData = {};
// 保存所有输入字段
document.querySelectorAll('input, textarea, select').forEach(el => {
if (el.name) {
formData[el.name] = el.value;
}
});
return formData;
}
// 恢复表单数据
function restoreFormData(formData) {
for (const name in formData) {
const el = document.querySelector(`[name="${name}"]`);
if (el) {
el.value = formData[name];
}
}
}
// 阻止Enter键提交表单
function preventEnterSubmit() {
document.addEventListener('keydown', function(event) {
// 如果按下Enter键,且当前没有输入框被选中,且预览功能未禁用
if (event.key === 'Enter' && !previewDisabled &&
!(document.activeElement &&
(document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA'))) {
event.preventDefault();
return false;
}
});
}
// 收集所有表单字段数据并获取原始数据
function collectFormData() {
const formData = {};
// 收集标题
const titleInput = document.querySelector('input[name="subject_title"]');
if (titleInput) {
formData.title = {
current: titleInput.value,
original: titleInput.defaultValue || ""
};
}
// 收集Infobox
const infobox = document.querySelector('#subject_infobox');
if (infobox) {
formData.infobox = {
current: infobox.value,
original: infobox.defaultValue || ""
};
}
// 收集简介
const summary = document.querySelector('textarea[name="subject_summary"]');
if (summary) {
formData.summary = {
current: summary.value,
original: summary.defaultValue || ""
};
}
// 收集入门模式下编辑器的内容
const normalModeEditor = document.querySelector('#editTopicContent_ifr');
if (normalModeEditor && normalModeEditor.contentWindow) {
try {
const editorBody = normalModeEditor.contentWindow.document.body;
if (editorBody) {
// 保存当前内容
const currentContent = editorBody.innerHTML;
// 如果能找到原始内容(可能需要在页面加载时保存)
// 简化处理,假设如果内容不为空则有变化
formData.infobox_normal = {
current: currentContent,
original: window.originalEditorContent || ""
};
// 如果没有保存过原始内容,则保存当前内容为原始内容
if (!window.originalEditorContent) {
window.originalEditorContent = currentContent;
}
}
} catch (e) {
console.error("Failed to access editor content:", e);
}
}
// 收集标签
const tags = document.querySelector('input[name="subject_meta_tags"]');
if (tags) {
formData.tags = {
current: tags.value,
original: tags.defaultValue || ""
};
}
// 收集编辑摘要
const editSummary = document.querySelector('input[name="editSummary"]');
if (editSummary) {
formData.editSummary = {
current: editSummary.value,
original: editSummary.defaultValue || ""
};
}
return formData;
}
// 条目信息匹配
function diffLines(originalLines, newLines) {
const result = [];
let i = 0, j = 0;
// 使用更精确的字符串比较来检测行级别的变化
while (i < originalLines.length || j < newLines.length) {
const oldLine = i < originalLines.length ? originalLines[i] : null;
const newLine = j < newLines.length ? newLines[j] : null;
// 规范化比较,确保即使有空格或编码差异也能正确识别相同内容
const normalizedOldLine = oldLine ? oldLine.trim() : null;
const normalizedNewLine = newLine ? newLine.trim() : null;
// 增强相等判断
const isEqual = normalizedOldLine === normalizedNewLine && normalizedOldLine !== null && normalizedNewLine !== null;
if (oldLine === null && newLine !== null) {
// 纯新增行
result.push({ old: '', new: newLine });
j++;
} else if (oldLine !== null && newLine === null) {
// 纯删除行
result.push({ old: oldLine, new: '' });
i++;
} else if (isEqual || calculateSimilarity(normalizedOldLine, normalizedNewLine) > 0.95) {
// 调高相似度阈值到0.95,确保相同或几乎相同的内容被识别为同一行
// 对于非拉丁字符尤其有效
result.push({ old: oldLine, new: newLine });
i++; j++;
} else {
// 经过修改的行 - 需要查找相似度
const similarity = calculateSimilarity(oldLine, newLine);
if (similarity > 0.7) { // 提高相似度阈值,减少误判
// 认为是同一行的修改,使用字符级别的差异检测
result.push({
old: oldLine,
new: newLine,
isDiff: true // 标记为差异行,用于后续处理
});
i++; j++;
} else {
// 查找更好的匹配
const nextOldMatch = findBestMatchIndex(newLine, originalLines.slice(i + 1));
const nextNewMatch = findBestMatchIndex(oldLine, newLines.slice(j + 1));
// 转换回实际索引
const nextOldIndex = nextOldMatch !== -1 ? i + 1 + nextOldMatch : -1;
const nextNewIndex = nextNewMatch !== -1 ? j + 1 + nextNewMatch : -1;
// 更复杂的逻辑来决定是删除、添加还是修改
if (nextOldIndex !== -1 && (nextNewIndex === -1 || nextOldIndex - i < nextNewIndex - j)) {
// 当前行在原始内容中被删除
result.push({ old: oldLine, new: '' });
i++;
} else if (nextNewIndex !== -1) {
// 当前行在新内容中被添加
result.push({ old: '', new: newLine });
j++;
} else {
// 没有找到更好的匹配,视为修改
result.push({
old: oldLine,
new: newLine,
isDiff: true
});
i++; j++;
}
}
}
}
return result.filter(p => p.old !== '' || p.new !== '');
}
// 保留相似度最高且超过设定阈值的行
function findBestMatchIndex(str, array) {
if (!str || !array || array.length === 0) return -1;
let bestIndex = -1;
let bestSimilarity = 0.7; // 提高最低相似度阈值
for (let i = 0; i < array.length; i++) {
const similarity = calculateSimilarity(str, array[i]);
if (similarity > bestSimilarity) {
bestSimilarity = similarity;
bestIndex = i;
}
}
return bestIndex; // 直接返回索引,不加1
}
// 改进计算相似度函数,更好地处理Unicode字符
function calculateSimilarity(str1, str2) {
if (!str1 && !str2) return 1.0;
if (!str1 || !str2) return 0.0;
// 规范化处理,去除多余空格
const s1 = str1.trim();
const s2 = str2.trim();
if (s1 === s2) return 1.0; // 完全相同
const len1 = s1.length;
const len2 = s2.length;
// 如果长度差异太大,认为不相似
if (Math.abs(len1 - len2) / Math.max(len1, len2) > 0.3) { // 降低长度差异阈值
return 0.0;
}
// 查找共同子串
let commonLength = 0;
const minLen = Math.min(len1, len2);
// 前缀匹配
for (let i = 0; i < minLen; i++) {
if (s1[i] === s2[i]) {
commonLength++;
} else {
break;
}
}
// 后缀匹配
let suffixMatch = 0;
for (let i = 1; i <= minLen - commonLength; i++) {
if (s1[len1 - i] === s2[len2 - i]) {
suffixMatch++;
} else {
break;
}
}
commonLength += suffixMatch;
// 特别处理日文和其他Unicode字符
// 对于非ASCII字符,使用更宽松的相似度判断
if (/[^\u0000-\u007F]/.test(s1) || /[^\u0000-\u007F]/.test(s2)) {
// 如果含有非ASCII字符,并且相似度较高,给予更高权重
const baseScore = commonLength / Math.max(len1, len2);
if (baseScore > 0.7) {
return Math.min(1.0, baseScore * 1.2); // 提高得分,但不超过1.0
}
}
// 返回相似度
return commonLength / Math.max(len1, len2);
}
// 精确标记两个字符串之间的差异,实现字符级别的差异高亮
function highlightCharDifferences(oldStr, newStr) {
// 如果字符串相同,直接返回
if (oldStr === newStr) {
return { old: escapeHtml(oldStr), new: escapeHtml(newStr) };
}
// 如果一方为空,整行高亮
if (!oldStr || !newStr) {
return {
old: oldStr ? `
${escapeHtml(oldStr)}` : ' ',
new: newStr ? `
${escapeHtml(newStr)}` : ' '
};
}
// 查找共同前缀和后缀
let prefixLength = 0;
const minLength = Math.min(oldStr.length, newStr.length);
while (prefixLength < minLength && oldStr[prefixLength] === newStr[prefixLength]) {
prefixLength++;
}
let suffixLength = 0;
while (
suffixLength < minLength - prefixLength &&
oldStr[oldStr.length - 1 - suffixLength] === newStr[newStr.length - 1 - suffixLength]
) {
suffixLength++;
}
// 提取差异部分
const oldPrefix = escapeHtml(oldStr.substring(0, prefixLength));
const oldSuffix = escapeHtml(oldStr.substring(oldStr.length - suffixLength));
const oldDiff = escapeHtml(oldStr.substring(prefixLength, oldStr.length - suffixLength));
const newPrefix = escapeHtml(newStr.substring(0, prefixLength));
const newSuffix = escapeHtml(newStr.substring(newStr.length - suffixLength));
const newDiff = escapeHtml(newStr.substring(prefixLength, newStr.length - suffixLength));
// 构建结果 - 只高亮实际变化的部分
return {
old: `${oldPrefix}${oldDiff ? `
${oldDiff}` : ''}${oldSuffix}`,
new: `${newPrefix}${newDiff ? `
${newDiff}` : ''}${newSuffix}`
};
}
// 创建差异对比视图的UI
function createDiffView(title, originalText, newText) {
const section = document.createElement('div');
section.className = 'preview-section';
// 标题
const sectionTitle = document.createElement('div');
sectionTitle.className = 'preview-section-title';
sectionTitle.textContent = title;
section.appendChild(sectionTitle);
// 对比容器
const diffContainer = document.createElement('div');
diffContainer.className = 'preview-diff-container';
// 标签行
const header = document.createElement('div');
header.className = 'preview-diff-header';
header.innerHTML = '
修改前(左)/修改后(右)';
diffContainer.appendChild(header);
// 对比内容区域
const content = document.createElement('div');
content.className = 'preview-diff-content';
const ori = originalText.split('\n'), neu = newText.split('\n');
const lines = diffLines(ori, neu);
lines.forEach((pair, idx) => {
const line = document.createElement('div');
line.className = 'preview-diff-line';
// 行号
const lineNumber = document.createElement('div');
lineNumber.className = 'preview-diff-line-number';
lineNumber.textContent = idx + 1;
line.appendChild(lineNumber);
// 旧内容
const oldContent = document.createElement('div');
oldContent.className = 'preview-diff-old';
// 新内容
const newContent = document.createElement('div');
newContent.className = 'preview-diff-new';
if (pair.old === pair.new) {
// 完全相同的行,不需要高亮
oldContent.innerHTML = escapeHtml(pair.old);
newContent.innerHTML = escapeHtml(pair.new);
} else if (pair.old === '' || pair.new === '') {
// 纯增加或纯删除的行,整行高亮
oldContent.innerHTML = pair.old ? `
${escapeHtml(pair.old)}` : ' ';
newContent.innerHTML = pair.new ? `
${escapeHtml(pair.new)}` : ' ';
} else {
// 修改的行 - 精确高亮差异部分
const diffs = highlightCharDifferences(pair.old, pair.new);
oldContent.innerHTML = diffs.old;
newContent.innerHTML = diffs.new;
}
line.appendChild(oldContent);
line.appendChild(newContent);
content.appendChild(line);
});
diffContainer.appendChild(content);
section.appendChild(diffContainer);
return section;
}
// 简单的 HTML 转义函数
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 检查是否已存在预览界面
function isPreviewActive() {
return document.querySelector('.preview-overlay') !== null;
}
// 关闭预览界面
function closePreview() {
const overlay = document.querySelector('.preview-overlay');
if (overlay) {
document.body.removeChild(overlay);
}
}
// 显示预览界面
function showPreview(originalButton) {
// 如果已经有预览界面,先关闭
if (isPreviewActive()) {
closePreview();
}
// 获取表单数据
const formData = collectFormData();
// 创建预览覆盖层
const overlay = document.createElement('div');
overlay.className = 'preview-overlay';
// 创建预览容器
const container = document.createElement('div');
container.className = 'preview-container';
// 添加关闭按钮
const closeButton = document.createElement('div');
closeButton.className = 'preview-close';
closeButton.textContent = '×';
closeButton.onclick = function() {
closePreview();
};
// 创建预览标题
const header = document.createElement('div');
header.className = 'preview-header';
header.textContent = '预览对比';
// 创建预览内容
const content = document.createElement('div');
content.className = 'preview-content';
// 字段映射表
const fieldNames = {
title: '标题',
infobox: '条目信息',
summary: '简介',
tags: '标签',
editSummary: '编辑摘要',
infobox_normal: '入门模式编辑内容'
};
// 添加所有字段的对比预览
let hasContent = false;
for (const key in formData) {
if (formData[key] && formData[key].current !== formData[key].original) {
const fieldTitle = fieldNames[key] || key;
content.appendChild(createDiffView(fieldTitle, formData[key].original, formData[key].current));
hasContent = true;
}
}
// 如果没有变化的内容,显示提示
if (!hasContent) {
const noChanges = document.createElement('div');
noChanges.style.textAlign = 'center';
noChanges.style.padding = '20px';
noChanges.textContent = '没有检测到内容变化';
content.appendChild(noChanges);
}
// 添加按钮
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'preview-buttons';
const cancelButton = document.createElement('button');
cancelButton.className = 'preview-button preview-button-cancel';
cancelButton.textContent = '取消';
cancelButton.onclick = function() {
closePreview();
};
const confirmButton = document.createElement('button');
confirmButton.className = 'preview-button preview-button-confirm';
confirmButton.textContent = '确认';
// 保存表单引用和按钮本身,用于提交操作
const form = originalButton.form;
confirmButton.onclick = function() {
closePreview();
// 修复:禁用预览功能,确保表单直接提交
previewDisabled = true;
// 确保我们处于正确的编辑模式并提交表单
const wikiModeLink = document.querySelector('a.l[onclick="NormaltoWCODE()"]');
if (wikiModeLink) {
// 在入门模式下,先切换到Wiki模式再提交
NormaltoWCODE();
setTimeout(() => {
// 直接通过form引用提交表单
if (form) {
// 触发表单提交
form.submit();
} else {
// 备用方案:直接触发原始按钮的点击事件
originalButton.click();
}
}, 200);
} else {
// 已经在Wiki模式,直接提交
if (form) {
form.submit();
} else {
// 备用方案:尝试点击原始按钮
originalButton.click();
}
}
};
buttonsContainer.appendChild(cancelButton);
buttonsContainer.appendChild(confirmButton);
// 组装预览界面
container.appendChild(closeButton);
container.appendChild(header);
container.appendChild(content);
container.appendChild(buttonsContainer);
overlay.appendChild(container);
// 添加到body
document.body.appendChild(overlay);
}
// 捕获入门模式编辑器的原始内容
function captureOriginalEditorContent() {
// 当页面加载完成后,尝试捕获编辑器的原始内容
setTimeout(() => {
const normalModeEditor = document.querySelector('#editTopicContent_ifr');
if (normalModeEditor && normalModeEditor.contentWindow) {
try {
const editorBody = normalModeEditor.contentWindow.document.body;
if (editorBody) {
window.originalEditorContent = editorBody.innerHTML;
}
} catch (e) {
console.error("Failed to capture original editor content:", e);
}
}
}, 1000); // 等待编辑器初始化完成
}
// 修复按钮消失问题
function fixButtonDisappearing() {
// 监听DOM变化,确保按钮不会消失
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// 如果某个元素被移除,检查是否需要重新拦截按钮
if (mutation.removedNodes.length > 0) {
// 检查是否有提交按钮被移除
Array.from(mutation.removedNodes).forEach(node => {
if (node.nodeType === 1 &&
(node.matches && node.matches('input.inputBtn[value="提交"]') ||
node.querySelector && node.querySelector('input.inputBtn[value="提交"]'))) {
// 重新拦截按钮
setTimeout(interceptSubmitButtons, 0);
}
});
}
});
});
// 配置观察选项
const config = { childList: true, subtree: true };
// 开始观察整个文档
observer.observe(document.body, config);
}
// 等待页面加载完成
window.addEventListener('load', function() {
// 捕获入门模式编辑器的原始内容
captureOriginalEditorContent();
// 拦截提交按钮
interceptSubmitButtons();
// 防止按钮消失
fixButtonDisappearing();
// 阻止Enter键提交
preventEnterSubmit();
});
}
/*===========
启动所有功能
============*/
function startEnhancer() {
initNavButtons();
observeURLChanges();
initCoverUpload();
initBatchRelation();
initBgmDropdownMenu();
initBgmCreateSubject();
initBgmPreview();
console.log("Bangumi Ultimate Enhancer 已启动");
}
// 在DOM加载完成后启动脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startEnhancer);
} else {
startEnhancer();
}
})();