From 179976ae57ba8fa9f10276665808e34698586ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 15 May 2026 17:49:33 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 122 ++++++++++++-- web/static/i18n/en-US.json | 1 + web/static/i18n/zh-CN.json | 1 + web/static/js/dashboard.js | 295 ++++++++++++++++++++++++++++++++- web/static/js/vulnerability.js | 18 +- web/templates/index.html | 19 ++- 6 files changed, 428 insertions(+), 28 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 8a58241b..694cbaff 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -14768,8 +14768,70 @@ header { stroke: #ffffff; stroke-width: 4; stroke-linejoin: round; + cursor: pointer; + outline: none; + transform-origin: 240px 215px; + transition: opacity 0.22s ease, filter 0.22s ease, transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1); +} + +.dashboard-severity-donut.donut-ready .donut-segment { + animation: donut-segment-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.dashboard-severity-donut.donut-ready .donut-segment.seg-critical { animation-delay: 0.02s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-high { animation-delay: 0.06s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-medium { animation-delay: 0.10s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-low { animation-delay: 0.14s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-info { animation-delay: 0.18s; } + +@keyframes donut-segment-in { + from { + opacity: 0; + transform: scale(0.92); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.dashboard-severity-donut.is-highlighting .donut-segment.is-dimmed, +.dashboard-severity-donut.is-highlighting .donut-label-text.is-dimmed, +.dashboard-severity-donut.is-highlighting .donut-leader.is-dimmed { + opacity: 0.32; +} + +.dashboard-severity-donut .donut-segment.is-active { + filter: drop-shadow(0 3px 10px rgba(15, 23, 42, 0.18)); + transform: scale(1.045); + stroke-width: 5; + z-index: 1; +} + +.dashboard-severity-donut .donut-segment:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.55); + outline-offset: 2px; +} + +.dashboard-severity-donut .donut-leader { + stroke: rgba(148, 163, 184, 0.55); + stroke-width: 1; + pointer-events: none; + transition: opacity 0.2s ease, stroke 0.2s ease; +} + +.dashboard-severity-donut .donut-leader.is-active { + stroke: rgba(100, 116, 139, 0.85); + stroke-width: 1.5; +} + +.dashboard-severity-donut .donut-label-text { + pointer-events: none; transition: opacity 0.2s ease; - cursor: default; +} + +.dashboard-severity-donut .donut-label-text.is-active { + font-weight: 800; } .dashboard-severity-donut .donut-segment.is-empty { @@ -14799,12 +14861,7 @@ header { .dashboard-severity-donut .donut-label-text.label-low { fill: #14b8a6; } .dashboard-severity-donut .donut-label-text.label-info { fill: #3b82f6; } -/* 半环形配色:保持原有浅色基调(红→橙→黄→青→蓝) */ -.dashboard-severity-donut .donut-segment.seg-critical { fill: #f87171; } -.dashboard-severity-donut .donut-segment.seg-high { fill: #fb923c; } -.dashboard-severity-donut .donut-segment.seg-medium { fill: #facc15; } -.dashboard-severity-donut .donut-segment.seg-low { fill: #2dd4bf; } -.dashboard-severity-donut .donut-segment.seg-info { fill: #60a5fa; } +/* 半环形配色由 SVG linearGradient(#donut-grad-*)提供 */ .dashboard-severity-center { position: absolute; @@ -14816,6 +14873,17 @@ header { text-align: center; pointer-events: none; width: 60%; + transition: transform 0.25s ease; +} + +.dashboard-severity-center.is-hovering { + transform: translate(-50%, -52%) scale(1.04); +} + +.dashboard-severity-center-label.is-severity { + font-weight: 700; + color: var(--text-primary); + letter-spacing: 0.02em; } .dashboard-severity-center-value { @@ -14856,12 +14924,46 @@ header { padding: 10px 4px; font-size: 0.9375rem; border-bottom: 1px solid transparent; - transition: background 0.2s, border-color 0.2s; + transition: background 0.2s, border-color 0.2s, box-shadow 0.2s, opacity 0.2s; border-radius: 4px; + cursor: pointer; } -.dashboard-severity-legend-item:hover { - background: rgba(0, 0, 0, 0.025); +.dashboard-severity-legend-item:hover, +.dashboard-severity-legend-item.is-active { + background: rgba(0, 102, 255, 0.06); + border-radius: 8px; +} + +.dashboard-severity-legend-item.is-active { + box-shadow: inset 3px 0 0 var(--accent-color, #0066ff); +} + +.dashboard-severity-legend-item.is-zero { + opacity: 0.55; +} + +.dashboard-severity-legend-item:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.45); + outline-offset: 2px; +} + +.dashboard-severity-donut-tooltip { + display: none; + position: fixed; + left: 0; + top: 0; + z-index: 10000; + max-width: 280px; + padding: 8px 12px; + font-size: 0.8125rem; + line-height: 1.45; + color: #fff; + background: rgba(15, 23, 42, 0.94); + border-radius: 8px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22); + pointer-events: none; + white-space: nowrap; } .dashboard-severity-legend-dot { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 26d435d7..f2a87da9 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -147,6 +147,7 @@ "active": "Active", "highFreq": "High frequency", "noCallData": "No call data", + "severityClickHint": "Click to view", "lastUpdated": "Last updated", "viewAll": "View all →", "recentVulns": "Recent vulnerabilities", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index c1c4beac..6a6568ac 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -147,6 +147,7 @@ "active": "活跃", "highFreq": "高频", "noCallData": "暂无调用数据", + "severityClickHint": "点击查看", "lastUpdated": "上次更新", "viewAll": "查看全部 →", "recentVulns": "最近漏洞", diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 546aa261..10eed416 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -1390,6 +1390,17 @@ function dashboardBarTooltipOnOut(ev) { if (dashboardBarTooltipEl) dashboardBarTooltipEl.style.display = 'none'; } +// 仪表盘 → 漏洞管理:带严重程度/状态筛选跳转 +function navigateToVulnerabilitiesWithFilter(opts) { + opts = opts || {}; + var params = new URLSearchParams(); + if (opts.severity) params.set('severity', opts.severity); + if (opts.status) params.set('status', opts.status); + var qs = params.toString(); + window.location.hash = qs ? 'vulnerabilities?' + qs : 'vulnerabilities'; +} +window.navigateToVulnerabilitiesWithFilter = navigateToVulnerabilitiesWithFilter; + // 漏洞严重程度分布:半环形(donut)渲染 // 几何参数固定,便于配合 viewBox 0 0 560 320 的 SVG 容器 // 段间分隔由 CSS 的白色 stroke 完成,不再使用 gapRad @@ -1402,9 +1413,27 @@ var SEVERITY_DONUT_CFG = { rOuter: 165, rInner: 115, // 环厚 = 50(介于原 90 和上一版 35 之间,自然且有质感) labelOffset: 14, - gapRad: 0 + gapRad: 0.012 }; +var SEVERITY_DONUT_GRADIENTS = { + critical: ['#fca5a5', '#ef4444'], + high: ['#fdba74', '#f97316'], + medium: ['#fde047', '#eab308'], + low: ['#5eead4', '#14b8a6'], + info: ['#93c5fd', '#3b82f6'] +}; + +var severityDonutState = { + bySeverity: {}, + total: 0, + hoverId: null, + bound: false +}; + +var severityDonutTooltipEl = null; +var severityDonutTooltipTimer = null; + var SEVERITY_DEFAULT_LABELS = { critical: '严重', high: '高危', @@ -1422,13 +1451,35 @@ function severityLabel(id) { return SEVERITY_DEFAULT_LABELS[id] || id; } +function ensureSeverityDonutGradients() { + var defsEl = document.getElementById('dashboard-severity-donut-defs'); + if (!defsEl || defsEl.hasChildNodes()) return; + var html = ''; + Object.keys(SEVERITY_DONUT_GRADIENTS).forEach(function (id) { + var stops = SEVERITY_DONUT_GRADIENTS[id]; + html += ''; + html += ''; + html += ''; + html += ''; + }); + defsEl.innerHTML = html; +} + function renderSeverityDonut(bySeverity, total) { + var svgEl = document.getElementById('dashboard-severity-donut'); var trackEl = document.getElementById('dashboard-severity-donut-track'); + var leadersEl = document.getElementById('dashboard-severity-donut-leaders'); var segmentsEl = document.getElementById('dashboard-severity-donut-segments'); var labelsEl = document.getElementById('dashboard-severity-donut-labels'); if (!trackEl || !segmentsEl || !labelsEl) return; + severityDonutState.bySeverity = bySeverity && typeof bySeverity === 'object' ? bySeverity : {}; + severityDonutState.total = total || 0; + severityDonutState.hoverId = null; + resetSeverityDonutCenter(); + var cfg = SEVERITY_DONUT_CFG; + ensureSeverityDonutGradients(); // 背景轨迹(完整半环)只渲染一次 if (!trackEl.hasChildNodes()) { @@ -1441,9 +1492,12 @@ function renderSeverityDonut(bySeverity, total) { }); var visible = severities.filter(function (s) { return s.value > 0; }); + if (svgEl) svgEl.classList.remove('is-highlighting'); if (!total || total <= 0 || visible.length === 0) { segmentsEl.innerHTML = ''; labelsEl.innerHTML = ''; + if (leadersEl) leadersEl.innerHTML = ''; + clearSeverityDonutLegendHighlight(); return; } @@ -1457,6 +1511,7 @@ function renderSeverityDonut(bySeverity, total) { var segmentsHtml = ''; var labelsHtml = ''; + var leadersHtml = ''; var cumRad = 0; visible.forEach(function (seg, i) { @@ -1466,17 +1521,19 @@ function renderSeverityDonut(bySeverity, total) { var angleEnd = angleStart - segRad; var path = arcSegmentPath(cfg.cx, cfg.cy, cfg.rOuter, cfg.rInner, angleStart, angleEnd); - segmentsHtml += ''; + var pctOfTotal = (seg.value / total) * 100; + var pctRounded = Math.round(pctOfTotal); + var name = esc(severityLabel(seg.id)); + var ariaLabel = name + ' ' + seg.value + ' (' + pctRounded + '%)'; + segmentsHtml += ''; // 仅当占比 >= 5% 时显示外置标签,避免小段标签互相重叠 - var pctOfTotal = (seg.value / total) * 100; if (pctOfTotal >= 5) { var midAngle = (angleStart + angleEnd) / 2; - var labelR = cfg.rOuter + cfg.labelOffset; + var labelR = cfg.rOuter + cfg.labelOffset + 6; var sinMid = Math.sin(midAngle); var cosMid = Math.cos(midAngle); var lx = cfg.cx + labelR * cosMid; - // 顶部区域标签整体向上抬一些,避免与外弧贴住;侧边标签则不调整 var topLift = sinMid > 0.4 ? Math.round((sinMid - 0.3) * 10) : 0; var ly = cfg.cy - labelR * sinMid - topLift; @@ -1484,11 +1541,15 @@ function renderSeverityDonut(bySeverity, total) { if (cosMid < -0.15) anchor = 'end'; else if (cosMid > 0.15) anchor = 'start'; - var pctText = Math.round(pctOfTotal) + '%'; - var name = esc(severityLabel(seg.id)); + var pctText = pctRounded + '%'; + var arcR = cfg.rOuter + 4; + var lineX1 = cfg.cx + arcR * cosMid; + var lineY1 = cfg.cy - arcR * sinMid; + var lineX2 = cfg.cx + (cfg.rOuter + cfg.labelOffset - 2) * cosMid; + var lineY2 = cfg.cy - (cfg.rOuter + cfg.labelOffset - 2) * sinMid; + leadersHtml += ''; - // 两行:第一行 "数量 (百分比)"(弧色),第二行 "严重度名称"(同色但稍小) - labelsHtml += ''; + labelsHtml += ''; labelsHtml += '' + seg.value + ' (' + pctText + ')'; labelsHtml += '' + name + ''; labelsHtml += ''; @@ -1498,8 +1559,224 @@ function renderSeverityDonut(bySeverity, total) { if (i < visibleCount - 1) cumRad += cfg.gapRad; }); + if (leadersEl) leadersEl.innerHTML = leadersHtml; segmentsEl.innerHTML = segmentsHtml; labelsEl.innerHTML = labelsHtml; + if (svgEl) svgEl.classList.add('donut-ready'); + attachSeverityDonutInteractivity(); +} + +function resetSeverityDonutCenter() { + var totalEl = document.getElementById('dashboard-severity-total'); + var labelEl = document.getElementById('dashboard-severity-center-label'); + var centerEl = document.getElementById('dashboard-severity-center'); + if (totalEl) totalEl.textContent = String(severityDonutState.total || 0); + if (labelEl) { + labelEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.totalVulns') : '总漏洞数'); + labelEl.classList.remove('is-severity'); + } + if (centerEl) centerEl.classList.remove('is-hovering'); +} + +function setSeverityDonutHover(severityId) { + var svgEl = document.getElementById('dashboard-severity-donut'); + var centerEl = document.getElementById('dashboard-severity-center'); + var totalEl = document.getElementById('dashboard-severity-total'); + var labelEl = document.getElementById('dashboard-severity-center-label'); + if (!severityId) { + severityDonutState.hoverId = null; + if (svgEl) svgEl.classList.remove('is-highlighting'); + clearSeverityDonutLegendHighlight(); + resetSeverityDonutCenter(); + return; + } + var count = (severityDonutState.bySeverity && severityDonutState.bySeverity[severityId]) || 0; + severityDonutState.hoverId = severityId; + if (svgEl) svgEl.classList.add('is-highlighting'); + highlightSeverityDonutParts(severityId); + highlightSeverityLegendItem(severityId); + if (totalEl) totalEl.textContent = String(count); + if (labelEl) { + labelEl.textContent = severityLabel(severityId); + labelEl.classList.add('is-severity'); + } + if (centerEl) centerEl.classList.add('is-hovering'); +} + +function highlightSeverityDonutParts(severityId) { + var svgEl = document.getElementById('dashboard-severity-donut'); + if (!svgEl) return; + svgEl.querySelectorAll('[data-severity]').forEach(function (el) { + var match = el.getAttribute('data-severity') === severityId; + el.classList.toggle('is-active', match); + el.classList.toggle('is-dimmed', !match); + }); +} + +function highlightSeverityLegendItem(severityId) { + var legend = document.getElementById('dashboard-vuln-bars'); + if (!legend) return; + legend.querySelectorAll('.dashboard-severity-legend-item').forEach(function (item) { + var match = item.getAttribute('data-severity') === severityId; + item.classList.toggle('is-active', match); + }); +} + +function clearSeverityDonutLegendHighlight() { + var legend = document.getElementById('dashboard-vuln-bars'); + if (legend) { + legend.querySelectorAll('.dashboard-severity-legend-item.is-active').forEach(function (el) { + el.classList.remove('is-active'); + }); + } + var svgEl = document.getElementById('dashboard-severity-donut'); + if (svgEl) { + svgEl.querySelectorAll('.is-active, .is-dimmed').forEach(function (el) { + el.classList.remove('is-active', 'is-dimmed'); + }); + } +} + +function severityDonutTooltipText(severityId) { + var count = (severityDonutState.bySeverity && severityDonutState.bySeverity[severityId]) || 0; + var pct = severityDonutState.total > 0 ? Math.round((count / severityDonutState.total) * 100) : 0; + var hint = (typeof window.t === 'function' ? window.t('dashboard.severityClickHint') : '点击查看'); + return severityLabel(severityId) + ' · ' + count + ' (' + pct + '%) — ' + hint; +} + +function showSeverityDonutTooltip(ev, severityId) { + if (!severityDonutTooltipEl) { + severityDonutTooltipEl = document.createElement('div'); + severityDonutTooltipEl.className = 'dashboard-severity-donut-tooltip'; + severityDonutTooltipEl.setAttribute('role', 'tooltip'); + document.body.appendChild(severityDonutTooltipEl); + } + clearTimeout(severityDonutTooltipTimer); + severityDonutTooltipTimer = setTimeout(function () { + severityDonutTooltipEl.textContent = severityDonutTooltipText(severityId); + severityDonutTooltipEl.style.display = 'block'; + requestAnimationFrame(function () { + var x = ev.clientX; + var y = ev.clientY; + var ttRect = severityDonutTooltipEl.getBoundingClientRect(); + var left = x - ttRect.width / 2; + var top = y - ttRect.height - 12; + if (top < 8) top = y + 16; + var pad = 8; + if (left < pad) left = pad; + if (left + ttRect.width > window.innerWidth - pad) left = window.innerWidth - ttRect.width - pad; + severityDonutTooltipEl.style.left = left + 'px'; + severityDonutTooltipEl.style.top = top + 'px'; + }); + }, 120); +} + +function hideSeverityDonutTooltip() { + clearTimeout(severityDonutTooltipTimer); + severityDonutTooltipTimer = null; + if (severityDonutTooltipEl) severityDonutTooltipEl.style.display = 'none'; +} + +function attachSeverityDonutInteractivity() { + var svgEl = document.getElementById('dashboard-severity-donut'); + var legend = document.getElementById('dashboard-vuln-bars'); + if (!svgEl) return; + + if (!severityDonutState.bound) { + severityDonutState.bound = true; + svgEl.addEventListener('mouseover', severityDonutPointerOver); + svgEl.addEventListener('mouseout', severityDonutPointerOut); + svgEl.addEventListener('click', severityDonutClick); + svgEl.addEventListener('keydown', severityDonutKeydown); + if (legend) { + legend.addEventListener('mouseover', severityLegendPointerOver); + legend.addEventListener('mouseout', severityLegendPointerOut); + legend.addEventListener('click', severityLegendClick); + legend.addEventListener('keydown', severityLegendKeydown); + } + } + + legend && legend.querySelectorAll('.dashboard-severity-legend-item').forEach(function (item) { + if (!item.getAttribute('data-severity')) return; + var sev = item.getAttribute('data-severity'); + var count = (severityDonutState.bySeverity && severityDonutState.bySeverity[sev]) || 0; + item.classList.toggle('is-zero', count === 0); + item.setAttribute('aria-label', severityDonutTooltipText(sev)); + }); +} + +function severityDonutTarget(el) { + return el && el.closest && el.closest('[data-severity]'); +} + +function severityDonutPointerOver(ev) { + var target = severityDonutTarget(ev.target); + if (!target || !target.classList.contains('donut-segment')) return; + var id = target.getAttribute('data-severity'); + if (!id) return; + setSeverityDonutHover(id); + showSeverityDonutTooltip(ev, id); +} + +function severityDonutPointerOut(ev) { + var from = severityDonutTarget(ev.target); + var to = ev.relatedTarget && severityDonutTarget(ev.relatedTarget); + if (from && from === to) return; + setSeverityDonutHover(null); + hideSeverityDonutTooltip(); +} + +function severityDonutClick(ev) { + var target = severityDonutTarget(ev.target); + if (!target || !target.classList.contains('donut-segment')) return; + var id = target.getAttribute('data-severity'); + if (!id) return; + ev.preventDefault(); + navigateToVulnerabilitiesWithFilter({ severity: id }); +} + +function severityDonutKeydown(ev) { + if (ev.key !== 'Enter' && ev.key !== ' ') return; + var target = severityDonutTarget(ev.target); + if (!target || !target.classList.contains('donut-segment')) return; + ev.preventDefault(); + var id = target.getAttribute('data-severity'); + if (id) navigateToVulnerabilitiesWithFilter({ severity: id }); +} + +function severityLegendPointerOver(ev) { + var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-severity-legend-item[data-severity]'); + if (!item) return; + var id = item.getAttribute('data-severity'); + if (!id) return; + setSeverityDonutHover(id); + showSeverityDonutTooltip(ev, id); +} + +function severityLegendPointerOut(ev) { + var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-severity-legend-item[data-severity]'); + var related = ev.relatedTarget && ev.relatedTarget.closest && ev.relatedTarget.closest('.dashboard-severity-legend-item[data-severity]'); + if (item && item === related) return; + setSeverityDonutHover(null); + hideSeverityDonutTooltip(); +} + +function severityLegendClick(ev) { + var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-severity-legend-item[data-severity]'); + if (!item) return; + var id = item.getAttribute('data-severity'); + if (!id) return; + ev.preventDefault(); + navigateToVulnerabilitiesWithFilter({ severity: id }); +} + +function severityLegendKeydown(ev) { + if (ev.key !== 'Enter' && ev.key !== ' ') return; + var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-severity-legend-item[data-severity]'); + if (!item) return; + ev.preventDefault(); + var id = item.getAttribute('data-severity'); + if (id) navigateToVulnerabilitiesWithFilter({ severity: id }); } // SVG 半环(背景轨迹)路径 diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index 67df391f..82711ee3 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -72,19 +72,27 @@ function syncVulnerabilityFiltersFromLocationHash() { const vid = (params.get('id') || '').trim(); const cid = (params.get('conversation_id') || '').trim(); const tid = (params.get('task_id') || '').trim(); - if (!vid && !cid && !tid) { + const sev = (params.get('severity') || '').trim(); + const st = (params.get('status') || '').trim(); + if (!vid && !cid && !tid && !sev && !st) { return; } vulnerabilityFilters.id = ''; vulnerabilityFilters.conversation_id = ''; vulnerabilityFilters.task_id = ''; + vulnerabilityFilters.severity = ''; + vulnerabilityFilters.status = ''; const idEl = document.getElementById('vulnerability-id-filter'); const convEl = document.getElementById('vulnerability-conversation-filter'); const taskEl = document.getElementById('vulnerability-task-filter'); + const sevEl = document.getElementById('vulnerability-severity-filter'); + const stEl = document.getElementById('vulnerability-status-filter'); if (idEl) idEl.value = ''; if (convEl) convEl.value = ''; if (taskEl) taskEl.value = ''; + if (sevEl) sevEl.value = ''; + if (stEl) stEl.value = ''; if (vid) { vulnerabilityFilters.id = vid; @@ -98,6 +106,14 @@ function syncVulnerabilityFiltersFromLocationHash() { vulnerabilityFilters.task_id = tid; if (taskEl) taskEl.value = tid; } + if (sev) { + vulnerabilityFilters.severity = sev; + if (sevEl) sevEl.value = sev; + } + if (st) { + vulnerabilityFilters.status = st; + if (stEl) stEl.value = st; + } vulnerabilityPagination.currentPage = 1; } diff --git a/web/templates/index.html b/web/templates/index.html index b4aac558..3d8d04da 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -449,42 +449,45 @@
-
+
0
-
总漏洞数
+
总漏洞数
-
+
严重 0 0%
-
+
高危 0 0%
-
+
中危 0 0%
-
+
低危 0 0%
-
+
信息 0