转跳到内容

【脚本】四面体的快速评分教室~


推荐贴

发布于 · 只看该作者

前言

大家好,我是四面体~

:NEKOMIMI_PARADISE_8:

作为版主们,评分是版务不可或缺的一部分

大多数评分的标准主要基于

  • 帖子的时间
  • 帖子的字数

在皮卡攸和卡米那君,和其他DC群众的协助下,四面体制作了三个帮助评分的脚本~

查看字数的脚本

Screenshot%202026-02-27%20at%203.56.48%E

只要使用这个脚本,即可在帖子的右下角快速查看帖子的字数~

这样,就可以快速评判字数是否满足标准的说~

查看链接对应字数和回复时间的脚本

Screenshot%202026-02-27%20at%203.42.12%E

 

只要使用这个脚本,即可查看一个帖子里所有链接对应的字数和回复时间~

这样,就就可以快速了解一个用户是否满足任务条件的说~

批量评分的脚本

作为版主,在举办活动时,可能有很多的回复需要评分~

只要使用这个脚本,就可以批量选中回复,然后一次性评分的说~

剧透

查看字数的脚本

// ==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();
})();

 

 

注释
哥特的亡零 哥特的亡零 100.00节操 重新获得电脑控制权!发糖!(≧∀≦)ゞ
Woodburn Woodburn 100.00节操 我看不懂,但我大受震撼!!
  • 喜欢(+1) 4
  • 感谢(+1) 1
  • 顶(+1) 2

创建帐号或登入才能点评

您必须成为用户才能点评

创建帐号

在我们社区注册个新的帐号。非常简单!

注册新帐号

登入

已有帐号? 登入

现在登入
×
×
  • 新建...

重要消息

为使您更好地使用该站点,请仔细阅读以下内容: 使用条款