mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 12:58:01 +02:00
Add files via upload
This commit is contained in:
+237
-54
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user