From 648002b09b603919a9fb62a1bbd2f6da62bf5441 Mon Sep 17 00:00:00 2001 From: Sycun <165354365+Sycun@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:24:08 +0800 Subject: [PATCH] refactor(vulnerabilities): use compact rows with drawer actions --- web/static/css/vulnerability-list.css | 139 +++++++++++++++++ web/static/i18n/ar-SA.json | 1 + web/static/i18n/en-US.json | 1 + web/static/i18n/ru-RU.json | 1 + web/static/i18n/zh-CN.json | 1 + web/static/js/vulnerability-workspace.js | 1 + web/static/js/vulnerability.js | 185 +++++------------------ web/templates/index.html | 5 +- 8 files changed, 181 insertions(+), 153 deletions(-) create mode 100644 web/static/css/vulnerability-list.css diff --git a/web/static/css/vulnerability-list.css b/web/static/css/vulnerability-list.css new file mode 100644 index 00000000..e1fab152 --- /dev/null +++ b/web/static/css/vulnerability-list.css @@ -0,0 +1,139 @@ +.vulnerability-record-list { + display: flex; + flex-direction: column; + gap: 8px; + list-style: none; + padding: 0; + margin: 0; + min-width: 0; +} + +.vulnerability-record-row { + display: grid; + grid-template-columns: 18px 52px minmax(0, 1fr) auto; + align-items: center; + column-gap: 12px; + min-height: 72px; + padding: 12px 16px; + border: 1px solid var(--border-color); + border-radius: 10px; + background: var(--bg-primary); +} + +.vulnerability-record-row:hover { + border-color: color-mix(in srgb, var(--accent-color) 35%, var(--border-color)); +} + +.vulnerability-record-row:has(.vulnerability-select-record:checked) { + background: color-mix(in srgb, var(--accent-color) 5%, var(--bg-primary)); +} + +.vulnerability-record-select { + display: flex; + align-items: center; + justify-content: center; +} + +.vulnerability-record-select .vulnerability-select-record { + margin: 0; +} + +.vulnerability-record-row > .severity-badge { + justify-self: start; + white-space: nowrap; +} + +.vulnerability-record-main { + display: block; + min-width: 0; + width: 100%; + padding: 0; + border: 0; + background: transparent; + color: var(--text-primary); + font: inherit; + text-align: start; + cursor: pointer; +} + +.vulnerability-record-title-line { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.vulnerability-record-title, +.vulnerability-record-summary { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.5; +} + +.vulnerability-record-title { + min-width: 0; + font-size: 14px; + font-weight: 600; +} + +.vulnerability-record-summary { + margin-top: 3px; + font-size: 12px; + color: var(--text-secondary); +} + +.vulnerability-record-title-line .vulnerability-case-badge { + margin: 0; + flex-shrink: 0; +} + +.vulnerability-record-actions { + display: flex; + align-items: center; + gap: 16px; +} + +.vulnerability-record-view { + padding: 6px 0; + border: 0; + background: transparent; + color: var(--text-primary); + font: inherit; + font-size: 13px; + white-space: nowrap; + cursor: pointer; +} + +.vulnerability-record-view:hover, +.vulnerability-record-main:hover .vulnerability-record-title { + color: var(--accent-color); +} + +.vulnerability-record-main:focus-visible, +.vulnerability-record-view:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 4px; +} + +.vulnerability-inspector-actions .btn-delete { + margin-inline-start: auto; + color: var(--error-color); +} + +@media (max-width: 600px) { + .vulnerability-record-row { + grid-template-columns: 18px 46px minmax(0, 1fr); + gap: 8px; + padding: 12px; + } + .vulnerability-record-main { + padding: 0 !important; + min-height: 0 !important; + } + .vulnerability-record-actions { + grid-column: 2 / -1; + justify-content: flex-end; + } +} diff --git a/web/static/i18n/ar-SA.json b/web/static/i18n/ar-SA.json index dd672dd7..fb21ccc4 100644 --- a/web/static/i18n/ar-SA.json +++ b/web/static/i18n/ar-SA.json @@ -2720,6 +2720,7 @@ "errorGeneric": "حدث خطأ ما. يُرجى المحاولة مرة أخرى." }, "vulnerabilityPage": { + "viewFinding": "عرض التفاصيل", "taskSearch": "البحث في المهام أو قوائم الانتظار أو الوسوم…", "allTasks": "جميع المهام", "clearTaskSelection": "مسح مرشح المهام", diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 8f0d1980..ad94cb37 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2862,6 +2862,7 @@ "errorGeneric": "Something went wrong. Please try again." }, "vulnerabilityPage": { + "viewFinding": "View details", "taskSearch": "Search tasks, queues or tags…", "allTasks": "All tasks", "clearTaskSelection": "Clear task filter", diff --git a/web/static/i18n/ru-RU.json b/web/static/i18n/ru-RU.json index 6edaa883..5bb8a202 100644 --- a/web/static/i18n/ru-RU.json +++ b/web/static/i18n/ru-RU.json @@ -2875,6 +2875,7 @@ "errorGeneric": "Что-то пошло не так. Повторите попытку." }, "vulnerabilityPage": { + "viewFinding": "Подробнее", "taskSearch": "Поиск задач, очередей или тегов…", "allTasks": "Все задачи", "clearTaskSelection": "Сбросить фильтр задач", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 88bcbc58..8b93aa90 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2850,6 +2850,7 @@ "errorGeneric": "操作失败,请稍后重试。" }, "vulnerabilityPage": { + "viewFinding": "查看详情", "taskSearch": "搜索任务、队列或标签…", "allTasks": "全部任务", "clearTaskSelection": "清除任务筛选", diff --git a/web/static/js/vulnerability-workspace.js b/web/static/js/vulnerability-workspace.js index 2b041075..a198d4d6 100644 --- a/web/static/js/vulnerability-workspace.js +++ b/web/static/js/vulnerability-workspace.js @@ -152,6 +152,7 @@ async function openVulnerabilityInspector(id) { +
${['overview','evidence','history'].map(tab => ``).join('')} diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index f853cb74..460acc7c 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -47,33 +47,6 @@ function getVulnerabilityScrollContainer() { 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'; - const row = content.closest('.vulnerability-table-detail'); - if (row) row.hidden = false; - const trigger = document.getElementById('vulnerability-row-toggle-' + id); - if (trigger) trigger.setAttribute('aria-expanded', 'true'); - 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); @@ -1304,13 +1277,9 @@ 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'); const hostOpenStates = preserveScroll @@ -1375,7 +1344,7 @@ async function loadVulnerabilities(page = null, options = {}) { } renderVulnerabilities(vulnerabilities, { - view, expandedIds: expandedIds || [], hostOpenStates, + view, hostOpenStates, preserveHostSelection: preserveScroll }); renderVulnerabilityPagination(); @@ -1419,18 +1388,15 @@ function renderVulnerabilities(vulnerabilities, renderOptions) { const exactMatch = vulnerabilities.length === 1 && vulnerabilityFilters.id === vulnerabilities[0].id ? vulnerabilities[0].id : null; - const expandedIds = exactMatch ? [exactMatch] : opts.expandedIds; - listContainer.innerHTML = renderVulnerabilityTable(vulnerabilities); + listContainer.innerHTML = renderVulnerabilityList(vulnerabilities); initVulnerabilityStatusPickers(listContainer); initVulnerabilityProjectBindSelects(listContainer); if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer); - if (exactMatch && vulnerabilities[0].summary_only && typeof openVulnerabilityInspector === 'function') { + if (exactMatch && typeof openVulnerabilityInspector === 'function') { openVulnerabilityInspector(exactMatch); - } else { - restoreExpandedVulnerabilityDetails(expandedIds); } updateVulnerabilityListSummary(); } @@ -1448,7 +1414,7 @@ function updateVulnerabilityListSummary() { if (!list || !toolbar) return; const count = ['host','task'].includes(vulnerabilityView) && list.querySelector('.vulnerability-host-browser') ? new Set(vulnerabilityBrowserRows.map(vuln => vuln.id)).size - : list.querySelectorAll('.vulnerability-card, .vulnerability-table-item').length; + : list.querySelectorAll('.vulnerability-record-row').length; document.getElementById('vulnerability-page-summary').textContent = count ? vulnT('vulnerabilityPage.pageFindings', { count }) : ''; } @@ -1547,7 +1513,7 @@ function renderVulnerabilityBrowser(vulnerabilities, opts) { !Array.from(list.querySelectorAll('[data-host-key]')).some(el => el.dataset.hostKey === vulnerabilityBrowserSelection)) { vulnerabilityBrowserSelection = ''; } - renderVulnerabilityBrowserTable(opts.expandedIds); + renderVulnerabilityBrowserTable(); if (activeSearch) { search.focus({ preventScroll: true }); search.setSelectionRange(caret, caret); @@ -1586,7 +1552,7 @@ function renderVulnerabilityHostRiskDot(items) { return highest ? `` : ''; } -function renderVulnerabilityBrowserTable(expandedIds) { +function renderVulnerabilityBrowserTable() { const selection = vulnerabilityBrowserSelection; const items = getVisibleVulnerabilityBrowserItems(); document.querySelectorAll('#vulnerabilities-list [data-host-key]').forEach(option => { @@ -1613,100 +1579,41 @@ function renderVulnerabilityBrowserTable(expandedIds) { ).join('') + `${escapeHtml(vulnT('vulnerabilityPage.groupPending', { count: pending }))}`; document.getElementById('vulnerability-host-export').disabled = items.length === 0; const container = document.getElementById('vulnerability-host-table'); - container.innerHTML = renderVulnerabilityTable(items); + container.innerHTML = renderVulnerabilityList(items); initVulnerabilityStatusPickers(container); initVulnerabilityProjectBindSelects(container); if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container); const exact = items.find(vuln => vulnerabilityFilters.id === vuln.id); - if (exact?.summary_only && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id); - else restoreExpandedVulnerabilityDetails(exact ? [exact.id] : expandedIds); + if (exact && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id); updateVulnerabilityListSummary(); } -function renderVulnerabilityTable(items) { - return items.length ? `
- - - - - - - - - ${['tableSeverity', 'tableTitle', 'detailTarget', 'tableStatus', 'tableTask', 'tableDate', 'tableActions'] - .map(key => '').join('')} - - ${items.map(renderVulnerabilityTableItem).join('')} -
${escapeHtml(vulnT('vulnerabilityPage.selectColumn'))}' + escapeHtml(vulnT('vulnerabilityPage.' + key)) + '
` : `
${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}
`; +function renderVulnerabilityList(items) { + if (!items.length) { + return `
${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}
`; + } + return ``; } -function renderVulnerabilityTableItem(vuln) { +function renderVulnerabilityListItem(vuln) { const id = escapeAttr(vuln.id); const jsID = escapeJsStringAttr(vuln.id); - const task = vuln.task_tag || vuln.task_id || vulnT('vulnerabilityPage.viewNoTask'); - const targetHosts = Array.from(new Map((vuln.targets || []).map(target => [target.host,target])).values()); - const created = new Date(vuln.created_at); - return ` - - ${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''} - ${escapeHtml(vulnSeverityLabel(vuln.severity))} - - -

${escapeHtml((vuln.description || '').slice(0, 160))}

+ const summary = (vuln.description || vuln.target || '').replace(/\s+/g, ' ').trim(); + return `
  • + ${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''} + ${escapeHtml(vulnSeverityLabel(vuln.severity))} + - - - -
  • - - ${vuln.summary_only ? '' : `${renderVulnerabilityDetailsContent(vuln)}`} - `; -} - -function renderVulnerabilityDetailsContent(vuln) { - return ` - - `; + + ${summary ? `${escapeHtml(summary)}` : ''} + +
    + ${buildVulnerabilityStatusPicker(vuln)} + +
    + `; } // 渲染分页控件 @@ -1977,7 +1884,6 @@ async function saveVulnerability() { if (typeof refreshVulnerabilityInspector === 'function') refreshVulnerabilityInspector(currentVulnerabilityId); const editedId = currentVulnerabilityId; const isEdit = !!editedId; - const expandedIds = isEdit ? getExpandedVulnerabilityIds() : []; closeVulnerabilityModal(); loadVulnerabilityStats(); @@ -1994,7 +1900,7 @@ async function saveVulnerability() { return; } - await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds }); + await loadVulnerabilities(null, { preserveScroll: true, silent: true }); } catch (error) { console.error('保存漏洞失败:', error); alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message); @@ -2014,6 +1920,9 @@ async function deleteVulnerability(id) { if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed')); + if (typeof vulnerabilityInspectorID !== 'undefined' && vulnerabilityInspectorID === id) { + closeAppModal('vulnerability-inspector'); + } loadVulnerabilityStats(); await removeVulnerabilityCard(id); } catch (error) { @@ -2151,28 +2060,6 @@ async function saveVulnerabilityAlertSubscription() { } } -// 切换漏洞详情展开/折叠 -function toggleVulnerabilityDetails(id) { - const content = document.getElementById(`content-${id}`); - const icon = document.getElementById(`expand-icon-${id}`); - - if (!content || !icon) return; - - if (content.style.display === 'none') { - content.style.display = 'block'; - icon.style.transform = 'rotate(90deg)'; - loadVulnerabilityRelatedFacts(id).catch((e) => console.warn(e)); - } else { - content.style.display = 'none'; - icon.style.transform = 'rotate(0deg)'; - } - const expanded = content.style.display !== 'none'; - const row = content.closest('.vulnerability-table-detail'); - if (row) row.hidden = !expanded; - const trigger = document.getElementById('vulnerability-row-toggle-' + id); - if (trigger) trigger.setAttribute('aria-expanded', String(expanded)); -} - /** 展开漏洞详情时加载关联项目事实 */ async function loadVulnerabilityRelatedFacts(vulnId) { const el = document.getElementById(`vuln-related-facts-${vulnId}`); @@ -2658,11 +2545,7 @@ async function bindVulnerabilityProject(vulnId, projectId, silent) { alert(vulnT('vulnerabilityPage.projectBindOk')); } loadVulnerabilityStats(); - const expandedIds = getExpandedVulnerabilityIds(); - if (!expandedIds.includes(vulnId)) { - expandedIds.push(vulnId); - } - await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds }); + await loadVulnerabilities(null, { preserveScroll: true, silent: true }); } catch (error) { console.error('绑定项目失败:', error); alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message); diff --git a/web/templates/index.html b/web/templates/index.html index 7aa83d35..f3069494 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -48,6 +48,7 @@ + @@ -7096,8 +7097,8 @@ - - + +