用户备注

为用户添加显式备注防改名,如果需要长备注可以使用详细备注。

// ==UserScript==
// @name         用户备注
// @version      3.5
// @description  为用户添加显式备注防改名,如果需要长备注可以使用详细备注。
// @author       age_anime
// @match        https://bgm.tv/*
// @match        https://chii.in/*
// @match        https://bangumi.tv/*
// @license      MIT
// @namespace https://gf.qytechs.cn/users/1426310
// ==/UserScript==

(function () {
    'use strict';
    // 1.3版本说明:功能:无新功能。使用体验优化:增加对详细备注的修改框,不用挤在原生的一行弹窗里进行修改。不再兼容更早期的版本(未在bangumi上线),以优化加载速度。
    // 组件函数功能说明:(数据保存在本地,显示和修改在前端,可直接修改和清除)
    // displayUserNote(userId, note):在指定用户的用户名旁边显示备注标签
    // markUserNotes():显示对应的备注标签
    // openSecondaryNoteEditor():详细备注编辑界面
    // openUserNoteManager():用户备注管理界面
    // openColorManager():颜色管理界面
    // addButtonFunctions():添加功能按钮
    const defaultNoteColors = {
        "预设0": "#40E0D0",
        "预设1": "#BB44BB",
        "预设2": "#000000",
        "预设3": "#DD6D22",
        "预设4": "#CC3333",
        "预设5": "#DD6D22",
    };
    let noteColors = JSON.parse(localStorage.getItem('userNoteColors_')) || defaultNoteColors;

    // 用户备注配置(默认为空白)
    let userNoteTable_AGE = JSON.parse(localStorage.getItem('userNoteTable_AGE')) || {};

    // 迁移旧版本数据结构
    function migrateUserNotes() {
        let needMigration = false;

        // 检查是否需要兼容
        Object.keys(userNoteTable_AGE).forEach(userId => {
            if (typeof userNoteTable_AGE[userId] === 'string') {
                needMigration = true;
            }
        });

        if (needMigration) {
            const newUserNoteTable = {};

            // 旧版本兼容:兼容v3.4(b1.2)之前的版本
            Object.keys(userNoteTable_AGE).forEach(userId => {
                if (typeof userNoteTable_AGE[userId] === 'string') {
                    newUserNoteTable[userId] = [userNoteTable_AGE[userId], ""];
                } else {
                    newUserNoteTable[userId] = userNoteTable_AGE[userId];
                }
            });

            userNoteTable_AGE = newUserNoteTable;
            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
        }
    }

    migrateUserNotes();

    const defaultNote = "…";
    const defaultColor = "rgba(255, 255, 255, 0)";
    const defaultNoteColor = "rgba(102, 170, 85, 0.2)";
    const defaultSecondaryNote = "点击修改详细备注...";

    function displayUserNote(userId, notes) {
        const userAnchor = $("strong a.l[href='/user/" + userId + "']");
        if (userAnchor.length > 0 && userAnchor.next(".note-badge").length === 0) {
            let primaryNote, secondaryNote;

            if (Array.isArray(notes)) {
                [primaryNote, secondaryNote] = notes;
                secondaryNote = secondaryNote || "";
            } else {
                primaryNote = notes;
                secondaryNote = "";
            }
            const badgeColor = (primaryNote === defaultNote) ? defaultColor : (noteColors[primaryNote] || defaultNoteColor);
            const noteColorSet = badgeColor === defaultColor ? "#9F9F9F" : "#FFF";

            const badge = $(`
            <span class="note-badge" style="
                background: ${badgeColor};
                font-size: 10px;
                padding: 1px 4px;
                color: ${noteColorSet};
                border-radius: 6px;
                line-height: 150%;
                display: inline-block;
                cursor: pointer;
                position: relative;
            " title="双击修改备注">${primaryNote}</span>`);

            const badgeId = 'note-badge-' + userId + '-' + Math.random().toString(36).substr(2, 9);
            badge.attr('id', badgeId);
            badge.attr('data-user-id', userId);
            badge.attr('data-primary-note', primaryNote);
            badge.attr('data-secondary-note', secondaryNote);
            badge.attr('data-secondary-loaded', 'false');
            // 一级备注点击事件
            badge.click(function(e) {
                const userId = $(this).attr('data-user-id');
                const primaryNote = $(this).attr('data-primary-note');
                const storedSecondaryNote = $(this).attr('data-secondary-note');
                const secondaryLoaded = $(this).attr('data-secondary-loaded') === 'true';

                const $target = $(e.target);
                const isInteractiveElement = $target.is('a, button, [contenteditable="true"], input, select, textarea');

                const isSecondaryArea = $target.closest('.secondary-note').length > 0;

                // 处理二级备注点击
                if (isSecondaryArea) {
                    if (isInteractiveElement) {
                        return;
                    }

                    // 处理二级备注编辑
                    openSecondaryNoteEditor($target.closest('.secondary-note'), storedSecondaryNote, userId, primaryNote);
                    e.stopPropagation();
                    return;
                }

                // 处理一级备注逻辑
                let currentSecondaryBadge = $(this).find('.secondary-note');
                if (!secondaryLoaded) {
                        currentSecondaryBadge = $(`
                        <div class="secondary-note" style="
                            display: none;
                            position: absolute;
                            top: 100%;
                            left: 0;
                            width: 200px;
                            background: white;
                            border: 1px solid #ccc;
                            border-radius: 4px;
                            padding: 5px;
                            margin-top: 5px;
                            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                            z-index: 1000;
                            font-size: 12px;
                            color: ${storedSecondaryNote ? '#333' : '#999'};
                            cursor: pointer;
                            max-height: 400px;
                            overflow-y: auto;
                            word-wrap: break-word;
                            resize: both;
                        " title="点击编辑">${storedSecondaryNote || defaultSecondaryNote}</div>`);
                        currentSecondaryBadge.on('click', 'a', function(e) {
                        e.stopPropagation();
                    });

                    $(this).append(currentSecondaryBadge);
                    $(this).attr('data-secondary-loaded', 'true');
                }

                if (currentSecondaryBadge.is(':visible')) {
                    // 保持原生弹框样式
                    const newNote = prompt("请输入新的备注:", primaryNote);
                    if (newNote !== null) {
                        const trimmedNote = newNote.trim();
                        if (trimmedNote === "" || trimmedNote === defaultNote) {
                            delete userNoteTable_AGE[userId];
                            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
                            $(this).remove();
                        } else if (trimmedNote !== primaryNote) {
                            if (Array.isArray(userNoteTable_AGE[userId])) {
                                userNoteTable_AGE[userId][0] = trimmedNote;
                            } else {
                                userNoteTable_AGE[userId] = [trimmedNote, storedSecondaryNote];
                            }
                            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
                            $(this).contents().first().replaceWith(document.createTextNode(trimmedNote));
                            const newBadgeColor = noteColors[trimmedNote] || defaultNoteColor;
                            $(this).css("background", newBadgeColor);
                            $(this).attr('data-primary-note', trimmedNote);
                        }
                    }
                } else {
                    $('.secondary-note').hide();
                    currentSecondaryBadge.show();
                }
            });

            // 点击其他区域关闭二级备注
            if (!window.noteClickHandlerAdded) {
                $(document).on('click', function(e) {
                    if (!$(e.target).closest('.note-badge').length) {
                        $('.secondary-note').hide();
                    }
                });
                window.noteClickHandlerAdded = true;
            }

            userAnchor.after(badge);
        }
    }

    // 打开二级备注编辑器
    function openSecondaryNoteEditor(secondaryNoteElement, currentNote, userId, primaryNote) {
    const modal = document.createElement('div');
    Object.assign(modal.style, {
        position: 'fixed',
        left: '50%',
        top: '50%',
        transform: 'translate(-50%, -50%)',
        backgroundColor: 'white',
        padding: '20px',
        borderRadius: '5px',
        boxShadow: '0 0 10px rgba(0,0,0,0.5)',
        zIndex: '9999',
        width: '300px'
    });

    const title = document.createElement('h3');
    title.textContent = '编辑详细备注';
    title.style.marginTop = '0';
    title.style.color = '#333';

    const noteTextarea = document.createElement('textarea');
    noteTextarea.value = currentNote;
    noteTextarea.style.width = '100%';
    noteTextarea.style.height = '150px';
    noteTextarea.style.padding = '5px';
    noteTextarea.style.boxSizing = 'border-box';
    noteTextarea.style.resize = 'vertical';

    // 添加链接
    const linkForm = document.createElement('div');
    linkForm.style.margin = '10px 0';
    linkForm.style.padding = '10px';
    linkForm.style.backgroundColor = '#f5f5f5';
    linkForm.style.borderRadius = '4px';

    const linkLabel = document.createElement('label');
    linkLabel.textContent = '添加链接:';
    linkLabel.style.color = 'black';
    linkLabel.style.display = 'block';
    linkLabel.style.marginBottom = '5px';

    const urlLabel = document.createElement('label');
    urlLabel.textContent = 'URL:';
    urlLabel.style.color = 'black';
    urlLabel.style.display = 'inline-block';
    urlLabel.style.width = '50px';

    const urlInput = document.createElement('input');
    urlInput.type = 'text';
    urlInput.placeholder = 'https://';
    urlInput.style.width = 'calc(100% - 60px)';
    urlInput.style.marginBottom = '5px';

    const titleLabel = document.createElement('label');
    titleLabel.textContent = '标题:';
    titleLabel.style.color = 'black';
    titleLabel.style.display = 'inline-block';
    titleLabel.style.width = '50px';

    const titleInput = document.createElement('input');
    titleInput.type = 'text';
    titleInput.placeholder = '链接标题';
    titleInput.style.width = 'calc(100% - 60px)';

    const addLinkButton = document.createElement('button');
    addLinkButton.textContent = '插入链接';
    addLinkButton.style.marginTop = '5px';
    addLinkButton.onclick = () => {
        const linkUrl = urlInput.value.trim();
        const linkTitle = titleInput.value.trim() || '链接';
        
        if (linkUrl) {
            const selectionStart = noteTextarea.selectionStart;
            const selectionEnd = noteTextarea.selectionEnd;
            const selectedText = noteTextarea.value.substring(selectionStart, selectionEnd);
            const finalTitle = selectedText || linkTitle;
            const linkHtml = `<a href="${linkUrl}" style="color:blue;" rel="nofollow">${finalTitle}</a>`;
            const beforeText = noteTextarea.value.substring(0, selectionStart);
            const afterText = noteTextarea.value.substring(selectionEnd);
            
            noteTextarea.value = beforeText + linkHtml + afterText;
            urlInput.value = '';
            titleInput.value = '';
            // 将光标定位到插入的内容之后
            const newCursorPos = selectionStart + linkHtml.length;
            noteTextarea.setSelectionRange(newCursorPos, newCursorPos);
            noteTextarea.focus();
        }
    };

    linkForm.append(linkLabel, urlLabel, urlInput, document.createElement('br'), 
                   titleLabel, titleInput, document.createElement('br'), addLinkButton);

    const buttonContainer = document.createElement('div');
    buttonContainer.style.cssText = 'text-align: right; margin-top: 15px;';

    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '取消';
    cancelBtn.onclick = () => modal.remove();

    const saveBtn = document.createElement('button');
    saveBtn.textContent = '保存';
    saveBtn.style.marginLeft = '8px';
    saveBtn.onclick = () => {
        const newSecondaryNote = noteTextarea.value.trim();
        if (primaryNote === defaultNote && newSecondaryNote === "") {
            delete userNoteTable_AGE[userId];
            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
            secondaryNoteElement.closest('.note-badge').remove();
        } else {
            if (Array.isArray(userNoteTable_AGE[userId])) {
                userNoteTable_AGE[userId][1] = newSecondaryNote;
            } else {
                userNoteTable_AGE[userId] = [primaryNote, newSecondaryNote];
            }
            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
            secondaryNoteElement.text(newSecondaryNote || defaultSecondaryNote)
                   .css('color', newSecondaryNote ? '#333' : '#999');
            secondaryNoteElement.closest('.note-badge').attr('data-secondary-note', newSecondaryNote);
        }
        modal.remove();
    };

    buttonContainer.append(cancelBtn, saveBtn);
    modal.append(title, noteTextarea, linkForm, buttonContainer);
    document.body.appendChild(modal);
}

    function markUserNotes() {
        $(".note-badge").remove();
        $("strong a.l:not(.avatar)").each(function () {
            const userLink = $(this).attr("href");
            const userId = userLink.split("/").pop();
            const notes = userNoteTable_AGE[userId] || defaultNote;
            displayUserNote(userId, notes);
        });
    }

    // 按钮颜色管理区
    function openColorManager() {
        const modal = document.createElement('div');
        Object.assign(modal.style, {
            position: 'fixed',
            left: '50%',
            top: '50%',
            transform: 'translate(-50%, -50%)',
            backgroundColor: 'white',
            padding: '20px',
            borderRadius: '5px',
            boxShadow: '0 0 10px rgba(0,0,0,0.5)',
            zIndex: '9999',
            width: '300px'
        });

        // 添加颜色
        const form = document.createElement('div');
        form.style.marginBottom = '5px';

        const nameLabel = document.createElement('label');
        nameLabel.textContent = '名称:';
        nameLabel.style.color = '#333333';
        nameLabel.style.marginRight = '5px';

        const nameInput = document.createElement('input');
        nameInput.type = 'text';
        nameInput.style.minWidth = '240px';
        nameInput.style.width = 'auto';
        nameInput.style.height = '22px';
        nameInput.style.marginRight = '5px';
        nameInput.style.position = 'relative';
        nameInput.style.top = '-5px';

        const colorLabel = document.createElement('label');
        colorLabel.textContent = '颜色:';
        colorLabel.style.color = '#333333';
        colorLabel.style.marginRight = '5px';

        const colorInput = document.createElement('input');
        colorInput.type = 'color';
        colorInput.style.marginRight = '5px';

        const addButton = document.createElement('button');
        addButton.textContent = '添加';
        addButton.onclick = () => {
            const name = nameInput.value.trim();
            const color = colorInput.value;
            if (name && color) {
                noteColors[name] = color;
                textarea.value = JSON.stringify(noteColors, null, 4);
                nameInput.value = '';
                colorInput.value = '#000000';
            } else {
                alert('名称和颜色不能为空!');
            }
        };

        form.append(nameLabel, nameInput, colorLabel, colorInput, addButton);

        const textarea = document.createElement('textarea');
        textarea.style.cssText = 'width: 100%; height: 200px; margin-bottom: 10px;';
        textarea.value = JSON.stringify(noteColors, null, 4);

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'text-align: right;';

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = '取消';
        cancelBtn.onclick = () => modal.remove();

        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存';
        saveBtn.style.marginLeft = '8px';
        saveBtn.onclick = () => {
            try {
                const newColors = JSON.parse(textarea.value);
                if (typeof newColors !== 'object' || newColors === null) {
                    throw new Error('请输入有效的JSON对象');
                }
                localStorage.setItem('userNoteColors_', JSON.stringify(newColors));
                noteColors = newColors;
                document.querySelectorAll('.note-badge').forEach(badge => {
                    const note = badge.textContent;
                    badge.style.backgroundColor = noteColors[note] || defaultNoteColor;
                });
                modal.remove();
            } catch (error) {
                alert('错误: 请检查JSON格式(其他行的末尾是有英文逗号的,最后一行的末尾是没有逗号的!),还不会就把错误代码和JSON内容放在AI里面问问: ' + error.message);
            }
        };

        buttonContainer.append(cancelBtn, saveBtn);
        modal.append(form, textarea, buttonContainer);
        document.body.appendChild(modal);
    }
    // 用户备注管理区
    function openUserNoteManager() {
        const modal = document.createElement('div');
        Object.assign(modal.style, {
            position: 'fixed',
            left: '50%',
            top: '50%',
            transform: 'translate(-50%, -50%)',
            backgroundColor: 'white',
            padding: '20px',
            borderRadius: '5px',
            boxShadow: '0 0 10px rgba(0,0,0,0.5)',
            zIndex: '9999',
            width: '300px'
        });

        // 添加上方输入框
        const form = document.createElement('div');
        form.style.marginBottom = '10px';

        const idInput = document.createElement('input');
        idInput.placeholder = '用户ID';
        idInput.style.marginRight = '5px';

        const noteInput = document.createElement('input');
        noteInput.placeholder = '备注';
        noteInput.style.marginRight = '5px';

        const secondaryNoteInput = document.createElement('input');
        secondaryNoteInput.placeholder = '详细备注(可选)';
        secondaryNoteInput.style.marginRight = '5px';
        secondaryNoteInput.style.width = '100%';

        const addButton = document.createElement('button');
        addButton.textContent = '添加';
        addButton.onclick = () => {
            const userId = idInput.value.trim();
            const note = noteInput.value.trim();
            const secondaryNote = secondaryNoteInput.value.trim();
            if (userId && note) {
                userNoteTable_AGE[userId] = [note, secondaryNote];
                textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
                idInput.value = '';
                noteInput.value = '';
                secondaryNoteInput.value = '';
            }
        };

        form.append(idInput, noteInput, document.createElement('br'), secondaryNoteInput, addButton);
        // JSON编辑区
        const textarea = document.createElement('textarea');
        textarea.style.cssText = 'width: 100%; height: 200px; margin: 10px 0;';
        textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'text-align: right;';
        const importBtn = document.createElement('button');
        importBtn.textContent = '导入';
        importBtn.onclick = () => {
            const input = document.createElement("input");
            input.type = "file";
            input.accept = "application/json";
            input.onchange = function (event) {
                const file = event.target.files[0];
                if (file) {
                    const reader = new FileReader();
                    reader.onload = function (e) {
                        try {
                            const imported = JSON.parse(e.target.result);
                            userNoteTable_AGE = imported; // 直接替换当前内容
                            textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
                            localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
                            markUserNotes(); // 刷新页面显示
                            alert("导入成功!");
                        } catch (error) {
                            alert("错误: 请检查JSON格式(其他行的末尾是有英文逗号的,最后一行的末尾是没有逗号的!),还不会就把错误代码和JSON内容放在AI里面询问:" + error);
                        }
                    };
                    reader.readAsText(file);
                }
            };
            input.click();
        };
        const exportBtn = document.createElement('button');
        exportBtn.textContent = '导出';
        exportBtn.style.marginLeft = '5px';
        exportBtn.onclick = () => {
            const blob = new Blob([textarea.value], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `user_notes_${Date.now()}.json`;
            a.click();
            URL.revokeObjectURL(url);
        };

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = '取消';
        cancelBtn.style.marginLeft = '5px';
        cancelBtn.onclick = () => modal.remove();

        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存';
        saveBtn.style.marginLeft = '5px';
        saveBtn.onclick = () => {
            try {
                const newNotes = JSON.parse(textarea.value);
                localStorage.setItem('userNoteTable_AGE', JSON.stringify(newNotes));
                userNoteTable_AGE = newNotes;
                markUserNotes();
                modal.remove();
            } catch (error) {
                alert('JSON格式错误:' + error.message);
            }
        };

        buttonContainer.append(importBtn, exportBtn, cancelBtn, saveBtn);
        modal.append(form, textarea, buttonContainer);
        document.body.appendChild(modal);
    }

    function addButtonFunctions() {
        const badgeUserPanel = document.querySelector("ul#badgeUserPanel");
        if (badgeUserPanel) {
            // 用户备注管理按钮
            const userNoteLi = document.createElement('li');
            const userNoteBtn = document.createElement('a');
            userNoteBtn.href = '#';
            userNoteBtn.textContent = '管理用户备注';
            userNoteBtn.onclick = (e) => { e.preventDefault(); openUserNoteManager(); };
            userNoteLi.appendChild(userNoteBtn);
            // 颜色管理按钮
            const colorLi = document.createElement('li');
            const colorBtn = document.createElement('a');
            colorBtn.href = '#';
            colorBtn.textContent = '管理颜色配置';
            colorBtn.onclick = (e) => { e.preventDefault(); openColorManager(); };
            colorLi.appendChild(colorBtn);

            badgeUserPanel.append(userNoteLi, colorLi);
        }
    }
    if (document.body) {
        markUserNotes();
    } else {
        window.onload = markUserNotes;
    }
    setTimeout(addButtonFunctions, 200);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址