// ==UserScript== // @name LGSinLG // @namespace https://www.luogu.com.cn/user/1816684 // @version 1.0.19 // @description 支持将个人主页、文章、剪贴板通过洛谷保存站API嵌入到国内站当中 // @author https://www.luogu.com.cn/user/1816684 // @match *://*.luogu.com.cn/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('[LGSinLG] 脚本启动,版本 1.0.19', new Date().toISOString()); window.addEventListener('unhandledrejection', function(event) { console.error('[LGSinLG] 未捕获的 Promise 拒绝:', event.reason); console.error('[LGSinLG] 当前 URL:', location.href); if (event.reason && event.reason.stack) { console.error('[LGSinLG] 堆栈:', event.reason.stack); } }); if (window.self !== window.top) return; if (window.__LGSinLG_script_running) return; window.__LGSinLG_script_running = true; const USER_CONFIG = { templatePasteId: 'o1lvqcmv', templateArticleId: 'omfzy7i3', colorMap: { 'Green': '#52c41a', 'Blue': '#3498db', 'Red': '#e74c3c', 'Orange': '#f39c12', 'Purple': '#9b59b6', 'Gray': '#95a5a6', 'Cheater': '#e74c3c', }, categoryMap: { 1: '个人纪录', 2: '题解', 3: '科技·工程', 4: '算法·理论', 5: '生活·游记', 6: '学习·文化课', 7: '休闲·娱乐', 8: '闲话' } }; // Shiki 代码块专用样式 const SHIKI_CSS = ` .shiki, .shiki span { color: var(--shiki-light, #24292e); } .shiki { background-color: var(--shiki-light-bg, #f6f8fa); } @media (prefers-color-scheme: dark) { .shiki, .shiki span { color: var(--shiki-dark, #c9d1d9); } .shiki { background-color: var(--shiki-dark-bg, #0d1117); } } pre.shiki { padding: 16px; border-radius: 8px; border: 1px solid #d0d7de; background-color: #f6f8fa !important; overflow-x: auto; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13px; line-height: 1.5; tab-size: 4; margin: 1em 0; } @media (prefers-color-scheme: dark) { pre.shiki { background-color: #0d1117 !important; border-color: #30363d; } } .shiki { counter-reset: line; } .shiki .line::before { counter-increment: line; content: counter(line); display: inline-block; width: 2em; margin-right: 1em; text-align: right; color: #8c959f; user-select: none; } @media (prefers-color-scheme: dark) { .shiki .line::before { color: #484f58; } } .code-line.highlight { background: rgba(255, 229, 100, 0.25); } .md-block.info, .md-block.warning, .md-block.success, .md-block.error { border: 0 solid rgba(228, 228, 228, 0); border-left-width: 6px; padding: 10px; margin: 12px 0; } .md-block.info { border-left-color: #7da7ff; } .md-block.warning { border-left-color: #ffb454; } .md-block.success { border-left-color: #4dd27a; } .md-block.error { border-left-color: #ff5757; } .md-block > .md-block-title { font-weight: 700; display: flex; justify-content: space-between; align-items: center; color: inherit; margin-bottom: 4px; position: relative; padding-left: 1.8em; } .md-block.info > .md-block-title { color: #7da7ff; } .md-block.warning > .md-block-title { color: #ffb454; } .md-block.success > .md-block-title { color: #4dd27a; } .md-block.error > .md-block-title { color: #ff5757; } .md-block > .md-block-body { margin-top: 4px; } .md-block > .md-block-body > p:last-child { margin-bottom: 0; } .md-block > .md-block-title::before { content: ''; display: inline-block; width: 1em; height: 1em; background-color: currentColor; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: contain; mask-size: contain; position: absolute; left: 0.2em; top: 50%; transform: translateY(-50%); } .md-block.success > .md-block-title::before { -webkit-mask-image: url('data:image/svg+xml;utf8,'); mask-image: url('data:image/svg+xml;utf8,'); } .md-block.warning > .md-block-title::before { -webkit-mask-image: url('data:image/svg+xml;utf8,'); mask-image: url('data:image/svg+xml;utf8,'); } .md-block.info > .md-block-title::before { -webkit-mask-image: url('data:image/svg+xml;utf8,'); mask-image: url('data:image/svg+xml;utf8,'); } .md-block.error > .md-block-title::before { -webkit-mask-image: url('data:image/svg+xml;utf8,'); mask-image: url('data:image/svg+xml;utf8,'); } .heading-pin-icon { width: 18px; height: 18px; stroke-width: 2; } .md-block-title { cursor: pointer; user-select: none; } .toggle-icon { display: inline-block; transition: transform 0.25s; margin-left: 0.5em; font-size: 0.9em; line-height: 1; flex-shrink: 0; } .toggle-icon.collapsed { transform: rotate(-90deg); } .shiki-wrapper { position: relative; } .shiki-copy-btn { position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.05); border: 1px solid #d0d7de; border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer; opacity: 0; transition: opacity 0.2s; z-index: 5; } .shiki-wrapper:hover .shiki-copy-btn { opacity: 1; } @media (prefers-color-scheme: dark) { .shiki-copy-btn { background: rgba(255,255,255,0.05); border-color: #30363d; } } /* 行号边距微调 */ .shiki .line { display: block; padding-left: 3.5em; /* 为行号留出固定空间,原来的 ::before 用了 2em + margin-right 1em,这里再加一点内边距 */ text-indent: -3.5em; /* 让行号向左突出,保持代码对齐 */ } pre:not(.shiki) { background-color: #f6f8fa !important; border-color: #d0d7de; } @media (prefers-color-scheme: dark) { pre:not(.shiki) { background-color: #0d1117 !important; border-color: #30363d; } } `; // ---------- 工具函数 ---------- function getUserIdFromUrl() { const m = location.pathname.match(/^\/user\/(\d+)\/?$/); return m ? m[1] : null; } function getPasteIdFromUrl() { const m = location.pathname.match(/\/paste\/([a-zA-Z0-9]+)/); return m ? m[1] : null; } function getArticleIdFromUrl() { const m = location.pathname.match(/\/article\/([a-zA-Z0-9]+)/); return m ? m[1] : null; } function parseApiTime(rawTime) { if (!rawTime) return null; if (typeof rawTime === 'number') { return new Date(rawTime * 1000).toISOString(); } if (typeof rawTime === 'string') { const d = new Date(rawTime); if (!isNaN(d.getTime())) return d.toISOString(); } return null; } function formatTime(isoStr) { if (!isoStr) return '未知'; const d = new Date(isoStr); if (isNaN(d.getTime())) return '未知'; const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function getRelativeTime(isoStr) { if (!isoStr) return ''; const now = new Date(); const d = new Date(isoStr); const diff = now - d; const seconds = Math.floor(diff / 1000); if (seconds < 60) return '刚刚'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes} 分钟前`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} 小时前`; const days = Math.floor(hours / 24); if (days < 30) return `${days} 天前`; const months = Math.floor(days / 30); if (months < 12) return `${months} 个月前`; const years = Math.floor(months / 12); return `${years} 年前`; } function constrainImages(container) { if (!container) return; const imgs = container.querySelectorAll('img'); imgs.forEach(img => { img.style.maxWidth = '100%'; img.style.height = 'auto'; }); } function forceExternalLinks(container) { if (!container) return; const links = container.querySelectorAll('a'); links.forEach(a => { let href = a.getAttribute('href'); if (!href) return; if (href.startsWith('#')) return; if (href.startsWith('/')) { href = `https://www.luogu.com.cn${href}`; a.setAttribute('href', href); } a.setAttribute('rel', 'noopener noreferrer'); }); } function createClickableUsername(name, color, userId) { const span = document.createElement('span'); span.style.cssText = `font-weight: bold; color: ${color}; cursor: pointer;`; span.textContent = name; span.addEventListener('click', function(e) { e.preventDefault(); window.top.location.href = `/user/${userId}`; }); return span; } function injectShikiCSS(doc) { if (!doc || !doc.head) return; const style = doc.createElement('style'); style.textContent = SHIKI_CSS; doc.head.appendChild(style); console.log('[LGSinLG] Shiki 代码块样式已注入'); } function addMdBlockToggle(doc) { if (!doc || !doc.body) return; const blocks = doc.querySelectorAll('.md-block'); blocks.forEach(block => { const title = block.querySelector('.md-block-title'); if (!title || title.dataset.toggleBound) return; const body = block.querySelector('.md-block-body'); if (!body) return; const icon = doc.createElement('span'); icon.className = 'toggle-icon'; icon.textContent = '▸'; title.appendChild(icon); title.addEventListener('click', () => { const collapsed = icon.classList.toggle('collapsed'); body.style.display = collapsed ? 'none' : ''; }); title.style.cursor = 'pointer'; title.dataset.toggleBound = 'true'; }); console.log('[LGSinLG] md-block 折叠功能已激活'); } function addShikiCopyButtons(doc) { if (!doc || !doc.body) return; const allPres = doc.querySelectorAll('pre'); allPres.forEach(pre => { // 避免重复处理 if (pre.parentElement && pre.parentElement.classList.contains('shiki-wrapper')) return; if (pre.dataset.lgsCopyProcessed) return; pre.dataset.lgsCopyProcessed = 'true'; // 包裹 wrapper const wrapper = doc.createElement('div'); wrapper.className = 'shiki-wrapper'; pre.parentNode.insertBefore(wrapper, pre); wrapper.appendChild(pre); // 对非 shiki 的 pre 添加基础外框样式(仅当没有 shiki 类时) if (!pre.classList.contains('shiki')) { pre.style.padding = '16px'; pre.style.borderRadius = '6px'; pre.style.border = '1px solid #d0d7de'; pre.style.backgroundColor = '#f6f8fa'; pre.style.overflowX = 'auto'; pre.style.fontFamily = 'monospace'; pre.style.fontSize = '14px'; pre.style.lineHeight = '1.6'; pre.style.margin = '1em 0'; pre.style.display = 'block'; } // 添加复制按钮(逻辑不变) const btn = doc.createElement('button'); btn.className = 'shiki-copy-btn'; btn.textContent = '复制'; btn.addEventListener('click', () => { const code = pre.textContent || ''; navigator.clipboard.writeText(code).then(() => { btn.textContent = '已复制'; setTimeout(() => { btn.textContent = '复制'; }, 2000); }).catch(() => { const ta = doc.createElement('textarea'); ta.value = code; ta.style.position = 'fixed'; ta.style.opacity = '0'; doc.body.appendChild(ta); ta.select(); doc.execCommand('copy'); doc.body.removeChild(ta); btn.textContent = '已复制'; setTimeout(() => { btn.textContent = '复制'; }, 2000); }); }); wrapper.appendChild(btn); }); console.log('[LGSinLG] 代码块复制按钮已添加(无行号修改)'); } // ---------- 个人介绍卡片 ---------- const CARD_ID = 'lgsinlg-intro-card'; function isAdminUser() { const rows = document.querySelectorAll('.l-flex-info-row'); for (const row of rows) { const span = row.querySelector('span'); if (span && span.textContent.trim() === '用户类型') { const right = row.querySelector('.right'); if (right && right.textContent.trim() === '管理员') return true; } } return false; } function removeUserCardIfExists() { const card = document.getElementById(CARD_ID); if (card) card.remove(); } function getLoginUid() { const selectors = [ '.lfe-header a[href^="/user/"]', '.header a[href^="/user/"]', 'a[href^="/user/"].avatar-link', 'a[href^="/user/"].user-link', '.user-menu a[href^="/user/"]', '.avatar a[href^="/user/"]', 'nav a[href^="/user/"]', '.navbar a[href^="/user/"]', '.app-header a[href^="/user/"]' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) { const match = el.getAttribute('href').match(/\/user\/(\d+)/); if (match) { console.log('[LGSinLG] getLoginUid 匹配到:', sel, 'ID:', match[1]); return match[1]; } } } console.warn('[LGSinLG] getLoginUid 未找到登录用户ID'); return null; } function getOrCreateCard(uid) { if (isAdminUser()) return null; let card = document.getElementById(CARD_ID); if (card && card.dataset.uid === uid) { return { card, contentDiv: card.querySelector('.lfe-marked-wrap.introduction'), refreshBtn: card.querySelector('button') }; } if (card) card.remove(); const container = document.querySelector('.sidebar-container > .main'); if (!container) return null; const templateCard = container.querySelector('.l-card'); if (!templateCard) return null; card = document.createElement('div'); card.id = CARD_ID; card.dataset.uid = uid; card.className = templateCard.className; for (const attr of templateCard.attributes) { if (attr.name.startsWith('data-v-')) card.setAttribute(attr.name, attr.value); } const header = document.createElement('div'); header.className = 'header'; header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;'; const h3 = document.createElement('h3'); h3.style.margin = '0'; h3.textContent = '个人介绍'; const editSpan = document.createElement('span'); editSpan.className = 'edit-button'; const refreshBtn = document.createElement('button'); refreshBtn.className = 'lform-size-small'; refreshBtn.type = 'button'; refreshBtn.textContent = '更新'; const style = refreshBtn.style; style.appearance = 'button'; style.backgroundColor = 'rgba(52,152,219,0)'; style.backgroundImage = 'none'; style.borderColor = 'rgb(52,152,219)'; style.borderRadius = '3px'; style.borderStyle = 'solid'; style.borderWidth = '1px'; style.boxSizing = 'border-box'; style.color = 'rgb(52,152,219)'; style.cursor = 'pointer'; style.display = 'block'; style.fontFamily = '-apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Noto Sans", "Noto Sans SC", "Source Sans Pro", "Source Han Sans", "Segoe UI", Arial, "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif'; style.fontSize = '14px'; style.fontWeight = '400'; style.lineHeight = '21px'; style.margin = '0'; style.minWidth = '44px'; style.outline = 'none'; style.overflow = 'visible'; style.padding = '2px 12px'; style.textAlign = 'center'; style.textIndent = '0'; style.textShadow = 'none'; style.textTransform = 'none'; style.verticalAlign = 'middle'; style.wordSpacing = '0'; editSpan.appendChild(refreshBtn); header.appendChild(h3); header.appendChild(editSpan); card.appendChild(header); const contentDiv = document.createElement('div'); contentDiv.className = 'lfe-marked-wrap introduction'; contentDiv.style.cssText = 'overflow-wrap: break-word; word-break: break-all;'; card.appendChild(contentDiv); container.appendChild(card); refreshBtn.addEventListener('click', () => handleRefresh(uid, refreshBtn, contentDiv)); return { card, contentDiv, refreshBtn }; } function fetchAndUpdateCard(uid) { if (isAdminUser()) { removeUserCardIfExists(); return; } const elements = getOrCreateCard(uid); if (!elements) { if (!fetchAndUpdateCard._retryCount) fetchAndUpdateCard._retryCount = 0; if (fetchAndUpdateCard._retryCount < 10) { fetchAndUpdateCard._retryCount++; setTimeout(() => fetchAndUpdateCard(uid), 300); } else { console.warn('[LGSinLG] 卡片容器多次重试仍未出现,放弃'); fetchAndUpdateCard._retryCount = 0; } return; } fetchAndUpdateCard._retryCount = 0; elements.contentDiv.innerHTML = '加载中...'; GM_xmlhttpRequest({ method: 'GET', url: `https://api.luogu.me/user/query/${uid}`, onload: function(res) { try { const data = JSON.parse(res.responseText); if (data.code === 200 && data.data) { const intro = data.data.renderedIntroduction || '
暂无个人介绍
'; elements.contentDiv.innerHTML = intro; constrainImages(elements.contentDiv); // 移除 heading-anchor 锚点 const headingAnchors = elements.contentDiv.querySelectorAll('a.heading-anchor'); headingAnchors.forEach(a => a.remove()); } else { elements.contentDiv.innerHTML = '获取简介失败
'; } } catch (e) { elements.contentDiv.innerHTML = '数据解析错误
'; } }, onerror: function() { elements.contentDiv.innerHTML = '网络请求失败
'; } }); } function handleRefresh(uid, btn, contentDiv) { if (btn.disabled) return; btn.textContent = '已发送'; btn.disabled = true; GM_xmlhttpRequest({ method: 'POST', url: `https://api.luogu.me/user/${uid}/refresh`, onload: function(res) { try { const data = JSON.parse(res.responseText); if (data.code === 200) { GM_xmlhttpRequest({ method: 'GET', url: `https://api.luogu.me/user/query/${uid}`, onload: function(resp) { try { const p = JSON.parse(resp.responseText); if (p.code === 200 && p.data) { contentDiv.innerHTML = p.data.renderedIntroduction || '暂无个人介绍
'; constrainImages(contentDiv); // 刷新后同样移除 heading-anchor const headingAnchors = contentDiv.querySelectorAll('a.heading-anchor'); headingAnchors.forEach(a => a.remove()); } else { contentDiv.innerHTML = '获取简介失败
'; } } catch (e) { contentDiv.innerHTML = '数据解析错误
'; } btn.textContent = '更新'; btn.disabled = false; }, onerror: function() { contentDiv.innerHTML = '网络请求失败
'; btn.textContent = '更新'; btn.disabled = false; } }); } else { btn.textContent = '更新'; btn.disabled = false; } } catch (e) { btn.textContent = '更新'; btn.disabled = false; } }, onerror: function() { btn.textContent = '更新'; btn.disabled = false; } }); } // ---------- 接管流程公共部分 ---------- function waitForIframeLoad(iframe, timeoutMs = 20000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { console.warn('[LGSinLG] iframe 加载超时'); reject(new Error('iframe 加载超时')); }, timeoutMs); const onLoad = () => { clearTimeout(timer); resolve(); }; const onError = () => { console.warn('[LGSinLG] iframe 加载失败'); clearTimeout(timer); reject(new Error('iframe 加载失败')); }; iframe.addEventListener('load', onLoad, { once: true }); iframe.addEventListener('error', onError, { once: true }); try { const doc = iframe.contentDocument || iframe.contentWindow.document; if (doc && doc.readyState === 'complete') { clearTimeout(timer); resolve(); } } catch (e) {} }); } function setupLinkInterceptor(doc) { if (!doc || !doc.body) return; const handler = (e) => { const target = e.target.closest('a'); if (!target) return; const href = target.getAttribute('href'); if (!href || href.startsWith('#') || href.startsWith('javascript:')) return; if (target.closest('.avatar')) { console.log('[LGSinLG] 头像链接放行,不拦截'); return; } e.preventDefault(); e.stopPropagation(); console.log('[LGSinLG] 顶层跳转:', href); window.top.location.href = href; }; doc.body.removeEventListener('click', handler, true); doc.body.addEventListener('click', handler, true); } function handlePastePage(pasteId) { console.log('[LGSinLG] 进入剪切板接管,pasteId:', pasteId); if (pasteId === USER_CONFIG.templatePasteId) return; if (window.__LGSinLG_paste_handled) return; window.__LGSinLG_paste_handled = true; const oldIframe = document.getElementById('lgsinlg-paste-iframe'); if (oldIframe) oldIframe.remove(); document.body.innerHTML = `暂无内容
'; constrainImages(markedDiv); forceExternalLinks(markedDiv); // 移除 heading-anchor 锚点 const headingAnchors = markedDiv.querySelectorAll('a.heading-anchor'); headingAnchors.forEach(a => a.remove()); injectShikiCSS(doc); const authorCaption = doc.querySelector('.author .lfe-caption.author-margin'); if (authorCaption) { const color = USER_CONFIG.colorMap[pasteData.author.color] || '#000'; authorCaption.innerHTML = '作者: '; authorCaption.appendChild(createClickableUsername(pasteData.author.name, color, pasteData.author.id)); } const createdTime = formatTime(pasteData.createdAt); const updatedTime = formatTime(pasteData.updatedAt); timeCaption.innerHTML = `发表时间: | 更新: `; const buttons = actionsDiv.querySelectorAll('button'); buttons.forEach(btn => { btn.style.display = 'none'; }); const updateBtn = document.createElement('button'); updateBtn.type = 'button'; updateBtn.textContent = '更新'; updateBtn.style.cssText = 'background: rgb(52, 152, 219); border: 1px solid rgb(52, 152, 219); border-radius: 3px; color: #fff; cursor: pointer; font-size: 14px; padding: 4px 12px;'; updateBtn.addEventListener('click', () => executeWorkflow(updateBtn, pasteId, 'paste')); actionsDiv.appendChild(updateBtn); return true; }; let attempts = 0; const interval = setInterval(() => { try { if (performCoreReplace() || ++attempts >= 50) { clearInterval(interval); if (!performCoreReplace()) { showError('页面结构加载超时', pasteId, 'paste'); return; } const doc = iframe.contentDocument || iframe.contentWindow.document; setupLinkInterceptor(doc); setupSourceObserver(doc, pasteData.content); addMdBlockToggle(doc); addShikiCopyButtons(doc); document.getElementById('lgsinlg-loading').style.display = 'none'; iframe.style.display = ''; window.__LGSinLG_active = true; console.log('[LGSinLG] 剪切板接管完成'); } } catch (e) { clearInterval(interval); console.error('[LGSinLG] 剪切板轮询异常', e); handleAutoRefresh(e); } }, 200); function setupSourceObserver(doc, rawContent) { const TARGET_SELECTOR = '.card.padding-default .code-card-top pre'; const PROCESSED_ATTR = 'data-lgs-processed'; const initialPre = doc.querySelector(TARGET_SELECTOR); if (initialPre && !initialPre.hasAttribute(PROCESSED_ATTR)) { initialPre.textContent = rawContent; initialPre.setAttribute(PROCESSED_ATTR, 'true'); } const observer = new MutationObserver(() => { const pre = doc.querySelector(TARGET_SELECTOR); if (pre && !pre.hasAttribute(PROCESSED_ATTR)) { pre.textContent = rawContent; pre.setAttribute(PROCESSED_ATTR, 'true'); } }); observer.observe(doc.body, { childList: true, subtree: true }); } }) .catch(err => { console.error('[LGSinLG] 剪切板 Promise 异常', err); showError('该剪切板尚未被保存站收录', pasteId, 'paste'); }); } function handleArticlePage(articleId) { console.log('[LGSinLG] 进入文章接管,articleId:', articleId); if (articleId === USER_CONFIG.templateArticleId) return; if (window.__LGSinLG_article_handled) return; window.__LGSinLG_article_handled = true; document.body.innerHTML = `暂无内容
'; constrainImages(contentDiv); forceExternalLinks(contentDiv); // 移除 heading-anchor 锚点(文章接管中保留原有处理) const headingAnchors = contentDiv.querySelectorAll('a.heading-anchor'); headingAnchors.forEach(a => a.remove()); injectShikiCSS(doc); const headings = contentDiv.querySelectorAll('h1, h2, h3, h4, h5, h6'); headings.forEach((heading, index) => { heading.id = `lgs-toc-${index}`; }); const tocContainer = doc.querySelector('.toc'); if (tocContainer) { const tocUl = tocContainer.querySelector('ul'); if (tocUl) { const templateLi = tocUl.querySelector('li'); const templateSpan = templateLi ? templateLi.querySelector('span') : null; tocUl.innerHTML = ''; if (headings.length > 0 && templateLi) { headings.forEach((heading, index) => { const li = templateLi.cloneNode(false); li.className = `title-${parseInt(heading.tagName.charAt(1)) - 1}`; li.title = heading.textContent.trim(); let span; if (templateSpan) { span = templateSpan.cloneNode(false); } else { span = document.createElement('span'); for (const attr of li.attributes) { if (attr.name.startsWith('data-v-')) span.setAttribute(attr.name, attr.value); } } span.textContent = heading.textContent.trim(); li.appendChild(span); li.addEventListener('click', (e) => { e.preventDefault(); heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); tocUl.appendChild(li); }); tocContainer.style.display = ''; } else { tocContainer.style.display = 'none'; } } } if (authorContainer) { const avatarImg = authorContainer.querySelector('.avatar'); if (avatarImg) avatarImg.src = `https://cdn.luogu.com.cn/upload/usericon/${articleData.author.id}.png`; const userNameDiv = authorContainer.querySelector('.user-name'); if (userNameDiv) { const color = USER_CONFIG.colorMap[articleData.author.color] || '#000'; userNameDiv.innerHTML = ''; userNameDiv.appendChild(createClickableUsername(articleData.author.name, color, articleData.author.id)); } const badges = authorContainer.querySelectorAll('a[target="_blank"], .fa-badge-check'); badges.forEach(b => b.remove()); } if (timeCaption) { timeCaption.innerHTML = `发布于 ${formatTime(articleData.createdAt)} | 更新于 ${formatTime(articleData.updatedAt)}`; } if (updateInfo) { updateInfo.innerHTML = ''; const authorSpan = document.createElement('span'); authorSpan.textContent = '作者:'; const color = USER_CONFIG.colorMap[articleData.author.color] || '#000'; authorSpan.appendChild(createClickableUsername(articleData.author.name, color, articleData.author.id)); updateInfo.appendChild(authorSpan); const sep = document.createTextNode(' \u00a0\u00a0 '); updateInfo.appendChild(sep); const timeSpan = document.createElement('span'); timeSpan.textContent = '创建时间:'; const timeTag = document.createElement('time'); timeTag.setAttribute('datetime', articleData.createdAt); timeTag.textContent = formatTime(articleData.createdAt); timeSpan.appendChild(timeTag); updateInfo.appendChild(timeSpan); } hijackActionButtons(doc, articleData, articleId); function replaceCategory(doc, categoryId) { const categoryName = USER_CONFIG.categoryMap[categoryId] || `分类${categoryId}`; const labelDivs = doc.querySelectorAll('.label'); labelDivs.forEach(label => { if (label.textContent.trim() === '分类') { const parent = label.parentElement; if (!parent) return; const nextDiv = parent.querySelector(':scope > div:not(.label)'); if (nextDiv) { const span = nextDiv.querySelector('span'); if (span) span.textContent = categoryName; else nextDiv.textContent = categoryName; } } }); const categoryLinks = doc.querySelectorAll('a[href^="/article?category="]'); categoryLinks.forEach(link => { link.textContent = categoryName; link.href = `/article?category=${categoryId}`; }); } replaceCategory(doc, articleData.category); return true; }; let attempts = 0; const interval = setInterval(() => { try { if (performCoreReplace() || ++attempts >= 50) { clearInterval(interval); if (!performCoreReplace()) { showError('页面结构加载超时', articleId, 'article'); return; } const doc = iframe.contentDocument || iframe.contentWindow.document; setupLinkInterceptor(doc); addMdBlockToggle(doc); addShikiCopyButtons(doc); const actionObserver = new MutationObserver(() => { hijackActionButtons(doc, articleData, articleId); }); actionObserver.observe(doc.body, { childList: true, subtree: true }); doc.addEventListener('click', function(e) { const editorLink = e.target.closest('a[data-lgs-editor]'); if (!editorLink) return; if (editorLink.hasAttribute('data-lgs-disabled')) { e.preventDefault(); e.stopPropagation(); return; } e.preventDefault(); e.stopPropagation(); editorLink.setAttribute('data-lgs-disabled', 'true'); executeWorkflow(editorLink, articleId, 'article'); }, true); setupSourceObserver(doc, articleData.content); document.getElementById('lgsinlg-loading').style.display = 'none'; iframe.style.display = ''; window.__LGSinLG_active = true; console.log('[LGSinLG] 文章接管完成'); setupComments(doc, articleId); } } catch (e) { clearInterval(interval); console.error('[LGSinLG] 文章轮询异常', e); handleAutoRefresh(e); } }, 200); function setupSourceObserver(doc, rawContent) { const TARGET_SELECTOR = '.card.padding-default .code-card-top pre'; const PROCESSED_ATTR = 'data-lgs-processed'; const initialPre = doc.querySelector(TARGET_SELECTOR); if (initialPre && !initialPre.hasAttribute(PROCESSED_ATTR)) { const innerDiv = initialPre.querySelector('div'); if (innerDiv) innerDiv.textContent = rawContent; else initialPre.textContent = rawContent; initialPre.setAttribute(PROCESSED_ATTR, 'true'); } const observer = new MutationObserver(() => { const pre = doc.querySelector(TARGET_SELECTOR); if (pre && !pre.hasAttribute(PROCESSED_ATTR)) { const innerDiv = pre.querySelector('div'); if (innerDiv) innerDiv.textContent = rawContent; else pre.textContent = rawContent; pre.setAttribute(PROCESSED_ATTR, 'true'); } }); observer.observe(doc.body, { childList: true, subtree: true }); } }) .catch(err => { console.error('[LGSinLG] 文章 Promise 异常', err); showError('该文章尚未被保存站收录', articleId, 'article'); }); } // ---------- 评论区接管 ---------- function fetchReplies(articleId, after = null) { let url = `https://www.luogu.com.cn/article/${articleId}/replies`; if (after !== null) url += `?after=${after}`; return fetch(url).then(res => { if (!res.ok) throw new Error(`评论接口 HTTP ${res.status}`); return res.json(); }).then(data => data.replySlice || []); } function renderReplyFromTemplate(templateRow, reply, doc) { console.log('[LGSinLG] 渲染评论,原始数据:', reply); const row = templateRow.cloneNode(true); const avatar = row.querySelector('.avatar'); if (avatar) avatar.src = `https://cdn.luogu.com.cn/upload/usericon/${reply.author.uid}.png`; const userLink = row.querySelector('.luogu-username a[href^="/user/"]'); if (userLink) { userLink.href = `/user/${reply.author.uid}`; userLink.textContent = reply.author.name; userLink.style.color = USER_CONFIG.colorMap[reply.author.color] || '#333'; userLink.addEventListener('click', (e) => { e.preventDefault(); window.top.location.href = `/user/${reply.author.uid}`; }); } const usernameContainer = row.querySelector('.luogu-username'); if (usernameContainer) { const existingBadges = usernameContainer.querySelectorAll('a[target="_blank"], .svg-inline--fa.fa-badge-check'); existingBadges.forEach(el => { const anchor = el.closest('a'); if (anchor) anchor.remove(); else el.remove(); }); } const timeEl = row.querySelector('time'); if (timeEl) { const rawTime = reply.time || reply.createdAt || reply.createTime; const isoTime = parseApiTime(rawTime); timeEl.setAttribute('datetime', isoTime || ''); timeEl.setAttribute('title', isoTime ? formatTime(isoTime) : '未知'); timeEl.textContent = isoTime ? getRelativeTime(isoTime) : ''; } const contentDiv = row.querySelector('.content'); if (contentDiv) { const content = reply.content || reply.renderedContent || ''; contentDiv.textContent = content; contentDiv.style.whiteSpace = 'pre-line'; constrainImages(contentDiv); forceExternalLinks(contentDiv); } const deleteLink = row.querySelector('.meta a[href*="javascript:void"]'); if (deleteLink) deleteLink.remove(); return row; } function setupComments(doc, articleId) { const commentContainer = doc.querySelector('.article-comment'); if (!commentContainer || commentContainer.dataset.lgsProcessed) return; commentContainer.dataset.lgsProcessed = 'true'; // 禁用排序下拉 const sortWrapper = commentContainer.querySelector('.combo-wrapper'); if (sortWrapper) { sortWrapper.style.pointerEvents = 'none'; sortWrapper.style.opacity = '0.5'; sortWrapper.title = '评论区已接管,排序不可用'; } // 禁用评论输入区域 const textarea = commentContainer.querySelector('textarea'); if (textarea) { const replyForm = textarea.closest('.l-card') || textarea.closest('form') || textarea.closest('.reply-box') || textarea.closest('.comment-editor'); if (replyForm) { replyForm.style.pointerEvents = 'none'; replyForm.style.opacity = '0.5'; replyForm.title = '评论区已由脚本接管,暂不支持发表'; } } // 尝试移除原生加载更多(可能此时还未渲染) const nativeLoadMoreLink = commentContainer.querySelector('p[style*="text-align: center"] a[href="javascript:void 0"]'); if (nativeLoadMoreLink && nativeLoadMoreLink.parentElement) { nativeLoadMoreLink.parentElement.remove(); } const rowWrap = commentContainer.querySelector('.row-wrap'); if (!rowWrap) return; const doInit = (templateRow) => { rowWrap.innerHTML = ''; const countSpan = commentContainer.querySelector('.comment-filter-line span[style*="flex"]'); if (countSpan) countSpan.textContent = '已加载 0 条评论'; let allReplies = []; let lastId = null; let isLoading = false; function updateCount() { if (countSpan) countSpan.textContent = `已加载 ${allReplies.length} 条评论`; } function appendReplies(replies) { replies.forEach(reply => { const el = renderReplyFromTemplate(templateRow, reply, doc); rowWrap.appendChild(el); allReplies.push(reply); }); if (replies.length > 0) lastId = replies[replies.length - 1].id; updateCount(); loadMoreBtn.style.display = replies.length === 0 ? 'none' : 'inline'; } async function loadNextPage() { if (isLoading) return; isLoading = true; loadMoreBtn.textContent = '加载中…'; try { const replies = await fetchReplies(articleId, lastId); appendReplies(replies); if (replies.length === 0) loadMoreBtn.style.display = 'none'; else loadMoreBtn.textContent = '加载更多'; } catch (e) { console.error('[LGSinLG] 加载评论失败', e); loadMoreBtn.textContent = '加载失败,点击重试'; loadMoreBtn.style.display = 'inline'; } finally { isLoading = false; } } function refreshComments() { rowWrap.innerHTML = ''; allReplies = []; lastId = null; updateCount(); loadMoreBtn.style.display = 'none'; isLoading = false; fetchReplies(articleId).then(replies => { appendReplies(replies); }).catch(err => { console.error('[LGSinLG] 刷新评论失败', err); if (countSpan) countSpan.textContent = '评论加载失败'; }); } let loadMoreBtn = doc.getElementById('lgs-load-more'); if (!loadMoreBtn) { loadMoreBtn = doc.createElement('button'); loadMoreBtn.id = 'lgs-load-more'; loadMoreBtn.textContent = '加载更多'; loadMoreBtn.style.display = 'none'; loadMoreBtn.addEventListener('click', loadNextPage); loadMoreBtn.style.border = 'none'; loadMoreBtn.style.backgroundColor = 'rgba(0, 0, 0, 0)'; loadMoreBtn.style.color = 'rgb(52, 152, 219)'; loadMoreBtn.style.cursor = 'pointer'; loadMoreBtn.style.fontFamily = '-apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Noto Sans", "Noto Sans SC", "Source Sans Pro", "Source Han Sans", "Segoe UI", Arial, "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif'; loadMoreBtn.style.fontSize = '16px'; loadMoreBtn.style.height = 'auto'; loadMoreBtn.style.lineHeight = '24px'; loadMoreBtn.style.textAlign = 'center'; loadMoreBtn.style.textDecoration = 'none'; loadMoreBtn.style.width = 'auto'; const wrapper = doc.createElement('p'); wrapper.style.textAlign = 'center'; wrapper.style.margin = '0'; wrapper.appendChild(loadMoreBtn); commentContainer.appendChild(wrapper); } fetchReplies(articleId).then(replies => { appendReplies(replies); }).catch(err => { console.error('[LGSinLG] 初始评论加载失败', err); if (countSpan) countSpan.textContent = '评论加载失败,点击刷新重试'; }); // 持续监听并移除后出现的原生“加载更多”按钮 const nativeCleaner = new MutationObserver(() => { const nativeLink = commentContainer.querySelector('p[style*="text-align: center"] a[href="javascript:void 0"]'); if (nativeLink && nativeLink.parentElement) { console.log('[LGSinLG] 移除后出现的原生加载更多按钮'); nativeLink.parentElement.remove(); } }); nativeCleaner.observe(commentContainer, { childList: true, subtree: true }); }; const templateRow = rowWrap.querySelector('.row'); if (templateRow) { doInit(templateRow); return; } const observer = new MutationObserver((mutations, obs) => { const row = rowWrap.querySelector('.row'); if (row) { obs.disconnect(); doInit(row); } }); observer.observe(rowWrap, { childList: true, subtree: true }); } // ---------- 其他接管辅助函数 ---------- function hijackActionButtons(doc, articleData, articleId) { const MARKER_EDIT = 'data-lgs-editor'; const MARKER_LIKE = 'data-lgs-like'; const MARKER_FAV = 'data-lgs-fav'; const MARKER_DISLIKE = 'data-lgs-dislike'; const actionDivs = doc.querySelectorAll('.actions'); actionDivs.forEach(actionsDiv => { const allButtons = actionsDiv.querySelectorAll('.button-2line'); allButtons.forEach(btn => { const rawText = (btn.querySelector('.text') || btn).textContent.trim(); if (rawText === '收藏' && !btn.hasAttribute(MARKER_FAV)) { const count = articleData.favorCount || 0; const textSpan = btn.querySelector('.text'); if (textSpan) textSpan.textContent = count; else btn.textContent = count; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.7'; btn.style.cursor = 'default'; const parentLink = btn.closest('a'); if (parentLink) { parentLink.removeAttribute('href'); parentLink.style.pointerEvents = 'none'; } btn.setAttribute(MARKER_FAV, 'true'); } else if (rawText === '点赞' && !btn.hasAttribute(MARKER_LIKE)) { const count = articleData.upvote || 0; const textSpan = btn.querySelector('.text'); if (textSpan) textSpan.textContent = count; else btn.textContent = count; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.7'; btn.style.cursor = 'default'; const parentLink = btn.closest('a'); if (parentLink) { parentLink.removeAttribute('href'); parentLink.style.pointerEvents = 'none'; } btn.setAttribute(MARKER_LIKE, 'true'); } else if (rawText.startsWith('不推荐') && !btn.hasAttribute(MARKER_DISLIKE)) { btn.style.pointerEvents = 'none'; btn.style.opacity = '0.7'; btn.style.cursor = 'default'; const parentLink = btn.closest('a'); if (parentLink) { parentLink.removeAttribute('href'); parentLink.style.pointerEvents = 'none'; } btn.setAttribute(MARKER_DISLIKE, 'true'); } }); const editLinks = actionsDiv.querySelectorAll('a[href*="/edit"]'); editLinks.forEach(link => { if (link.hasAttribute(MARKER_EDIT)) return; link.removeAttribute('href'); const textSpan = link.querySelector('.text'); if (textSpan) textSpan.textContent = ' 更新 '; link.setAttribute(MARKER_EDIT, 'true'); }); }); } function handleAutoRefresh(error) { console.error('[LGSinLG] handleAutoRefresh:', error && error.message); if (error && error.message && (error.message.includes('cross-origin') || error.message.includes('Blocked a frame'))) { console.warn('[LGSinLG] 跨域错误,刷新页面'); location.reload(); } else { console.warn('[LGSinLG] 非跨域错误,清理 iframe'); const ifr = document.getElementById('lgsinlg-article-iframe') || document.getElementById('lgsinlg-paste-iframe'); if (ifr) ifr.remove(); if (document.getElementById('lgsinlg-article-iframe')) window.__LGSinLG_article_handled = false; if (document.getElementById('lgsinlg-paste-iframe')) window.__LGSinLG_paste_handled = false; window.__LGSinLG_active = false; } } // ---------- 公开工作流(无需 token) ---------- function executeWorkflow(btn, targetId, type, onSuccess) { setButtonState(btn, '发送中...'); const workflowName = type === 'article' ? 'article-save-pipeline' : 'paste-save-pipeline'; GM_xmlhttpRequest({ method: 'POST', url: `https://api.luogu.me/workflow/create/template/${workflowName}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ targetId }), onload: function(res) { try { const data = JSON.parse(res.responseText); if (data.code === 200) { resetButtonState(btn, '更新'); if (typeof onSuccess === 'function') onSuccess(); } else { alert('工作流启动失败:' + (data.message || '未知错误')); resetButtonState(btn, '更新'); } } catch (e) { alert('响应解析失败'); resetButtonState(btn, '更新'); } }, onerror: function(err) { console.error('[LGSinLG] 网络错误', err); alert('网络错误,请稍后重试'); resetButtonState(btn, '更新'); } }); } function setButtonState(btn, text) { const textSpan = btn.querySelector && btn.querySelector('.text'); if (textSpan) textSpan.textContent = ` ${text} `; else btn.textContent = text; if (btn.tagName === 'BUTTON') btn.disabled = true; else btn.style.pointerEvents = 'none'; } function resetButtonState(btn, text) { const textSpan = btn.querySelector && btn.querySelector('.text'); if (textSpan) textSpan.textContent = ` ${text} `; else btn.textContent = text; if (btn.tagName === 'BUTTON') btn.disabled = false; else btn.style.pointerEvents = ''; btn.removeAttribute('data-lgs-disabled'); } // ---------- 路由与生命周期 ---------- function cleanupTakeover() { console.log('[LGSinLG] 执行接管清理'); if (waitForSecurityTimer) { clearTimeout(waitForSecurityTimer); waitForSecurityTimer = null; } window.__LGSinLG_active = false; window.__LGSinLG_paste_handled = false; window.__LGSinLG_article_handled = false; ['lgsinlg-paste-iframe', 'lgsinlg-article-iframe'].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); const loading = document.getElementById('lgsinlg-loading'); if (loading) loading.remove(); const error = document.getElementById('lgsinlg-error'); if (error) error.remove(); } let currentPath = location.pathname; let pending = false; let waitForSecurityTimer = null; function recoverActiveIfNeeded() { const pasteIfr = document.getElementById('lgsinlg-paste-iframe'); const articleIfr = document.getElementById('lgsinlg-article-iframe'); if ((pasteIfr || articleIfr) && !window.__LGSinLG_active) { window.__LGSinLG_active = true; if (waitForSecurityTimer) { clearTimeout(waitForSecurityTimer); waitForSecurityTimer = null; } } } function isSecurityPage() { const warningTitle = document.querySelector('body > div:nth-child(1) > div > h3'); return document.title === '安全访问中心 - 洛谷' && warningTitle && warningTitle.textContent.trim() === '即将离开洛谷'; } function processRoute() { recoverActiveIfNeeded(); console.log('[LGSinLG] processRoute active:', window.__LGSinLG_active, 'path:', location.pathname); if (window.__LGSinLG_active) { const currentPasteId = getPasteIdFromUrl(); const currentArticleId = getArticleIdFromUrl(); if ((currentPasteId && window.__LGSinLG_paste_handled) || (currentArticleId && window.__LGSinLG_article_handled)) { console.log('[LGSinLG] 仍在接管页面,忽略路由变化'); return; } console.log('[LGSinLG] 接管已失效,清理并刷新'); cleanupTakeover(); location.reload(); return; } // 移除 luogu.me 相关处理 if (waitForSecurityTimer) { clearTimeout(waitForSecurityTimer); waitForSecurityTimer = null; } const pasteId = getPasteIdFromUrl(); const articleId = getArticleIdFromUrl(); if (pasteId) { console.log('[LGSinLG] 粘贴页 pasteId:', pasteId); if (isSecurityPage()) { handlePastePage(pasteId); return; } waitForSecurity(pasteId, 'paste'); return; } if (articleId) { console.log('[LGSinLG] 文章页 articleId:', articleId); if (isSecurityPage()) { handleArticlePage(articleId); return; } waitForSecurity(articleId, 'article'); return; } const uid = getUserIdFromUrl(); if (uid) { const loginUid = getLoginUid(); if (loginUid && loginUid === uid) { console.log('[LGSinLG] 自己的主页,跳过卡片'); removeUserCardIfExists(); return; } // 等待侧边栏容器就绪,否则稍后再试 if (!document.querySelector('.sidebar-container > .main')) { setTimeout(() => processRoute(), 300); return; } fetchAndUpdateCard(uid); return; } console.log('[LGSinLG] 未匹配接管规则,清理遗留 iframe'); removeUserCardIfExists(); const pasteIfr = document.getElementById('lgsinlg-paste-iframe'); if (pasteIfr) pasteIfr.remove(); const articleIfr = document.getElementById('lgsinlg-article-iframe'); if (articleIfr) articleIfr.remove(); window.__LGSinLG_paste_handled = false; window.__LGSinLG_article_handled = false; window.__LGSinLG_active = false; } function waitForSecurity(id, type) { let attempts = 0; const maxAttempts = 100; const check = () => { if (isSecurityPage()) { console.log(`[LGSinLG] 安全页就绪,接管 ${type} ${id}`); if (type === 'paste') handlePastePage(id); else handleArticlePage(id); return; } if (++attempts < maxAttempts) { waitForSecurityTimer = setTimeout(check, 200); } else { console.warn(`[LGSinLG] 安全页等待超时,放弃接管`); waitForSecurityTimer = null; window.__LGSinLG_paste_handled = false; window.__LGSinLG_article_handled = false; window.__LGSinLG_active = false; } }; check(); } const observer = new MutationObserver(() => { if (pending || window.__LGSinLG_active) return; const newPath = location.pathname; if (newPath !== currentPath) { console.log('[LGSinLG] 路径变化:', currentPath, '->', newPath); currentPath = newPath; pending = true; requestAnimationFrame(() => { processRoute(); pending = false; }); } }); const app = document.querySelector('.main-container'); if (app) observer.observe(app, { childList: true, subtree: true }); else observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('popstate', () => { console.log('[LGSinLG] popstate 触发,新路径:', location.pathname); const errorDiv = document.getElementById('lgsinlg-error'); if (errorDiv && errorDiv.style.display !== 'none') { location.reload(); return; } currentPath = location.pathname; processRoute(); }); if (getPasteIdFromUrl() || getArticleIdFromUrl()) { processRoute(); } else { let initTimer = setInterval(() => { const container = document.querySelector('.sidebar-container > .main, .full-container, main'); if (container) { clearInterval(initTimer); processRoute(); } }, 200); } })();