Greasy Fork

CC98-TAGs

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

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