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