Mobile01 會員標籤

為 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或关注我们的公众号极客氢云获取最新地址