mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-26 16:00:06 +02:00
Add files via upload
This commit is contained in:
+434
-17
@@ -39,6 +39,220 @@ function vulnStatusLabel(code) {
|
||||
return m[code] ? vulnT(m[code]) : code;
|
||||
}
|
||||
|
||||
const VULN_STATUS_CODES = ['open', 'confirmed', 'fixed', 'false_positive', 'ignored'];
|
||||
const VULNERABILITY_REMOVE_ANIM_MS = 200;
|
||||
|
||||
function getVulnerabilityScrollContainer() {
|
||||
const page = document.getElementById('page-vulnerabilities');
|
||||
return page ? page.querySelector('.page-content') : null;
|
||||
}
|
||||
|
||||
function getExpandedVulnerabilityIds() {
|
||||
const ids = [];
|
||||
document.querySelectorAll('#vulnerabilities-list .vulnerability-content').forEach(function (el) {
|
||||
if (el.style.display !== 'none') {
|
||||
const id = (el.id || '').replace(/^content-/, '');
|
||||
if (id) ids.push(id);
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function restoreExpandedVulnerabilityDetails(expandedIds) {
|
||||
if (!expandedIds || !expandedIds.length) return;
|
||||
expandedIds.forEach(function (id) {
|
||||
const content = document.getElementById('content-' + id);
|
||||
const icon = document.getElementById('expand-icon-' + id);
|
||||
if (!content || content.style.display !== 'none') return;
|
||||
content.style.display = 'block';
|
||||
if (icon) icon.style.transform = 'rotate(90deg)';
|
||||
loadVulnerabilityRelatedFacts(id).catch(function (e) { console.warn(e); });
|
||||
});
|
||||
}
|
||||
|
||||
function buildVulnerabilityStatusPicker(vuln) {
|
||||
const current = vuln.status || 'open';
|
||||
const id = escapeHtml(vuln.id);
|
||||
const label = escapeHtml(vulnT('vulnerabilityPage.statusChangeLabel'));
|
||||
const caretSvg = '<svg class="vuln-status-picker-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
const options = VULN_STATUS_CODES.map(function (code) {
|
||||
const selected = code === current;
|
||||
const selCls = selected ? ' is-selected' : '';
|
||||
const ariaSel = selected ? ' aria-selected="true"' : ' aria-selected="false"';
|
||||
return '<button type="button" class="vuln-status-picker-option' + selCls + '" role="option" data-value="' + code + '"' + ariaSel + '>' +
|
||||
'<span class="vuln-status-picker-check" aria-hidden="true">✓</span>' +
|
||||
'<span class="vuln-status-picker-label">' + escapeHtml(vulnStatusLabel(code)) + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
return '<div class="vuln-status-picker status-' + escapeHtml(current) + '" data-vuln-id="' + id + '" data-prev-status="' + escapeHtml(current) + '">' +
|
||||
'<button type="button" class="vuln-status-picker-trigger" aria-label="' + label + '" aria-haspopup="listbox" aria-expanded="false">' +
|
||||
'<span class="vuln-status-picker-value">' + escapeHtml(vulnStatusLabel(current)) + '</span>' +
|
||||
caretSvg +
|
||||
'</button>' +
|
||||
'<div class="vuln-status-picker-menu" role="listbox" hidden>' + options + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
const VULN_STATUS_PICKER_STATUS_CLASSES = VULN_STATUS_CODES.map(function (code) {
|
||||
return 'status-' + code;
|
||||
});
|
||||
|
||||
function setVulnerabilityStatusPickerDisabled(pickerEl, disabled) {
|
||||
if (!pickerEl) return;
|
||||
pickerEl.classList.toggle('is-disabled', !!disabled);
|
||||
const trigger = pickerEl.querySelector('.vuln-status-picker-trigger');
|
||||
if (trigger) trigger.disabled = !!disabled;
|
||||
}
|
||||
|
||||
function updateVulnerabilityStatusPicker(pickerEl, status) {
|
||||
if (!pickerEl) return;
|
||||
const code = status || 'open';
|
||||
VULN_STATUS_PICKER_STATUS_CLASSES.forEach(function (cls) {
|
||||
pickerEl.classList.remove(cls);
|
||||
});
|
||||
pickerEl.classList.add('status-' + code);
|
||||
pickerEl.dataset.prevStatus = code;
|
||||
const valueEl = pickerEl.querySelector('.vuln-status-picker-value');
|
||||
if (valueEl) valueEl.textContent = vulnStatusLabel(code);
|
||||
pickerEl.querySelectorAll('.vuln-status-picker-option').forEach(function (opt) {
|
||||
const isSel = opt.getAttribute('data-value') === code;
|
||||
opt.classList.toggle('is-selected', isSel);
|
||||
opt.setAttribute('aria-selected', isSel ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
let vulnerabilityStatusPickerDocBound = false;
|
||||
|
||||
function closeAllVulnerabilityStatusPickers() {
|
||||
document.querySelectorAll('.vuln-status-picker.open').forEach(function (picker) {
|
||||
picker.classList.remove('open');
|
||||
const menu = picker.querySelector('.vuln-status-picker-menu');
|
||||
const trigger = picker.querySelector('.vuln-status-picker-trigger');
|
||||
if (menu) menu.hidden = true;
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function initVulnerabilityStatusPickers(root) {
|
||||
if (!vulnerabilityStatusPickerDocBound) {
|
||||
document.addEventListener('click', closeAllVulnerabilityStatusPickers);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeAllVulnerabilityStatusPickers();
|
||||
});
|
||||
vulnerabilityStatusPickerDocBound = true;
|
||||
}
|
||||
|
||||
const scope = root || document.getElementById('vulnerabilities-list');
|
||||
if (!scope) return;
|
||||
|
||||
scope.querySelectorAll('.vuln-status-picker').forEach(function (picker) {
|
||||
if (picker.dataset.bound === '1') return;
|
||||
picker.dataset.bound = '1';
|
||||
|
||||
picker.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||
picker.addEventListener('keydown', function (e) { e.stopPropagation(); });
|
||||
|
||||
const trigger = picker.querySelector('.vuln-status-picker-trigger');
|
||||
const menu = picker.querySelector('.vuln-status-picker-menu');
|
||||
if (!trigger || !menu) return;
|
||||
|
||||
trigger.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (picker.classList.contains('is-disabled')) return;
|
||||
const wasOpen = picker.classList.contains('open');
|
||||
closeAllVulnerabilityStatusPickers();
|
||||
if (!wasOpen) {
|
||||
picker.classList.add('open');
|
||||
menu.hidden = false;
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
const opt = e.target.closest('.vuln-status-picker-option');
|
||||
if (!opt || picker.classList.contains('is-disabled')) return;
|
||||
const newStatus = opt.getAttribute('data-value');
|
||||
const vulnId = picker.dataset.vulnId;
|
||||
closeAllVulnerabilityStatusPickers();
|
||||
changeVulnerabilityStatus(vulnId, newStatus, picker);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function vulnerabilityStatusMatchesFilter(status) {
|
||||
const filterStatus = (vulnerabilityFilters.status || '').trim();
|
||||
return !filterStatus || filterStatus === status;
|
||||
}
|
||||
|
||||
function removeVulnerabilityCard(vulnId, options) {
|
||||
const opts = options || {};
|
||||
const card = document.getElementById('vulnerability-card-' + vulnId) ||
|
||||
document.querySelector('.vulnerability-card[data-vuln-id="' + vulnId + '"]');
|
||||
if (!card) return;
|
||||
|
||||
const nextCard = card.nextElementSibling;
|
||||
card.classList.add('vulnerability-card--removing');
|
||||
|
||||
setTimeout(function () {
|
||||
card.remove();
|
||||
if (opts.decrementTotal !== false) {
|
||||
vulnerabilityPagination.total = Math.max(0, (vulnerabilityPagination.total || 0) - 1);
|
||||
vulnerabilityPagination.totalPages = Math.max(
|
||||
1,
|
||||
Math.ceil(vulnerabilityPagination.total / vulnerabilityPagination.pageSize)
|
||||
);
|
||||
renderVulnerabilityPagination();
|
||||
}
|
||||
|
||||
const list = document.getElementById('vulnerabilities-list');
|
||||
const remaining = list ? list.querySelectorAll('.vulnerability-card').length : 0;
|
||||
if (remaining === 0) {
|
||||
if (vulnerabilityPagination.currentPage > 1) {
|
||||
vulnerabilityPagination.currentPage--;
|
||||
}
|
||||
loadVulnerabilities();
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.focusNext !== false && nextCard && nextCard.classList.contains('vulnerability-card')) {
|
||||
nextCard.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}, VULNERABILITY_REMOVE_ANIM_MS);
|
||||
}
|
||||
|
||||
async function changeVulnerabilityStatus(vulnId, newStatus, pickerEl) {
|
||||
if (!vulnId || !pickerEl) return;
|
||||
const prevStatus = pickerEl.dataset.prevStatus || newStatus;
|
||||
if (newStatus === prevStatus) return;
|
||||
|
||||
setVulnerabilityStatusPickerDisabled(pickerEl, true);
|
||||
try {
|
||||
const response = await apiFetch('/api/vulnerabilities/' + encodeURIComponent(vulnId), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(function () { return {}; });
|
||||
throw new Error(err.error || vulnT('vulnerabilityPage.statusUpdateFailed'));
|
||||
}
|
||||
|
||||
updateVulnerabilityStatusPicker(pickerEl, newStatus);
|
||||
loadVulnerabilityStats();
|
||||
|
||||
if (!vulnerabilityStatusMatchesFilter(newStatus)) {
|
||||
removeVulnerabilityCard(vulnId, { decrementTotal: true, focusNext: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新漏洞状态失败:', error);
|
||||
updateVulnerabilityStatusPicker(pickerEl, prevStatus);
|
||||
alert(vulnT('vulnerabilityPage.statusUpdateFailed') + ': ' + error.message);
|
||||
} finally {
|
||||
setVulnerabilityStatusPickerDisabled(pickerEl, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 从localStorage读取每页显示数量,默认为20
|
||||
const getVulnerabilityPageSize = () => {
|
||||
const saved = localStorage.getItem('vulnerabilityPageSize');
|
||||
@@ -175,6 +389,7 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
syncVulnerabilityStatCardActiveState();
|
||||
updateVulnerabilityFilterPanelState();
|
||||
renderVulnerabilityFilterChips();
|
||||
syncAllVulnFilterCustomSelects();
|
||||
}
|
||||
|
||||
// 初始化漏洞管理页面
|
||||
@@ -387,6 +602,7 @@ function initVulnerabilityFilterPanel() {
|
||||
|
||||
if (vulnerabilityFilterPanelBound) {
|
||||
updateVulnerabilityFilterPanelState();
|
||||
syncAllVulnFilterCustomSelects();
|
||||
return;
|
||||
}
|
||||
vulnerabilityFilterPanelBound = true;
|
||||
@@ -448,6 +664,146 @@ function initVulnerabilityFilterPanel() {
|
||||
});
|
||||
|
||||
bindVulnerabilityFilterTypeaheads();
|
||||
initVulnerabilityFilterSelects();
|
||||
}
|
||||
|
||||
const VULN_FILTER_CUSTOM_SELECT_IDS = ['vulnerability-project-filter', 'vulnerability-status-filter'];
|
||||
const vulnFilterCustomSelectMap = {};
|
||||
let vulnFilterCustomSelectDocBound = false;
|
||||
|
||||
const VULN_FILTER_SELECT_CARET = '<svg class="vuln-filter-select-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
function closeAllVulnFilterCustomSelects() {
|
||||
Object.keys(vulnFilterCustomSelectMap).forEach(function (id) {
|
||||
const reg = vulnFilterCustomSelectMap[id];
|
||||
if (!reg || !reg.wrapper) return;
|
||||
reg.wrapper.classList.remove('open');
|
||||
if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function syncVulnFilterCustomSelect(selectId) {
|
||||
const reg = vulnFilterCustomSelectMap[selectId];
|
||||
if (!reg) return;
|
||||
const select = reg.select;
|
||||
const dropdown = reg.dropdown;
|
||||
const trigger = reg.trigger;
|
||||
const valueSpan = trigger.querySelector('.vuln-filter-select-value');
|
||||
|
||||
dropdown.innerHTML = '';
|
||||
Array.prototype.forEach.call(select.options, function (opt) {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'vuln-filter-select-option';
|
||||
item.setAttribute('role', 'option');
|
||||
item.setAttribute('data-value', opt.value);
|
||||
if (opt.value === select.value) {
|
||||
item.classList.add('is-selected');
|
||||
item.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
item.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
const check = document.createElement('span');
|
||||
check.className = 'vuln-filter-select-check';
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = '✓';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'vuln-filter-select-label';
|
||||
label.textContent = opt.textContent;
|
||||
item.appendChild(check);
|
||||
item.appendChild(label);
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
const selectedOpt = select.options[select.selectedIndex];
|
||||
if (valueSpan) {
|
||||
valueSpan.textContent = selectedOpt ? selectedOpt.textContent : '';
|
||||
}
|
||||
trigger.disabled = !!select.disabled;
|
||||
reg.wrapper.classList.toggle('is-disabled', !!select.disabled);
|
||||
}
|
||||
|
||||
function syncAllVulnFilterCustomSelects() {
|
||||
VULN_FILTER_CUSTOM_SELECT_IDS.forEach(syncVulnFilterCustomSelect);
|
||||
}
|
||||
|
||||
function enhanceVulnFilterCustomSelect(selectId) {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
if (select.dataset.vulnCustomSelect === '1') {
|
||||
syncVulnFilterCustomSelect(selectId);
|
||||
return;
|
||||
}
|
||||
select.dataset.vulnCustomSelect = '1';
|
||||
select.classList.add('vuln-filter-native-select');
|
||||
select.tabIndex = -1;
|
||||
select.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'vuln-filter-select';
|
||||
|
||||
const trigger = document.createElement('button');
|
||||
trigger.type = 'button';
|
||||
trigger.className = 'vuln-filter-select-trigger';
|
||||
trigger.setAttribute('aria-haspopup', 'listbox');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
const valueSpan = document.createElement('span');
|
||||
valueSpan.className = 'vuln-filter-select-value';
|
||||
trigger.appendChild(valueSpan);
|
||||
trigger.insertAdjacentHTML('beforeend', VULN_FILTER_SELECT_CARET);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'vuln-filter-select-dropdown';
|
||||
dropdown.setAttribute('role', 'listbox');
|
||||
|
||||
const parent = select.parentNode;
|
||||
parent.insertBefore(wrapper, select);
|
||||
wrapper.appendChild(trigger);
|
||||
wrapper.appendChild(dropdown);
|
||||
wrapper.appendChild(select);
|
||||
|
||||
vulnFilterCustomSelectMap[selectId] = { wrapper: wrapper, trigger: trigger, dropdown: dropdown, select: select };
|
||||
|
||||
trigger.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (select.disabled) return;
|
||||
if (typeof closeAllVulnerabilityStatusPickers === 'function') {
|
||||
closeAllVulnerabilityStatusPickers();
|
||||
}
|
||||
const open = wrapper.classList.contains('open');
|
||||
closeAllVulnFilterCustomSelects();
|
||||
if (!open) {
|
||||
wrapper.classList.add('open');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener('click', function (e) {
|
||||
const opt = e.target.closest('.vuln-filter-select-option');
|
||||
if (!opt) return;
|
||||
e.stopPropagation();
|
||||
const val = opt.getAttribute('data-value');
|
||||
if (val === null) return;
|
||||
if (select.value !== val) {
|
||||
select.value = val;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
wrapper.classList.remove('open');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
syncVulnFilterCustomSelect(selectId);
|
||||
});
|
||||
}
|
||||
|
||||
function initVulnerabilityFilterSelects() {
|
||||
if (!vulnFilterCustomSelectDocBound) {
|
||||
document.addEventListener('click', closeAllVulnFilterCustomSelects);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeAllVulnFilterCustomSelects();
|
||||
});
|
||||
vulnFilterCustomSelectDocBound = true;
|
||||
}
|
||||
VULN_FILTER_CUSTOM_SELECT_IDS.forEach(enhanceVulnFilterCustomSelect);
|
||||
syncAllVulnFilterCustomSelects();
|
||||
}
|
||||
|
||||
function countVulnerabilityAdvancedFiltersActive() {
|
||||
@@ -559,6 +915,9 @@ function removeVulnerabilityFilterByKey(key) {
|
||||
if (Object.prototype.hasOwnProperty.call(vulnerabilityFilters, key)) {
|
||||
vulnerabilityFilters[key] = '';
|
||||
}
|
||||
if (key === 'project_id' || key === 'status') {
|
||||
syncAllVulnFilterCustomSelects();
|
||||
}
|
||||
applyVulnerabilityFilters();
|
||||
}
|
||||
|
||||
@@ -779,9 +1138,22 @@ function updateVulnerabilityStats(stats) {
|
||||
}
|
||||
|
||||
// 加载漏洞列表
|
||||
async function loadVulnerabilities(page = null) {
|
||||
async function loadVulnerabilities(page = null, options = {}) {
|
||||
const opts = options && typeof options === 'object' ? options : {};
|
||||
const preserveScroll = !!opts.preserveScroll;
|
||||
const silent = !!opts.silent;
|
||||
let expandedIds = opts.expandedIds;
|
||||
|
||||
const scrollEl = preserveScroll ? getVulnerabilityScrollContainer() : null;
|
||||
const scrollTop = scrollEl ? scrollEl.scrollTop : 0;
|
||||
if (expandedIds === undefined && preserveScroll) {
|
||||
expandedIds = getExpandedVulnerabilityIds();
|
||||
}
|
||||
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
listContainer.innerHTML = `<div class="loading-spinner">${escapeHtml(vulnT('vulnerabilityPage.loading'))}</div>`;
|
||||
if (!silent) {
|
||||
listContainer.innerHTML = `<div class="loading-spinner">${escapeHtml(vulnT('vulnerabilityPage.loading'))}</div>`;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查apiFetch是否可用
|
||||
@@ -830,8 +1202,14 @@ async function loadVulnerabilities(page = null) {
|
||||
console.error('未知的响应格式:', data);
|
||||
}
|
||||
|
||||
renderVulnerabilities(vulnerabilities);
|
||||
renderVulnerabilities(vulnerabilities, { expandedIds: expandedIds || [] });
|
||||
renderVulnerabilityPagination();
|
||||
|
||||
if (preserveScroll && scrollEl) {
|
||||
requestAnimationFrame(function () {
|
||||
scrollEl.scrollTop = scrollTop;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载漏洞列表失败:', error);
|
||||
listContainer.innerHTML = `<div class="error-message">${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(error.message)}</div>`;
|
||||
@@ -839,7 +1217,8 @@ async function loadVulnerabilities(page = null) {
|
||||
}
|
||||
|
||||
// 渲染漏洞列表
|
||||
function renderVulnerabilities(vulnerabilities) {
|
||||
function renderVulnerabilities(vulnerabilities, renderOptions) {
|
||||
const opts = renderOptions && typeof renderOptions === 'object' ? renderOptions : {};
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
|
||||
// 处理空值情况(使用 data-i18n 以便语言切换时自动更新)
|
||||
@@ -862,7 +1241,6 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
const html = vulnerabilities.map(vuln => {
|
||||
const severityClass = `severity-${vuln.severity}`;
|
||||
const severityText = vulnSeverityLabel(vuln.severity);
|
||||
const statusText = vulnStatusLabel(vuln.status);
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale());
|
||||
const projectLabel = vuln.project_id
|
||||
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
|
||||
@@ -875,7 +1253,7 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
const deleteTitle = escapeHtml(vulnT('common.delete'));
|
||||
|
||||
return `
|
||||
<div class="vulnerability-card ${severityClass}">
|
||||
<div class="vulnerability-card ${severityClass}" id="vulnerability-card-${vuln.id}" data-vuln-id="${escapeHtml(vuln.id)}">
|
||||
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails('${vuln.id}')" style="cursor: pointer;">
|
||||
<div class="vulnerability-title-section">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
@@ -886,7 +1264,7 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
</div>
|
||||
<div class="vulnerability-meta">
|
||||
<span class="severity-badge ${severityClass}">${severityText}</span>
|
||||
<span class="status-badge status-${vuln.status}">${statusText}</span>
|
||||
${buildVulnerabilityStatusPicker(vuln)}
|
||||
${projectBadge}
|
||||
<span class="vulnerability-date">${createdDate}</span>
|
||||
</div>
|
||||
@@ -935,10 +1313,13 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
}).join('');
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
initVulnerabilityStatusPickers(listContainer);
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
|
||||
restoreExpandedVulnerabilityDetails(opts.expandedIds);
|
||||
|
||||
// 如果通过漏洞ID筛选且只返回一条记录,自动展开详情(提升“点击查看”的用户体验)
|
||||
if (vulnerabilities.length === 1 && vulnerabilityFilters.id && vulnerabilityFilters.id === vulnerabilities[0].id) {
|
||||
setTimeout(() => {
|
||||
@@ -1191,11 +1572,27 @@ async function saveVulnerability() {
|
||||
throw new Error(error.error || vulnT('vulnerabilityPage.saveFailed'));
|
||||
}
|
||||
|
||||
const updated = await response.json();
|
||||
const editedId = currentVulnerabilityId;
|
||||
const isEdit = !!editedId;
|
||||
const expandedIds = isEdit ? getExpandedVulnerabilityIds() : [];
|
||||
|
||||
closeVulnerabilityModal();
|
||||
loadVulnerabilityStats();
|
||||
// 保存/更新后,重置到第一页
|
||||
vulnerabilityPagination.currentPage = 1;
|
||||
loadVulnerabilities();
|
||||
|
||||
if (!isEdit) {
|
||||
vulnerabilityPagination.currentPage = 1;
|
||||
loadVulnerabilities();
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatus = (updated && updated.status) || data.status;
|
||||
if (!vulnerabilityStatusMatchesFilter(newStatus)) {
|
||||
removeVulnerabilityCard(editedId, { decrementTotal: true, focusNext: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
|
||||
} catch (error) {
|
||||
console.error('保存漏洞失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
|
||||
@@ -1216,14 +1613,20 @@ async function deleteVulnerability(id) {
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
|
||||
|
||||
loadVulnerabilityStats();
|
||||
// 删除后,如果当前页没有数据了,回到上一页
|
||||
const card = document.getElementById('vulnerability-card-' + id) ||
|
||||
document.querySelector('.vulnerability-card[data-vuln-id="' + id + '"]');
|
||||
if (card) {
|
||||
removeVulnerabilityCard(id, { decrementTotal: true, focusNext: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (vulnerabilityPagination.currentPage > 1 && vulnerabilityPagination.total > 0) {
|
||||
const itemsOnCurrentPage = vulnerabilityPagination.total - (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize;
|
||||
if (itemsOnCurrentPage <= 1) {
|
||||
vulnerabilityPagination.currentPage--;
|
||||
}
|
||||
}
|
||||
loadVulnerabilities();
|
||||
await loadVulnerabilities(null, { preserveScroll: true });
|
||||
} catch (error) {
|
||||
console.error('删除漏洞失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.deleteFailed') + ': ' + error.message);
|
||||
@@ -1263,6 +1666,7 @@ function clearVulnerabilityFilters() {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
syncAllVulnFilterCustomSelects();
|
||||
|
||||
vulnerabilityFilters = {
|
||||
q: '',
|
||||
@@ -1685,10 +2089,16 @@ window.onclick = function(event) {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
document.addEventListener('languagechange', async function () {
|
||||
const page = document.getElementById('page-vulnerabilities');
|
||||
if (page && page.classList.contains('active')) {
|
||||
const panel = document.getElementById('vulnerability-filter-panel');
|
||||
if (panel && typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(panel);
|
||||
}
|
||||
renderVulnerabilityFilterChips();
|
||||
await refreshVulnerabilityProjectFilter();
|
||||
syncAllVulnFilterCustomSelects();
|
||||
loadVulnerabilities();
|
||||
}
|
||||
});
|
||||
@@ -1709,11 +2119,15 @@ async function bindVulnerabilityProject(vulnId, projectId, silent) {
|
||||
alert(vulnT('vulnerabilityPage.projectBindOk'));
|
||||
}
|
||||
loadVulnerabilityStats();
|
||||
loadVulnerabilities();
|
||||
const expandedIds = getExpandedVulnerabilityIds();
|
||||
if (!expandedIds.includes(vulnId)) {
|
||||
expandedIds.push(vulnId);
|
||||
}
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
|
||||
} catch (error) {
|
||||
console.error('绑定项目失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
|
||||
loadVulnerabilities();
|
||||
await loadVulnerabilities(null, { preserveScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1738,15 +2152,16 @@ async function refreshVulnerabilityProjectFilter() {
|
||||
list.forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
|
||||
}
|
||||
const cur = vulnerabilityFilters.project_id || sel.value || '';
|
||||
let html = '<option value="">全部项目</option>';
|
||||
let html = '<option value="">' + escapeHtml(vulnT('vulnerabilityPage.allProjects')) + '</option>';
|
||||
(list || []).forEach((p) => {
|
||||
if (!p.id) return;
|
||||
const selected = p.id === cur ? ' selected' : '';
|
||||
const arch = p.status === 'archived' ? ' [归档]' : '';
|
||||
const arch = p.status === 'archived' ? ' [' + vulnT('projects.archived') + ']' : '';
|
||||
html += `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
|
||||
});
|
||||
sel.innerHTML = html;
|
||||
if (cur) sel.value = cur;
|
||||
syncVulnFilterCustomSelect('vulnerability-project-filter');
|
||||
const modalSel = document.getElementById('vulnerability-project-id');
|
||||
if (modalSel && isAppModalOpen('vulnerability-modal')) {
|
||||
const modalCur = modalSel.value || '';
|
||||
@@ -1762,6 +2177,7 @@ function setVulnerabilityProjectFilter(projectId) {
|
||||
vulnerabilityFilters.project_id = projectId || '';
|
||||
const sel = document.getElementById('vulnerability-project-filter');
|
||||
if (sel) sel.value = projectId || '';
|
||||
syncVulnFilterCustomSelect('vulnerability-project-filter');
|
||||
applyVulnerabilityFilters();
|
||||
}
|
||||
|
||||
@@ -1777,4 +2193,5 @@ window.setVulnerabilityProjectFilter = setVulnerabilityProjectFilter;
|
||||
window.setVulnerabilityIdFilter = setVulnerabilityIdFilter;
|
||||
window.bindVulnerabilityProject = bindVulnerabilityProject;
|
||||
window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml;
|
||||
window.changeVulnerabilityStatus = changeVulnerabilityStatus;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user