您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
关联 番組 WIKI 計画 有史以来所有讨论与其对应条目,并提供全站共用备忘录。
// ==UserScript== // @name 维基连线 League of Bangumi Wiki // @description 关联 番組 WIKI 計画 有史以来所有讨论与其对应条目,并提供全站共用备忘录。 // @match *://bgm.tv/subject/*/edit // @match *://bangumi.tv/subject/*/edit // @match *://chii.in/subject/*/edit // @match *://bgm.tv/person/*/edit // @match *://bangumi.tv/person/*/edit // @match *://chii.in/person/*/edit // @match *://bgm.tv/character/*/edit // @match *://bangumi.tv/character/*/edit // @match *://chii.in/character/*/edit // @match *://bgm.tv/wiki* // @match *://bangumi.tv/wiki* // @match *://chii.in/wiki* // @grant none // @version 0.0.1.20250926165142 // @namespace https://greasyfork.org/users/1389779 // ==/UserScript== (function () { 'use strict'; const MEMO_TOPIC_ID = '436973'; const API_BASE_URL = 'https://bgmwiki.ry.mk/api'; // --- 1. 定义样式 --- const styles = ` /* General Panel Styles */ #wiki-discussion-panel { margin-top: 15px; height: 500px; display: flex; flex-direction: column; } .discussion-tabs { display: flex; border-bottom: 1px solid #E0E0E0; margin: 0 -10px 10px -10px; padding: 0 10px; flex-shrink: 0; } .discussion-tabs .tab-item { padding: 8px 15px; cursor: pointer; color: #666; border-bottom: 2px solid transparent; transition: color .2s, border-color .2s; margin-bottom: -1px; display: flex; align-items: center; gap: 6px; } .discussion-tabs .tab-item:hover { color: #000; } .discussion-tabs .tab-item.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .tab-count { font-size: 11px; font-weight: 600; color: #888; background-color: #0000000d; padding: 1px 6px; border-radius: 8px; transition: color .2s, background-color .2s; } .discussion-tabs .tab-item.active .tab-count { color: var(--primary-color); background-color: var(--primary-color-l10); } .discussion-content { display: none; flex-grow: 1; min-height: 0; flex-direction: column;} .discussion-content.active { display: flex; } .history-list { flex-grow: 1; overflow-y: auto; padding: 5px 5px 5px 0; margin-right: -5px; display: flex; flex-direction: column; gap: 12px; overscroll-behavior: contain; } .loading-placeholder, .empty-placeholder { text-align: center; padding: 20px; margin: auto; color: #999; } /* Memo/Discussion Entry Styles */ .memo-entry { display: flex; gap: 8px; align-items: flex-start; } .memo-entry.is-current-user { flex-direction: row-reverse; } .memo-entry .user-avatar { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; cursor: pointer; } .memo-entry .post-content { max-width: 85%; display: flex; flex-direction: column; position: relative; } .memo-entry.is-current-user .post-content { align-items: flex-end; } .memo-entry .nickname { font-size: 12px; margin-bottom: 4px; color: #666; } .memo-entry .nickname a { color: #000; text-decoration: none; font-weight: 700; } .memo-entry .post-bubble { padding: 10px 12px; border-radius: 4px 15px 15px 15px; word-wrap: break-word; overflow-wrap: anywhere; font-size: 14px; line-height: 1.5; white-space: pre-wrap; background-color: #f8f8f8; color: #000; } .memo-entry.is-current-user .post-bubble { border-radius: 15px 4px 15px 15px; background: linear-gradient(rgba(255,255,255,.85),rgba(255,255,255,.85)),var(--primary-color); color: #111; } .memo-entry .timestamp { font-size: 11px; margin-top: 4px; padding: 0 5px; color: #999; } .post-bubble a.is-current-subject-link { color: var(--primary-color); } /* Input Area Styles */ .input-area { padding-top: 10px; border-top: 1px solid #E0E0E0; flex-shrink: 0; } .input-area textarea { width: 100%; border: 1px solid #ccc; background: #fcfcfc; color: #000; padding: 10px; resize: none; box-sizing: border-box; font-family: inherit; line-height: 1.5; min-height: 80px; max-height: 150px; border-radius: 5px; } .memo-options { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; gap: 10px; } .memo-options label { display: flex; align-items: center; gap: 5px; font-size: 12px; cursor: pointer; } .memo-options input[type="date"] { padding: 4px 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; background: #fcfcfc; color: #333; } .memo-options input[type="date"].hidden { display: none; } .input-area button { margin-top: 10px; width: 100%; padding: 8px; color: white; border: none; border-radius: 50px; cursor: pointer; font-size: 14px; font-weight: bold; transition: filter .2s; background-color: var(--primary-color); } .input-area button:hover { filter: brightness(1.1); } .input-area button:disabled { background-color: #ccc; cursor: not-allowed; } /* Todo Item Styles (Optimized) */ .todo-item { display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; background-color: #f9f9f9; border: 1px solid #eee; transition: background-color 0.2s, opacity 0.3s; margin-bottom: 8px; } .todo-item:hover { background-color: #f0f0f0; } .todo-item.is-completed { opacity: 0.6; background-color: #fafafa; } .todo-item.is-completed .todo-content-text { text-decoration: line-through; color: #888; } .todo-item input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--primary-color); cursor: pointer; margin: 0; } .todo-details { flex-grow: 1; } .todo-content-text { font-size: 14px; color: #333; } .todo-meta { font-size: 11px; color: #888; margin-top: 4px; } .todo-meta .due-date { font-weight: bold; color: var(--primary-color); } /* Wiki Connect Page Styles */ #wiki-connect-container { padding: 0 15px; } .wiki-connect-feed .user-avatar { max-height: 50px; } .wiki-connect-feed .feed-item { padding: 12px 0; border-bottom: 1px solid #E0E0E0; display: flex; gap: 12px; } .wiki-connect-feed .feed-item:last-child { border-bottom: none; } .wiki-connect-feed .avatar-col { flex-shrink: 0; } .wiki-connect-feed .content-col { flex-grow: 1; } .wiki-connect-feed .feed-header { font-size: 13px; color: #666; margin-bottom: 6px; } .wiki-connect-feed .feed-header a { font-weight: bold; color: #000; text-decoration: none; } .wiki-connect-feed .feed-header a:hover { text-decoration: underline; } .wiki-connect-feed .feed-content { font-size: 14px; } .wiki-connect-feed .feed-footer { font-size: 12px; color: #999; margin-top: 8px; } /* Dark Theme Adjustments (Official Style) */ html[data-theme=dark] .discussion-tabs { border-bottom-color: #444; } html[data-theme=dark] .discussion-tabs .tab-item { color: #aaa; } html[data-theme=dark] .discussion-tabs .tab-item:hover, html[data-theme=dark] .memo-entry .nickname a:hover { color: #fff; } html[data-theme=dark] .tab-count { color: #ccc; background-color: rgba(255,255,255,0.1); } html[data-theme=dark] .discussion-tabs .tab-item.active .tab-count { background-color: var(--primary-color-d10); } html[data-theme=dark] .memo-entry .nickname { color: #888; } html[data-theme=dark] .memo-entry .nickname a { color: #e0e0e1; } html[data-theme=dark] .memo-entry .post-bubble { background-color: #38393a; color: #e0e0e1; } html[data-theme=dark] .memo-entry.is-current-user .post-bubble { color: #fff; background: var(--primary-color); } html[data-theme=dark] .input-area { border-top-color: #444; } html[data-theme=dark] .input-area textarea, html[data-theme=dark] .memo-options input[type="date"] { background: #303132; color: #e0e0e1; border-color: #555; } html[data-theme=dark] .memo-options label { color: #ccc; } html[data-theme=dark] .todo-item { background-color: rgba(255, 255, 255, 0.05); border-color: #444; } html[data-theme=dark] .todo-item:hover { background-color: rgba(255, 255, 255, 0.1); } html[data-theme=dark] .todo-item.is-completed { background-color: transparent; opacity: 0.5; } html[data-theme=dark] .todo-content-text { color: #ccc; } html[data-theme=dark] .todo-item.is-completed .todo-content-text { color: #777; } html[data-theme=dark] .todo-meta { color: #888; } html[data-theme=dark] .todo-meta .due-date { color: var(--primary-color); } html[data-theme=dark] .wiki-connect-feed .feed-item { border-bottom-color: #444; } html[data-theme=dark] .wiki-connect-feed .feed-header { color: #aaa; } html[data-theme=dark] .wiki-connect-feed .feed-header a { color: #fff; } html[data-theme=dark] .wiki-connect-feed .feed-content { color: #e0e0e1; } `; // --- 2. HTML 结构 --- const panelHTML = ` <div id="wiki-discussion-panel" class="menu_inner clearit"> <div class="discussion-tabs"> <div class="tab-item" data-tab="memo">备忘录 <span class="tab-count" data-type="memos"></span><span class="tab-count" data-type="todos" style="display:none;"></span></div> <div class="tab-item active" data-tab="discussion">相关讨论 <span class="tab-count"></span></div> </div> <div id="memo-content" class="discussion-content"> <div class="history-list"></div> <div class="input-area"> <textarea id="memo-textarea" placeholder="记录编辑要点、待办事项..."></textarea> <div class="memo-options"> <label> <input type="checkbox" id="is-todo-checkbox"> <span>作为待办事项</span> </label> <input type="date" id="todo-date-picker" class="hidden"> </div> <button id="save-memo-btn" class="chiiBtn">发送</button> </div> </div> <div id="discussion-content" class="discussion-content active"> <div class="history-list"></div> </div> </div>`; // --- 3. 脚本主逻辑 --- const dataCache = new Map(); function addStyles(css) { const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = css; document.head.appendChild(styleSheet); } function getCurrentPageInfo() { const match = location.pathname.match(/\/(subject|person|character)\/(\d+)/); if (!match) return null; return { type: match[1], id: match[2] }; } function formatTimestamp(timestamp) { const date = new Date(timestamp); const pad = (n) => n.toString().padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } function showLoadingPlaceholder(container) { if (!container) return; container.innerHTML = `<div class="loading-placeholder">正在加载...</div>`; } // --- 渲染函数 --- function renderMemos(data, entityId) { const historyContainer = document.querySelector('#memo-content .history-list'); if (!historyContainer) return; if (!data) { showLoadingPlaceholder(historyContainer); return; } if (data.length === 0) { historyContainer.innerHTML = '<div class="empty-placeholder">暂无备忘录或待办</div>'; return; } const currentUser = window.CHOBITS_USERNAME; historyContainer.innerHTML = data.map(item => item.type === 'todo' ? renderTodoItem(item) : renderMemoItem(item, currentUser)).join(''); historyContainer.querySelectorAll('.todo-item input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', (e) => updateTodoStatus(e.target.closest('.todo-item').dataset.replyId, e.target.checked)); }); } function renderMemoItem(memo, currentUser) { const creator = memo.creator; const isCurrentUser = currentUser && creator && creator.username === currentUser; const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif'; const userName = creator?.nickname || '未知用户'; const postUrl = `${location.origin}/group/topic/${MEMO_TOPIC_ID}#post_${memo.replyId}`; return `<div class="memo-entry ${isCurrentUser ? 'is-current-user' : ''}"> <a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" alt="${userName}" class="user-avatar"></a> <div class="post-content"> <div class="nickname"><a href="/user/${creator.username}" target="_blank">${userName}</a></div> <div class="post-bubble">${memo.content}</div> <div class="timestamp"><a href="${postUrl}" target="_blank" style="color:inherit">${formatTimestamp(memo.createdAt)} (查看来源)</a></div> </div> </div>`; } function renderTodoItem(todo) { const creator = todo.creator; const userName = creator?.nickname || '未知用户'; const dueDate = todo.dueDate ? ` | 截止: <span class="due-date">${new Date(todo.dueDate).toLocaleDateString()}</span>` : ''; const postUrl = `${location.origin}/group/topic/${MEMO_TOPIC_ID}#post_${todo.replyId}`; const sourceLink = `<a href="${postUrl}" target="_blank" style="color:inherit; text-decoration:none;">(来源)</a>`; return `<div class="todo-item ${todo.isCompleted ? 'is-completed' : ''}" data-reply-id="${todo.replyId}"> <input type="checkbox" ${todo.isCompleted ? 'checked' : ''} title="标记完成/未完成"> <div class="todo-details"> <div class="todo-content-text">${todo.content}</div> <div class="todo-meta">由 ${userName} 创建于 ${formatTimestamp(todo.createdAt)} ${sourceLink}${dueDate}</div> </div> </div>`; } function renderDiscussions(data, entityId) { const container = document.querySelector('#discussion-content .history-list'); if (!container) return; if (!data) { showLoadingPlaceholder(container); return; } if (data.length === 0) { container.innerHTML = '<div class="empty-placeholder">暂无相关讨论</div>'; return; } const currentUser = window.CHOBITS_USERNAME; container.innerHTML = data.map(post => { const creator = post.creator; const isCurrentUser = currentUser && creator && creator.username === currentUser; const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif'; const userName = creator?.nickname || '未知用户'; const postUrl = `${location.origin}/group/topic/${post.topicId}#post_${post.replyId}`; let postHTML = post.content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); const highlightClass = (url) => (url.match(/\/(subject|person|character)\/(\d+)/)?.[2] === entityId) ? 'is-current-subject-link' : ''; postHTML = postHTML.replace(/\[b\]([\s\S]*?)\[\/b\]/gs, '<strong>$1</strong>').replace(/\[url=(.*?)\]([\s\S]*?)\[\/url\]/gs, (m, url, text) => `<a href="${url}" class="${highlightClass(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`); postHTML = postHTML.replace(/\[url\](https?:\/\/[^\[]+)\[\/url\]/gs, (m, url) => `<a href="${url.trim()}" class="${highlightClass(url.trim())}" target="_blank" rel="noopener noreferrer">${url.trim()}</a>`); return `<div class="memo-entry ${isCurrentUser ? 'is-current-user' : ''}"><a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" alt="${userName}" class="user-avatar"></a><div class="post-content"><div class="nickname"><a href="/user/${creator.username}" target="_blank">${userName}</a></div><div class="post-bubble">${postHTML}</div><div class="timestamp"><a href="${postUrl}" target="_blank" style="color:inherit">${formatTimestamp(post.createdAt)} (${post.topicTitle || '查看来源'})</a></div></div></div>`; }).join(''); } // --- 数据获取与操作 --- async function fetchData(pageInfo, type) { const cacheKey = `${type}-${pageInfo.type}-${pageInfo.id}`; if (dataCache.has(cacheKey)) { if (type === 'memo') renderMemos(dataCache.get(cacheKey), pageInfo.id); else if (type === 'discussion') renderDiscussions(dataCache.get(cacheKey), pageInfo.id); return; } const endpoint = type === 'memo' ? 'memos' : 'discussions'; const renderFunc = type === 'memo' ? renderMemos : renderDiscussions; renderFunc(null, pageInfo.id); try { const res = await fetch(`${API_BASE_URL}/${pageInfo.type}/${pageInfo.id}/${endpoint}?_=${Date.now()}`); if (!res.ok) throw new Error(`服务器错误: ${res.status}`); const data = await res.json(); dataCache.set(cacheKey, data); renderFunc(data, pageInfo.id); } catch (error) { console.error(`获取 ${type} 数据失败:`, error); const container = document.querySelector(`#${type}-content .history-list`); if (container) container.innerHTML = `<div class="empty-placeholder">加载失败: ${error.message}</div>`; } } async function sendMemo(button, textarea, isTodoCheckbox, datePicker) { const content = textarea.value.trim(); if (!content) return alert('内容不能为空!'); const isTodo = isTodoCheckbox.checked; const dueDate = datePicker.value; if (isTodo && !dueDate) return alert('作为待办事项时,必须选择一个截止日期!'); const pageInfo = getCurrentPageInfo(); if (!pageInfo) return alert('无法识别当前页面。'); button.disabled = true; button.textContent = '正在发送...'; try { const formhash = document.querySelector('a[href*="/logout/"]')?.href.split('/logout/')[1]; if (!formhash) throw new Error('无法找到 formhash,请确认您已登录。'); const subjectTitle = document.querySelector('h1.nameSingle a').textContent.trim(); const entityUrl = `${location.origin}/${pageInfo.type}/${pageInfo.id}`; const wrappedContent = isTodo ? `[todo][due=${dueDate}]${content}[/todo]` : content; const postContent = `[b]关联条目:[/b][url=${entityUrl}]${subjectTitle}[/url]\n[b]编辑者:[/b][user]${window.CHOBITS_USERNAME || ''}[/user]\n[quote]\n${wrappedContent}\n[/quote]`; const res = await fetch(`${location.origin}/group/topic/${MEMO_TOPIC_ID}/new_reply`, { method: 'POST', body: new URLSearchParams({ formhash, content: postContent, submit: '加上去' }), credentials: 'include' }); if (!res.ok || !res.redirected) throw new Error(`发送失败: ${res.status}`); textarea.value = ''; isTodoCheckbox.checked = false; datePicker.classList.add('hidden'); button.textContent = '正在刷新...'; await fetch(`${API_BASE_URL}/memos/refresh`, { method: 'POST' }); await new Promise(resolve => setTimeout(resolve, 3000)); dataCache.delete(`memo-${pageInfo.type}-${pageInfo.id}`); await Promise.all([fetchData(pageInfo, 'memo'), fetchAndRenderCounts(pageInfo)]); alert('发送成功!'); } catch (error) { console.error('发送时出错:', error); alert(`发送失败: ${error.message}`); } finally { button.disabled = false; button.textContent = '发送'; } } async function updateTodoStatus(replyId, isCompleted) { try { const res = await fetch(`${API_BASE_URL}/memos/${replyId}/toggle`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_completed: isCompleted }) }); if (!res.ok) throw new Error('更新状态失败'); const isWikiPage = location.pathname.startsWith('/wiki'); const pageInfo = getCurrentPageInfo(); if (isWikiPage) { dataCache.delete('wiki-connect-todos'); await fetchAndRenderWikiConnectFeed('todos'); } else if (pageInfo) { dataCache.delete(`memo-${pageInfo.type}-${pageInfo.id}`); await Promise.all([fetchData(pageInfo, 'memo'), fetchAndRenderCounts(pageInfo)]); } } catch (error) { console.error('更新待办状态失败:', error); alert('更新状态失败,请刷新重试。'); } } async function fetchAndRenderCounts(pageInfo) { if (!pageInfo) return; try { const res = await fetch(`${API_BASE_URL}/${pageInfo.type}/${pageInfo.id}/counts?_=${Date.now()}`); if (!res.ok) return; const counts = await res.json(); const memoTab = document.querySelector('.tab-item[data-tab="memo"] .tab-count[data-type="memos"]'); const discussionTab = document.querySelector('.tab-item[data-tab="discussion"] .tab-count'); const todoBadge = document.querySelector('.tab-count[data-type="todos"]'); if (memoTab) memoTab.textContent = counts.memos > 0 ? counts.memos : ''; if (discussionTab) discussionTab.textContent = counts.discussions > 0 ? counts.discussions : ''; if (todoBadge) { todoBadge.textContent = counts.todos; todoBadge.style.display = counts.todos > 0 ? 'inline-block' : 'none'; } } catch (error) { console.error('获取计数失败:', error); } } // --- 初始化函数 --- function initPanel() { const targetColumn = document.getElementById('columnInSubjectB') || document.getElementById('columnCrtB'); if (!targetColumn) return; addStyles(styles); targetColumn.insertAdjacentHTML('afterbegin', panelHTML); const pageInfo = getCurrentPageInfo(); if (!pageInfo) return; fetchAndRenderCounts(pageInfo); // Event Listeners const tabs = document.querySelectorAll('#wiki-discussion-panel .tab-item'); const contents = document.querySelectorAll('#wiki-discussion-panel .discussion-content'); tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('active')); contents.forEach(c => c.classList.remove('active')); tab.classList.add('active'); document.getElementById(tab.dataset.tab + '-content').classList.add('active'); fetchData(pageInfo, tab.dataset.tab); }); }); const saveMemoBtn = document.getElementById('save-memo-btn'), memoTextarea = document.getElementById('memo-textarea'); const isTodoCheckbox = document.getElementById('is-todo-checkbox'), todoDatePicker = document.getElementById('todo-date-picker'); isTodoCheckbox.addEventListener('change', () => { todoDatePicker.classList.toggle('hidden', !isTodoCheckbox.checked); if (isTodoCheckbox.checked && !todoDatePicker.value) todoDatePicker.valueAsDate = new Date(); }); saveMemoBtn.addEventListener('click', () => sendMemo(saveMemoBtn, memoTextarea, isTodoCheckbox, todoDatePicker)); fetchData(pageInfo, 'discussion'); } function initWikiConnectPage() { addStyles(styles); const navTabs = document.querySelector('.navTabs'); if (!navTabs) return; const navSubTabs = document.querySelector('.navSubTabsWrapper'); const columnB = document.getElementById('columnB'); const originalContent = document.querySelector('#columnA'); const connectTab = document.createElement('li'); connectTab.innerHTML = `<a href="#wiki-connect">维基连线</a>`; navTabs.appendChild(connectTab); const handleTabClick = (e) => { e.preventDefault(); const target = e.currentTarget; navTabs.querySelectorAll('li a').forEach(a => a.classList.remove('focus')); target.classList.add('focus'); if (target.href.endsWith('#wiki-connect')) { document.title = '维基连线 | Bangumi'; history.pushState(null, '', '/wiki#connect'); if (originalContent) originalContent.style.display = 'none'; if (navSubTabs) navSubTabs.style.display = 'none'; if (columnB) columnB.style.display = 'none'; let container = document.getElementById('wiki-connect-container'); if (!container) { container = document.createElement('div'); container.id = 'wiki-connect-container'; container.style.width = '100%'; container.innerHTML = ` <h2 class="title">维基连线 <small class="grey">最新动态</small></h2> <div class="discussion-tabs"> <div class="tab-item active" data-tab="connect-memos" data-type="memos">备忘录</div> <div class="tab-item" data-tab="connect-todos" data-type="todos">待办事项</div> </div> <div id="connect-memos-content" class="discussion-content active"> <div class="wiki-connect-feed history-list"></div> </div> <div id="connect-todos-content" class="discussion-content"> <div class="wiki-connect-feed history-list"></div> </div>`; originalContent.parentNode.insertBefore(container, originalContent); container.querySelectorAll('.discussion-tabs .tab-item').forEach(tab => { tab.addEventListener('click', () => { if (tab.classList.contains('active')) return; container.querySelectorAll('.discussion-tabs .tab-item').forEach(t => t.classList.remove('active')); container.querySelectorAll('.discussion-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); document.getElementById(tab.dataset.tab + '-content').classList.add('active'); fetchAndRenderWikiConnectFeed(tab.dataset.type); }); }); fetchAndRenderWikiConnectFeed('memos'); } container.style.display = 'block'; } else { if (location.hash === '#connect') { location.href = target.href; } } }; connectTab.querySelector('a').addEventListener('click', handleTabClick); if (location.hash === '#connect') { setTimeout(() => connectTab.querySelector('a').click(), 0); } } async function fetchAndRenderWikiConnectFeed(type = 'memos') { const container = document.querySelector(`#connect-${type}-content .wiki-connect-feed`); if (!container) return; const cacheKey = `wiki-connect-${type}`; if (dataCache.has(cacheKey)) { renderWikiConnectItems(dataCache.get(cacheKey), container); return; } showLoadingPlaceholder(container); try { const res = await fetch(`${API_BASE_URL}/memos/latest/${type}?_=${Date.now()}`); if (!res.ok) throw new Error(`服务器错误: ${res.status}`); const data = await res.json(); dataCache.set(cacheKey, data); renderWikiConnectItems(data, container); } catch (error) { container.innerHTML = `<div class="empty-placeholder">加载失败: ${error.message}</div>`; } } function renderWikiConnectItems(data, container) { if (!container) return; if (data.length === 0) { container.innerHTML = '<div class="empty-placeholder">暂无内容</div>'; return; } container.innerHTML = data.map(item => { const creator = item.creator; const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif'; const userName = creator?.nickname || '未知用户'; const entityUrl = `/${item.entityType}/${item.entityId}`; const entityTitle = item.entityTitle || `${item.entityType} #${item.entityId}`; let contentHTML = ''; if (item.type === 'todo') { const dueDate = item.dueDate ? ` (截止: ${new Date(item.dueDate).toLocaleDateString()})` : ''; contentHTML = `<div class="todo-item ${item.isCompleted ? 'is-completed' : ''}" data-reply-id="${item.replyId}"> <input type="checkbox" ${item.isCompleted ? 'checked' : ''} title="标记完成/未完成"> <div class="todo-details"> <div class="todo-content-text"><strong>[待办]</strong> ${item.content}${dueDate}</div> </div> </div>`; } else { contentHTML = `<div class="feed-content">${item.content}</div>`; } return `<div class="feed-item"> <div class="avatar-col"> <a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" class="user-avatar" alt="${userName}"></a> </div> <div class="content-col"> <div class="feed-header"> <a href="/user/${creator.username}" target="_blank">${userName}</a> 发布于 <a href="${entityUrl}/edit" target="_blank">${entityTitle}</a> </div> ${contentHTML} <div class="feed-footer">${formatTimestamp(item.createdAt)}</div> </div> </div>`; }).join(''); container.querySelectorAll('.todo-item input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', (e) => updateTodoStatus(e.target.closest('.todo-item').dataset.replyId, e.target.checked)); }); } // --- Main Execution --- function main() { if (location.pathname.match(/\/(subject|person|character)\/\d+\/edit/)) { initPanel(); } else if (location.pathname.startsWith('/wiki')) { initWikiConnectPage(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();