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