影刀社区助手-极简版

显示影刀社区用户的优质标记

// ==UserScript==
// @name         影刀社区助手-极简版
// @namespace    DLjun
// @version      1.0
// @description  显示影刀社区用户的优质标记
// @author       过客&DLjun
// @match        https://www.yingdao.com/community/*
// @match        https://www.yingdao.com/community/discuss
// @connect      yingdao.com
// @grant        GM_xmlhttpRequest
// @license		 GPLv3
// @run-at		 document-idle
// ==/UserScript==

(function() {
    'use strict';

    // 存储数据
    const dataStore = {
        creatorIdsMap: {},
        userPublishDict: {},
        userAcceptRateDict: {}
    };

    // 添加CSS样式
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .yd-toast {
                position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px;
                border-radius: 4px; z-index: 10000; font-size: 14px; text-align: center;
            }
            .yd-tag-container { display: inline-block; margin-left: 10px; }
            .yd-tag {
                display: inline-block; margin-right: 8px; padding: 2px 6px;
                border-radius: 4px; font-size: 12px; color: white; line-height: 1.5;
                white-space: nowrap; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
            }
            .yd-full { background-color: #52c41a; }
            .yd-high { background-color: #95de64; }
            .yd-medium { background-color: rgb(231, 194, 29); }
            .yd-low { background-color: #ff7875; }
            .yd-none { background-color: #f5222d; }
            .yd-newbie { background-color: rgb(241, 127, 207); }
        `;
        document.head.appendChild(style);
    }

    // Toast提示管理
    const Toast = {
        element: null,
        create(message, duration = 3000) {
            document.getElementById('yd-toast')?.remove();
            
            this.element = document.createElement('div');
            this.element.id = 'yd-toast';
            this.element.className = 'yd-toast';
            this.element.textContent = message;
            document.body.appendChild(this.element);
            
            if (duration > 0) {
                setTimeout(() => this.element?.remove(), duration);
            }
            
            return this.element;
        },
        update(message) {
            if (!this.element || !this.element.parentNode) {
                return this.create(message);
            }
            this.element.textContent = message;
            return this.element;
        }
    };

    // 获取当前页面的页码
    function getCurrentPageNumber() {
        try {
            const urlParams = new URLSearchParams(window.location.search);
            const pageFromUrl = urlParams.get('page');
            if (pageFromUrl) return parseInt(pageFromUrl, 10);

            const activeItem = document.querySelector('.ant-pagination-item.ant-pagination-item-active');
            if (activeItem) return parseInt(activeItem.textContent.trim(), 10);
        } catch (error) {
            console.error('获取页码失败', error);
        }
        return 1;
    }

    // API请求
    const API = {
        async fetchQuestionData(page = 1, size = 20) {
            const toast = Toast.create("正在获取数据...");
            
            try {
                const url = `https://api.yingdao.com/api/noauth/v1/sns/forum/question/query?page=${page}&size=${size}&tags=%E9%97%AE%E7%AD%94&sort=createTime`;
                const response = await fetch(url, {
                    method: 'GET',
                    headers: {
                        'accept': 'application/json',
                        'content-type': 'application/json;charset=UTF-8',
                        'origin': 'https://www.yingdao.com',
                        'referer': 'https://www.yingdao.com/community'
                    },
                    credentials: 'include'
                });
                
                if (!response.ok) throw new Error(`请求失败: ${response.status}`);
                
                const data = await response.json();
                Toast.update("数据获取成功,正在处理...", toast);
                
                if (!data) throw new Error('数据为空');
                
                // 提取问题列表
                let questionList = [];
                if (data.data && Array.isArray(data.data)) {
                    questionList = data.data;
                } else if (data.data && data.data.list && Array.isArray(data.data.list)) {
                    questionList = data.data.list;
                } else if (data.list && Array.isArray(data.list)) {
                    questionList = data.list;
                } else if (Array.isArray(data)) {
                    questionList = data;
                } else {
                    throw new Error('无法提取问题列表');
                }
                
                // 重置数据
                dataStore.creatorIdsMap = {};
                dataStore.userPublishDict = {};
                dataStore.userAcceptRateDict = {};
                
                // 提取创建者ID
                questionList.forEach(item => {
                    if (item?.creator && item?.uuid) {
                        dataStore.creatorIdsMap[item.creator] = {
                            creatorName: item.creatorName || '未知'
                        };
                    }
                });
                
                if (Object.keys(dataStore.creatorIdsMap).length === 0) {
                    throw new Error('未找到创建者信息');
                }

                Toast.update(`正在获取用户数据...`, toast);
                
                // 获取所有创建者的发布列表
                await this.fetchAllUserPublishList(Object.keys(dataStore.creatorIdsMap), toast);
            } catch (error) {
                console.error('获取数据出错', error);
                Toast.update(`获取数据出错: ${error.message}`, toast);
            }
        },
        
        async fetchAllUserPublishList(creatorIds, toast) {
            if (!creatorIds?.length) {
                Toast.update("没有找到用户ID", toast);
                return;
            }
            
            // 批处理用户请求,每次处理5个
            const batchSize = 5;
            const batches = [];
            
            for (let i = 0; i < creatorIds.length; i += batchSize) {
                batches.push(creatorIds.slice(i, i + batchSize));
            }
            
            let completedCount = 0;
            const totalCount = creatorIds.length;
            
            for (const batch of batches) {
                await Promise.all(batch.map(async (creatorId) => {
                    try {
                        const data = await this.fetchUserPublishListPromise(creatorId);
                        completedCount++;
                        const percentage = Math.round((completedCount / totalCount) * 100);
                        Toast.update(`正在处理数据 ${completedCount}/${totalCount} (${percentage}%)`, toast);
                    } catch (error) {
                        completedCount++;
                        console.error(`获取用户 ${creatorId} 数据失败`, error);
                    }
                }));
            }
            
            Toast.update(`数据处理完成,成功:${Object.keys(dataStore.userPublishDict).length}/${creatorIds.length}`, toast);
            
            // 计算采纳率并添加标签
            UserTagManager.calculateUserRates();
            
            // 关闭提示
            setTimeout(() => toast?.remove(), 2000);
        },
        
        async fetchUserPublishListPromise(userUuid) {
            const url = 'https://api.yingdao.com/api/noauth/v1/sns/forum/question/queryUserPublishList';
            
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'accept': 'application/json',
                    'content-type': 'application/json;charset=UTF-8',
                    'origin': 'https://www.yingdao.com',
                    'referer': 'https://www.yingdao.com/'
                },
                credentials: 'include',
                body: JSON.stringify({
                    userUuid: userUuid,
                    tags: "问答",
                    behavior: "publish",
                    sort: "createTime",
                    page: 1,
                    size: 50
                })
            });
            
            if (!response.ok) throw new Error(`请求失败: ${response.status}`);
            
            const responseData = await response.json();
            if (responseData && responseData.data && Array.isArray(responseData.data)) {
                dataStore.userPublishDict[userUuid] = responseData.data;
            } else {
                dataStore.userPublishDict[userUuid] = responseData;
            }
            
            return responseData;
        }
    };

    // 用户标签管理
    const UserTagManager = {
        // 计算所有用户的采纳率
        calculateUserRates() {
            dataStore.userAcceptRateDict = {};
            
            Object.keys(dataStore.userPublishDict).forEach(userUuid => {
                const userData = dataStore.userPublishDict[userUuid];
                
                if (!userData) return;
                
                let publishList = [];
                if (Array.isArray(userData)) {
                    publishList = userData;
                } else if (userData.data && Array.isArray(userData.data)) {
                    publishList = userData.data;
                } else {
                    return;
                }
                
                if (publishList.length > 0) {
                    const totalQuestions = publishList.length;
                    
                    // 计算采纳的问题数量
                    const acceptedQuestions = publishList.filter(item => 
                        item.isAccept === true || 
                        item.status === 'accepted' ||
                        item.status === 'ACCEPTED'
                    ).length;
                    
                    // 计算采纳率
                    const acceptRate = totalQuestions > 0 ? (acceptedQuestions / totalQuestions) : 0;
                    
                    // 存储采纳率信息
                    dataStore.userAcceptRateDict[userUuid] = {
                        userName: dataStore.creatorIdsMap[userUuid]?.creatorName || '未知',
                        totalQuestions,
                        acceptedQuestions,
                        acceptRate
                    };
                }
            });
            
            // 添加标签到页面
            this.addUserTags();
        },
        
        // 为用户添加标记
        addUserTags() {
            if (Object.keys(dataStore.userAcceptRateDict).length === 0) return;
            
            // 获取所有已知用户名
            const knownUserNames = new Set();
            for (const [, data] of Object.entries(dataStore.creatorIdsMap)) {
                if (data.creatorName) knownUserNames.add(data.creatorName);
            }
            
            // 查找可能的用户元素
            const selectors = [
                '.creator___12fdW > span:not([data-yd-tagged])',
                '.discuss___27Ane .creator___12fdW > span:not([data-yd-tagged])'
            ];
            
            let userElements = selectors.flatMap(sel => [...document.querySelectorAll(sel)]);
            
            // 如果没有通过选择器找到,则使用通用匹配方法
            if (userElements.length === 0) {
                const allTextElements = document.querySelectorAll('a:not([data-yd-tagged]), span:not([data-yd-tagged]), div:not([data-yd-tagged])');
                
                userElements = Array.from(allTextElements).filter(element => {
                    const text = element.textContent.trim();
                    return text.length >= 2 && text.length <= 20 && knownUserNames.has(text);
                });
            }
            
            // 处理找到的用户名元素
            userElements.forEach(element => {
                try {
                    const userName = element.textContent.trim();
                    
                    // 查找对应的用户ID
                    let userUuid = Object.entries(dataStore.creatorIdsMap)
                        .find(([, data]) => data.creatorName === userName)?.[0];
                    
                    if (userUuid) {
                        this.addTagToElement(element, userUuid);
                    }
                } catch (error) {
                    console.error('处理用户名元素出错', error);
                }
            });
            
            // 如果没有找到任何用户名或添加了标签,延迟再尝试一次
            if (userElements.length === 0 || document.querySelectorAll('.yd-tag-container').length === 0) {
                setTimeout(() => this.addUserTags(), 3000);
            }
        },
        
        // 向元素添加标记
        addTagToElement(element, userUuid) {
            try {
                if (element.getAttribute('data-yd-tagged')) return false;
                
                const askRateInfo = dataStore.userAcceptRateDict[userUuid] || {};
                
                const tagContainer = document.createElement('div');
                tagContainer.className = 'yd-tag-container';
                
                element.setAttribute('data-user-uuid', userUuid);
                
                // 计算质量标记
                const qualityInfo = this.calculateQualityTag(userUuid);
                if (qualityInfo) {
                    const qualityTag = document.createElement('span');
                    qualityTag.className = `yd-tag ${qualityInfo.className}`;
                    qualityTag.textContent = `${qualityInfo.text}: ${qualityInfo.rate} (${askRateInfo.acceptedQuestions}/${askRateInfo.totalQuestions})`;
                    tagContainer.appendChild(qualityTag);
                }
                
                if (tagContainer.children.length > 0) {
                    // 尝试插入标签
                    try {
                        if (element.nextSibling) {
                            element.parentNode.insertBefore(tagContainer, element.nextSibling);
                        } else {
                            element.parentNode.appendChild(tagContainer);
                        }
                        element.setAttribute('data-yd-tagged', 'true');
                        return true;
                    } catch (insertError) {
                        try {
                            element.insertAdjacentElement('afterend', tagContainer);
                            element.setAttribute('data-yd-tagged', 'true');
                            return true;
                        } catch (alternativeError) {
                            return false;
                        }
                    }
                }
            } catch (error) {
                return false;
            }
            return false;
        },
        
        // 计算用户标记
        calculateQualityTag(userUuid) {
            const askRateInfo = dataStore.userAcceptRateDict[userUuid] || {};
            
            // 提问数量和采纳率
            const askCount = askRateInfo.totalQuestions || 0;
            const askRate = askRateInfo.acceptRate || 0;
            
            // 检查是否为新人(只进行过一次提问)
            if (askCount === 1) {
                return {
                    level: 'newbie',
                    text: '新人',
                    rate: (askRate * 100).toFixed(0) + '%',
                    className: 'yd-newbie'
                };
            }
            
            // 计算标记等级
            const percentage = askRate * 100;
            
            if (percentage === 100) {
                return { level: 'full', text: '全采纳', rate: percentage.toFixed(0) + '%', className: 'yd-full' };
            } else if (percentage >= 90) {
                return { level: 'high', text: '高采纳', rate: percentage.toFixed(0) + '%', className: 'yd-high' };
            } else if (percentage >= 70) {
                return { level: 'medium', text: '中采纳', rate: percentage.toFixed(0) + '%', className: 'yd-medium' };
            } else if (percentage > 50 && percentage < 70) {
                return { level: 'medium', text: '中采纳', rate: percentage.toFixed(0) + '%', className: 'yd-medium' };
            } else if (percentage > 10 && percentage <= 50) {
                return { level: 'low', text: '低采纳', rate: percentage.toFixed(0) + '%', className: 'yd-low' };
            } else {
                return { level: 'none', text: '不采纳', rate: percentage.toFixed(0) + '%', className: 'yd-none' };
            }
        }
    };

    // 页面监控
    function setupPageMonitoring() {
        // 初始加载
        setTimeout(() => {
            API.fetchQuestionData(getCurrentPageNumber());
        }, 1000);
        
        // 监听翻页
        let currentPage = getCurrentPageNumber();
        
        // 检查页码变化
        const checkPageChange = () => {
            const newPage = getCurrentPageNumber();
            if (newPage !== currentPage) {
                currentPage = newPage;
                
                // 清除之前的标签
                document.querySelectorAll('.yd-tag-container').forEach(el => el.remove());
                document.querySelectorAll('[data-yd-tagged]').forEach(el => el.removeAttribute('data-yd-tagged'));
                
                // 重置数据并获取新数据
                API.fetchQuestionData(newPage);
                
                Toast.create(`正在获取数据...`, 2000);
            }
        };
        
        // 定期检查页码变化
        const pageInterval = setInterval(checkPageChange, 1000);
        
        // 5分钟后清除定时器,减少资源消耗
        setTimeout(() => clearInterval(pageInterval), 300000);
        
        // 节流函数
        function throttle(fn, delay) {
            let lastCall = 0;
            return function(...args) {
                const now = new Date().getTime();
                if (now - lastCall < delay) return;
                lastCall = now;
                return fn(...args);
            };
        }
        
        // 监听点击事件(使用事件委托和节流)
        document.addEventListener('click', throttle((e) => {
            let target = e.target;
            
            // 检查是否点击了分页元素
            while (target && target !== document) {
                if (
                    target.classList?.contains('ant-pagination-item') ||
                    target.classList?.contains('ant-pagination-next') ||
                    target.classList?.contains('ant-pagination-prev') ||
                    target.classList?.contains('ant-pagination-jump-next') ||
                    target.classList?.contains('ant-pagination-jump-prev')
                ) {
                    setTimeout(checkPageChange, 300);
                    break;
                }
                target = target.parentElement;
            }
        }, 200));
        
        // 使用 MutationObserver 监听DOM变化(降低频率)
        const observer = new MutationObserver(throttle(() => {
            UserTagManager.addUserTags();
            checkPageChange();
        }, 500));
        
        observer.observe(document.body, { 
            childList: true, 
            subtree: true
        });
        
        // 定期尝试添加标记,确保不遗漏,使用递减间隔
        let retryCount = 0;
        const maxRetries = 5;
        
        function retryAddTags() {
            if (retryCount < maxRetries) {
                UserTagManager.addUserTags();
                retryCount++;
                setTimeout(retryAddTags, 3000 - retryCount * 500); // 递减间隔
            }
        }
        
        // 初始加载后尝试添加标签
        setTimeout(retryAddTags, 3000);
    }

    // 初始化
    if (window.location.href.includes('https://www.yingdao.com/community')) {
        addStyles();
        Toast.create("影刀社区助手已启动", 2000);
        setupPageMonitoring();
    }
})();