CC98-TAGs

为CC98用户添加可持久化的多标签功能

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         CC98-TAGs
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  为CC98用户添加可持久化的多标签功能
// @license      MIT
// @author       萌萌人
// @match        http://www-cc98-org-s.webvpn.zju.edu.cn:8001/*
// @match        https://www.cc98.org/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// ==/UserScript==

(function () {
    'use strict';

    // 自定义样式
    const css = `
.user-tags-container {
    margin-top: 8px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    width: 100%; /* 让容器宽度跟随父容器 */
}
.user-tag {
    display: inline-flex;
    align-items: center;
    padding: 2px 6px; /* 上下左右的内边距 */
    border-radius: 4px;
    font-size: 0.75rem;
    color: white;
    cursor: pointer;
    position: relative;
    max-width: 90%; /* 限制最大宽度 */
    overflow: hidden; /* 超出部分隐藏 */
    text-overflow: ellipsis; /* 超出部分显示省略号 */
    white-space: normal; /* 允许换行 */
    flex-shrink: 0; /* 允许缩小 */
    box-sizing: border-box; /* 确保 padding 不影响宽度计算 */
    word-wrap: break-word; /* 允许长单词换行 */
    overflow-wrap: break-word; /* 确保长单词和字符串在必要时换行 */
}
.user-tag:hover::after {
    content: '×';
    margin-left: 4px;
    font-size: 0.9em;
}
.add-tag-btn {
    background: #ddd;
    color: #666;
    border: 1px dashed #999;
    cursor: pointer;
    max-width: 100%; /* 限制最大宽度 */
    overflow: hidden; /* 超出部分隐藏 */
    text-overflow: ellipsis; /* 超出部分显示省略号 */
    white-space: nowrap; /* 不换行 */
}
.add-tag-btn:hover {
    background: #eee;
}
.import-export-menu {
    position: relative; /* 使用绝对定位 */
    right: 0; /* 对齐到页面最右边 */
    top: 55%; /* 垂直居中 */
    transform: translateY(-50%); /* 通过 transform 微调垂直居中 */
    display: inline-block;
    margin-left: 10px;
    padding-bottom: 5px; /* 增加底部 padding,扩展悬停区域 */
}

.import-export-menu button {
    background: none;
    border: none;
    color: white; /* 文字颜色为白色 */
    cursor: pointer;
    font-size: 16px;
    display: flex; /* 使用 flexbox 布局 */
    align-items: center; /* 垂直居中 */
    justify-content: center; /* 水平居中 */
    height: 100%; /* 确保按钮高度与父容器一致 */
}

.import-export-menu button:hover {
    color: #ccc; /* 鼠标悬停时文字颜色变为浅灰色 */
}

.import-export-dropdown {
    display: none;
    position: absolute;
    background-color: #f9f9f9;
    width: 100%; /* 宽度与按钮对齐 */
    min-width: 80pt;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    z-index: 1;
    right: 0; /* 下拉菜单也对齐到最右边 */
    top: 100%; /* 下拉菜单在按钮下方 */
    margin-top: 0;
    text-align: center; /* 下拉菜单文字居中 */
}

.import-export-dropdown a {
    color: black;
    padding: 8px 16px; /* 调整下拉菜单项的内边距 */
    text-decoration: none;
    display: block;
    text-align: center; /* 下拉菜单文字居中 */
}

.import-export-dropdown a:hover {
    background-color: #f1f1f1;
}

.import-export-menu:hover .import-export-dropdown {
    display: block;
}
`;
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // 获取UID
    const getUidFromLink = (link) => {
        const match = link.href.match(/\/user\/id\/(\d+)/);
        return match ? match[1] : null;
    };

    // 固定颜色数组
    const colors = ['#FF6B6B', '#4ECDC4', '#45B7D5', '#54C6EB', '#6BFF6B', '#FFD166', '#A06CD5'];

    // 根据UID获取颜色
    const getColorByUid = (uid, index) => {
        const uidNumber = parseInt(uid, 10); // 将UID转换为数字
        const colorIndex = (uidNumber + index) % colors.length; // 对颜色数组长度取模
        return colors[colorIndex];
    };

    // 创建标签
    const createTag = (uid, tag, index) => {
        const tagElem = document.createElement('div');
        tagElem.className = 'user-tag';
        tagElem.textContent = tag;
        tagElem.style.backgroundColor = getColorByUid(uid, index); // 使用UID确定颜色
        tagElem.onclick = () => {
            if (confirm(`确定要删除标签 "${tag}" 吗?`)) {
                const tags = GM_getValue(`tags_${uid}`, []);
                tags.splice(index, 1);
                GM_setValue(`tags_${uid}`, tags);
                updateTags(uid);
            }
        };
        return tagElem;
    };

    // 创建添加标签按钮
    const createAddButton = (uid) => {
        const btn = document.createElement('div');
        btn.className = 'user-tag add-tag-btn';
        btn.textContent = '+';
        btn.style.backgroundColor = '#FFFFFF'; // 使用UID确定颜色
        btn.onclick = () => {
            const tag = prompt('请输入新标签:');
            if (tag && tag.trim()) {
                const tags = GM_getValue(`tags_${uid}`, []);
                tags.push(tag.trim());
                GM_setValue(`tags_${uid}`, tags);
                updateTags(uid);
            }
        };
        return btn;
    };

    // 更新标签显示
    const updateTags = (uid) => {
        const containers = document.querySelectorAll(`.user-tags-container[data-uid="${uid}"]`);
        containers.forEach(container => {
            container.innerHTML = '';
            const tags = GM_getValue(`tags_${uid}`, []);
            tags.forEach((tag, index) => {
                container.appendChild(createTag(uid, tag, index));
            });
            container.appendChild(createAddButton(uid));

            // 动态调整标签宽度
            const userMessage = container.closest('.userMessage');
            if (userMessage) {
                const userMessageWidth = userMessage.offsetWidth * 0.75;
                container.style.width = `${userMessageWidth}px`; // 设置标签容器宽度
            }
        });
    };

    // 初始化标签容器
    const initTagsContainer = (uid, postId) => {
        const container = document.createElement('div');
        container.className = 'user-tags-container';
        container.setAttribute('data-uid', uid);
        container.setAttribute('data-post-id', postId); // 添加帖子ID作为唯一标识
        return container;
    };

    // 检查是否需要刷新标签
    const shouldUpdateTags = () => {
        const currentUrl = window.location.href;
        // 检查URL是否以 /topic/数字 结尾
        return /\/topic\/\d+(\/.+)?$/.test(currentUrl);
    };

    // 主更新函数
    const updateAllTags = () => {
        if (!shouldUpdateTags()) return; // 如果不需要刷新标签,则直接返回

        document.querySelectorAll('.userMessage-left').forEach(container => {
            const link = container.querySelector('a[href*="/user/id/"]');
            if (!link) return;

            const uid = getUidFromLink(link);
            const postId = container.closest('.reply').id; // 获取当前楼层的ID
            const existingContainer = container.querySelector(`.user-tags-container[data-post-id="${postId}"]`);
            if (existingContainer) existingContainer.remove();

            const tagsContainer = initTagsContainer(uid, postId);
            const infoContainer = container.querySelector('.column[style*="padding-left: 1.5rem"]');
            if (infoContainer) {
                infoContainer.appendChild(tagsContainer);
                updateTags(uid);
            }
        });
    };

    // 导出标签数据(明文或Base64编码)
    const exportTags = (encode = false, copyToClipboard = false) => {
        const allTags = {};
        const allKeys = GM_listValues();
        allKeys.forEach(key => {
            if (key.startsWith('tags_')) {
                const uid = key.replace('tags_', '');
                allTags[uid] = GM_getValue(key, []);
            }
        });
        const jsonData = JSON.stringify(allTags, null, 2);
        const outputData = encode ? btoa(unescape(encodeURIComponent(jsonData))) : jsonData;

        if (copyToClipboard) {
            // 创建一个文本框用于显示导出的数据
            const textarea = document.createElement('textarea');
            textarea.style.position = 'fixed';
            textarea.style.top = '0';
            textarea.style.left = '0';
            textarea.style.width = '100%';
            textarea.style.height = '200px';
            textarea.style.zIndex = '10000';
            textarea.style.backgroundColor = '#fff';
            textarea.style.border = '1px solid #ccc';
            textarea.style.padding = '10px';
            textarea.style.boxSizing = 'border-box';
            textarea.style.fontFamily = 'monospace';
            textarea.style.fontSize = '14px';
            textarea.value = outputData;

            // 添加一个关闭按钮
            const closeButton = document.createElement('button');
            closeButton.textContent = '关闭';
            closeButton.style.position = 'fixed';
            closeButton.style.top = '210px';
            closeButton.style.left = '50%';
            closeButton.style.zIndex = '10001';
            closeButton.style.backgroundColor = '#f44336';
            closeButton.style.color = '#fff';
            closeButton.style.border = 'none';
            closeButton.style.padding = '5px 10px';
            closeButton.style.cursor = 'pointer';
            closeButton.onclick = () => {
                document.body.removeChild(textarea);
                document.body.removeChild(closeButton);
            };

            // 将文本框和按钮添加到页面
            document.body.appendChild(textarea);
            document.body.appendChild(closeButton);

            // 自动选中文本框内容
            textarea.select();
        } else {
            const blob = new Blob([outputData], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = encode ? 'cc98_tags_export_base64.txt' : 'cc98_tags_export.json';
            a.click();
            URL.revokeObjectURL(url);
        }
    };

    // 导入标签数据(明文或Base64编码)
    const importTags = (encoded = false, useTextInput = false) => {
        if (useTextInput) {
            // 创建一个大文本框
            const textarea = document.createElement('textarea');
            textarea.style.position = 'fixed';
            textarea.style.top = '0';
            textarea.style.left = '0';
            textarea.style.width = '100%';
            textarea.style.height = '200px';
            textarea.style.zIndex = '10000';
            textarea.style.backgroundColor = '#fff';
            textarea.style.border = '1px solid #ccc';
            textarea.style.padding = '10px';
            textarea.style.boxSizing = 'border-box';
            textarea.style.fontFamily = 'monospace';
            textarea.style.fontSize = '14px';
            textarea.placeholder = '请在此粘贴要导入的标签数据...';

            // 创建导入按钮
            const importButton = document.createElement('button');
            importButton.textContent = '导入';
            importButton.style.position = 'fixed';
            importButton.style.top = '210px';
            importButton.style.left = '50%';
            importButton.style.transform = 'translateX(-100%)'; // 向左偏移 50% 的宽度
            importButton.style.zIndex = '10001';
            importButton.style.backgroundColor = '#4CAF50';
            importButton.style.color = '#fff';
            importButton.style.border = 'none';
            importButton.style.padding = '5px 10px';
            importButton.style.cursor = 'pointer';
            importButton.onclick = () => {
                const jsonData = textarea.value.trim();
                if (!jsonData) {
                    alert('请输入要导入的数据!');
                    return;
                }

                try {
                    let parsedData = jsonData;
                    if (encoded) {
                        parsedData = decodeURIComponent(escape(atob(jsonData)));
                    }
                    const tagsData = JSON.parse(parsedData);
                    for (const uid in tagsData) {
                        if (tagsData.hasOwnProperty(uid)) {
                            GM_setValue(`tags_${uid}`, tagsData[uid]);
                        }
                    }
                    alert('标签导入成功!');
                    updateAllTags();
                    document.body.removeChild(textarea);
                    document.body.removeChild(importButton);
                    document.body.removeChild(closeButton);
                } catch (error) {
                    alert('导入失败:数据格式不正确!');
                }
            };

            // 创建关闭按钮
            const closeButton = document.createElement('button');
            closeButton.textContent = '关闭';
            closeButton.style.position = 'fixed';
            closeButton.style.top = '210px';
            closeButton.style.left = '50%';
            closeButton.style.zIndex = '10001';
            closeButton.style.backgroundColor = '#f44336';
            closeButton.style.color = '#fff';
            closeButton.style.border = 'none';
            closeButton.style.padding = '5px 10px';
            closeButton.style.cursor = 'pointer';
            closeButton.onclick = () => {
                document.body.removeChild(textarea);
                document.body.removeChild(importButton);
                document.body.removeChild(closeButton);
            };

            // 将文本框和按钮添加到页面
            document.body.appendChild(textarea);
            document.body.appendChild(importButton);
            document.body.appendChild(closeButton);

            // 自动聚焦文本框
            textarea.focus();
        } else {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.txt,.json';
            input.onchange = (event) => {
                const file = event.target.files[0];
                if (!file) return;

                const reader = new FileReader();
                reader.onload = (e) => {
                    try {
                        let jsonData = e.target.result;
                        if (encoded) {
                            jsonData = decodeURIComponent(escape(atob(jsonData)));
                        }
                        const tagsData = JSON.parse(jsonData);
                        for (const uid in tagsData) {
                            if (tagsData.hasOwnProperty(uid)) {
                                GM_setValue(`tags_${uid}`, tagsData[uid]);
                            }
                        }
                        alert('标签导入成功!');
                        updateAllTags();
                    } catch (error) {
                        alert('导入失败:文件格式不正确!');
                    }
                };
                reader.readAsText(file);
            };
            input.click();
        }
    };


    const clearTags = () => {
        if (confirm("确定要清除所有用户的标签吗?")) {
            GM_listValues().forEach(key => {
                if (key.startsWith('tags_')) GM_deleteValue(key);
            });
            updateAllTags();
        }
    }

    // 创建导入导出菜单
    const createImportExportMenu = () => {
        const menuContainer = document.createElement('div');
        menuContainer.className = 'import-export-menu';

        const menuButton = document.createElement('button');
        menuButton.textContent = '导入/导出';
        menuContainer.appendChild(menuButton);

        const dropdown = document.createElement('div');
        dropdown.className = 'import-export-dropdown';

        const exportBase64 = document.createElement('a');
        exportBase64.textContent = '导出标签';
        exportBase64.onclick = () => exportTags(true, true);
        dropdown.appendChild(exportBase64);

        const importBase64 = document.createElement('a');
        importBase64.textContent = '导入标签';
        importBase64.onclick = () => importTags(true, true);
        dropdown.appendChild(importBase64);

        const clearAllTags = document.createElement('a');
        clearAllTags.textContent = '清除所有';
        clearAllTags.onclick = () => clearTags();
        dropdown.appendChild(clearAllTags);

        menuContainer.appendChild(dropdown);

        // 将菜单插入到页面左上方
        const topBar = document.querySelector('.topBar');
        if (topBar) {
            topBar.appendChild(menuContainer);
        }
    };


    // 初始化
    const init = () => {
        updateAllTags();
        createImportExportMenu();

        setInterval(updateAllTags, 1000); // 每1000ms更新一次tag
    };

    // 延迟初始化,确保页面加载完成
    setTimeout(init, 1000);

    // Tampermonkey菜单命令
    GM_registerMenuCommand("清除所有用户标签", () => {
        if (confirm("确定要清除所有用户的标签吗?")) {
            GM_listValues().forEach(key => {
                if (key.startsWith('tags_')) GM_deleteValue(key);
            });
            updateAllTags();
        }
    });

    GM_registerMenuCommand("导出标签(json)", () => exportTags(false));
    GM_registerMenuCommand("导入标签(json)", () => importTags(false));
})();