From 9f6eb330478ee41151a30c40f7008567da22722c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:02:24 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 798 +++++++++++++++++++++++-- web/static/i18n/en-US.json | 21 + web/static/i18n/zh-CN.json | 21 + web/static/js/audit-datetime-picker.js | 428 +++++++++++++ web/static/js/audit.js | 330 ++++++++-- web/templates/index.html | 89 ++- 6 files changed, 1546 insertions(+), 141 deletions(-) create mode 100644 web/static/js/audit-datetime-picker.js diff --git a/web/static/css/style.css b/web/static/css/style.css index 73677e60..055b59fa 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -4588,46 +4588,199 @@ header { } /* 系统设置 - 日志审计 */ -.audit-logs-toolbar { +.audit-section-head { display: flex; - flex-wrap: wrap; - align-items: flex-end; + align-items: center; justify-content: space-between; - gap: 16px; - margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px 16px; + margin: 0 0 12px; } -.audit-logs-filters { +.audit-section-head h3 { + margin: 0; + font-size: 1.05rem; + font-weight: 600; +} + +.audit-summary-tags { display: flex; flex-wrap: wrap; - align-items: flex-end; - gap: 12px; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex: 1; + min-width: 0; } -.audit-logs-filters > .btn-secondary { - align-self: flex-end; - margin-bottom: 0; +.audit-summary-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 999px; + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--bg-secondary, rgba(0, 0, 0, 0.03)); + border: 1px solid var(--border-color); } -.audit-logs-filters label { +.audit-summary-tag strong { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-primary); +} + +.audit-summary-tag--ok strong { + color: #0d7a3e; +} + +.audit-summary-tag--warn strong { + color: #c0392b; +} + +.audit-summary-tag-label { + white-space: nowrap; +} + +.audit-filter-card { display: flex; flex-direction: column; - gap: 4px; + gap: 14px; + padding: 14px 16px; + margin-bottom: 16px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); +} + +.audit-filter-fields { + display: flex; + flex-direction: column; + gap: 0; +} + +.audit-filter-row { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 12px 14px; +} + +.audit-field--event { + flex: 0 1 360px; + min-width: 200px; + max-width: 420px; +} + +.audit-field--result { + flex: 0 0 112px; + min-width: 112px; +} + +.audit-filter-time-group { + display: flex; + flex: 1 1 360px; + gap: 12px 14px; + margin-left: auto; + min-width: 320px; +} + +.audit-filter-time-group .audit-field--time { + flex: 1 1 0; + min-width: 150px; +} + +.audit-field:not(.audit-field--event):not(.audit-field--keyword):not(.audit-field--result):not(.audit-field--time) { + flex: 0 1 150px; + min-width: 130px; +} + +.audit-filter-bottom { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 12px 14px; +} + +.audit-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.audit-field > span { font-size: 0.8125rem; color: var(--text-secondary); + line-height: 1.2; +} + +.audit-field--keyword { + flex: 1 1 240px; + min-width: 200px; +} + +.audit-field select, +.audit-field input[type="text"]:not(.audit-datetime-input) { + width: 100%; + height: 34px; + box-sizing: border-box; + padding: 0 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.8125rem; +} + +.audit-field .audit-datetime-field { + width: 100%; + min-width: 0; +} + +.audit-logs-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex: 0 0 auto; + margin-left: auto; +} + +.audit-logs-actions .btn-secondary, +.audit-logs-actions .btn-primary { + height: 34px; + padding: 0 14px; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + box-sizing: border-box; + font-size: 0.8125rem; } /* 事件类型:两个下拉与「结果」等控件同款边框,无外层套框 */ .audit-filter-cascade { display: flex; align-items: center; - gap: 8px; + gap: 6px; + width: 100%; } .audit-filter-cascade select { - flex: 0 1 auto; - min-width: 120px; - max-width: 148px; + flex: 1 1 0; + min-width: 0; + max-width: none; + height: 34px; + box-sizing: border-box; + padding: 0 8px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.8125rem; } .audit-filter-cascade select:disabled { @@ -4636,6 +4789,142 @@ header { background: var(--bg-secondary, #f5f6f8); } +.audit-native-select { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.audit-filter-cascade .audit-custom-select, +.audit-field--result .audit-custom-select { + flex: 1 1 0; + min-width: 0; + position: relative; +} + +.audit-field--result .audit-custom-select { + width: 100%; +} + +.audit-custom-select-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + width: 100%; + height: 34px; + box-sizing: border-box; + padding: 0 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.8125rem; + line-height: 1.2; + cursor: pointer; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.audit-custom-select-value { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; +} + +.audit-custom-select-caret { + flex-shrink: 0; + font-size: 0.7rem; + color: var(--text-secondary); + line-height: 1; +} + +.audit-custom-select-trigger:hover:not(:disabled) { + border-color: rgba(0, 102, 255, 0.45); +} + +.audit-custom-select.open .audit-custom-select-trigger { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1); +} + +.audit-custom-select.is-disabled .audit-custom-select-trigger { + opacity: 0.55; + cursor: not-allowed; + background: var(--bg-secondary, #f5f6f8); +} + +.audit-custom-select-dropdown { + display: none; + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + z-index: 2000; + max-height: 280px; + overflow-y: auto; + padding: 4px 0; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: var(--shadow-lg); +} + +.audit-custom-select.open .audit-custom-select-dropdown { + display: block; +} + +.audit-custom-select-option { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + font-size: 0.8125rem; + line-height: 1.2; + color: var(--text-primary); + cursor: pointer; + transition: background-color 0.12s ease; +} + +.audit-custom-select-option:hover { + background: var(--bg-secondary); +} + +.audit-custom-select-check { + flex: 0 0 14px; + width: 14px; + font-size: 0.75rem; + line-height: 1; + color: var(--accent-color); + opacity: 0; + text-align: center; +} + +.audit-custom-select-option.is-selected .audit-custom-select-check { + opacity: 1; +} + +.audit-custom-select-option.is-selected { + color: var(--accent-color); + font-weight: 500; +} + +.audit-custom-select-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .audit-filter-cascade-arrow { flex-shrink: 0; font-size: 0.8125rem; @@ -4645,31 +4934,426 @@ header { pointer-events: none; } -.audit-logs-filters select, -.audit-logs-filters input[type="text"], -.audit-logs-filters input[type="datetime-local"] { - min-width: 140px; - padding: 0.35rem 0.5rem; +.audit-time-presets { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.audit-time-presets-label { + font-size: 0.8125rem; + color: var(--text-secondary); + flex-shrink: 0; +} + +.audit-time-preset-btn { + padding: 0.25rem 0.65rem; + font-size: 0.8125rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); - color: var(--text-primary); + color: var(--text-secondary); + cursor: pointer; + transition: border-color 0.15s, color 0.15s; } -.audit-logs-actions { +.audit-time-preset-btn:hover { + border-color: var(--accent-color); + color: var(--accent-color); +} + +.audit-datetime-field { display: flex; + align-items: center; + gap: 2px; + height: 34px; + box-sizing: border-box; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + padding-right: 2px; +} + +.audit-datetime-field:focus-within { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.12); +} + +.audit-datetime-input { + flex: 1; + min-width: 0; + border: none; + background: transparent; + color: var(--text-primary); + padding: 0.35rem 0.5rem; + font-size: 0.8125rem; + cursor: pointer; +} + +.audit-datetime-input:focus { + outline: none; +} + +.audit-datetime-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + flex-shrink: 0; +} + +.audit-datetime-btn:hover { + background: var(--bg-secondary, #f5f6f8); + color: var(--accent-color); +} + +.audit-datetime-clear-btn { + font-size: 1.1rem; + line-height: 1; +} + +.audit-dt-popover { + position: fixed; + z-index: 1100; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + padding: 12px; +} + +.audit-dt-popover-inner { + display: flex; + flex-direction: column; + gap: 10px; +} + +.audit-dt-head { + display: flex; + align-items: center; + justify-content: space-between; gap: 8px; } -/* 列表 + 底部分页合并为一张卡片,避免双边框/底部分隔线 */ -#settings-section-audit .audit-log-list.c2-event-list { +.audit-dt-month-label { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-primary); + flex: 1; + text-align: center; +} + +.audit-dt-nav { + width: 28px; + height: 28px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-secondary); + cursor: pointer; + font-size: 1.1rem; + line-height: 1; +} + +.audit-dt-nav:hover { + border-color: var(--accent-color); + color: var(--accent-color); +} + +.audit-dt-body { + display: flex; + gap: 10px; +} + +.audit-dt-calendar { + flex: 1; + min-width: 0; +} + +.audit-dt-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; + margin-bottom: 4px; +} + +.audit-dt-weekdays span { + text-align: center; + font-size: 0.7rem; + color: var(--text-secondary); + padding: 2px 0; +} + +.audit-dt-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +.audit-dt-day { + height: 30px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-primary); + font-size: 0.8125rem; + cursor: pointer; +} + +.audit-dt-day:hover { + background: var(--bg-secondary, #f5f6f8); +} + +.audit-dt-day.is-other-month { + color: var(--text-secondary); + opacity: 0.45; +} + +.audit-dt-day.is-selected { + background: var(--accent-color); + color: #fff; +} + +.audit-dt-day.is-selected:hover { + background: var(--accent-hover); +} + +.audit-dt-time { + display: flex; + gap: 6px; + flex-shrink: 0; + border-left: 1px solid var(--border-color); + padding-left: 8px; +} + +.audit-dt-time-col { + display: flex; + flex-direction: column; + width: 52px; + min-width: 52px; +} + +.audit-dt-time-label { + text-align: center; + font-size: 0.7rem; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.audit-dt-time-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 210px; + overflow-y: auto; + scrollbar-width: thin; +} + +.audit-dt-time-item { + height: 28px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--text-primary); + font-size: 0.8125rem; + cursor: pointer; + flex-shrink: 0; +} + +.audit-dt-time-item:hover { + background: var(--bg-secondary, #f5f6f8); +} + +.audit-dt-time-item.is-selected { + background: var(--accent-color); + color: #fff; +} + +.audit-dt-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 4px; + border-top: 1px solid var(--border-color); +} + +.audit-dt-footer-btn { + padding: 0.3rem 0.75rem; + font-size: 0.8125rem; + border: none; + border-radius: 6px; + background: transparent; + color: var(--accent-color); + cursor: pointer; +} + +.audit-dt-footer-btn:hover { + background: rgba(0, 102, 255, 0.08); +} + +.audit-dt-footer-btn--primary { + background: var(--accent-color); + color: #fff; +} + +.audit-dt-footer-btn--primary:hover { + background: var(--accent-hover); +} + + +#settings-section-audit .audit-log-list { margin-bottom: 0; +} + +.audit-log-empty { + padding: 32px 16px; + text-align: center; + color: var(--text-secondary); + font-size: 0.875rem; + border: 1px dashed var(--border-color); + border-radius: 8px; +} + +.audit-log-table-wrap { + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; + overflow-x: auto; + width: 100%; + box-sizing: border-box; +} + +.audit-log-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; + table-layout: auto; + min-width: 720px; +} + +.audit-log-table th, +.audit-log-table td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid var(--border-color); + vertical-align: middle; +} + +.audit-log-table thead { + background: var(--bg-secondary); + color: var(--text-secondary); + font-weight: 600; + font-size: 0.75rem; +} + +.audit-log-table th { + white-space: nowrap; +} + +.audit-log-table tbody tr:last-child td { border-bottom: none; - border-radius: 8px 8px 0 0; +} + +.audit-log-row { + cursor: pointer; + transition: background 0.12s ease; +} + +.audit-log-row:hover { + background: rgba(0, 102, 255, 0.06); +} + +.audit-log-col-time { + white-space: nowrap; + color: var(--text-secondary); + font-size: 0.75rem; + font-variant-numeric: tabular-nums; +} + +.audit-log-col-msg { + max-width: 280px; + font-weight: 500; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.audit-log-col-ip { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.75rem; + white-space: nowrap; +} + +.audit-log-col-resource { + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.7rem; + color: var(--text-secondary); +} + +.audit-log-cell-muted { + color: var(--text-muted); +} + +.audit-tag { + display: inline-flex; + align-items: center; + padding: 3px 8px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 500; + line-height: 1.4; + white-space: nowrap; + border: 1px solid transparent; +} + +.audit-tag--ok { + color: #0d7a3e; + background: rgba(13, 122, 62, 0.1); + border-color: rgba(13, 122, 62, 0.22); +} + +.audit-tag--fail { + color: #c0392b; + background: rgba(192, 57, 43, 0.1); + border-color: rgba(192, 57, 43, 0.22); +} + +.audit-tag--cat { + color: var(--accent-color); + background: rgba(0, 102, 255, 0.08); + border-color: rgba(0, 102, 255, 0.18); +} + +.audit-tag--act { + color: var(--text-secondary); + background: var(--bg-secondary, rgba(0, 0, 0, 0.03)); + border-color: var(--border-color); +} + +.audit-tag--ip { + color: #5c6b7a; + background: rgba(92, 107, 122, 0.08); + border-color: rgba(92, 107, 122, 0.18); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } #settings-section-audit .audit-logs-pagination { - margin-top: 0; + margin-top: 12px; padding: 0; border: none; box-shadow: none; @@ -4679,11 +5363,7 @@ header { #settings-section-audit .audit-logs-pagination .monitor-pagination { margin-top: 0; border: 1px solid var(--border-color); - border-radius: 0 0 8px 8px; -} - -.audit-log-item { - cursor: pointer; + border-radius: 8px; } .audit-detail-pre { @@ -4696,41 +5376,41 @@ header { margin-top: 12px; } -.audit-summary-stats { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin: 12px 0 16px; +.audit-timezone-hint { + margin: 0; + padding-top: 10px; + border-top: 1px solid var(--border-color); + font-size: 0.75rem; + color: var(--text-secondary); + opacity: 0.9; } -.audit-stat-card { - flex: 1; - min-width: 120px; - padding: 12px 16px; - border-radius: 8px; - background: var(--bg-secondary, rgba(255, 255, 255, 0.04)); - border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); -} +@media (max-width: 640px) { + .audit-filter-bottom { + flex-direction: column; + align-items: stretch; + } -.audit-stat-card strong { - display: block; - font-size: 1.35rem; - margin-top: 4px; -} + .audit-filter-time-group { + margin-left: 0; + flex: 1 1 100%; + min-width: 0; + } -.audit-stat-label { - font-size: 0.85rem; - opacity: 0.75; -} + .audit-logs-actions { + margin-left: 0; + justify-content: flex-start; + } -.audit-retention-hint { - margin-top: 4px; - opacity: 0.85; + .audit-summary-tags { + justify-content: flex-start; + } } .audit-export-dropdown { position: relative; - display: inline-block; + display: inline-flex; + align-items: flex-end; } .audit-export-trigger { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index b94c1c59..891ea4d4 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2086,14 +2086,35 @@ "filterResult": "Result", "pageSize": "Per page", "statTotal": "Filtered total", + "statSuccess": "Success", "statFailures": "Failures", "statRecent7d": "Last 7 days", "retentionHint": "Audit records are kept for {{days}} days, then purged automatically.", "disabledHint": "Audit logging is disabled; new actions are not written.", "filterSince": "From", "filterUntil": "Until", + "filterTimeZone": "Timezone: {{tz}} (filter uses your browser's local time)", + "datetimePlaceholder": "Select date & time", + "timePresets": "Quick range", + "preset15m": "Last 15 min", + "preset1h": "Last 1 hour", + "preset24h": "Last 24 hours", + "preset7d": "Last 7 days", + "presetToday": "Today", + "pickerHour": "Hour", + "pickerMinute": "Min", + "pickerClear": "Clear", + "pickerToday": "Today", + "pickerConfirm": "OK", "filterQuery": "Keyword", "filterQueryPlaceholder": "Message / resource ID / action", + "colTime": "Time", + "colMessage": "Message", + "colCategory": "Category", + "colAction": "Action", + "colResult": "Result", + "colIp": "IP", + "colResource": "Resource ID", "cat": { "auth": "Auth", "config": "Config", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index c7529210..2f08f2b8 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2074,14 +2074,35 @@ "filterResult": "结果", "pageSize": "每页", "statTotal": "当前筛选", + "statSuccess": "成功", "statFailures": "失败", "statRecent7d": "近 7 天", "retentionHint": "审计记录保留 {{days}} 天,超期自动清理。", "disabledHint": "审计功能已关闭,新操作不会写入审计表。", "filterSince": "开始时间", "filterUntil": "结束时间", + "filterTimeZone": "时区:{{tz}}(筛选按浏览器本地时间)", + "datetimePlaceholder": "选择日期时间", + "timePresets": "快捷", + "preset15m": "最近15分钟", + "preset1h": "最近1小时", + "preset24h": "最近24小时", + "preset7d": "最近7天", + "presetToday": "今天", + "pickerHour": "时", + "pickerMinute": "分", + "pickerClear": "清除", + "pickerToday": "今天", + "pickerConfirm": "确定", "filterQuery": "关键词", "filterQueryPlaceholder": "消息 / 资源 ID / 操作名", + "colTime": "时间", + "colMessage": "说明", + "colCategory": "类别", + "colAction": "操作", + "colResult": "结果", + "colIp": "IP", + "colResource": "资源 ID", "cat": { "auth": "认证", "config": "配置", diff --git a/web/static/js/audit-datetime-picker.js b/web/static/js/audit-datetime-picker.js new file mode 100644 index 00000000..ef2fad23 --- /dev/null +++ b/web/static/js/audit-datetime-picker.js @@ -0,0 +1,428 @@ +/** + * Audit log datetime picker — cross-browser, locale-aware (SLS-style calendar + time columns). + */ +(function () { + 'use strict'; + + var registry = {}; + var popover = null; + var activeFieldId = null; + var draft = null; + var viewYear = 0; + var viewMonth = 0; + + function pad2(n) { + return String(n).padStart(2, '0'); + } + + function pickerLocale() { + if (typeof auditLocale === 'function') return auditLocale(); + if (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) return 'zh-CN'; + return 'en-US'; + } + + function pickerT(key, fallback) { + if (typeof auditT === 'function') return auditT(key, null, fallback); + if (typeof t === 'function') { + var v = t(key); + if (v && v !== key) return v; + } + return fallback; + } + + function partsToStorage(p) { + if (!p) return ''; + return p.y + '-' + pad2(p.m) + '-' + pad2(p.d) + 'T' + pad2(p.h) + ':' + pad2(p.mi); + } + + function parseStorage(value) { + if (!value) return null; + var m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(String(value).trim()); + if (!m) return null; + return { y: +m[1], m: +m[2], d: +m[3], h: +m[4], mi: +m[5] }; + } + + function formatDisplay(parts) { + if (!parts) return ''; + var loc = pickerLocale(); + try { + var d = new Date(parts.y, parts.m - 1, parts.d, parts.h, parts.mi, 0, 0); + return d.toLocaleString(loc, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + } catch (_) { + return partsToStorage(parts).replace('T', ' '); + } + } + + function nowParts() { + var n = new Date(); + return { y: n.getFullYear(), m: n.getMonth() + 1, d: n.getDate(), h: n.getHours(), mi: n.getMinutes() }; + } + + function startOfTodayParts() { + var n = new Date(); + return { y: n.getFullYear(), m: n.getMonth() + 1, d: n.getDate(), h: 0, mi: 0 }; + } + + function monthTitle(year, month) { + var loc = pickerLocale(); + if (loc.startsWith('zh')) { + return year + '\u5e74' + pad2(month) + '\u6708'; + } + try { + return new Date(year, month - 1, 1).toLocaleString(loc, { month: 'long', year: 'numeric' }); + } catch (_) { + return year + '-' + pad2(month); + } + } + + function weekdayHeaders() { + var loc = pickerLocale(); + if (loc.startsWith('zh')) { + return ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d']; + } + return ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + } + + function buildMonthGrid(year, month) { + var first = new Date(year, month - 1, 1); + var start = new Date(first); + start.setDate(first.getDate() - first.getDay()); + var cells = []; + var cursor = new Date(start); + for (var i = 0; i < 42; i++) { + cells.push({ + y: cursor.getFullYear(), + m: cursor.getMonth() + 1, + d: cursor.getDate(), + inMonth: cursor.getMonth() === month - 1 + }); + cursor.setDate(cursor.getDate() + 1); + } + return cells; + } + + function ensurePopover() { + if (popover) return popover; + popover = document.createElement('div'); + popover.className = 'audit-dt-popover'; + popover.hidden = true; + popover.setAttribute('role', 'dialog'); + popover.innerHTML = + '
' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '
'; + document.body.appendChild(popover); + + popover.addEventListener('click', function (ev) { + ev.stopPropagation(); + var btn = ev.target.closest('[data-nav]'); + if (btn) { + if (btn.getAttribute('data-nav') === 'prev') { + viewMonth -= 1; + if (viewMonth < 1) { viewMonth = 12; viewYear -= 1; } + } else { + viewMonth += 1; + if (viewMonth > 12) { viewMonth = 1; viewYear += 1; } + } + renderPopover(); + return; + } + var dayBtn = ev.target.closest('[data-day]'); + if (dayBtn && draft) { + draft.y = +dayBtn.getAttribute('data-y'); + draft.m = +dayBtn.getAttribute('data-m'); + draft.d = +dayBtn.getAttribute('data-d'); + if (draft.y !== viewYear || draft.m !== viewMonth) { + viewYear = draft.y; + viewMonth = draft.m; + renderCalendar(); + } else { + updateDaySelection(); + } + return; + } + var timeBtn = ev.target.closest('[data-time]'); + if (timeBtn && draft) { + var part = timeBtn.getAttribute('data-part'); + var val = +timeBtn.getAttribute('data-time'); + if (part === 'hour') draft.h = val; + if (part === 'minute') draft.mi = val; + updateTimeSelection(); + return; + } + var actionBtn = ev.target.closest('[data-action]'); + if (!actionBtn) return; + var action = actionBtn.getAttribute('data-action'); + if (action === 'clear') { + applyValue(activeFieldId, ''); + closePopover(); + } else if (action === 'today') { + if (draft) { + var t = nowParts(); + draft.y = t.y; draft.m = t.m; draft.d = t.d; + viewYear = t.y; viewMonth = t.m; + } + renderPopover(); + } else if (action === 'confirm') { + applyValue(activeFieldId, partsToStorage(draft)); + closePopover(); + } + }); + + document.addEventListener('click', onDocumentClick); + document.addEventListener('keydown', onDocumentKeydown); + document.addEventListener('languagechange', function () { + if (!popover.hidden) renderPopover(); + refreshAllDisplays(); + }); + + return popover; + } + + function onDocumentClick(ev) { + if (!popover || popover.hidden) return; + if (popover.contains(ev.target)) return; + if (activeFieldId && registry[activeFieldId] && registry[activeFieldId].wrap.contains(ev.target)) return; + closePopover(); + } + + function onDocumentKeydown(ev) { + if (ev.key === 'Escape' && popover && !popover.hidden) { + closePopover(); + } + } + + function positionPopover(fieldWrap) { + var rect = fieldWrap.getBoundingClientRect(); + var width = 320; + popover.style.width = width + 'px'; + var left = rect.left; + if (left + width > window.innerWidth - 12) { + left = Math.max(12, window.innerWidth - width - 12); + } + popover.style.left = left + 'px'; + var top = rect.bottom + 6; + if (top + 340 > window.innerHeight - 12) { + top = Math.max(12, rect.top - 340 - 6); + } + popover.style.top = top + 'px'; + } + + function renderCalendar() { + if (!popover || !draft) return; + popover.querySelector('.audit-dt-month-label').textContent = monthTitle(viewYear, viewMonth); + var cal = popover.querySelector('.audit-dt-calendar'); + var headers = weekdayHeaders(); + var html = '
'; + headers.forEach(function (h) { html += '' + h + ''; }); + html += '
'; + buildMonthGrid(viewYear, viewMonth).forEach(function (cell) { + var cls = 'audit-dt-day'; + if (!cell.inMonth) cls += ' is-other-month'; + if (draft && cell.y === draft.y && cell.m === draft.m && cell.d === draft.d) cls += ' is-selected'; + html += ''; + }); + html += '
'; + cal.innerHTML = html; + } + + function renderTimeLists() { + if (!popover || !draft) return; + var hourList = popover.querySelector('[data-part="hour"] .audit-dt-time-list'); + var minuteList = popover.querySelector('[data-part="minute"] .audit-dt-time-list'); + var hourHtml = ''; + var minuteHtml = ''; + var h; + for (h = 0; h < 24; h++) { + hourHtml += ''; + } + for (h = 0; h < 60; h++) { + minuteHtml += ''; + } + hourList.innerHTML = hourHtml; + minuteList.innerHTML = minuteHtml; + scrollTimeSelection(hourList, draft.h); + scrollTimeSelection(minuteList, draft.mi); + } + + function updateDaySelection() { + if (!popover || !draft) return; + popover.querySelectorAll('.audit-dt-day').forEach(function (btn) { + var selected = +btn.getAttribute('data-y') === draft.y && + +btn.getAttribute('data-m') === draft.m && + +btn.getAttribute('data-d') === draft.d; + btn.classList.toggle('is-selected', selected); + }); + } + + function updateTimeSelection() { + if (!popover || !draft) return; + var hourList = popover.querySelector('[data-part="hour"] .audit-dt-time-list'); + var minuteList = popover.querySelector('[data-part="minute"] .audit-dt-time-list'); + if (!hourList || !minuteList || !hourList.children.length) { + renderTimeLists(); + return; + } + hourList.querySelectorAll('.audit-dt-time-item').forEach(function (btn) { + btn.classList.toggle('is-selected', +btn.getAttribute('data-time') === draft.h); + }); + minuteList.querySelectorAll('.audit-dt-time-item').forEach(function (btn) { + btn.classList.toggle('is-selected', +btn.getAttribute('data-time') === draft.mi); + }); + scrollTimeSelection(hourList, draft.h); + scrollTimeSelection(minuteList, draft.mi); + } + + function renderPopover() { + if (!popover || !draft) return; + popover.querySelector('.audit-dt-hour-label').textContent = pickerT('settingsAudit.pickerHour', 'Hour'); + popover.querySelector('.audit-dt-minute-label').textContent = pickerT('settingsAudit.pickerMinute', 'Min'); + popover.querySelector('[data-action="clear"]').textContent = pickerT('settingsAudit.pickerClear', 'Clear'); + popover.querySelector('[data-action="today"]').textContent = pickerT('settingsAudit.pickerToday', 'Today'); + popover.querySelector('[data-action="confirm"]').textContent = pickerT('settingsAudit.pickerConfirm', 'OK'); + renderCalendar(); + renderTimeLists(); + } + + function scrollTimeSelection(listEl, value) { + var sel = listEl.querySelector('.is-selected'); + if (sel && sel.scrollIntoView) { + sel.scrollIntoView({ block: 'center' }); + } + } + + function openPopover(fieldId) { + ensurePopover(); + var entry = registry[fieldId]; + if (!entry) return; + activeFieldId = fieldId; + var stored = entry.wrap.dataset.value || ''; + draft = parseStorage(stored) || nowParts(); + viewYear = draft.y; + viewMonth = draft.m; + renderPopover(); + positionPopover(entry.wrap); + popover.hidden = false; + } + + function closePopover() { + if (!popover) return; + popover.hidden = true; + activeFieldId = null; + draft = null; + } + + function refreshDisplay(fieldId) { + var entry = registry[fieldId]; + if (!entry) return; + var parts = parseStorage(entry.wrap.dataset.value || ''); + entry.input.value = parts ? formatDisplay(parts) : ''; + entry.input.placeholder = pickerT('settingsAudit.datetimePlaceholder', 'Select date & time'); + entry.clearBtn.hidden = !parts; + } + + function refreshAllDisplays() { + Object.keys(registry).forEach(refreshDisplay); + } + + function applyValue(fieldId, storageValue) { + var entry = registry[fieldId]; + if (!entry) return; + entry.wrap.dataset.value = storageValue || ''; + refreshDisplay(fieldId); + } + + function bindField(fieldId) { + var wrap = document.getElementById(fieldId); + if (!wrap || wrap.dataset.auditDtBound === '1') return; + var input = wrap.querySelector('.audit-datetime-input'); + var openBtn = wrap.querySelector('.audit-datetime-open-btn'); + var clearBtn = wrap.querySelector('.audit-datetime-clear-btn'); + if (!input || !openBtn || !clearBtn) return; + + wrap.dataset.auditDtBound = '1'; + registry[fieldId] = { wrap: wrap, input: input, clearBtn: clearBtn }; + + openBtn.addEventListener('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + if (!popover || popover.hidden || activeFieldId !== fieldId) { + openPopover(fieldId); + } else { + closePopover(); + } + }); + input.addEventListener('click', function (ev) { + ev.stopPropagation(); + openPopover(fieldId); + }); + clearBtn.addEventListener('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + applyValue(fieldId, ''); + }); + refreshDisplay(fieldId); + } + + window.AuditDatetimePicker = { + init: function () { + bindField('audit-filter-since-field'); + bindField('audit-filter-until-field'); + refreshAllDisplays(); + }, + getValue: function (inputId) { + var fieldId = inputId === 'audit-filter-since' ? 'audit-filter-since-field' : 'audit-filter-until-field'; + var entry = registry[fieldId]; + return entry ? (entry.wrap.dataset.value || '') : ''; + }, + setValue: function (inputId, dateObj) { + if (!dateObj || Number.isNaN(dateObj.getTime())) return; + var fieldId = inputId === 'audit-filter-since' ? 'audit-filter-since-field' : 'audit-filter-until-field'; + var p = { + y: dateObj.getFullYear(), + m: dateObj.getMonth() + 1, + d: dateObj.getDate(), + h: dateObj.getHours(), + mi: dateObj.getMinutes() + }; + applyValue(fieldId, partsToStorage(p)); + }, + clearAll: function () { + applyValue('audit-filter-since-field', ''); + applyValue('audit-filter-until-field', ''); + closePopover(); + } + }; +})(); diff --git a/web/static/js/audit.js b/web/static/js/audit.js index 30572951..7b061e40 100644 --- a/web/static/js/audit.js +++ b/web/static/js/audit.js @@ -52,24 +52,76 @@ function auditActionLabel(action) { return auditT('settingsAudit.act.' + action, null, action); } +function auditLocale() { + if (typeof window.__locale === 'string' && window.__locale.length) { + return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US'; + } + return (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en-US'; +} + +function auditTimezoneShortLabel() { + try { + const parts = new Intl.DateTimeFormat(auditLocale(), { timeZoneName: 'short' }).formatToParts(new Date()); + const tz = parts.find(function (p) { return p.type === 'timeZoneName'; }); + return tz ? tz.value : ''; + } catch (_) { + return ''; + } +} + function formatAuditTime(iso) { if (!iso) return ''; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; - return d.toLocaleString(); + return d.toLocaleString(auditLocale(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short' + }); } catch (_) { return iso; } } +/** Read stored local datetime (YYYY-MM-DDTHH:mm) from custom picker or raw input. */ +function getAuditFilterDatetimeValue(inputId) { + if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.getValue === 'function') { + return window.AuditDatetimePicker.getValue(inputId) || ''; + } + var el = document.getElementById(inputId); + return el ? (el.value || '') : ''; +} + +/** datetime-local / picker storage -> UTC RFC3339 for API. */ function auditDatetimeLocalToRFC3339(value) { if (!value || !value.trim()) return ''; - const d = new Date(value); + const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value.trim()); + if (!m) return ''; + const d = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0, 0); if (Number.isNaN(d.getTime())) return ''; return d.toISOString(); } +function updateAuditTimezoneHint() { + const el = document.getElementById('audit-filter-timezone-hint'); + if (!el) return; + const tz = auditTimezoneShortLabel(); + if (!tz) { + el.hidden = true; + el.textContent = ''; + return; + } + el.hidden = false; + el.textContent = auditT('settingsAudit.filterTimeZone', { tz: tz }, + '时区:' + tz + '(筛选按浏览器本地时间,API 使用 UTC)'); +} + function initAuditPageSizeFromStorage() { try { const saved = parseInt(localStorage.getItem(AUDIT_PAGE_SIZE_KEY), 10); @@ -113,6 +165,7 @@ function rebuildAuditActionSelect() { actEl.disabled = true; actEl.value = ''; actEl.title = hint; + syncAuditCustomSelect('audit-filter-action'); return; } @@ -129,6 +182,7 @@ function rebuildAuditActionSelect() { if (prev && Array.prototype.some.call(actEl.options, function (o) { return o.value === prev; })) { actEl.value = prev; } + syncAuditCustomSelect('audit-filter-action'); } function onAuditCategoryFilterChange() { @@ -145,43 +199,17 @@ function buildAuditQueryParams(forExport) { const act = document.getElementById('audit-filter-action'); const res = document.getElementById('audit-filter-result'); const q = document.getElementById('audit-filter-q'); - const since = document.getElementById('audit-filter-since'); - const until = document.getElementById('audit-filter-until'); if (cat && cat.value) params.set('category', cat.value); if (act && !act.disabled && act.value) params.set('action', act.value); if (res && res.value) params.set('result', res.value); if (q && q.value.trim()) params.set('q', q.value.trim()); - const sinceISO = since ? auditDatetimeLocalToRFC3339(since.value) : ''; - const untilISO = until ? auditDatetimeLocalToRFC3339(until.value) : ''; + const sinceISO = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-since')); + const untilISO = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-until')); if (sinceISO) params.set('since', sinceISO); if (untilISO) params.set('until', untilISO); return params.toString(); } -async function loadAuditMeta() { - if (typeof apiFetch !== 'function') return; - const hint = document.getElementById('audit-retention-hint'); - try { - const r = await apiFetch('/api/audit/meta'); - if (!r.ok) return; - const data = await r.json(); - if (!hint) return; - if (!data.enabled) { - hint.hidden = false; - hint.textContent = auditT('settingsAudit.disabledHint', null, '审计功能已关闭,新操作不会写入审计表。'); - return; - } - const days = data.retention_days; - if (days > 0) { - hint.hidden = false; - hint.textContent = auditT('settingsAudit.retentionHint', { days: days }, - '审计记录保留 ' + days + ' 天,超期自动清理。'); - } else { - hint.hidden = true; - } - } catch (_) { /* ignore */ } -} - async function loadAuditSummary() { if (typeof apiFetch !== 'function') return; const wrap = document.getElementById('audit-summary-stats'); @@ -191,10 +219,14 @@ async function loadAuditSummary() { const data = await r.json(); if (wrap) wrap.hidden = false; const elTotal = document.getElementById('audit-stat-total'); + const elSuccess = document.getElementById('audit-stat-success'); const elFail = document.getElementById('audit-stat-failures'); const elRecent = document.getElementById('audit-stat-recent'); - if (elTotal) elTotal.textContent = String(data.total != null ? data.total : 0); - if (elFail) elFail.textContent = String(data.failures != null ? data.failures : 0); + const total = data.total != null ? data.total : 0; + const failures = data.failures != null ? data.failures : 0; + if (elTotal) elTotal.textContent = String(total); + if (elSuccess) elSuccess.textContent = String(Math.max(0, total - failures)); + if (elFail) elFail.textContent = String(failures); if (elRecent) elRecent.textContent = String(data.recent_7d != null ? data.recent_7d : 0); } catch (_) { /* ignore */ } } @@ -234,37 +266,57 @@ async function loadAuditLogs(page) { } } +function auditResultTagClass(result) { + return result === 'failure' ? 'audit-tag--fail' : 'audit-tag--ok'; +} + function renderAuditLogs(logs) { const listEl = document.getElementById('audit-log-list'); if (!listEl) return; const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; if (!logs.length) { - listEl.innerHTML = '
' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '
'; + listEl.innerHTML = '
' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '
'; return; } - listEl.innerHTML = logs.map(function (log) { - const lvl = log.result === 'failure' ? 'warn' : (log.level || 'info'); + const dash = ''; + const head = ( + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); + const rows = logs.map(function (log) { const catLabel = esc(auditCategoryLabel(log.category || '')); const actionLabel = esc(auditActionLabel(log.action || '')); const msg = esc(log.message || ''); const ip = esc(log.clientIp || ''); const when = esc(formatAuditTime(log.createdAt)); const res = esc(log.result || ''); - const rid = log.resourceId || ''; - const meta = rid ? (' · ' + esc(rid)) : ''; + const rid = log.resourceId ? esc(log.resourceId) : ''; const eid = esc(log.id || ''); + const resultCls = auditResultTagClass(log.result || ''); + const rowClick = 'onclick="showAuditLogDetail(\'' + eid + '\')" ' + + 'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}"'; return ( - '
' + - '
' + - '
' + - '
' + msg + '
' + - '
' + when + ' · ' + catLabel + '/' + actionLabel + ' · ' + res + meta + - (ip ? ' · IP ' + ip : '') + - '
' + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' ); }).join(''); + listEl.innerHTML = head + rows + '
时间说明类别操作结果IP资源 ID
' + when + '' + (msg || dash) + '' + (catLabel ? '' + catLabel + '' : dash) + '' + (actionLabel ? '' + actionLabel + '' : dash) + '' + (res ? '' + res + '' : dash) + '' + (ip || dash) + '' + (rid || dash) + '
'; if (typeof applyTranslations === 'function') { applyTranslations(listEl); } @@ -326,17 +378,58 @@ function resetAuditLogFilters() { const act = document.getElementById('audit-filter-action'); const res = document.getElementById('audit-filter-result'); const q = document.getElementById('audit-filter-q'); - const since = document.getElementById('audit-filter-since'); - const until = document.getElementById('audit-filter-until'); if (cat) cat.value = ''; if (res) res.value = ''; if (q) q.value = ''; - if (since) since.value = ''; - if (until) until.value = ''; + if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.clearAll === 'function') { + window.AuditDatetimePicker.clearAll(); + } rebuildAuditActionSelect(); + syncAuditCustomSelect('audit-filter-category'); + syncAuditCustomSelect('audit-filter-result'); filterAuditLogs(); } +function applyAuditTimePreset(preset) { + if (typeof window.AuditDatetimePicker === 'undefined') return; + const now = new Date(); + let since = new Date(now.getTime()); + let until = new Date(now.getTime()); + switch (preset) { + case '15m': + since = new Date(now.getTime() - 15 * 60 * 1000); + break; + case '1h': + since = new Date(now.getTime() - 60 * 60 * 1000); + break; + case '24h': + since = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case '7d': + since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case 'today': + since = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0); + break; + default: + return; + } + window.AuditDatetimePicker.setValue('audit-filter-since', since); + window.AuditDatetimePicker.setValue('audit-filter-until', until); + filterAuditLogs(); +} + +function initAuditTimePresets() { + const wrap = document.getElementById('audit-time-presets'); + if (!wrap || wrap.dataset.bound === '1') return; + wrap.dataset.bound = '1'; + wrap.addEventListener('click', function (ev) { + const btn = ev.target.closest('[data-preset]'); + if (!btn) return; + applyAuditTimePreset(btn.getAttribute('data-preset')); + }); +} + /** 资源已被删除/移除的审计操作,不再提供「打开关联资源」 */ const AUDIT_ACTIONS_RESOURCE_REMOVED = { delete: true, @@ -597,7 +690,142 @@ async function showAuditLogDetail(id) { function initAuditLogsSection() { if (!document.getElementById('audit-log-list')) return; initAuditPageSizeFromStorage(); + initAuditFilterSelects(); rebuildAuditActionSelect(); - loadAuditMeta(); + if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.init === 'function') { + window.AuditDatetimePicker.init(); + } + initAuditTimePresets(); + updateAuditTimezoneHint(); loadAuditLogs(1); } + +var auditCustomSelectMap = {}; +var auditFilterSelectsDocListener = false; + +function closeAllAuditCustomSelects() { + Object.keys(auditCustomSelectMap).forEach(function (id) { + auditCustomSelectMap[id].wrapper.classList.remove('open'); + }); +} + +function syncAuditCustomSelect(selectId) { + var reg = auditCustomSelectMap[selectId]; + if (!reg) return; + var select = reg.select; + var dropdown = reg.dropdown; + var trigger = reg.trigger; + var wrapper = reg.wrapper; + var valueSpan = trigger.querySelector('.audit-custom-select-value'); + + dropdown.innerHTML = ''; + Array.prototype.forEach.call(select.options, function (opt) { + var item = document.createElement('div'); + item.className = 'audit-custom-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'); + } + var check = document.createElement('span'); + check.className = 'audit-custom-select-check'; + check.setAttribute('aria-hidden', 'true'); + check.textContent = '✓'; + var label = document.createElement('span'); + label.className = 'audit-custom-select-label'; + label.textContent = opt.textContent; + item.appendChild(check); + item.appendChild(label); + dropdown.appendChild(item); + }); + + var selectedOpt = select.options[select.selectedIndex]; + if (valueSpan) { + valueSpan.textContent = selectedOpt ? selectedOpt.textContent : ''; + } + trigger.disabled = !!select.disabled; + wrapper.classList.toggle('is-disabled', !!select.disabled); +} + +function enhanceAuditFilterSelect(selectId) { + var select = document.getElementById(selectId); + if (!select) return; + if (select.dataset.auditCustom === '1') { + syncAuditCustomSelect(selectId); + return; + } + select.dataset.auditCustom = '1'; + select.classList.add('audit-native-select'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + var wrapper = document.createElement('div'); + wrapper.className = 'audit-custom-select'; + + var trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'audit-custom-select-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + var valueSpan = document.createElement('span'); + valueSpan.className = 'audit-custom-select-value'; + trigger.appendChild(valueSpan); + var caret = document.createElement('span'); + caret.className = 'audit-custom-select-caret'; + caret.setAttribute('aria-hidden', 'true'); + caret.textContent = '▾'; + trigger.appendChild(caret); + + var dropdown = document.createElement('div'); + dropdown.className = 'audit-custom-select-dropdown'; + dropdown.setAttribute('role', 'listbox'); + + var parent = select.parentNode; + parent.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(dropdown); + wrapper.appendChild(select); + + auditCustomSelectMap[selectId] = { + wrapper: wrapper, + trigger: trigger, + dropdown: dropdown, + select: select + }; + + trigger.addEventListener('click', function (e) { + e.stopPropagation(); + if (select.disabled) return; + var open = wrapper.classList.contains('open'); + closeAllAuditCustomSelects(); + if (!open) wrapper.classList.add('open'); + }); + + dropdown.addEventListener('click', function (e) { + var opt = e.target.closest('.audit-custom-select-option'); + if (!opt) return; + var val = opt.getAttribute('data-value'); + if (val === null) val = ''; + if (select.value !== val) { + select.value = val; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + wrapper.classList.remove('open'); + syncAuditCustomSelect(selectId); + }); + + syncAuditCustomSelect(selectId); +} + +function initAuditFilterSelects() { + if (!document.getElementById('audit-filter-category')) return; + if (!auditFilterSelectsDocListener) { + document.addEventListener('click', function () { + closeAllAuditCustomSelects(); + }); + auditFilterSelectsDocListener = true; + } + enhanceAuditFilterSelect('audit-filter-category'); + enhanceAuditFilterSelect('audit-filter-action'); + enhanceAuditFilterSelect('audit-filter-result'); +} diff --git a/web/templates/index.html b/web/templates/index.html index cac331cb..56467593 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3010,19 +3010,27 @@
-
+

日志审计

-

记录平台管理类操作(登录、配置、删除等),不记录对话正文、终端/WebShell 每次命令与工具调用明细。

- +
- -
-
-