Add files via upload

This commit is contained in:
公明
2026-05-15 18:03:59 +08:00
committed by GitHub
parent 179976ae57
commit d4e1fe3bbe
3 changed files with 410 additions and 100 deletions
+237 -54
View File
@@ -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 {
+172 -46
View File
@@ -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 += '<linearGradient id="donut-track-face" x1="0%" y1="0%" x2="0%" y2="100%">';
html += '<stop offset="0%" stop-color="#f8fafc"/>';
html += '<stop offset="55%" stop-color="#e8eef5"/>';
html += '<stop offset="100%" stop-color="#dce5ef"/>';
html += '</linearGradient>';
html += '<radialGradient id="donut-track-vignette" cx="50%" cy="85%" r="75%" fx="50%" fy="85%">';
html += '<stop offset="0%" stop-color="#ffffff" stop-opacity="0.35"/>';
html += '<stop offset="70%" stop-color="#ffffff" stop-opacity="0"/>';
html += '</radialGradient>';
html += '<radialGradient id="donut-inner-gloss" cx="35%" cy="75%" r="55%">';
html += '<stop offset="0%" stop-color="#ffffff" stop-opacity="0.45"/>';
html += '<stop offset="55%" stop-color="#ffffff" stop-opacity="0.08"/>';
html += '<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>';
html += '</radialGradient>';
html += '<filter id="donut-segment-soften" x="-18%" y="-18%" width="136%" height="136%" color-interpolation-filters="sRGB">';
html += '<feGaussianBlur in="SourceAlpha" stdDeviation="0.8" result="blur"/>';
html += '<feOffset dx="0" dy="1.5" in="blur" result="off"/>';
html += '<feFlood flood-color="#0f172a" flood-opacity="0.13" result="flood"/>';
html += '<feComposite in="flood" in2="off" operator="in" result="shadow"/>';
html += '<feMerge><feMergeNode in="shadow"/><feMergeNode in="SourceGraphic"/></feMerge>';
html += '</filter>';
Object.keys(SEVERITY_DONUT_GRADIENTS).forEach(function (id) {
var stops = SEVERITY_DONUT_GRADIENTS[id];
html += '<linearGradient id="donut-grad-' + id + '" x1="0%" y1="0%" x2="100%" y2="100%">';
html += '<linearGradient id="donut-grad-' + id + '" x1="18%" y1="12%" x2="88%" y2="94%">';
html += '<stop offset="0%" stop-color="' + stops[0] + '"/>';
html += '<stop offset="100%" stop-color="' + stops[1] + '"/>';
html += '<stop offset="52%" stop-color="' + stops[1] + '"/>';
html += '<stop offset="100%" stop-color="' + stops[2] + '"/>';
html += '</linearGradient>';
});
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 = '<path class="donut-track" d="' + halfRingPath(cfg.cx, cfg.cy, cfg.rOuter, cfg.rInner) + '"/>';
var trackPath = halfRingPath(cfg.cx, cfg.cy, cfg.rOuter, cfg.rInner);
trackEl.innerHTML =
'<path class="donut-track-shadow" d="' + trackPath + '"/>' +
'<path class="donut-track" fill="url(#donut-track-face)" d="' + trackPath + '"/>' +
'<path class="donut-track-vignette" fill="url(#donut-track-vignette)" d="' + trackPath + '"/>';
}
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 += '<path class="donut-segment seg-' + seg.id + '" data-severity="' + seg.id + '" data-count="' + seg.value + '" data-pct="' + pctRounded + '" fill="url(#donut-grad-' + seg.id + ')" d="' + path + '" tabindex="0" role="button" aria-label="' + ariaLabel + '"/>';
segmentsHtml += '<path class="donut-segment seg-' + seg.id + '" data-severity="' + seg.id + '" data-count="' + seg.value + '" data-pct="' + pctRounded + '" fill="url(#donut-grad-' + seg.id + ')" d="' + path + '"/>';
hitsHtml += '<path class="donut-segment-hit seg-' + seg.id + '" data-severity="' + seg.id + '" fill="transparent" d="' + path + '" tabindex="0" role="button" aria-label="' + ariaLabel + '"/>';
glossHtml += '<path class="donut-segment-gloss seg-' + seg.id + '" data-severity="' + seg.id + '" fill="url(#donut-inner-gloss)" d="' + arcSegmentPath(cfg.cx, cfg.cy, cfg.rOuter - 2, cfg.rInner + 6, angleStart, angleEnd) + '" pointer-events="none"/>';
// 仅当占比 >= 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 += '<line class="donut-leader label-' + seg.id + '" data-severity="' + seg.id + '" x1="' + lineX1.toFixed(1) + '" y1="' + lineY1.toFixed(1) + '" x2="' + lineX2.toFixed(1) + '" y2="' + lineY2.toFixed(1) + '"/>';
leadersHtml += '<line class="donut-leader label-' + seg.id + '" data-severity="' + seg.id + '" pathLength="100" x1="' + lineX1.toFixed(1) + '" y1="' + lineY1.toFixed(1) + '" x2="' + lineX2.toFixed(1) + '" y2="' + lineY2.toFixed(1) + '"/>';
labelsHtml += '<text class="donut-label-text label-' + seg.id + '" data-severity="' + seg.id + '" text-anchor="' + anchor + '" x="' + lx.toFixed(1) + '" y="' + ly.toFixed(1) + '">';
labelsHtml += '<tspan x="' + lx.toFixed(1) + '" dy="0">' + seg.value + ' <tspan class="donut-label-pct">(' + pctText + ')</tspan></tspan>';
@@ -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) {
+1
View File
@@ -455,6 +455,7 @@
<g id="dashboard-severity-donut-track"></g>
<g id="dashboard-severity-donut-leaders"></g>
<g id="dashboard-severity-donut-segments"></g>
<g id="dashboard-severity-donut-hits"></g>
<g id="dashboard-severity-donut-labels"></g>
</svg>
<div class="dashboard-severity-center" id="dashboard-severity-center">