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 @@
+