From d4e1fe3bbecd690b2a6a2998dba36da257e9fb38 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 18:03:59 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 291 ++++++++++++++++++++++++++++++------- web/static/js/dashboard.js | 218 +++++++++++++++++++++------ web/templates/index.html | 1 + 3 files changed, 410 insertions(+), 100 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 694cbaff..51763b97 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -14749,6 +14749,76 @@ header { max-width: 480px; margin: 0 auto; aspect-ratio: 480 / 260; + isolation: isolate; +} + +/* 底部氛围光:轻微呼吸 + 悬停扇区时整体染上该等级色调 */ +.dashboard-severity-chart::before { + content: ''; + position: absolute; + inset: -14% -12% -10%; + border-radius: 50%; + pointer-events: none; + z-index: 0; + opacity: 0.92; + background: + radial-gradient(ellipse 82% 64% at 50% 74%, rgba(99, 102, 241, 0.17), transparent 58%), + radial-gradient(ellipse 52% 42% at 14% 94%, rgba(56, 189, 248, 0.11), transparent 52%), + radial-gradient(ellipse 48% 38% at 88% 90%, rgba(244, 114, 182, 0.08), transparent 50%); + animation: dashboard-donut-aura 7s ease-in-out infinite alternate; +} + +.dashboard-severity-chart[data-hover-severity="critical"]::before { + opacity: 1; + animation: none; + background: radial-gradient(ellipse 82% 64% at 50% 74%, rgba(239, 68, 68, 0.38), transparent 58%), + radial-gradient(ellipse 50% 44% at 22% 92%, rgba(249, 115, 22, 0.18), transparent 54%); +} + +.dashboard-severity-chart[data-hover-severity="high"]::before { + opacity: 1; + animation: none; + background: radial-gradient(ellipse 82% 64% at 50% 74%, rgba(249, 115, 22, 0.36), transparent 58%), + radial-gradient(ellipse 48% 40% at 78% 88%, rgba(234, 179, 8, 0.14), transparent 52%); +} + +.dashboard-severity-chart[data-hover-severity="medium"]::before { + opacity: 1; + animation: none; + background: radial-gradient(ellipse 82% 64% at 50% 74%, rgba(234, 179, 8, 0.34), transparent 58%), + radial-gradient(ellipse 46% 38% at 18% 88%, rgba(250, 204, 21, 0.16), transparent 52%); +} + +.dashboard-severity-chart[data-hover-severity="low"]::before { + opacity: 1; + animation: none; + background: radial-gradient(ellipse 82% 64% at 50% 74%, rgba(45, 212, 191, 0.34), transparent 58%), + radial-gradient(ellipse 46% 38% at 86% 88%, rgba(14, 165, 233, 0.14), transparent 52%); +} + +.dashboard-severity-chart[data-hover-severity="info"]::before { + opacity: 1; + animation: none; + background: radial-gradient(ellipse 82% 64% at 50% 74%, rgba(59, 130, 246, 0.34), transparent 58%), + radial-gradient(ellipse 46% 38% at 30% 86%, rgba(129, 140, 248, 0.16), transparent 52%); +} + +@keyframes dashboard-donut-aura { + 0% { + opacity: 0.78; + transform: scale(0.97); + filter: saturate(0.92); + } + 100% { + opacity: 1; + transform: scale(1.03); + filter: saturate(1.08); + } +} + +.dashboard-severity-chart > .dashboard-severity-donut { + position: relative; + z-index: 1; } .dashboard-severity-donut { @@ -14758,90 +14828,168 @@ header { overflow: visible; } -.dashboard-severity-donut .donut-track { - fill: #f1f5f9; +.dashboard-severity-donut .donut-track-shadow { + fill: #c9d4e3; + opacity: 0.85; +} + +.dashboard-severity-donut .donut-track-vignette { + pointer-events: none; +} + +.dashboard-severity-donut .donut-segment-gloss { + mix-blend-mode: soft-light; + opacity: 0.48; + transition: opacity 0.26s ease; + pointer-events: none; +} + +.dashboard-severity-donut .donut-segment-gloss.is-active { + opacity: 0.72; } .dashboard-severity-donut .donut-segment { - /* 段与段之间用白色描边制造“切割线”效果,与参考图二一致; - 环回到黄金比例(厚度 50)后,描边也用回 4,切割线感更强 */ + filter: url(#donut-segment-soften); stroke: #ffffff; stroke-width: 4; stroke-linejoin: round; + pointer-events: none; + transition: opacity 0.22s ease, filter 0.22s ease; +} + +/* 透明命中层:几何固定,悬停时只改视觉层,避免 scale/描边导致边缘频闪 */ +.dashboard-severity-donut .donut-segment-hit { + fill: transparent; + stroke: transparent; + stroke-width: 0; 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); + pointer-events: visible; +} + +.dashboard-severity-donut .donut-segment-hit:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.55); + outline-offset: 2px; } .dashboard-severity-donut.donut-ready .donut-segment { - animation: donut-segment-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) backwards; + animation: donut-segment-in 0.72s cubic-bezier(0.22, 1.18, 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; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-critical { animation-delay: 0.03s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-high { animation-delay: 0.07s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-medium { animation-delay: 0.11s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-low { animation-delay: 0.15s; } +.dashboard-severity-donut.donut-ready .donut-segment.seg-info { animation-delay: 0.19s; } @keyframes donut-segment-in { from { opacity: 0; - transform: scale(0.92); + transform: scale(0.72) translateY(10px); + } + 72% { + opacity: 1; + transform: scale(1.06) translateY(0); } to { opacity: 1; - transform: scale(1); + transform: scale(1) translateY(0); } } .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.is-highlighting .donut-leader.is-dimmed, +.dashboard-severity-donut.is-highlighting .donut-segment-gloss.is-dimmed { + opacity: 0.26; } .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; + /* 不用 scale / stroke-width,防止命中区抖动 */ 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[data-hover-severity="critical"] .donut-segment.is-active { + filter: url(#donut-segment-soften) drop-shadow(0 0 28px rgba(239, 68, 68, 0.55)) drop-shadow(0 10px 26px rgba(239, 68, 68, 0.28)); +} + +.dashboard-severity-donut[data-hover-severity="high"] .donut-segment.is-active { + filter: url(#donut-segment-soften) drop-shadow(0 0 26px rgba(249, 115, 22, 0.52)) drop-shadow(0 10px 24px rgba(249, 115, 22, 0.26)); +} + +.dashboard-severity-donut[data-hover-severity="medium"] .donut-segment.is-active { + filter: url(#donut-segment-soften) drop-shadow(0 0 26px rgba(234, 179, 8, 0.48)) drop-shadow(0 10px 22px rgba(202, 138, 4, 0.22)); +} + +.dashboard-severity-donut[data-hover-severity="low"] .donut-segment.is-active { + filter: url(#donut-segment-soften) drop-shadow(0 0 26px rgba(45, 212, 191, 0.48)) drop-shadow(0 10px 22px rgba(13, 148, 136, 0.22)); +} + +.dashboard-severity-donut[data-hover-severity="info"] .donut-segment.is-active { + filter: url(#donut-segment-soften) drop-shadow(0 0 26px rgba(59, 130, 246, 0.48)) drop-shadow(0 10px 22px rgba(37, 99, 235, 0.22)); } .dashboard-severity-donut .donut-leader { - stroke: rgba(148, 163, 184, 0.55); - stroke-width: 1; + stroke: rgba(148, 163, 184, 0.45); + stroke-width: 1.25; pointer-events: none; - transition: opacity 0.2s ease, stroke 0.2s ease; + stroke-linecap: round; + transition: opacity 0.22s ease, stroke 0.22s ease; +} + +.dashboard-severity-donut.donut-ready .donut-leader { + stroke-dasharray: 100; + stroke-dashoffset: 100; + animation: donut-leader-draw 0.75s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +.dashboard-severity-donut.donut-ready .donut-leader.label-critical { animation-delay: 0.12s; } +.dashboard-severity-donut.donut-ready .donut-leader.label-high { animation-delay: 0.18s; } +.dashboard-severity-donut.donut-ready .donut-leader.label-medium { animation-delay: 0.24s; } +.dashboard-severity-donut.donut-ready .donut-leader.label-low { animation-delay: 0.30s; } +.dashboard-severity-donut.donut-ready .donut-leader.label-info { animation-delay: 0.36s; } + +@keyframes donut-leader-draw { + to { stroke-dashoffset: 0; } } .dashboard-severity-donut .donut-leader.is-active { - stroke: rgba(100, 116, 139, 0.85); - stroke-width: 1.5; + stroke: rgba(71, 85, 105, 0.95); + stroke-width: 2; } .dashboard-severity-donut .donut-label-text { pointer-events: none; - transition: opacity 0.2s ease; + transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.34, 1.35, 0.48, 1); + font-size: 14px; + font-weight: 700; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; +} + +.dashboard-severity-donut.donut-ready .donut-label-text { + animation: donut-label-pop 0.58s cubic-bezier(0.34, 1.25, 0.48, 1) backwards; +} + +.dashboard-severity-donut.donut-ready .donut-label-text.label-critical { animation-delay: 0.2s; } +.dashboard-severity-donut.donut-ready .donut-label-text.label-high { animation-delay: 0.26s; } +.dashboard-severity-donut.donut-ready .donut-label-text.label-medium { animation-delay: 0.32s; } +.dashboard-severity-donut.donut-ready .donut-label-text.label-low { animation-delay: 0.38s; } +.dashboard-severity-donut.donut-ready .donut-label-text.label-info { animation-delay: 0.44s; } + +@keyframes donut-label-pop { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } .dashboard-severity-donut .donut-label-text.is-active { font-weight: 800; -} - -.dashboard-severity-donut .donut-segment.is-empty { - display: none; -} - -.dashboard-severity-donut .donut-label-text { - font-size: 14px; - font-weight: 700; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + transform: translateY(-2px); } .dashboard-severity-donut .donut-label-text .donut-label-pct { @@ -14861,51 +15009,86 @@ header { .dashboard-severity-donut .donut-label-text.label-low { fill: #14b8a6; } .dashboard-severity-donut .donut-label-text.label-info { fill: #3b82f6; } -/* 半环形配色由 SVG linearGradient(#donut-grad-*)提供 */ +/* 半环形主体配色由 SVG linearGradient(#donut-grad-*)提供 */ +.dashboard-severity-donut .donut-segment.is-empty { + display: none; +} + +@media (prefers-reduced-motion: reduce) { + .dashboard-severity-chart::before { + animation: none; + } + .dashboard-severity-donut.donut-ready .donut-segment, + .dashboard-severity-donut.donut-ready .donut-leader, + .dashboard-severity-donut.donut-ready .donut-label-text { + animation: none !important; + } + .dashboard-severity-center.is-hovering { + transform: translateX(-50%); + } +} + +/* 中心数字:纯文字,贴在半圆开口下方(直径线附近),不遮挡彩色弧带 */ .dashboard-severity-center { position: absolute; left: 50%; - /* cy 在 viewBox(0,0,480,260) 中是 215,约 83% 处; - 这里把中心文字放在内圈靠下、靠近直径线的位置,让数字看起来"坐"在半圆里。 */ - top: 76%; - transform: translate(-50%, -50%); + bottom: 6%; + transform: translateX(-50%); text-align: center; pointer-events: none; - width: 60%; - transition: transform 0.25s ease; + width: auto; + max-width: 7rem; + padding: 0; + margin: 0; + background: none; + border: none; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + transition: transform 0.28s cubic-bezier(0.34, 1.35, 0.48, 1); + z-index: 2; } .dashboard-severity-center.is-hovering { - transform: translate(-50%, -52%) scale(1.04); + transform: translateX(-50%) scale(1.06); } .dashboard-severity-center-label.is-severity { font-weight: 700; - color: var(--text-primary); letter-spacing: 0.02em; } .dashboard-severity-center-value { - font-size: 2.75rem; + font-size: 2.5rem; font-weight: 800; line-height: 1; color: var(--text-primary); - letter-spacing: -0.03em; + letter-spacing: -0.04em; font-variant-numeric: tabular-nums; + text-shadow: + 0 0 20px rgba(255, 255, 255, 0.95), + 0 1px 2px rgba(255, 255, 255, 0.8); } .dashboard-severity-center-label { - font-size: 0.8125rem; + font-size: 0.75rem; color: var(--text-secondary); - margin-top: 8px; - letter-spacing: 0.04em; + margin-top: 4px; + letter-spacing: 0.06em; font-weight: 500; + text-shadow: 0 0 12px rgba(255, 255, 255, 0.9); } +.dashboard-severity-center-label[data-severity="critical"] { color: #dc2626; } +.dashboard-severity-center-label[data-severity="high"] { color: #ea580c; } +.dashboard-severity-center-label[data-severity="medium"] { color: #b45309; } +.dashboard-severity-center-label[data-severity="low"] { color: #0f766e; } +.dashboard-severity-center-label[data-severity="info"] { color: #2563eb; } + @media (max-width: 720px) { - .dashboard-severity-center-value { font-size: 2.25rem; } - .dashboard-severity-center-label { font-size: 0.75rem; } + .dashboard-severity-center-value { font-size: 2.1rem; } + .dashboard-severity-center-label { font-size: 0.6875rem; } } .dashboard-severity-legend { diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 10eed416..8ac5eb19 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -202,7 +202,6 @@ async function refreshDashboard() { openHighCount = pickOpenCount(openHighRes, highCount); openMediumCount = pickOpenCount(openMediumRes, mediumCount); openLowCount = pickOpenCount(openLowRes, lowCount); - if (severityTotalEl) severityTotalEl.textContent = String(total); severityIds.forEach(sev => { const count = bySeverity[sev] || 0; const el = document.getElementById('dashboard-severity-' + sev); @@ -1416,14 +1415,17 @@ var SEVERITY_DONUT_CFG = { gapRad: 0.012 }; +// 三段渐变:[高光浅调, 中段饱和色, 深色边缘] —— 做出类似 3D 釉面的层次 var SEVERITY_DONUT_GRADIENTS = { - critical: ['#fca5a5', '#ef4444'], - high: ['#fdba74', '#f97316'], - medium: ['#fde047', '#eab308'], - low: ['#5eead4', '#14b8a6'], - info: ['#93c5fd', '#3b82f6'] + critical: ['#fecaca', '#f87171', '#dc2626'], + high: ['#fed7aa', '#fb923c', '#ea580c'], + medium: ['#fef08a', '#facc15', '#ca8a04'], + low: ['#99f6e4', '#2dd4bf', '#0f766e'], + info: ['#bfdbfe', '#60a5fa', '#2563eb'] }; +var severityDonutCenterDisplayed = { total: null, hoverCount: null }; + var severityDonutState = { bySeverity: {}, total: 0, @@ -1433,6 +1435,7 @@ var severityDonutState = { var severityDonutTooltipEl = null; var severityDonutTooltipTimer = null; +var severityDonutHoverClearTimer = null; var SEVERITY_DEFAULT_LABELS = { critical: '严重', @@ -1451,15 +1454,37 @@ function severityLabel(id) { return SEVERITY_DEFAULT_LABELS[id] || id; } -function ensureSeverityDonutGradients() { +function ensureSeverityDonutDefs() { var defsEl = document.getElementById('dashboard-severity-donut-defs'); if (!defsEl || defsEl.hasChildNodes()) return; var html = ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; Object.keys(SEVERITY_DONUT_GRADIENTS).forEach(function (id) { var stops = SEVERITY_DONUT_GRADIENTS[id]; - html += ''; + html += ''; html += ''; - html += ''; + html += ''; + html += ''; html += ''; }); defsEl.innerHTML = html; @@ -1470,20 +1495,24 @@ function renderSeverityDonut(bySeverity, total) { 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 hitsEl = document.getElementById('dashboard-severity-donut-hits'); 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(); + ensureSeverityDonutDefs(); - // 背景轨迹(完整半环)只渲染一次 + // 背景轨迹(完整半环):双层填充营造凹槽 + 高光 if (!trackEl.hasChildNodes()) { - trackEl.innerHTML = ''; + var trackPath = halfRingPath(cfg.cx, cfg.cy, cfg.rOuter, cfg.rInner); + trackEl.innerHTML = + '' + + '' + + ''; } var ids = ['critical', 'high', 'medium', 'low', 'info']; @@ -1492,15 +1521,24 @@ function renderSeverityDonut(bySeverity, total) { }); var visible = severities.filter(function (s) { return s.value > 0; }); - if (svgEl) svgEl.classList.remove('is-highlighting'); + if (svgEl) { + svgEl.classList.remove('is-highlighting'); + svgEl.removeAttribute('data-hover-severity'); + } if (!total || total <= 0 || visible.length === 0) { segmentsEl.innerHTML = ''; + if (hitsEl) hitsEl.innerHTML = ''; labelsEl.innerHTML = ''; if (leadersEl) leadersEl.innerHTML = ''; clearSeverityDonutLegendHighlight(); + resetSeverityDonutCenter(false); + _clearSeverityDonutChartWrapHover(); + if (svgEl) svgEl.classList.remove('donut-ready'); return; } + resetSeverityDonutCenter(true); + // 弧长按 value/total 计算;若严重度求和 < total(存在未分级),右侧会保留背景轨迹的空白 var sumVisible = visible.reduce(function (s, seg) { return s + seg.value; }, 0); var coverage = sumVisible / total; // 半环被实际段覆盖的比例 @@ -1510,6 +1548,8 @@ function renderSeverityDonut(bySeverity, total) { var arcsTotalRad = Math.max(0, Math.PI * coverage - totalGapRad); var segmentsHtml = ''; + var hitsHtml = ''; + var glossHtml = ''; var labelsHtml = ''; var leadersHtml = ''; var cumRad = 0; @@ -1525,7 +1565,9 @@ function renderSeverityDonut(bySeverity, total) { var pctRounded = Math.round(pctOfTotal); var name = esc(severityLabel(seg.id)); var ariaLabel = name + ' ' + seg.value + ' (' + pctRounded + '%)'; - segmentsHtml += ''; + segmentsHtml += ''; + hitsHtml += ''; + glossHtml += ''; // 仅当占比 >= 5% 时显示外置标签,避免小段标签互相重叠 if (pctOfTotal >= 5) { @@ -1547,7 +1589,7 @@ function renderSeverityDonut(bySeverity, total) { 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 += ''; + leadersHtml += ''; labelsHtml += ''; labelsHtml += '' + seg.value + ' (' + pctText + ')'; @@ -1560,20 +1602,66 @@ function renderSeverityDonut(bySeverity, total) { }); if (leadersEl) leadersEl.innerHTML = leadersHtml; - segmentsEl.innerHTML = segmentsHtml; + segmentsEl.innerHTML = segmentsHtml + glossHtml; + if (hitsEl) hitsEl.innerHTML = hitsHtml; labelsEl.innerHTML = labelsHtml; - if (svgEl) svgEl.classList.add('donut-ready'); + if (svgEl) { + svgEl.classList.remove('donut-ready'); + void svgEl.offsetWidth; + requestAnimationFrame(function () { + svgEl.classList.add('donut-ready'); + }); + } + scheduleSeverityCenterCountUp(total); attachSeverityDonutInteractivity(); } -function resetSeverityDonutCenter() { +function scheduleSeverityCenterCountUp(targetTotal) { + if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + var totalEl = document.getElementById('dashboard-severity-total'); + if (totalEl) totalEl.textContent = String(targetTotal); + severityDonutCenterDisplayed.total = targetTotal; + return; + } + var totalEl = document.getElementById('dashboard-severity-total'); + if (!totalEl || severityDonutState.hoverId) return; + var from = typeof severityDonutCenterDisplayed.total === 'number' ? severityDonutCenterDisplayed.total : 0; + var to = targetTotal; + if (from === to) { + totalEl.textContent = String(to); + severityDonutCenterDisplayed.total = to; + return; + } + var start = null; + var dur = Math.min(520, 180 + Math.abs(to - from) * 28); + function tick(now) { + if (!start) start = now; + var t = Math.min(1, (now - start) / dur); + var eased = 1 - Math.pow(1 - t, 3); + var val = Math.round(from + (to - from) * eased); + totalEl.textContent = String(val); + if (t < 1) { + requestAnimationFrame(tick); + } else { + totalEl.textContent = String(to); + severityDonutCenterDisplayed.total = to; + } + } + requestAnimationFrame(tick); +} + +function resetSeverityDonutCenter(skipTotalSnapshot) { 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); + var n = severityDonutState.total || 0; + if (!skipTotalSnapshot && totalEl) totalEl.textContent = String(n); + if (!skipTotalSnapshot) severityDonutCenterDisplayed.total = n; + severityDonutCenterDisplayed.hoverCount = null; if (labelEl) { labelEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.totalVulns') : '总漏洞数'); labelEl.classList.remove('is-severity'); + labelEl.removeAttribute('data-severity'); } if (centerEl) centerEl.classList.remove('is-hovering'); } @@ -1585,28 +1673,46 @@ function setSeverityDonutHover(severityId) { var labelEl = document.getElementById('dashboard-severity-center-label'); if (!severityId) { severityDonutState.hoverId = null; - if (svgEl) svgEl.classList.remove('is-highlighting'); + if (svgEl) { + svgEl.classList.remove('is-highlighting'); + svgEl.removeAttribute('data-hover-severity'); + } clearSeverityDonutLegendHighlight(); - resetSeverityDonutCenter(); + resetSeverityDonutCenter(false); + _clearSeverityDonutChartWrapHover(); return; } var count = (severityDonutState.bySeverity && severityDonutState.bySeverity[severityId]) || 0; severityDonutState.hoverId = severityId; - if (svgEl) svgEl.classList.add('is-highlighting'); + if (svgEl) { + svgEl.classList.add('is-highlighting'); + svgEl.setAttribute('data-hover-severity', severityId); + } highlightSeverityDonutParts(severityId); highlightSeverityLegendItem(severityId); - if (totalEl) totalEl.textContent = String(count); + if (totalEl) { + totalEl.textContent = String(count); + severityDonutCenterDisplayed.hoverCount = count; + } if (labelEl) { labelEl.textContent = severityLabel(severityId); labelEl.classList.add('is-severity'); + labelEl.setAttribute('data-severity', severityId); } if (centerEl) centerEl.classList.add('is-hovering'); + var chartWrap = document.querySelector('.dashboard-severity-chart'); + if (chartWrap) chartWrap.setAttribute('data-hover-severity', severityId); +} + +function _clearSeverityDonutChartWrapHover() { + var chartWrap = document.querySelector('.dashboard-severity-chart'); + if (chartWrap) chartWrap.removeAttribute('data-hover-severity'); } function highlightSeverityDonutParts(severityId) { var svgEl = document.getElementById('dashboard-severity-donut'); if (!svgEl) return; - svgEl.querySelectorAll('[data-severity]').forEach(function (el) { + svgEl.querySelectorAll('.donut-segment[data-severity], .donut-segment-gloss[data-severity], .donut-leader[data-severity], .donut-label-text[data-severity]').forEach(function (el) { var match = el.getAttribute('data-severity') === severityId; el.classList.toggle('is-active', match); el.classList.toggle('is-dimmed', !match); @@ -1678,16 +1784,16 @@ function hideSeverityDonutTooltip() { } function attachSeverityDonutInteractivity() { - var svgEl = document.getElementById('dashboard-severity-donut'); + var hitsEl = document.getElementById('dashboard-severity-donut-hits'); var legend = document.getElementById('dashboard-vuln-bars'); - if (!svgEl) return; + if (!hitsEl) return; if (!severityDonutState.bound) { severityDonutState.bound = true; - svgEl.addEventListener('mouseover', severityDonutPointerOver); - svgEl.addEventListener('mouseout', severityDonutPointerOut); - svgEl.addEventListener('click', severityDonutClick); - svgEl.addEventListener('keydown', severityDonutKeydown); + hitsEl.addEventListener('mouseover', severityDonutPointerOver); + hitsEl.addEventListener('mouseout', severityDonutPointerOut); + hitsEl.addEventListener('click', severityDonutClick); + hitsEl.addEventListener('keydown', severityDonutKeydown); if (legend) { legend.addEventListener('mouseover', severityLegendPointerOver); legend.addEventListener('mouseout', severityLegendPointerOut); @@ -1705,30 +1811,50 @@ function attachSeverityDonutInteractivity() { }); } -function severityDonutTarget(el) { - return el && el.closest && el.closest('[data-severity]'); +function severityDonutHitTarget(el) { + return el && el.closest && el.closest('.donut-segment-hit'); +} + +function severityDonutCancelHoverClear() { + clearTimeout(severityDonutHoverClearTimer); + severityDonutHoverClearTimer = null; +} + +function severityDonutScheduleHoverClear() { + severityDonutCancelHoverClear(); + severityDonutHoverClearTimer = setTimeout(function () { + severityDonutHoverClearTimer = null; + setSeverityDonutHover(null); + hideSeverityDonutTooltip(); + }, 60); } function severityDonutPointerOver(ev) { - var target = severityDonutTarget(ev.target); - if (!target || !target.classList.contains('donut-segment')) return; + var target = severityDonutHitTarget(ev.target); + if (!target) return; var id = target.getAttribute('data-severity'); if (!id) return; + severityDonutCancelHoverClear(); + if (severityDonutState.hoverId === 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(); + var related = ev.relatedTarget; + if (related) { + if (severityDonutHitTarget(related)) return; + var legendItem = related.closest && related.closest('.dashboard-severity-legend-item[data-severity]'); + if (legendItem) return; + var hitsRoot = document.getElementById('dashboard-severity-donut-hits'); + if (hitsRoot && hitsRoot.contains(related)) return; + } + severityDonutScheduleHoverClear(); } function severityDonutClick(ev) { - var target = severityDonutTarget(ev.target); - if (!target || !target.classList.contains('donut-segment')) return; + var target = severityDonutHitTarget(ev.target); + if (!target) return; var id = target.getAttribute('data-severity'); if (!id) return; ev.preventDefault(); @@ -1737,8 +1863,8 @@ function severityDonutClick(ev) { function severityDonutKeydown(ev) { if (ev.key !== 'Enter' && ev.key !== ' ') return; - var target = severityDonutTarget(ev.target); - if (!target || !target.classList.contains('donut-segment')) return; + var target = severityDonutHitTarget(ev.target); + if (!target) return; ev.preventDefault(); var id = target.getAttribute('data-severity'); if (id) navigateToVulnerabilitiesWithFilter({ severity: id }); @@ -1749,6 +1875,7 @@ function severityLegendPointerOver(ev) { if (!item) return; var id = item.getAttribute('data-severity'); if (!id) return; + severityDonutCancelHoverClear(); setSeverityDonutHover(id); showSeverityDonutTooltip(ev, id); } @@ -1757,8 +1884,7 @@ 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(); + severityDonutScheduleHoverClear(); } function severityLegendClick(ev) { diff --git a/web/templates/index.html b/web/templates/index.html index 3d8d04da..85248cd7 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -455,6 +455,7 @@ +