热门帖子 tetrohedro 发布于11 小时前 热门帖子 发布于11 小时前 · 只看该作者 前言 大家好,我是四面体~ 作为版主们,评分是版务不可或缺的一部分 大多数评分的标准主要基于 帖子的时间 帖子的字数 在皮卡攸和卡米那君,和其他DC群众的协助下,四面体制作了三个帮助评分的脚本~ 查看字数的脚本 只要使用这个脚本,即可在帖子的右下角快速查看帖子的字数~ 这样,就可以快速评判字数是否满足标准的说~ 查看链接对应字数和回复时间的脚本 只要使用这个脚本,即可查看一个帖子里所有链接对应的字数和回复时间~ 这样,就就可以快速了解一个用户是否满足任务条件的说~ 批量评分的脚本 作为版主,在举办活动时,可能有很多的回复需要评分~ 只要使用这个脚本,就可以批量选中回复,然后一次性评分的说~ 剧透 查看字数的脚本 // ==UserScript== // @name SSTM Chinese Character Counter // @namespace https://sstm.moe/ // @version 1.0 // @description Shows the number of Chinese characters at the bottom right of each post // @match https://sstm.moe/topic/* // @grant none // ==/UserScript== (function () { 'use strict'; const CHINESE_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf]/g; function countChinese(text) { const matches = text.match(CHINESE_REGEX); return matches ? matches.length : 0; } function addCounters() { const posts = document.querySelectorAll('[data-role="commentContent"]'); posts.forEach((content) => { if (content.dataset.chineseCounted) return; content.dataset.chineseCounted = '1'; const clone = content.cloneNode(true); clone.querySelectorAll('blockquote').forEach((q) => q.remove()); const count = countChinese(clone.textContent); const badge = document.createElement('div'); badge.textContent = `字数: ${count}`; Object.assign(badge.style, { textAlign: 'right', fontSize: '12px', color: '#888', marginTop: '8px', }); content.appendChild(badge); }); } addCounters(); // Handle dynamically loaded posts const observer = new MutationObserver(addCounters); observer.observe(document.body, { childList: true, subtree: true }); })(); 查看链接对应字数和回复时间的脚本 // ==UserScript== // @name SSTM Post Link Character Counter // @namespace https://sstm.moe/ // @version 1.1 // @description Shows Chinese character count next to post links within posts // @match https://sstm.moe/topic/* // @grant none // ==/UserScript== (function () { 'use strict'; const CHINESE_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf]/g; const pageCache = new Map(); function countChinese(text) { const matches = text.match(CHINESE_REGEX); return matches ? matches.length : 0; } function getTextExcludingQuotes(el) { const clone = el.cloneNode(true); clone.querySelectorAll('blockquote').forEach((q) => q.remove()); return clone.textContent; } async function fetchPage(url) { if (pageCache.has(url)) return pageCache.get(url); const resp = await fetch(url); const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); pageCache.set(url, doc); return doc; } function findComment(doc, commentId) { if (!commentId) return doc.querySelector('[data-role="commentContent"]'); // IPS4 uses id="elComment_XXXXX" or similar patterns const selectors = [ `#comment-${commentId} [data-role="commentContent"]`, `[data-commentid="${commentId}"] [data-role="commentContent"]`, `#elComment_${commentId} [data-role="commentContent"]`, ]; for (const sel of selectors) { const el = doc.querySelector(sel); if (el) return el; } return null; } function findPostDate(doc, commentId) { // Walk up from the comment content to find the enclosing article, // then pick up the IPS <time datetime="..."> in the post header. const contentEl = findComment(doc, commentId); const article = contentEl && contentEl.closest('article'); const timeEl = article && article.querySelector('time[datetime]'); if (!timeEl) return null; const dt = new Date(timeEl.getAttribute('datetime')); if (isNaN(dt)) return null; // Format as YYYY-MM-DD HH:MM (local time of the viewer) const pad = (n) => String(n).padStart(2, '0'); return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; } async function processLinks() { const posts = document.querySelectorAll('[data-role="commentContent"]'); const links = []; posts.forEach((content) => { content.querySelectorAll('a[href*="/topic/"]').forEach((a) => { if (a.dataset.charCounted) return; // Only match sstm.moe topic links try { const url = new URL(a.href, location.origin); if (url.hostname !== 'sstm.moe') return; if (!url.pathname.startsWith('/topic/')) return; } catch { return; } a.dataset.charCounted = '1'; links.push(a); }); }); for (const a of links) { try { const url = new URL(a.href, location.origin); const commentMatch = url.hash.match(/findComment-(\d+)/); const commentId = commentMatch ? commentMatch[1] : null; url.hash = ''; const doc = await fetchPage(url.toString()); const contentEl = findComment(doc, commentId); if (!contentEl) continue; const text = getTextExcludingQuotes(contentEl); const count = countChinese(text); const postDate = findPostDate(doc, commentId); const badge = document.createElement('span'); const datePart = postDate ? ` · ${postDate}` : ''; badge.textContent = ` (字数: ${count}字${datePart})`; Object.assign(badge.style, { fontSize: '12px', color: '#888', }); a.after(badge); } catch (e) { console.error('Failed to count chars for:', a.href, e); } } } processLinks(); // Handle dynamically loaded posts const observer = new MutationObserver(processLinks); observer.observe(document.body, { childList: true, subtree: true }); })(); 批量评分的脚本 // ==UserScript== // @name SSTM 批量评分 (Batch Rating) // @namespace https://sstm.moe/ // @version 1.0 // @description Select multiple posts and batch submit 评分 in one click // @author sstm-scripts // @match https://sstm.moe/topic/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ── State ──────────────────────────────────────────────────────────────── const selectedPosts = new Set(); // Set of comment IDs (strings) let batchMode = false; // ── Styles ─────────────────────────────────────────────────────────────── GM_addStyle(` /* Checkbox next to paw button */ .ss-check { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: 2px solid #aaa; border-radius: 4px; cursor: pointer; background: #fff; flex-shrink: 0; font-size: 13px; color: transparent; transition: background 0.15s, border-color 0.15s, color 0.15s; vertical-align: middle; margin-right: 4px; user-select: none; } .ss-check.ss-checked { background: #4a9eff; border-color: #4a9eff; color: #fff; } .ss-paw-wrapper { display: inline-flex; align-items: center; } /* Floating toggle button */ #ss-batch-toggle { position: fixed; top: 68px; right: 14px; background: #4a9eff; color: #fff; border: none; padding: 5px 13px; border-radius: 14px; cursor: pointer; font-size: 13px; z-index: 9999; box-shadow: 0 2px 8px rgba(0,0,0,0.25); transition: background 0.15s; } #ss-batch-toggle.active { background: #e05; } /* Floating status bar */ #ss-batch-bar { position: fixed; bottom: 70px; left: 50%; transform: translateX(-50%); background: rgba(30,30,30,0.93); color: #fff; padding: 7px 16px; border-radius: 20px; display: flex; align-items: center; gap: 10px; z-index: 9999; box-shadow: 0 2px 12px rgba(0,0,0,0.35); font-size: 13px; white-space: nowrap; } #ss-batch-bar button { background: #4a9eff; color: #fff; border: none; padding: 3px 11px; border-radius: 11px; cursor: pointer; font-size: 12px; } #ss-batch-bar button.muted { background: #666; } /* Post highlight when selected */ article.ss-post-selected { outline: 2px solid #4a9eff; outline-offset: -2px; } /* Dialog batch badge */ #ss-batch-badge { background: #4a9eff; color: #fff; border-radius: 8px; padding: 4px 10px; font-size: 13px; margin-bottom: 10px; text-align: center; } /* Toast notification */ #ss-toast { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); background: rgba(30,30,30,0.95); color: #fff; padding: 10px 20px; border-radius: 12px; font-size: 14px; z-index: 99999; box-shadow: 0 4px 16px rgba(0,0,0,0.4); text-align: center; line-height: 1.5; max-width: 280px; pointer-events: none; } #ss-toast.success { border-left: 4px solid #4caf50; } #ss-toast.error { border-left: 4px solid #f44336; } `); // ── Helper: get comment ID from a paw button ───────────────────────────── function getCommentId(btn) { try { return new URL(btn.href).searchParams.get('comment'); } catch { return null; } } // ── Toggle a post selection ────────────────────────────────────────────── function togglePost(commentId, checkbox, article) { if (selectedPosts.has(commentId)) { selectedPosts.delete(commentId); checkbox.classList.remove('ss-checked'); article?.classList.remove('ss-post-selected'); } else { selectedPosts.add(commentId); checkbox.classList.add('ss-checked'); article?.classList.add('ss-post-selected'); } updateBatchBar(); } // ── Add checkboxes next to every paw button not yet processed ──────────── function addCheckboxes() { const pawBtns = document.querySelectorAll( 'a[data-ipsdialog][data-ipsdialog-title="评分"]:not([data-ss-done])' ); pawBtns.forEach(btn => { btn.setAttribute('data-ss-done', '1'); // Only attach to the actual rating button (not secondary paw actions) try { if (new URL(btn.href).searchParams.get('do') !== 'pointsRatingComment') return; } catch { return; } const commentId = getCommentId(btn); if (!commentId) return; btn.setAttribute('data-ss-rating', '1'); const article = btn.closest('article[id^="elComment_"]'); const wrapper = document.createElement('span'); wrapper.className = 'ss-paw-wrapper'; const checkbox = document.createElement('span'); checkbox.className = 'ss-check'; checkbox.textContent = '✓'; checkbox.title = '选择此帖'; checkbox.style.display = batchMode ? 'inline-flex' : 'none'; // Restore checked state if already selected if (selectedPosts.has(commentId)) { checkbox.classList.add('ss-checked'); article?.classList.add('ss-post-selected'); } checkbox.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); togglePost(commentId, checkbox, article); }); btn.parentNode.insertBefore(wrapper, btn); wrapper.appendChild(checkbox); wrapper.appendChild(btn); }); } // ── Batch bar ──────────────────────────────────────────────────────────── function updateBatchBar() { let bar = document.getElementById('ss-batch-bar'); if (!batchMode) { bar?.remove(); return; } if (!bar) { bar = document.createElement('div'); bar.id = 'ss-batch-bar'; document.body.appendChild(bar); } bar.innerHTML = ` <span>已选 <strong>${selectedPosts.size}</strong> 帖</span> <button id="ss-sel-all">全选</button> <button id="ss-sel-none" class="muted">取消</button> `; bar.querySelector('#ss-sel-all').onclick = selectAll; bar.querySelector('#ss-sel-none').onclick = clearAll; } function selectAll() { document.querySelectorAll('a[data-ss-rating]').forEach(btn => { const commentId = getCommentId(btn); if (!commentId) return; selectedPosts.add(commentId); const checkbox = btn.parentNode.querySelector('.ss-check'); checkbox?.classList.add('ss-checked'); btn.closest('article[id^="elComment_"]')?.classList.add('ss-post-selected'); }); updateBatchBar(); } function clearAll() { selectedPosts.clear(); document.querySelectorAll('.ss-check').forEach(c => c.classList.remove('ss-checked')); document.querySelectorAll('article.ss-post-selected').forEach(a => a.classList.remove('ss-post-selected')); updateBatchBar(); } // ── Batch mode toggle button ───────────────────────────────────────────── function addToggleButton() { if (document.getElementById('ss-batch-toggle')) return; const btn = document.createElement('button'); btn.id = 'ss-batch-toggle'; btn.textContent = '批量评分'; btn.title = '进入/退出批量评分模式'; btn.onclick = () => { batchMode = !batchMode; btn.textContent = batchMode ? '退出批量' : '批量评分'; btn.classList.toggle('active', batchMode); // Show/hide all checkboxes document.querySelectorAll('.ss-check').forEach(c => { c.style.display = batchMode ? 'inline-flex' : 'none'; }); if (!batchMode) { clearAll(); } else { updateBatchBar(); } }; document.body.appendChild(btn); } // ── Toast notification (non-blocking, avoids alert() issues) ──────────── function showToast(msg, type = 'success', durationMs = 4000) { document.getElementById('ss-toast')?.remove(); const toast = document.createElement('div'); toast.id = 'ss-toast'; toast.className = type; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => toast.remove(), durationMs); } // ── Fetch the 评分 form for a given comment ID ─────────────────────────── async function fetchRatingForm(topicUrl, commentId) { const url = new URL(topicUrl); url.searchParams.set('do', 'pointsRatingComment'); url.searchParams.set('comment', commentId); const resp = await fetch(url.href, { credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); return doc.querySelector('form'); } // ── Submit the rating for one comment ──────────────────────────────────── async function submitRating(topicUrl, commentId, userValues) { // Fetch the form to get fresh hidden fields (csrfKey, plupload, etc.) const remoteForm = await fetchRatingForm(topicUrl, commentId); if (!remoteForm) throw new Error('Could not load rating form'); const data = new FormData(); // Copy all hidden fields from the remote form remoteForm.querySelectorAll('input[type="hidden"]').forEach(input => { data.set(input.name, input.value); }); // Apply user-entered values data.set('points_currency_1', userValues.currency1); data.set('points_currency_2', userValues.currency2); data.set('points_currency_3', userValues.currency3); data.set('points_dash_transfer_message', userValues.message); data.set('points_dashboard_transfer_submitted', '1'); // POST to topic URL with comment param const postUrl = new URL(topicUrl); postUrl.searchParams.set('do', 'pointsRatingComment'); postUrl.searchParams.set('comment', commentId); const resp = await fetch(postUrl.href, { method: 'POST', body: data, credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!resp.ok) throw new Error(`POST HTTP ${resp.status}`); return resp; } // ── Hook into the 评分 dialog ──────────────────────────────────────────── function hookDialog(dialogEl) { const form = dialogEl.querySelector('form'); if (!form || form.dataset.ssBatchHooked) return; form.dataset.ssBatchHooked = '1'; // Inject batch badge if in batch mode with posts selected function updateBadge() { let badge = dialogEl.querySelector('#ss-batch-badge'); if (selectedPosts.size > 0 && batchMode) { if (!badge) { badge = document.createElement('div'); badge.id = 'ss-batch-badge'; form.prepend(badge); } badge.textContent = `批量模式:将对 ${selectedPosts.size} 个帖子评分`; } else { badge?.remove(); } } updateBadge(); // Intercept submit form.addEventListener('submit', async function handler(e) { // Only intercept if batch mode is on and posts are selected if (!batchMode || selectedPosts.size === 0) return; e.preventDefault(); e.stopImmediatePropagation(); // Read user-entered values const userValues = { currency1: form.elements['points_currency_1']?.value || '0', currency2: form.elements['points_currency_2']?.value || '0', currency3: form.elements['points_currency_3']?.value || '0', message: form.elements['points_dash_transfer_message']?.value || '', }; // Skip if all values are zero/empty if ( userValues.currency1 === '0' && userValues.currency2 === '0' && userValues.currency3 === '0' && !userValues.message ) { showToast('请先填写评分数值', 'error'); return; } const targets = [...selectedPosts]; const topicUrl = window.location.origin + window.location.pathname; // Disable submit button and show progress const submitBtn = form.querySelector('[type="submit"], button[data-action="submit"]'); const originalLabel = submitBtn?.textContent; if (submitBtn) submitBtn.disabled = true; let success = 0; let fail = 0; const errors = []; for (const commentId of targets) { if (submitBtn) { submitBtn.textContent = `${success + fail + 1}/${targets.length} 提交中…`; } try { await submitRating(topicUrl, commentId, userValues); success++; } catch (err) { console.error('[ss-batch-rate] Failed for comment', commentId, err); errors.push(commentId); fail++; } } // Restore button if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalLabel || '提交'; } // Close the dialog const closeBtn = dialogEl.querySelector('.ipsDialog_close a, [data-action="closeDialog"]'); closeBtn?.click(); // Result notification const type = fail === 0 ? 'success' : 'error'; let msg = `批量评分完成!✅ 成功:${success} 帖`; if (fail > 0) msg += ` ❌ 失败:${fail} 帖`; showToast(msg, type, 5000); clearAll(); }, true /* capture — run before IPS handlers */); } // ── Watch for the rating form being injected into any IPS dialog ───────── // IPS loads dialog content asynchronously, so we watch for points_currency_1 // appearing anywhere in the DOM (it only exists inside the 评分 form). const dialogObserver = new MutationObserver(mutations => { for (const mut of mutations) { for (const node of mut.addedNodes) { if (node.nodeType !== 1) continue; // Is this node (or a descendant) the points_currency_1 input? const isTarget = node.name === 'points_currency_1'; const hasTarget = !isTarget && node.querySelector?.('[name="points_currency_1"]'); if (isTarget || hasTarget) { const dialog = node.closest?.('.ipsDialog') || document.querySelector('.ipsDialog'); if (dialog) hookDialog(dialog); } } } }); dialogObserver.observe(document.body, { childList: true, subtree: true }); // ── Watch for dynamically loaded posts ─────────────────────────────────── const postObserver = new MutationObserver(() => addCheckboxes()); postObserver.observe(document.body, { childList: true, subtree: true }); // ── Init ───────────────────────────────────────────────────────────────── addCheckboxes(); addToggleButton(); })(); 剧透 @羊駝 @攸薩 @lemon19 @哥特的亡零 @MCIN @较好的书呆子 @ㅤ凯ㅤ @莱斯 @厭世平胸雞 @阿露今日也在歌唱 @aruui@喵了个喵,咪 @月晓 @吴景焕 @IJN_Hibiki @safcz @Woodburn@TsumiKAMI @Rathnait @missalot @8661 @fireball2236 @growl @风荷 @箜茗潇 @孤冥 @好想吃奶油小蛋糕 @Dorothy @Tokur @悠哉卡萌睡大觉 @Kris Dreemurr 召唤版主和未来的版主们~ 注释 JimmyLok4379 20.00节操 aruui 39.00节操 很便利呢~ MCIN 99.00节操 好活!打钱!! safcz 100.00节操 伟大的发明 ㅤ凯ㅤ 10.00节操 真棒! lemon19 100.00节操 好东西 当赏 月晓 100.00节操 真的牛逼 好想吃奶油小蛋糕 39.00节操 给Tetro酱递小蛋糕~写脚本辛苦啦~! 羊駝 100.00节操 TsumiKAMI 99.00节操 劲爽啊-劲爽啊- 6 5 3
Kris Dreemurr 发布于6 小时前 发布于6 小时前 · 只看该作者 好厉害 话说这是什么召唤阵 那我@人事和幕僚来看吧·· 剧透 人事组 & 幕僚团 人事管理: @天空 全部权限 @苍云静岳 全部权限 @ppzt @凌若 有用户组与用户调整权限 @闇夜の影 有接近全部权限 @ベルンカステル @风荷 @乱跑的泰兰德 (辅助) 幕僚团: @悠哉卡萌睡大觉 @予鲤倾碧塘 @悠哉卡萌睡大觉 (有用户调整权限) @Dorothy @奈々原風子(美工相关) @阿梓喵 @endymx(活动相关) @提辖 @AzuXiie @梦幻妖精 (勋章相关) @内鬼 @Tokur (wiki相关) @mylifeyouwill (外宣相关) @苍云静岳 (论坛整体调整相关) 注释 tetrohedro 1.00节操 母母的召唤阵好可怕~
JimmyLok4379 发布于1 小时前 发布于1 小时前 · 只看该作者 7 小时前,哥特的亡零说道: 太好了!俺终于可以和字数计算说拜拜哩 同盟黑科技大佬+1 话说四面体+科技是不是很有科幻感 大佬你回來了。 不用再數字數AI或手動了,一鍵評分
推荐贴
创建帐号或登入才能点评
您必须成为用户才能点评
创建帐号
在我们社区注册个新的帐号。非常简单!
注册新帐号登入
已有帐号? 登入
现在登入