// ==UserScript== // @name 目录内搜索添加条目/可加入页面和目录页加入同时修改评价和排序 // @namespace https://bgm.tv/group/topic/409246 // @version 0.6.3 // @description 为 bangumi 增加在目录内搜索条目并添加的功能,添加无需刷新 // @author mmm // @match http*://bgm.tv/index/* // @match http*://chii.in/index/* // @match http*://bangumi.tv/index/* // @match http*://bgm.tv/subject/* // @match http*://chii.in/subject/* // @match http*://bangumi.tv/subject/* // @match http*://bgm.tv/character/* // @match http*://chii.in/character/* // @match http*://bangumi.tv/character/* // @match http*://bgm.tv/person/* // @match http*://chii.in/person/* // @match http*://bangumi.tv/person/* // @match http*://bgm.tv/ep/* // @match http*://chii.in/ep/* // @match http*://bangumi.tv/ep/* // @match http*://bgm.tv/subject/topic/* // @match http*://chii.in/subject/topic/* // @match http*://bangumi.tv/subject/topic/* // @match http*://bgm.tv/group/topic/* // @match http*://chii.in/group/topic/* // @match http*://bangumi.tv/group/topic/* // @match http*://bgm.tv/blog/* // @match http*://chii.in/blog/* // @match http*://bangumi.tv/blog/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bgm.tv // @grant none // @license MIT // @gf https://greasyfork.org/zh-CN/scripts/516479 // @gadget https://bgm.tv/dev/app/3372 // @downloadURL https://update.greasyfork.icu/scripts/516479/%E7%9B%AE%E5%BD%95%E5%86%85%E6%90%9C%E7%B4%A2%E6%B7%BB%E5%8A%A0%E6%9D%A1%E7%9B%AE%E5%8F%AF%E5%8A%A0%E5%85%A5%E9%A1%B5%E9%9D%A2%E5%92%8C%E7%9B%AE%E5%BD%95%E9%A1%B5%E5%8A%A0%E5%85%A5%E5%90%8C%E6%97%B6%E4%BF%AE%E6%94%B9%E8%AF%84%E4%BB%B7%E5%92%8C%E6%8E%92%E5%BA%8F.user.js // @updateURL https://update.greasyfork.icu/scripts/516479/%E7%9B%AE%E5%BD%95%E5%86%85%E6%90%9C%E7%B4%A2%E6%B7%BB%E5%8A%A0%E6%9D%A1%E7%9B%AE%E5%8F%AF%E5%8A%A0%E5%85%A5%E9%A1%B5%E9%9D%A2%E5%92%8C%E7%9B%AE%E5%BD%95%E9%A1%B5%E5%8A%A0%E5%85%A5%E5%90%8C%E6%97%B6%E4%BF%AE%E6%94%B9%E8%AF%84%E4%BB%B7%E5%92%8C%E6%8E%92%E5%BA%8F.meta.js // ==/UserScript== (function () { 'use strict'; // #region 样式 const style = document.createElement('style'); style.textContent = /* css */` ul.ajaxSubjectList li { ul.prg_list li { border-bottom: none; border-top: none; padding: 0; } &:hover ul.prg_list li a { color: #06C; } a.avatar { transition: 0ms; } } #indexSelectorWrapper { display: flex; align-items: center; gap: 4px; margin-bottom: 10px; position: relative; } #indexSelector { font-size: 15px; padding: 5px 5px; line-height: 22px; flex: 1; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box; background-color: #fff; color: #000; border: 1px solid #d9d9d9; } html[data-theme="dark"] #indexSelector { background-color: #303132; color: #e0e0e1; border: 1px solid #5c5c5c; } #TB_ajaxContent { scrollbar-gutter: stable; } /* 新建目录表单样式 */ #createIndexForm { margin: 10px 0; padding: 15px; border: 1px dashed #d9d9d9; border-radius: 5px; } #createIndexForm .form-group { margin-bottom: 15px; display: flex; flex-direction: column; gap: 5px; } #createIndexDesc { height: 60px; resize: vertical; } #toggleCreateFormBtn { word-break: keep-all; padding: 8px 16px; cursor: pointer; } /* 搜索选择器样式 */ html[data-theme="dark"] #indexSelectorWrapper { .dropdown-icon::before, .dropdown-icon::after { background-color: #aaa; } .dropdown-menu { background: rgba(80, 80, 80, 0.7); color: rgba(255, 255, 255, .7); } .search-box { border-bottom-color: #444; } .search-box input { background-color: #202122; color: #e0e0e0; border-color: #5c5c5c; } } #indexSelectorWrapper { .custom-select { width: 100%; position: relative; } .select-input { cursor: pointer; } .dropdown-icon { position: absolute; right: 12px; top: 50%; width: 10px; height: 10px; transform: translateY(-50%); pointer-events: none; } .dropdown-icon::before, .dropdown-icon::after { content: ''; position: absolute; width: 6px; height: 2px; background-color: #666; border-radius: 1px; transition: background-color 0.2s; } .dropdown-icon::before { transform: rotate(45deg); left: 0; bottom: 4px; } .dropdown-icon::after { transform: rotate(-45deg); right: 0; bottom: 4px; } .dropdown-icon.open { transform: translateY(-50%) rotate(180deg); } .dropdown-menu { position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; scrollbar-width: thin; border-top: none; border-radius: 0 0 5px 5px; z-index: 100; display: none; background-color: rgba(254, 254, 254, 0.9); box-shadow: inset 0 1px 1px hsla(0, 0%, 100%, 0.3), inset 0 -1px 0 hsla(0, 0%, 100%, 0.1), 0 2px 4px hsla(0, 0%, 0%, 0.2); backdrop-filter: blur(5px); color: rgba(0, 0, 0, .7); } .dropdown-menu.show { display: block; } .search-box { padding: 8px; border-bottom: 1px solid #eee; } .search-box input { width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; font-size: 15px; } .option-list { list-style: none; margin: 0; padding: 0; } .option-item { padding: 8px 10px; cursor: pointer; font-size: 15px; } .option-item:hover { background-color: #e9f5ff; color: #007bff; } html[data-theme="dark"] .option-item:hover { background-color: #2d3b4d; color: #8ab4f8; } .option-item.selected { background-color: #369cf8; color: #fff; } .no-result { padding: 10px; text-align: center; color: #999; font-size: 15px; } .hidden-field { display: none; } } .search-results-container { margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } html[data-theme="dark"] .search-results-container { border-color: #444; } :not(.prg_list) > li.selected-result, .prg_list li.selected-result a { background-color: var(--primary-color); color: white !important; a, .tip, .grey { color: white !important; } } .custom-search-wrapper { width: fit-content; margin: auto; border-radius: 100px; box-shadow: none; border: 1px solid rgba(200, 200, 200, 0.5); background-color: rgba(255, 255, 255, 0.2); } input[type="text"].custom-search-input { font-size: 1em; width: 120px; -webkit-appearance: none; -moz-appearance: none; box-shadow: none; background: transparent !important; line-height: 20px; border: none; padding: 4px 8px; box-shadow: none; } .custom-search-select { font-size: 1em; padding: 4px 4px 4px 5px; width: fit-content; border: none; outline: none; box-shadow: none; background-color: transparent; background-image: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; border-radius: 0; border-right: 1px solid rgba(200, 200, 200, 0.5); text-align: center; } .custom-search-btn { text-wrap: nowrap; width: fit-content; height: fit-content; border: none; border-left: 1px solid rgba(200, 200, 200, 0.5); padding: 4px 5px; cursor: pointer; background: transparent; } `; document.head.append(style); // #endregion // #region 请求函数 const createFetch = method => async (url, body, serializer = body => JSON.stringify(body)) => { const options = method === 'POST' ? { method, body: serializer(body) } : { method }; const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const text = await response.text(); try { return JSON.parse(text); } catch { return text; } }; const fetchGet = createFetch('GET'); const fetchPost = createFetch('POST'); const postSearch = async (cat, keyword, { filter = {}, offset = 0 }) => { const url = `https://api.bgm.tv/v0/search/${cat}?limit=10&offset=${offset}`; const body = { keyword, filter }; const result = await fetchPost(url, body); return result.data; }; const searchSubject = async (keyword, { type = '', start = 0 }) => { // 旧API结果为空时发生CORS错误,但新API搜索结果不准确,仍用旧API const url = `https://api.bgm.tv/search/subject/${encodeURIComponent(keyword)}?type=${type}&max_results=10&start=${start}`; const result = await fetchGet(url); return result.list; }; // const searchSubject = (keyword, type) => postSearch('subjects', keyword, { type: [+type].filter(a => a) }); const searchPrsn = postSearch.bind(null, 'persons'); const searchCrt = postSearch.bind(null, 'characters'); const getSearchMethod = { 'subject': [searchSubject, 'start'], 'person': [searchPrsn, 'offset'], 'character': [searchCrt, 'offset'], 'ep': [searchSubject, 'start'], }; const getEps = async (subject_id) => { const url = `https://api.bgm.tv/v0/episodes?subject_id=${subject_id}`; const result = await fetchGet(url); return result.data; }; const myUsername = document.querySelector('#dock a').href.split('/').pop(); let formhash; const getFormhash = async () => { if (!formhash) { // 非目录页且过去未创建过目录时 const html = await fetchGet('/index/create'); const doc = new DOMParser().parseFromString(html, 'text/html'); formhash = doc.querySelector('input[name="formhash"]').value; } return formhash; } const getDoc = (html) => new DOMParser().parseFromString(html, 'text/html'); const getIndices = async (forceRefresh = false) => { const cache = JSON.parse(sessionStorage.getItem('user_indices') || 'null'); if (!forceRefresh && cache?.formhash) { formhash = cache.formhash; return cache.data; } const allIndices = []; let currentUrl = `/user/${myUsername}/index?add_related=1`; try { while (currentUrl) { const html = await fetchGet(currentUrl); const doc = getDoc(html); const indexLinks = [...doc.querySelectorAll('#timeline ul a')]; const currentPageIndices = indexLinks.map(a => ({ title: a.textContent.trim(), id: a.href.split('/')[4] })); allIndices.push(...currentPageIndices); formhash ||= new URLSearchParams(indexLinks[0]?.href).get('gh'); const nextPageLink = doc.querySelector('.page_inner a:nth-last-child(1)'); if (nextPageLink) { currentUrl = nextPageLink.href; } else { currentUrl = null; } } sessionStorage.setItem('user_indices', JSON.stringify({ ...(formhash ? { formhash } : {}), data: allIndices })); return allIndices; } catch (e) { console.error('获取目录失败:', e); if (allIndices.length) { return allIndices; } throw e; } } const addItem = async (add_related, indexId) => { const url = `/index/${indexId}/add_related`; const body = { formhash: await getFormhash(), add_related, submit: '添加' }; const result = await fetchPost(url, body, body => new URLSearchParams(body)); return result; }; const modifyItem = async (id, content, order) => { const url = `/index/related/${id}/modify`; const body = { formhash: await getFormhash(), content, order, submit: '提交' }; const result = await fetchPost(url, body, body => new URLSearchParams(body)); return result; }; const addAndModify = async (cat, subjectId, indexId, content, order, idxTitle = '') => { const add_related = `/${cat}/${subjectId}`; const ukagaka = document.querySelector('#robot'); ukagaka.style.zIndex = '103'; chiiLib.ukagaka.presentSpeech('添加中,请稍候...'); try { const addedHTML = await addItem(add_related, indexId); const parser = new DOMParser(); const query = `[href="/${cat}/${subjectId}"]`; const getAdded = dom => dom.querySelector(query)?.closest('[id^="item_"], [attr-index-related]'); const addedDOM = parser.parseFromString(addedHTML, 'text/html'); let added = getAdded(addedDOM); if (!added) throw Error('添加失败'); let modifyFailed = false; if (content || !isNaN(order)) { try { const rlt = added.querySelector('a.tb_idx_rlt'); const rlt_id = rlt.id.split('_')[1]; await modifyItem(rlt_id, content, order); } catch (e) { modifyFailed = true; console.error('修改失败:', e); } } const toIdxAnchor = ` 点击查看`; const successTip = idxTitle ? `已收集至目录「${idxTitle}」~${toIdxAnchor}` : '添加成功!'; const modifyFailedTip = `添加成功,但修改失败了T T${idxTitle ? toIdxAnchor : ''}` chiiLib.ukagaka.presentSpeech(modifyFailed ? modifyFailedTip : successTip, true); return added; } catch (e) { console.error(e); chiiLib.ukagaka.presentSpeech('添加失败了T T', true); } finally { setTimeout(() => ukagaka.style.zIndex = '90', 3500); } }; const createIndex = async (title, desc) => { await fetchPost('/index/create', { formhash: await getFormhash(), title: title.trim(), desc: desc.trim(), submit: '创建目录' }, body => new URLSearchParams(body)); }; // #endregion // #region 目录页 if (location.pathname.startsWith('/index/')) { formhash = document.querySelector('input[name="formhash"]')?.value; const addBtn = document.querySelector('a.add.primary'); if (!formhash || !addBtn) return; addBtn.href = '#TB_inline?tb&height=300&width=450&inlineId=newIndexRelated'; const indexId = location.pathname.split('/')[2]; const boxes = document.querySelectorAll('.newIndexSection'); boxes.forEach((box) => { const boxNum = box.id.split('_')[1]; let cat = ['subject', 'character', 'person', 'ep', 'blog', 'group/topic', 'subject/topic'][boxNum]; const input = box.querySelector('.inputtext'); input.style.position = 'sticky'; input.style.top = 0; input.style.zIndex = 2; if (boxNum < 4) { // 'subject', 'character', 'person', 'ep' // 找到原始提交按钮 const submitBtn = box.querySelector('#submitBtnO'); if (!submitBtn) return; // 创建搜索框容器并添加到提交按钮右侧 const searchWrapper = document.createElement('div'); searchWrapper.className = 'custom-search-wrapper'; submitBtn.append(searchWrapper); // 创建搜索结果容器 const result = document.createElement('div'); result.classList.add('subjectListWrapper', 'search-results-container'); result.style.display = 'none'; // 默认隐藏 submitBtn.after(result); // 为subject类型添加分类选择器 let typeSelect = null; if (cat === 'subject') { typeSelect = document.createElement('select'); typeSelect.className = 'custom-search-select'; typeSelect.innerHTML = ` `; searchWrapper.appendChild(typeSelect); } // 创建搜索输入框 const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = 'custom-search-input'; searchInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); result.style.display = 'block'; searchAndRender(cat, searchInput, result, input, false, typeSelect?.value); } }); // 创建搜索按钮 const searchBtn = document.createElement('button'); searchBtn.className = 'custom-search-btn'; searchBtn.textContent = '🔍'; searchBtn.addEventListener('click', (event) => { event.preventDefault(); result.style.display = 'block'; searchAndRender(cat, searchInput, result, input, false, typeSelect?.value); }); // 组装搜索框 - 添加必要的布局样式 searchWrapper.style.display = 'inline-flex'; searchWrapper.style.alignItems = 'center'; searchWrapper.appendChild(searchInput); searchWrapper.appendChild(searchBtn); } const contentTextarea = document.createElement('textarea'); contentTextarea.className = 'reply'; contentTextarea.style.resize = 'vertical'; const orderInput = document.createElement('input'); orderInput.type = 'text'; orderInput.className = 'inputtext'; input.after(makeTip('评价:'), document.createElement('br'), contentTextarea, document.createElement('br'), makeTip('排序:'), document.createElement('br'), orderInput); const newRelatedForm = box.querySelector('#newIndexRelatedForm'); newRelatedForm.addEventListener('submit', (e) => { e.preventDefault(); const ukagaka = document.querySelector('#robot'); ukagaka.style.zIndex = '103'; chiiLib.ukagaka.presentSpeech('添加中,请稍候...'); const v = input.value.trim(); let subjectId; try { const res = getCatAndId(input.value); cat = res.cat; subjectId = res.id; } catch { const add_related = input.value.match(/^\d+$/) ? `/${cat}/${v}` : v; subjectId = add_related.split('/').pop(); } addAndModify(cat, subjectId, indexId, contentTextarea.value.trim(), parseInt(orderInput.value)).then(added => { const neibourSelector = added.id ? candidate => `#${candidate.id}` : candidate => `[attr-index-related="${candidate.getAttribute('attr-index-related')}"]`; const modifyBtn = added.querySelector('a.tb_idx_rlt'); const eraseBtn = added.querySelector('a.erase_idx_rlt'); const previousAnchor = added.previousElementSibling; const nextAnchor = added.nextElementSibling; if (previousAnchor) { document.querySelector(neibourSelector(previousAnchor)).after(added); } else if (nextAnchor) { document.querySelector(neibourSelector(nextAnchor)).before(added); } else { const parent = added.parentElement; const sameParent = parent.id ? document.querySelector(`#${parent.id}`) : null; if (sameParent) { sameParent.append(added); } else { const header = parent.previousElementSibling; if (header.tagName === 'H2') { const line = document.createElement('div'); line.className = 'section_line no_border'; document.querySelector('#columnSubjectBrowserA').append(line, header, parent); } else { // subject const segmentBar = document.querySelector('.segment-container'); segmentBar.after(parent); } } } // 激活修改功能 tb_init(modifyBtn); // from chiiLib.user_index.manage /* eslint-disable */ $(modifyBtn).click(function () { var $rlt_id = $(this).attr('id').split('_')[1], $order = $(this).attr('order'), $content = $(this).parent().parent().find('div.text').text().trim(); $('#ModifyRelatedForm').attr('action', '/index/related/' + $rlt_id + '/modify'); $('#modify_order').attr('value', $order); $('#modify_content').attr('value', $content); return false; }); $(eraseBtn).click(function () { if (confirm('确认删除该关联条目?')) { var tml_id = $(this).attr('id').split('_')[1]; chiiLib.ukagaka.presentSpeech(' 请稍候,正在删除关联条目...'); $.ajax({ type: "GET", url: this + '&ajax=1', success: function (html) { $('[attr-index-related="' + tml_id + '"]').fadeOut(500); chiiLib.ukagaka.presentSpeech('你选择的关联条目已经删除咯~', true); }, error: function (html) { chiiLib.ukagaka.presentSpeech(AJAXtip['error'], true); } }); } return false; }); /* eslint-enable */ added.scrollIntoView({ behavior: 'smooth' }); added.style.boxShadow = '0 0 8px #0084b4'; added.style.position = 'relative'; // subject 以外 added.style.zIndex = '2'; // subject setTimeout(() => { added.style.boxShadow = ''; added.style.position = ''; added.style.zIndex = ''; }, 3500); }); }); }); // #region 兼容“目录批量添加与编辑” monitorElement('.bibeBox', bibeBox => { const container = document.createElement('div'); container.style = `display: flex; justify-content: space-evenly; height: 300px; padding: 5px; overflow-y: auto;`; const textarea = bibeBox.querySelector('textarea'); textarea.rows = 8; bibeBox.previousSibling.after(container); bibeBox.parentNode.style.marginTop = '-150px'; const submitWrapper = document.createElement('div'); submitWrapper.style.width = '50%'; submitWrapper.append(bibeBox, document.querySelector('#submit_list')); const searchPanel = document.createElement('div'); searchPanel.style = 'width: 50%' const inputWrapper = document.createElement('div'); inputWrapper.className = 'custom-search-wrapper'; const input = document.createElement('input'); input.type = 'text'; input.className = 'custom-search-input'; input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); newSearchAndRender(); } }); const result = document.createElement('div'); result.classList.add('subjectListWrapper', 'custom-result-list'); const select = document.createElement('select'); select.onchange = newSearchAndRender; select.className = 'custom-search-select'; select.innerHTML = ` `; const btn = document.createElement('button'); btn.className = 'custom-search-btn'; btn.textContent = '🔍'; btn.addEventListener('click', (event) => { event.preventDefault(); newSearchAndRender(); }); searchPanel.append(inputWrapper, result); inputWrapper.append(select, input, btn); container.append(submitWrapper, searchPanel); function newSearchAndRender() { const cat = select.value; searchAndRender(cat, input, result, bibeBox.querySelector('textarea'), true); } }); // #endregion } // #endregion // #region 条目/角色/人物/章节页修改加入目录按钮 if (location.pathname.match(/^\/(subject|character|person|ep|(group|subject)\/topic|blog)\/\d+/)) { const relateLinks = document.querySelectorAll('[href*="add_related="]'); if (!relateLinks.length) return; for (const relateLink of relateLinks) { relateLink.href = '#TB_inline?tb&height=300&width=500&inlineId=newIndexRelated'; relateLink.title = '加入我的目录'; tb_init(relateLink); relateLink.addEventListener('click', async () => { const tbContent = document.getElementById('TB_ajaxContent'); if (!tbContent) return; const { cat, id: subjectId } = getCatAndId(location.href); tbContent.innerHTML = `
选择目录:
新建
评价:
排序:
`; const selectorInstance = createSearchableSelect(); selectorInstance.init(); try { const indices = await getIndices(); if (indices.length) { selectorInstance.updateOptions(indices.map(idx => ({ value: idx.id, text: idx.title }))); } else { document.querySelector('.select-input').placeholder = '未找到目录'; } } catch (e) { console.error(e); document.querySelector('.select-input').placeholder = '获取目录失败,请刷新重试'; } // 新建目录表单显示/隐藏切换 const toggleBtn = document.getElementById('toggleCreateFormBtn'); const createForm = document.getElementById('createIndexForm'); toggleBtn.addEventListener('click', () => { const isVisible = createForm.style.display !== 'none'; createForm.style.display = isVisible ? 'none' : 'block'; toggleBtn.textContent = isVisible ? '新建' : '收起'; }); // 创建目录功能 const createBtn = document.getElementById('createIndexBtn'); const titleInput = document.getElementById('createIndexTitle'); const descInput = document.getElementById('createIndexDesc'); createBtn.addEventListener('click', async () => { const title = titleInput.value.trim(); const desc = descInput.value.trim(); if (!title) { chiiLib.ukagaka.presentSpeech('请输入目录标题', true); return; } if (!desc) { chiiLib.ukagaka.presentSpeech('请输入目录描述', true); return; } const ukagaka = document.querySelector('#robot'); ukagaka.style.zIndex = '103'; chiiLib.ukagaka.presentSpeech('创建目录中...'); try { await createIndex(title, desc); const indices = await getIndices(true); // 更新选择器选项 selectorInstance.updateOptions(indices.map(idx => ({ value: idx.id, text: idx.title }))); // 选中新创建的目录 const newIndex = indices.find(idx => idx.title === title); if (newIndex) { document.querySelector('.select-input').value = newIndex.title; document.getElementById('selectedDirectory').value = newIndex.id; } else { throw new Error('无法确认是否创建成功,请刷新再试') } chiiLib.ukagaka.presentSpeech('目录创建成功!', true); createForm.style.display = 'none'; toggleBtn.textContent = '新建'; } catch (e) { console.error(e); chiiLib.ukagaka.presentSpeech(`创建失败: ${e.message}`, true); } finally { setTimeout(() => ukagaka.style.zIndex = '90', 3500); } }); // 绑定提交功能 const submitBtn = document.getElementById('submitAddBtn'); const commentInput = document.getElementById('commentInput'); const orderInput = document.getElementById('orderInput'); submitBtn.addEventListener('click', (e) => { e.preventDefault(); const selectedIndexId = document.getElementById('selectedDirectory').value; if (!selectedIndexId) { chiiLib.ukagaka.presentSpeech('请选择目录', true); return; } addAndModify(cat, subjectId, selectedIndexId, commentInput.value.trim(), parseInt(orderInput.value), document.querySelector('.select-input').value).then(tb_remove); }); }); } } // #endregion // #region 搜索选择器功能 function createSearchableSelect() { // 私有变量 let selectContainer, selectInput, dropdownIcon, dropdownMenu, searchBox, optionList, hiddenField; // 私有方法 function openDropdown() { dropdownMenu.classList.add('show'); dropdownIcon.classList.add('open'); searchBox.focus(); searchBox.value = ''; // 显示所有选项 const options = optionList.querySelectorAll('.option-item'); options.forEach(item => item.style.display = 'block'); // 移除无结果提示 const noResultEl = optionList.querySelector('.no-result'); if (noResultEl) optionList.removeChild(noResultEl); } function closeDropdown() { dropdownMenu.classList.remove('show'); dropdownIcon.classList.remove('open'); } function toggleDropdown() { if (dropdownMenu.classList.contains('show')) { closeDropdown(); } else { openDropdown(); } } // 搜索功能 function handleSearch(e) { const searchTerm = e.target.value.toLowerCase().trim(); let hasResults = false; // 清除之前的无结果提示 const noResultEl = optionList.querySelector('.no-result'); if (noResultEl) { optionList.removeChild(noResultEl); } // 筛选选项 const options = optionList.querySelectorAll('.option-item'); options.forEach(item => { const text = item.textContent.toLowerCase(); const isMatch = text.includes(searchTerm); item.style.display = isMatch ? 'block' : 'none'; if (isMatch) hasResults = true; }); // 显示无结果提示 if (!hasResults && options.length) { const noResult = document.createElement('li'); noResult.className = 'no-result'; noResult.textContent = '没有找到匹配的目录'; optionList.appendChild(noResult); } } // 公共方法 return { init() { // 获取DOM元素 selectContainer = document.querySelector('.custom-select'); selectInput = selectContainer.querySelector('.select-input'); dropdownIcon = selectContainer.querySelector('.dropdown-icon'); dropdownMenu = selectContainer.querySelector('.dropdown-menu'); searchBox = selectContainer.querySelector('.search-box input'); optionList = selectContainer.querySelector('.option-list'); hiddenField = selectContainer.querySelector('.hidden-field'); // 绑定事件 selectInput.addEventListener('click', toggleDropdown); dropdownIcon.addEventListener('click', toggleDropdown); // 点击外部关闭下拉菜单 document.addEventListener('click', (e) => { if (!selectContainer.contains(e.target)) { closeDropdown(); } }); // 搜索功能 searchBox.addEventListener('input', handleSearch); // 键盘导航 selectInput.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); openDropdown(); searchBox.focus(); } }); }, updateOptions(options) { // 清空现有选项 optionList.innerHTML = ''; // 添加新选项 options.forEach(option => { const li = document.createElement('li'); li.className = 'option-item'; li.setAttribute('data-value', option.value); li.textContent = option.text; li.addEventListener('click', () => { selectInput.value = option.text; hiddenField.value = option.value; // 更新选中状态 document.querySelectorAll('.option-item').forEach(i => i.classList.remove('selected') ); li.classList.add('selected'); closeDropdown(); }); optionList.appendChild(li); }); // 设置第一个选项为默认选中 if (options.length) { const firstOption = optionList.querySelector('.option-item'); if (firstOption) { firstOption.classList.add('selected'); selectInput.value = firstOption.textContent; hiddenField.value = firstOption.getAttribute('data-value'); } } } }; } // #endregion // #region 工具函数 function makeTip(text) { const tip = document.createElement('span'); tip.classList.add('tip'); tip.textContent = text; return tip; }; const makeLoading = (prompt = '搜索中……') => document.createTextNode(prompt); async function searchAndRender(cat, input, result, target = input, append = false, type = '') { const [method, key] = getSearchMethod[cat]; const keyword = input.value.trim(); if (keyword === '') return; // 对于subject类型,传递分类参数 const loader = (offset) => method(keyword, { [key]: offset, ...(cat === 'subject' ? { type } : {}) }); const clickHandler = e => { e.preventDefault(); if (target.tagName === 'INPUT') { document.querySelectorAll('.ajaxSubjectList li.selected-result').forEach(el => { el.classList.remove('selected-result'); }); const liElement = e.currentTarget.closest('li'); if (liElement) { liElement.classList.add('selected-result'); } } if (cat === 'ep') { renderEps(e.currentTarget, target, append); } else { if (append) { target.value += e.currentTarget.href + '\n'; } else { target.value = e.currentTarget.href; } } }; renderList(loader, result, cat, a => a.addEventListener('click', clickHandler)); } const listHTML = (list, cat = 'subject') => { const isEp = cat === 'ep'; if (isEp) cat = 'subject'; return list.reduce((m, { id, type, images, name, name_cn, career, infobox }) => { if (isEp && ![2, 6].includes(type)) return m; name_cn ??= infobox?.find(({ key }) => key === '简体中文名')?.value; if (cat !== 'subject') cat = career ? 'person' : 'character'; type = cat === 'subject' ? ['书籍', '动画', '音乐', '游戏', '', '三次元'][type - 1] : null; const grid = cat === 'subject' ? images?.grid : images?.grid.replace('/g/', '/s/'); const exist = v => v ? v : ''; m += `
  • ${grid ? `` : ''}
    ${exist(type)}

    ${name}

    ${exist(name_cn)}
  • `; return m; }, ''); } const makeLiTip = (text = '') => { const more = document.createElement('li'); more.classList.add('clearit'); more.textContent = text; more.style.textAlign = 'center'; more.style.listStyle = 'none'; return more; } const makeMoreBtn = (ul, cat, loader, applyHandler, initStart = 1) => { const searching = makeLoading(); const more = makeLiTip(); const a = document.createElement('a'); a.textContent = '加载更多'; a.href = 'javascript:;'; a.style.display = 'block'; more.append(a); more.start = initStart; a.addEventListener('click', async (e) => { e.preventDefault(); more.before(searching); const nextList = await loader(more.start); if (!nextList) { searching.remove(); return; } ul.insertAdjacentHTML('beforeend', listHTML(nextList, cat)); applyHandler(); searching.remove(); if (nextList.length < 10 && !['subject', 'ep'].includes(cat)) { more.replaceWith(makeLiTip('没有啦')); return; } more.start += nextList.length; }); return more; } async function renderList(loader, container, cat, handler = () => { }) { const applyHandler = () => ul.querySelectorAll('a').forEach(handler); const searching = makeLoading(); let initStart = 1; container.innerHTML = ''; container.append(searching); let firstList = await loader(); if (firstListEnd()) return; let firstHTML = listHTML(firstList, cat); while (firstHTML === '' && cat === 'ep') { firstList = await loader(initStart += firstList.length); if (firstListEnd()) return; firstHTML = listHTML(firstList, cat); } const ul = document.createElement('ul'); ul.id = 'subjectList'; ul.classList.add('subjectList', 'ajaxSubjectList'); ul.innerHTML = firstHTML; initStart += firstList.length; const more = firstList.length === 10 || ['subject', 'ep'].includes(cat) ? makeMoreBtn(ul, cat, loader, applyHandler, initStart) : makeLiTip('没有啦'); container.append(ul, more); applyHandler(); searching.remove(); function firstListEnd() { if (!firstList) { container.textContent = '搜索失败'; return true; } else if (firstList.length === 0) { container.textContent = '未找到相关条目'; return true; } } } async function renderEps(elem, target, append) { const parent = elem.closest('li').querySelector('.inner'); const fetching = makeLoading('获取中……'); parent.append(fetching); const eps = await getEps(elem.href.split('/').pop()); const epsByType = Object.groupBy?.(eps, ({ type }) => ['0', 'SP', 'OP', 'ED'][type]) ?? eps.reduce((acc, ep) => { const type = ['0', 'SP', 'OP', 'ED'][ep.type]; if (!acc[type]) acc[type] = []; acc[type].push(ep); return acc; }, {}); fetching.remove(); if (!eps) { parent.append('获取失败'); return; } const ul = document.createElement('ul'); ul.className = 'prg_list clearit'; Object.entries(epsByType).forEach(([type, eps]) => { if (type !== '0') { const subtitle = document.createElement('li'); subtitle.className = 'subtitle'; const span = document.createElement('span'); span.textContent = type; subtitle.append(span); ul.append(subtitle); } eps.map(({ id, name, sort }) => { const li = document.createElement('li'); const a = document.createElement('a'); a.href = `/ep/${id}`; a.className = 'load-epinfo epBtnAir'; a.title = name; a.textContent = String(sort).padStart(2, '0'); li.addEventListener('click', e => { e.preventDefault(); // 移除之前所有选中项的高亮 document.querySelectorAll('.ajaxSubjectList li.selected-result').forEach(el => { el.classList.remove('selected-result'); }); // 为当前选中项添加高亮 const topLi = li.closest('.ajaxSubjectList li'); if (topLi) { topLi.classList.add('selected-result'); } if (append) { target.value += a.href + '\n'; } else { target.value = a.href; } }); li.append(a); ul.append(li); }); }); parent.append(ul); } function getCatAndId(href) { const url = new URL(href); const pathname = url.pathname; const parts = pathname.split('/'); const idIdx = parts.findIndex(part => part && part == +part); const id = parts[idIdx]; const cat = parts.slice(1, idIdx).join('/'); return { cat, id }; } function monitorElement(selector, callback) { const targetNode = document.body; const config = { childList: true, subtree: true }; const observer = new MutationObserver((mutationsList, observer) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(node => { if (node.matches?.(selector)) { observer.disconnect(); callback(node); observer.observe(targetNode, config); } else if (node.querySelectorAll) { observer.disconnect(); const matchingElements = node.querySelectorAll(selector); matchingElements.forEach(matchingNode => callback(matchingNode)); observer.observe(targetNode, config); } }); } } }); observer.observe(targetNode, config); } // #endregion })();