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