您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
优化第一版
// ==UserScript== // @name 去尼玛的滚动条(某60众包平台漏洞列表) // @namespace https://greasyfork.org/en/users/1522931-hongzh0 // @version 1.02 // @description 优化第一版 // @author hongzh0 // @match https://src.360.net/hacker/bug/list // @grant GM_xmlhttpRequest // @connect src.360.net // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 配置与映射 --- const API_URL = 'https://src.360.net/api/frontend/hacker/usercenter/mysubmittedbugs'; const PAGE = 1; const PAGE_NUM = 500; const STORAGE_KEY = '360src_viewer_theme'; const VIEWER_POSITION_KEY = '360src_viewer_pos'; const VIEWER_SIZE_KEY = '360src_viewer_size'; const ICON_POSITION_KEY = '360src_icon_pos'; const STATUS_MAP = { 1: '待初审', 2: '待确认', 5: '已完成', 6: '已完成', 7: '已完成', 10: '已完成', 15: '已忽略', 17: '已驳回' }; const LEVEL_MAP = { 1: '严重', 5: '高危', 10: '中危', 15: '低危', 0: '-' }; let currentTheme = localStorage.getItem(STORAGE_KEY) || 'dark'; let viewerContainer = null; let floatButton = null; // --- 辅助函数 --- function createEditUrlParam(bugId) { const jsonString = JSON.stringify({ "id": bugId }); const encoded1 = encodeURIComponent(jsonString); return encoded1; } /** * 加载元素状态 (位置和尺寸)。 */ function loadState(element, posKey, sizeKey) { const savedPos = localStorage.getItem(posKey); const hasSavedPos = savedPos && element; if (hasSavedPos) { const { x, y } = JSON.parse(savedPos); element.style.left = `${x}px`; element.style.top = `${y}px`; element.style.right = 'auto'; element.style.bottom = 'auto'; element.style.transform = 'none'; // 关键:移除居中类,确保它使用绝对定位 if (element === viewerContainer) { element.classList.remove('is-centered'); } } else if (element === viewerContainer) { // 如果是查看器且没有保存位置,则添加居中类 element.classList.add('is-centered'); } if (sizeKey && element) { const savedSize = localStorage.getItem(sizeKey); if (savedSize) { const { w, h } = JSON.parse(savedSize); element.style.width = `${w}px`; element.style.height = `${h}px`; element.style.maxWidth = 'none'; element.style.maxHeight = 'none'; const tableWrapper = document.getElementById('bug-viewer-table-wrapper'); if (tableWrapper) { tableWrapper.style.maxHeight = `calc(${h}px - 100px)`; } } } } // --- 拖动和调整大小逻辑 --- let isDragging = false; let isResizing = false; let isInteracting = false; let offsetX, offsetY; let dragElement, posKey; function startInteraction(e) { if (e.button !== 0) return; // 排除交互元素: if (e.target.tagName.toLowerCase() === 'a' || e.target.tagName.toLowerCase() === 'button' || e.target.closest('#theme-toggle') || e.target.closest('#bug-viewer-close')) { return; } const containerRect = viewerContainer.getBoundingClientRect(); let shouldStart = false; let interactiveElement = null; // 用于计算初始偏移量的元素 // 1. 检查是否在拖动浮动按钮 if (e.target.id === 'floating-bug-button') { dragElement = floatButton; posKey = ICON_POSITION_KEY; isDragging = true; shouldStart = true; interactiveElement = floatButton; } // 2. 检查是否在调整窗口大小 else if (viewerContainer.style.display !== 'none' && e.clientX > containerRect.right - 25 && e.clientY > containerRect.bottom - 25) { isResizing = true; shouldStart = true; document.body.style.cursor = 'nwse-resize'; interactiveElement = viewerContainer; // 调整大小基于容器本身 } // 3. 检查是否在拖动窗口本身 else if (e.target.closest('#bug-viewer-header')) { dragElement = viewerContainer; posKey = VIEWER_POSITION_KEY; viewerContainer.style.cursor = 'grabbing'; isDragging = true; shouldStart = true; interactiveElement = viewerContainer; } if (shouldStart) { isInteracting = true; e.preventDefault(); // 关键修复 1: 禁用动画,防止拖拽/调整大小过程中干扰,但保留 is-active viewerContainer.style.transition = 'none'; // 关键修复 2: 如果是拖动或调整窗口,并且窗口是居中显示的,必须先转换为绝对定位 if ((isDragging && dragElement === viewerContainer) || isResizing) { if (viewerContainer.classList.contains('is-centered')) { const currentRect = viewerContainer.getBoundingClientRect(); viewerContainer.style.transform = 'none'; viewerContainer.style.left = `${currentRect.left}px`; viewerContainer.style.top = `${currentRect.top}px`; viewerContainer.classList.remove('is-centered'); // 拖动/调整后就不是居中了 } } // 计算偏移量 const rect = interactiveElement.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.addEventListener('mousemove', interaction); document.addEventListener('mouseup', stopInteraction); } } function interaction(e) { if (!isDragging && !isResizing) return; // 1. 调整大小 if (isResizing) { const minWidth = 600; const minHeight = 480; // 关键: 必须使用 getBoundingClientRect() 获取当前的左上角位置 const containerRect = viewerContainer.getBoundingClientRect(); let newWidth = e.clientX - containerRect.left; let newHeight = e.clientY - containerRect.top; newWidth = Math.max(newWidth, minWidth); newHeight = Math.max(newHeight, minHeight); newWidth = Math.min(newWidth, window.innerWidth - containerRect.left - 10); newHeight = Math.min(newHeight, window.innerHeight - containerRect.top - 10); viewerContainer.style.width = `${newWidth}px`; viewerContainer.style.height = `${newHeight}px`; localStorage.setItem(VIEWER_SIZE_KEY, JSON.stringify({ w: newWidth, h: newHeight })); document.getElementById('bug-viewer-table-wrapper').style.maxHeight = `calc(${newHeight}px - 100px)`; } // 2. 拖动 else if (isDragging) { let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; newX = Math.max(0, Math.min(newX, window.innerWidth - dragElement.offsetWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - dragElement.offsetHeight)); dragElement.style.left = `${newX}px`; dragElement.style.top = `${newY}px`; // 实时保存位置 localStorage.setItem(posKey, JSON.stringify({x: newX, y: newY})); } } function stopInteraction() { isDragging = false; isResizing = false; isInteracting = false; document.body.style.cursor = 'default'; if (viewerContainer) { viewerContainer.style.cursor = 'default'; viewerContainer.style.transition = 'opacity 0.3s, transform 0.3s'; viewerContainer.classList.add('is-active'); // 确保窗口在停止交互后保持可见 } if (floatButton) floatButton.style.transition = 'all 0.3s'; document.removeEventListener('mousemove', interaction); document.removeEventListener('mouseup', stopInteraction); } // --- 主题切换函数 --- function toggleTheme() { const newTheme = currentTheme === 'light' ? 'dark' : 'light'; document.body.classList.remove(currentTheme + '-theme'); document.body.classList.add(newTheme + '-theme'); currentTheme = newTheme; localStorage.setItem(STORAGE_KEY, newTheme); updateThemeToggleButton(newTheme); } function updateThemeToggleButton(theme) { const button = document.getElementById('theme-toggle'); if(button) { button.textContent = theme === 'light' ? '🌙' : '☀️'; button.title = theme === 'light' ? '切换到深色模式' : '切换到浅色模式'; } } // --- 样式注入 --- function injectStyles() { const style = document.createElement('style'); style.id = 'bug-viewer-styles'; style.textContent = ` /* --- 动画定义 --- */ @keyframes slideInFromTop { 0% { opacity: 0; transform: translate(-50%, -100px) scale(0.95); } 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* --- 主题变量 --- */ .light-theme { --bg-primary: #f8f9fa; --bg-secondary: #ffffff; --text-primary: #212529; --text-muted: #6c757d; --border-color: #dee2e6; --hover-color: #e9ecef; --brand-color: #483d8b; --brand-accent: #6a5acd; --link-primary: #5f9ea0; --critical-color: #dc3545; --scroll-track: #e0e0e0; --scroll-thumb: #adb5bd; --status-pending-bg: #fff3cd; --status-done-bg: #d4edda; --status-reject-bg: #f8d7da; --status-text-pending: #856404; --status-text-done: #155724; --status-text-reject: #721c24; } .dark-theme { --bg-primary: #2b3035; --bg-secondary: #343a40; --text-primary: #f8f9fa; --text-muted: #adb5bd; --border-color: #495057; --hover-color: #495057; --brand-color: #7b68ee; --brand-accent: #8a2be2; --link-primary: #7fffd4; --critical-color: #dc3545; --scroll-track: #495057; --scroll-thumb: #6c757d; --status-pending-bg: #4e4035; --status-done-bg: #344e3a; --status-reject-bg: #5a3c42; --status-text-pending: #ffc107; --status-text-done: #90ee90; --status-text-reject: #ffb6c1; } /* --- 容器和全局样式 --- */ #bug-viewer-container { position: fixed; min-width: 600px; min-height: 480px; width: 80%; max-width: 1600px; max-height: 95vh; background: var(--bg-primary); color: var(--text-primary); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2), 0 0 0 2px var(--brand-color); border-radius: 12px; z-index: 9999; padding: 25px; overflow: hidden; display: none; font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; transition: opacity 0.3s, transform 0.3s; /* 默认定位为左上角,等待 JS 设置 */ top: 50px; left: 50px; right: auto; bottom: auto; transform: none; opacity: 0; } /* 第一次打开或没有保存位置时,应用居中定位和动画 */ #bug-viewer-container.is-centered { top: 50%; left: 50%; transform: translate(-50%, -50%); } #bug-viewer-container::after { content: ''; position: absolute; bottom: 0; right: 0; width: 25px; height: 25px; cursor: nwse-resize; z-index: 10000; background: none; } #bug-viewer-container.is-active { display: block; animation: none; opacity: 1; } /* 如果居中,应用动画 */ #bug-viewer-container.is-active.is-centered { animation: slideInFromTop 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } #bug-viewer-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; margin-bottom: 15px; cursor: grab; user-select: none; border-bottom: 3px solid var(--brand-color); } #bug-viewer-header h2 { font-size: 1.4em; font-weight: 700; color: var(--brand-color); } /* --- 浮动图标按钮 (略) --- */ #floating-bug-button { width: 55px; height: 55px; background-color: var(--brand-color); color: var(--bg-primary); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); font-size: 26px; border-radius: 50%; cursor: grab; z-index: 10001; position: fixed; border: 2px solid var(--bg-secondary); transition: all 0.2s; display: flex; align-items: center; justify-content: center; right: 30px; top: 100px; } #floating-bug-button:hover { transform: scale(1.1); background-color: var(--brand-accent); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3); } #floating-bug-button.is-loading { animation: spin 1s linear infinite; } /* --- 表格和按钮样式 (略) --- */ #bug-viewer-table-wrapper { max-height: calc(95vh - 100px); overflow-y: auto; overflow-x: hidden; padding-right: 5px; scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) var(--scroll-track); } #bug-viewer-table-wrapper::-webkit-scrollbar { width: 8px; } #bug-viewer-table-wrapper::-webkit-scrollbar-thumb { background-color: var(--scroll-thumb); border-radius: 10px; border: 2px solid var(--scroll-track); } #bug-viewer-table { width: 100%; border-collapse: separate; border-spacing: 0 10px; table-layout: fixed; font-size: 14px; } #bug-viewer-table th { background-color: var(--bg-primary); font-weight: 700; padding: 12px 15px; color: var(--text-muted); position: sticky; top: -10px; z-index: 10; border-bottom: 1px solid var(--border-color); } #bug-viewer-table td { padding: 16px 15px; border: none; word-wrap: break-word; font-weight: 400; background-color: var(--bg-secondary); } .status-row-1 td, .status-row-2 td { background-color: var(--status-pending-bg) !important; } .status-row-5 td, .status-row-6 td, .status-row-7 td, .status-row-10 td { background-color: var(--status-done-bg) !important; } .status-row-15 td, .status-row-17 td { background-color: var(--status-reject-bg) !important; } #bug-viewer-table tbody tr { transition: transform 0.2s, box-shadow 0.2s; border-radius: 10px; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); overflow: hidden; } #bug-viewer-table tbody tr:hover { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 0 0 2px var(--brand-accent); } #bug-viewer-table tbody tr td:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; } #bug-viewer-table tbody tr td:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; } .level-tag { display: inline-block; padding: 5px 12px; border-radius: 6px; font-size: 0.9em; font-weight: 700; line-height: 1.2; letter-spacing: 0.5px; color: white; text-shadow: 1px 1px 1px rgba(0,0,0,0.1); } .level-tag-1 { background-color: #f44336; border-color: #ffcdd2; } .level-tag-5 { background-color: #ff9800; border-color: #ffe0b2; } .level-tag-10 { background-color: var(--brand-color); border-color: #b0c4de; } .level-tag-15 { background-color: var(--brand-accent); border-color: #e1bee7; } .status-row-1 .status-cell, .status-row-2 .status-cell { color: var(--status-text-pending); font-weight: 700; } .status-row-5 .status-cell, .status-row-6 .status-cell, .status-row-7 .status-cell, .status-row-10 .status-cell { color: var(--status-text-done); font-weight: 700; } .status-row-15 .status-cell, .status-row-17 .status-cell { color: var(--status-text-reject); font-weight: 700; } .action-btn { padding: 6px 18px; margin-right: 10px; border: 1px solid transparent; border-radius: 9999px; cursor: pointer; font-size: 14px; text-decoration: none; display: inline-block; transition: all 0.2s; font-weight: 600; } .btn-view { background-color: var(--brand-accent); color: white; border-color: var(--brand-accent); } .btn-edit { background-color: transparent; color: var(--link-primary); border-color: var(--link-primary); } .btn-view:hover { background-color: #8a2be2; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .btn-edit:hover { background-color: var(--link-primary); color: var(--bg-primary); transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } `; document.head.appendChild(style); document.body.classList.add(currentTheme + '-theme'); } // --- 核心逻辑 --- function showViewer() { // 1. 加载上次保存的位置和尺寸 loadState(viewerContainer, VIEWER_POSITION_KEY, VIEWER_SIZE_KEY); // 2. 确保窗口可见 viewerContainer.classList.remove('is-active'); viewerContainer.style.display = 'block'; // 3. 强制重绘,确保动画从正确的位置开始 void viewerContainer.offsetWidth; // 4. 应用激活状态 viewerContainer.classList.add('is-active'); } function fetchBugs() { if (isInteracting) return; const statusSpan = document.getElementById('viewer-status'); const tableBody = document.getElementById('viewer-tbody'); floatButton.classList.add('is-loading'); statusSpan.textContent = '正在请求数据...'; tableBody.innerHTML = ''; showViewer(); // 显示窗口 const requestBody = JSON.stringify({ page: String(PAGE), page_num: String(PAGE_NUM) }); GM_xmlhttpRequest({ method: "POST", url: API_URL, headers: { "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest" }, data: requestBody, onload: function(response) { floatButton.classList.remove('is-loading'); try { const data = JSON.parse(response.responseText); if (data.code === 200) { renderTable(data.result); statusSpan.textContent = `加载成功! (总数: ${data.result.total_bug_num} | 当前 ${data.result.bug_list.length} 条)`; } else { statusSpan.textContent = `API 错误: ${data.msg} (Code: ${data.code}). 请检查是否登录。`; } } catch (e) { statusSpan.textContent = '数据解析失败。'; console.error("Tampermonkey Script Error (Parse):", e); } }, onerror: function(response) { floatButton.classList.remove('is-loading'); statusSpan.textContent = '网络请求失败。请检查网络或登录状态。'; console.error("GM_xmlhttpRequest Error:", response); } }); } function renderTable(result) { // ... (保持不变) ... const tableBody = document.getElementById('viewer-tbody'); const bugList = result.bug_list || []; tableBody.innerHTML = ''; const getLevelTagHtml = (levelId) => { const levelText = LEVEL_MAP[levelId] || '-'; return `<span class="level-tag level-tag-${levelId}">${levelText}</span>`; }; bugList.forEach(bug => { const row = tableBody.insertRow(); const statusId = bug.status; const statusText = STATUS_MAP[statusId] || `未知 (${statusId})`; row.classList.add(`status-row-${statusId}`); let rewardText = ''; if (bug.reward && bug.reward !== '0.00') { rewardText = `¥${bug.reward}`; } else if (bug.point && bug.point !== '') { rewardText = `${bug.point} 积分`; } else { rewardText = '-'; } const bugId = bug.bug_id; // 1. 数据列 row.insertCell().textContent = bug.bug_no; row.insertCell().textContent = bug.bug_name; const selfLevelCell = row.insertCell(); selfLevelCell.innerHTML = getLevelTagHtml(bug.self_bug_level); const finalLevelCell = row.insertCell(); finalLevelCell.innerHTML = getLevelTagHtml(bug.bug_level); row.insertCell().textContent = bug.submit_time; const statusCell = row.insertCell(); statusCell.textContent = statusText; statusCell.classList.add('status-cell'); row.insertCell().textContent = rewardText; // 2. 操作列 (查看 & 编辑) const actionCell = row.insertCell(); const encodedBugId = encodeURIComponent(bugId); const viewLink = document.createElement('a'); viewLink.href = `https://src.360.net/hacker/bug/detail/${encodedBugId}`; viewLink.textContent = '查看'; viewLink.target = '_blank'; viewLink.className = 'action-btn btn-view'; actionCell.appendChild(viewLink); const editLink = document.createElement('a'); const editParam = createEditUrlParam(bugId); editLink.href = `https://src.360.net/submit-bug?q=${editParam}`; editLink.textContent = '编辑'; editLink.target = '_blank'; editLink.className = 'action-btn btn-edit'; actionCell.appendChild(editLink); }); } function setupUI() { injectStyles(); // 1. 创建浮动图标按钮 floatButton = document.createElement('button'); floatButton.id = 'floating-bug-button'; floatButton.innerHTML = '⚙️'; floatButton.title = '加载我的漏洞列表 (可拖动)'; floatButton.addEventListener('mousedown', startInteraction); floatButton.addEventListener('click', fetchBugs); document.body.appendChild(floatButton); loadState(floatButton, ICON_POSITION_KEY, null); // 2. 创建查看器容器 viewerContainer = document.createElement('div'); viewerContainer.id = 'bug-viewer-container'; // 2.1 头部控制区 (拖动区域) const headerHTML = ` <div id="bug-viewer-header"> <h2 style="margin: 0;">360SRC 漏洞报告列表</h2> <div style="display: flex; align-items: center;"> <span id="viewer-status">点击图标加载数据</span> <button id="theme-toggle"></button> <span id="bug-viewer-close">×</span> </div> </div> `; // 2.2 表格区 const tableHTML = ` <div id="bug-viewer-table-wrapper"> <table id="bug-viewer-table"> <thead> <tr> <th style="width: 8%;">ID</th> <th style="width: 25%;">漏洞名称</th> <th style="width: 8%;">自评</th> <th style="width: 8%;">确认</th> <th style="width: 14%;">提交时间</th> <th style="width: 10%;">状态</th> <th style="width: 10%;">奖励</th> <th style="width: 17%;">操作</th> </tr> </thead> <tbody id="viewer-tbody"> <tr><td colspan="8" style="text-align: center; padding: 50px; background: var(--bg-secondary);">点击右上角的 ⚙️ 图标加载您的漏洞列表。</td></tr> </tbody> </table> </div> `; viewerContainer.innerHTML = headerHTML + tableHTML; document.body.appendChild(viewerContainer); loadState(viewerContainer, VIEWER_POSITION_KEY, VIEWER_SIZE_KEY); // 3. 绑定事件 document.getElementById('bug-viewer-close').addEventListener('click', () => { viewerContainer.classList.remove('is-active'); setTimeout(() => { viewerContainer.style.display = 'none'; // 关闭后清除 transform,防止下次打开时位置冲突 viewerContainer.style.transform = 'none'; }, 300); }); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); updateThemeToggleButton(currentTheme); // 4. 绑定悬浮窗拖动/调整大小事件 viewerContainer.addEventListener('mousedown', startInteraction); } // 确保只在漏洞列表页运行 if (window.location.href.includes('src.360.net/hacker/bug/list')) { setupUI(); } })();