您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
為 Mobile01 支持自訂會員標籤,便於區分特定帳號
// ==UserScript== // @name Mobile01 會員標籤 // @namespace http://tampermonkey.net/ // @version 1.0.3 // @author ziugat // @description 為 Mobile01 支持自訂會員標籤,便於區分特定帳號 // @license MIT // @homepage https://github.com/ZiugatWong/mobile01-user-tags // @supportURL https://github.com/ZiugatWong/mobile01-user-tags // @match http://www.mobile01.com/forumtopic.php* // @match http://www.mobile01.com/topiclist.php* // @match http://www.mobile01.com/topicdetail.php* // @match https://www.mobile01.com/forumtopic.php* // @match https://www.mobile01.com/topiclist.php* // @match https://www.mobile01.com/topicdetail.php* // @icon https://www.mobile01.com/favicon.ico // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; let tags = GM_getValue('tags', []); let userTags = GM_getValue('userTags', {}); let nameSelectors = '.u-username,.c-link--gn'; // global css GM_addStyle(` .tag-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #323231; padding: 20px 20px 20px 20px; border-radius: 8px; border: 1px solid #ccc; z-index: 9999; box-shadow: 0 0 10px rgba(0,0,0,0.2); min-width: 300px; } .tag-item { margin: 2px; padding: 2px; display: flex; justify-content: space-between; align-items: center; } .user-list { height: 300px; overflow-y: auto; margin: 10px 0; border-top: 1px solid #eee; padding-top: 10px; } .tag-list { height: 300px; overflow-y: auto; margin: 10px 0; border-top: 1px solid #eee; padding-top: 10px; } .user-tag { display: inline-block; margin-left: 5px; padding: 2px 5px; border-radius: 3px; font-size: 0.8em; color: white; text-shadow: 0 1px 1px rgba(0,0,0,0.2); } .user-tags { display: inline !important; margin-left: 8px; } .context-menu { position: absolute; background: #323231; border: 1px solid #555; border-radius: 4px; padding: 5px 0; z-index: 10000; box-shadow: 0 2px 10px rgba(0,0,0,0.2); } .context-menu-item { padding: 8px 15px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: space-between; } .context-menu-item:hover { background: #444; } .context-menu-item .context-menu-user-tag { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; color: white; text-shadow: 0 1px 1px rgba(0,0,0,0.2); margin-left: 10px; white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; } .context-menu-item .menu-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tag-controls { display: flex; gap: 5px; } `); // panel for tag manager class TagManager { constructor() { this.currentTag = null; this.initPanel(); } initPanel() { this.panel = document.createElement('div'); this.panel.className = 'tag-panel'; this.panel.innerHTML = ` <div class="panel-header"> <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;"> <h3 style="margin: 0;">標籤儀表盤</h3> <div style="display: flex; align-items: center; justify-content: space-between; width: 45%;"> <button id="exportBtn" class="action-btn">匯出</button> <button id="importBtn" class="action-btn">匯入</button> <div class="close-btn">×</div> </div> </div> </div> <div class="panel-body"> <div class="input-group"> <h4 style="margin:0 0 10px">標籤管理</h4> <input type="text" id="tagName" placeholder="填入標籤名稱"> <input type="color" id="tagColor"> <button id="saveTag">保存</button> </div> <div class="tag-list"></div> <div class="user-list"></div> </div> `; document.body.appendChild(this.panel); // css addition GM_addStyle(` .panel-header { position: relative; padding: 0 0 15px 0; margin-bottom: 15px; border-bottom: 1px solid #555; } .close-btn { width: 24px; height: 24px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; border-radius: 4px; background: #444; border: 1px solid #555; } .close-btn:hover { color: #ff4444; background: #555; } `); // define css for export-panel GM_addStyle(` .export-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #323231; padding: 20px; border-radius: 8px; border: 1px solid #555; z-index: 10001; box-shadow: 0 0 20px rgba(0,0,0,0.5); width: 80%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; } .export-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #555; } .export-content { flex: 1; padding: 10px; background: #2a2a2a; border-radius: 4px; overflow: auto; margin-bottom: 15px; box-sizing: border-box; } .export-actions { display: flex; justify-content: flex-end; gap: 10px; } .export-btn { padding: 6px 12px; background: #444; border: 1px solid #555; border-radius: 4px; color: #eee; cursor: pointer; transition: all 0.2s; } .export-btn:hover { background: #555; } #exportData { font-family: monospace; font-size: 13px; line-height: 1.5; color: #ddd; margin: 0; white-space: pre-wrap; word-break: break-all; } `); GM_addStyle(` #importData { font-family: monospace; font-size: 13px; color: white; background: #2a2a2a; width: 100%; box-sizing: border-box; height: 300px; resize: none; padding: 8px; border: 1px solid #555; border-radius: 0px; } `); this.tagList = this.panel.querySelector('.tag-list'); this.userList = this.panel.querySelector('.user-list'); this.bindEvents(); this.renderTags(); } bindEvents() { this.panel.querySelector('#saveTag').addEventListener('click', () => this.saveTag()); this.panel.querySelector('.close-btn').addEventListener('click', () => { this.panel.remove(); }); this.panel.querySelector('#exportBtn').addEventListener('click', () => this.showExportPanel()); this.panel.querySelector('#importBtn').addEventListener('click', () => this.showImportPanel()); } renderTags() { this.tagList.innerHTML = ` <h4 style="margin:10px 0 10px">標籤清單</h4> ${tags.map((tag, index) => ` <div class="tag-item" data-index="${index}" title="點擊展示關聯會員"> <span class="user-tag" style="background:${tag.color}">${tag.name}</span> <div class="tag-controls"> <button class="edit">編輯</button> <button class="delete">移除</button> </div> </div> `).join('')} ${tags?.length ? '' : '<div style="color:#666">無標籤</div>'} `; this.tagList.querySelectorAll('.tag-item').forEach(item => { item.querySelector('.edit').addEventListener('click', (e) => { e.stopPropagation(); this.editTag(item.dataset.index); }); item.querySelector('.delete').addEventListener('click', (e) => { e.stopPropagation(); if (confirm('確定移除此標籤?')) this.deleteTag(item.dataset.index); }); item.addEventListener('click', () => this.showUsers(item.dataset.index)); }); } showUsers(index) { const tag = tags[index]; this.userList.innerHTML = ` <h4 style="margin:0 0 10px"><span class="user-tag" style="background:${tag.color}">${tag.name}</span> 關聯會員</h4> ${(userTags[tag.name] || []).map(user => ` <div style="padding:5px"> ${user} <button data-user="${user}" style="float:right">移除</button> </div> `).join('')} ${userTags[tag.name]?.length ? '' : '<div style="color:#666">未關聯會員</div>'} `; this.userList.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => this.removeUser(tag.name, btn.dataset.user)); }); } removeUser(tagName, user) { userTags[tagName] = (userTags[tagName] || []).filter(u => u !== user); GM_setValue('userTags', userTags); this.showUsers(tags.findIndex(t => t.name === tagName)); applyTags(); } saveTag() { const nameInput = this.panel.querySelector('#tagName'); const colorInput = this.panel.querySelector('#tagColor'); const name = nameInput.value.trim(); const color = colorInput.value; if (!name) { alert('必須填入標籤名稱'); return; } // rename tag let oldName = null; if (this.currentTag !== null) { oldName = tags[this.currentTag].name; } // upadate data if (this.currentTag !== null) { tags[this.currentTag] = { name, color }; if (oldName && oldName !== name && userTags[oldName]) { userTags[name] = userTags[oldName]; delete userTags[oldName]; } } else { tags.push({ name, color }); } GM_setValue('tags', tags); GM_setValue('userTags', userTags); this.currentTag = null; nameInput.value = ''; colorInput.value = '#000000'; this.renderTags(); applyTags(); } editTag(index) { const tag = tags[index]; this.panel.querySelector('#tagName').value = tag.name; this.panel.querySelector('#tagColor').value = tag.color; this.currentTag = index; } deleteTag(index) { const tagName = tags[index].name; tags.splice(index, 1); delete userTags[tagName]; GM_setValue('tags', tags); GM_setValue('userTags', userTags); this.renderTags(); applyTags(); } showExportPanel() { const exportData = { tags: GM_getValue('tags', []), userTags: GM_getValue('userTags', {}) }; const formattedData = JSON.stringify(exportData, null, 2); const exportPanel = document.createElement('div'); exportPanel.className = 'export-panel'; exportPanel.innerHTML = ` <div class="export-header"> <h3 style="margin:0">匯出標籤數據</h3> <div class="close-btn">×</div> </div> <div class="export-content"> <pre id="exportData">${formattedData}</pre> </div> <div class="export-actions"> <button id="copyExportBtn" class="export-btn">複製到剪貼簿</button> <button id="closeExportBtn" class="export-btn">關閉</button> </div> `; document.body.appendChild(exportPanel); exportPanel.querySelector('.close-btn').addEventListener('click', () => { exportPanel.remove(); }); exportPanel.querySelector('#closeExportBtn').addEventListener('click', () => { exportPanel.remove(); }); exportPanel.querySelector('#copyExportBtn').addEventListener('click', () => { this.copyToClipboard(formattedData, exportPanel.querySelector('#copyExportBtn')); }); const closeOnOutsideClick = (e) => { if (!exportPanel.contains(e.target)) { exportPanel.remove(); document.removeEventListener('click', closeOnOutsideClick); } }; setTimeout(() => { document.addEventListener('click', closeOnOutsideClick); }); } copyToClipboard(text, button) { navigator.clipboard.writeText(text).then(() => { const originalText = button.textContent; button.textContent = '已複製'; button.style.background = '#2e7d32'; setTimeout(() => { button.textContent = originalText; button.style.background = '#444'; }, 2000); }).catch(err => { console.error('複製失敗:', err); button.textContent = '複製失敗'; button.style.background = '#c62828'; setTimeout(() => { button.textContent = '複製到剪貼簿'; button.style.background = '#444'; }, 2000); }); } showImportPanel() { const importPanel = document.createElement('div'); importPanel.className = 'export-panel'; importPanel.innerHTML = ` <div class="export-header"> <h3 style="margin:0">匯入標籤數據</h3> <div class="close-btn">×</div> </div> <div class="export-content"> <textarea id="importData" placeholder="請貼上匯出的JSON格式標籤數據..."></textarea> </div> <div class="export-actions"> <button id="confirmImport" class="export-btn">確認匯入</button> <button id="cancelImport" class="export-btn">取消</button> </div> `; document.body.appendChild(importPanel); importPanel.querySelector('#importData').focus(); importPanel.querySelector('.close-btn').addEventListener('click', () => { importPanel.remove(); }); importPanel.querySelector('#cancelImport').addEventListener('click', () => { importPanel.remove(); }); importPanel.querySelector('#confirmImport').addEventListener('click', () => { this.handleImport(importPanel); }); const closeOnOutsideClick = (e) => { if (!importPanel.contains(e.target)) { importPanel.remove(); document.removeEventListener('click', closeOnOutsideClick); } }; setTimeout(() => { document.addEventListener('click', closeOnOutsideClick); }); } handleImport(panel) { const importData = panel.querySelector('#importData').value.trim(); if (!importData) { alert('請填入要匯入的JSON數據'); return; } try { const parsedData = JSON.parse(importData); if (!parsedData.tags || !parsedData.userTags) { throw new Error('JSON數據格式不正確,缺少tags或userTags索引鍵'); } if (!Array.isArray(parsedData.tags)) { throw new Error('tags必須是陣列'); } if (!confirm('匯入將覆蓋現有標籤數據,確定要繼續嗎?')) { return; } GM_setValue('tags', parsedData.tags); GM_setValue('userTags', parsedData.userTags); tags = parsedData.tags; userTags = parsedData.userTags; this.renderTags(); applyTags(); panel.remove(); alert('匯入成功'); } catch (error) { alert(`匯入失敗: ${error.message}`); } } } // show tags function applyTags() { document.querySelectorAll(nameSelectors).forEach(userEl => { const username = userEl.textContent.trim(); let tagsHtml = ''; Object.entries(userTags).forEach(([tagName, users]) => { const tagExists = tags.some(t => t.name === tagName); if (tagExists && users.includes(username)) { const tag = tags.find(t => t.name === tagName); tagsHtml += `<span class="user-tag" style="background:${tag.color}">${tag.name}</span>`; } }); // remove old tags let tagContainer = userEl.nextElementSibling; if (tagContainer?.classList?.contains('user-tags')) { tagContainer.remove(); } // show tags if (tagsHtml) { userEl.insertAdjacentHTML( "afterend", `<span class="user-tags" style="display: inline;">${tagsHtml}</span>` ); } }); } document.addEventListener('contextmenu', (e) => { const target = e.target.closest(nameSelectors); if (target) { e.preventDefault(); showContextMenu(target, e.pageX, e.pageY); } }); function showContextMenu(target, x, y) { const menu = document.createElement('div'); menu.className = 'context-menu'; menu.style.left = `${x}px`; menu.style.top = `${y}px`; if (tags.length === 0) { const item = document.createElement('div'); item.className = 'context-menu-item'; item.textContent = '請先創建標籤'; item.style.color = '#999'; item.style.cursor = 'default'; menu.appendChild(item); } else { const username = target.textContent.trim(); const hasTags = Object.entries(userTags).some(([tagName, users]) => tags.some(t => t.name === tagName) && users.includes(username) ); if (hasTags) { const removeItem = document.createElement('div'); removeItem.className = 'context-menu-item'; removeItem.innerHTML = '取消關聯的所有標籤'; removeItem.addEventListener('click', () => { Object.keys(userTags).forEach(tagName => { userTags[tagName] = userTags[tagName].filter(u => u !== username); }); GM_setValue('userTags', userTags); applyTags(); menu.remove(); }); menu.appendChild(removeItem); const divider = document.createElement('div'); divider.style.height = '1px'; divider.style.background = '#555'; divider.style.margin = '5px 0'; menu.appendChild(divider); } tags.forEach(tag => { const item = document.createElement('div'); item.className = 'context-menu-item'; const isTagged = (userTags[tag.name] || []).includes(username); const menuText = document.createElement('span'); menuText.className = 'menu-text'; menuText.textContent = isTagged ? '取消關聯' : '關聯到'; const tagSpan = document.createElement('span'); tagSpan.className = 'context-menu-user-tag'; tagSpan.textContent = tag.name; tagSpan.style.backgroundColor = tag.color; item.appendChild(menuText); item.appendChild(tagSpan); item.addEventListener('click', () => { if (isTagged) { // 取消關聯 userTags[tag.name] = (userTags[tag.name] || []).filter(u => u !== username); } else { // 添加關聯 userTags[tag.name] = [...new Set([...(userTags[tag.name] || []), username])]; } GM_setValue('userTags', userTags); applyTags(); menu.remove(); }); menu.appendChild(item); }); } document.body.appendChild(menu); // click other location to close const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeMenu); document.removeEventListener('contextmenu', closeMenu); } }; setTimeout(() => { document.addEventListener('click', closeMenu); document.addEventListener('contextmenu', closeMenu); }); } GM_registerMenuCommand("標籤儀表盤", () => { new TagManager(); }); applyTags(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址