Add files via upload

This commit is contained in:
公明
2026-06-17 12:02:24 +08:00
committed by GitHub
parent 616d87f4cc
commit 9f6eb33047
6 changed files with 1546 additions and 141 deletions
+739 -59
View File
@@ -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 {
+21
View File
@@ -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",
+21
View File
@@ -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": "配置",
+428
View File
@@ -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 =
'<div class="audit-dt-popover-inner">' +
'<div class="audit-dt-head">' +
'<button type="button" class="audit-dt-nav" data-nav="prev" aria-label="prev">&lsaquo;</button>' +
'<span class="audit-dt-month-label"></span>' +
'<button type="button" class="audit-dt-nav" data-nav="next" aria-label="next">&rsaquo;</button>' +
'</div>' +
'<div class="audit-dt-body">' +
'<div class="audit-dt-calendar"></div>' +
'<div class="audit-dt-time">' +
'<div class="audit-dt-time-col" data-part="hour">' +
'<span class="audit-dt-time-label audit-dt-hour-label"></span>' +
'<div class="audit-dt-time-list"></div>' +
'</div>' +
'<div class="audit-dt-time-col" data-part="minute">' +
'<span class="audit-dt-time-label audit-dt-minute-label"></span>' +
'<div class="audit-dt-time-list"></div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="audit-dt-footer">' +
'<button type="button" class="audit-dt-footer-btn" data-action="clear"></button>' +
'<button type="button" class="audit-dt-footer-btn" data-action="today"></button>' +
'<button type="button" class="audit-dt-footer-btn audit-dt-footer-btn--primary" data-action="confirm"></button>' +
'</div>' +
'</div>';
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 = '<div class="audit-dt-weekdays">';
headers.forEach(function (h) { html += '<span>' + h + '</span>'; });
html += '</div><div class="audit-dt-days">';
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 += '<button type="button" class="' + cls + '" data-day="1" data-y="' + cell.y +
'" data-m="' + cell.m + '" data-d="' + cell.d + '">' + cell.d + '</button>';
});
html += '</div>';
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 += '<button type="button" class="audit-dt-time-item' + (draft && draft.h === h ? ' is-selected' : '') +
'" data-part="hour" data-time="' + h + '">' + pad2(h) + '</button>';
}
for (h = 0; h < 60; h++) {
minuteHtml += '<button type="button" class="audit-dt-time-item' + (draft && draft.mi === h ? ' is-selected' : '') +
'" data-part="minute" data-time="' + h + '">' + pad2(h) + '</button>';
}
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();
}
};
})();
+279 -51
View File
@@ -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 = '<div class="c2-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
listEl.innerHTML = '<div class="audit-log-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
return;
}
listEl.innerHTML = logs.map(function (log) {
const lvl = log.result === 'failure' ? 'warn' : (log.level || 'info');
const dash = '<span class="audit-log-cell-muted">—</span>';
const head = (
'<div class="audit-log-table-wrap">' +
'<table class="audit-log-table">' +
'<thead><tr>' +
'<th data-i18n="settingsAudit.colTime">时间</th>' +
'<th data-i18n="settingsAudit.colMessage">说明</th>' +
'<th data-i18n="settingsAudit.colCategory">类别</th>' +
'<th data-i18n="settingsAudit.colAction">操作</th>' +
'<th data-i18n="settingsAudit.colResult">结果</th>' +
'<th data-i18n="settingsAudit.colIp">IP</th>' +
'<th data-i18n="settingsAudit.colResource">资源 ID</th>' +
'</tr></thead><tbody>'
);
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 (
'<div class="c2-event-item audit-log-item" role="button" tabindex="0" ' +
'onclick="showAuditLogDetail(\'' + eid + '\')" ' +
'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}">' +
'<div class="c2-event-level ' + esc(lvl) + '"></div>' +
'<div class="c2-event-content">' +
'<div class="c2-event-message">' + msg + '</div>' +
'<div class="c2-event-meta">' + when + ' · ' + catLabel + '/' + actionLabel + ' · ' + res + meta +
(ip ? ' · IP ' + ip : '') +
'</div></div></div>'
'<tr class="audit-log-row" role="button" tabindex="0" ' + rowClick + '>' +
'<td class="audit-log-col-time">' + when + '</td>' +
'<td class="audit-log-col-msg" title="' + msg + '">' + (msg || dash) + '</td>' +
'<td>' + (catLabel ? '<span class="audit-tag audit-tag--cat">' + catLabel + '</span>' : dash) + '</td>' +
'<td>' + (actionLabel ? '<span class="audit-tag audit-tag--act">' + actionLabel + '</span>' : dash) + '</td>' +
'<td>' + (res ? '<span class="audit-tag ' + resultCls + '">' + res + '</span>' : dash) + '</td>' +
'<td class="audit-log-col-ip">' + (ip || dash) + '</td>' +
'<td class="audit-log-col-resource" title="' + rid + '">' + (rid || dash) + '</td>' +
'</tr>'
);
}).join('');
listEl.innerHTML = head + rows + '</tbody></table></div>';
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');
}
+58 -31
View File
@@ -3010,19 +3010,27 @@
<!-- 日志审计 -->
<div id="settings-section-audit" class="settings-section-content">
<div class="settings-section-header">
<div class="audit-section-head">
<h3 data-i18n="settingsAudit.title">日志审计</h3>
<p class="settings-description" data-i18n="settingsAudit.description">记录平台管理类操作(登录、配置、删除等),不记录对话正文、终端/WebShell 每次命令与工具调用明细。</p>
<p id="audit-retention-hint" class="settings-description audit-retention-hint" hidden></p>
<div id="audit-summary-stats" class="audit-summary-tags" hidden>
<span class="audit-summary-tag"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statTotal">当前筛选</span><strong id="audit-stat-total">0</strong></span>
<span class="audit-summary-tag audit-summary-tag--ok"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statSuccess">成功</span><strong id="audit-stat-success">0</strong></span>
<span class="audit-summary-tag audit-summary-tag--warn"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statFailures">失败</span><strong id="audit-stat-failures">0</strong></span>
<span class="audit-summary-tag"><span class="audit-summary-tag-label" data-i18n="settingsAudit.statRecent7d">近 7 天</span><strong id="audit-stat-recent">0</strong></span>
</div>
</div>
<div id="audit-summary-stats" class="audit-summary-stats" hidden>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statTotal">当前筛选</span><strong id="audit-stat-total">0</strong></div>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statFailures">失败</span><strong id="audit-stat-failures">0</strong></div>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statRecent7d">近 7 天</span><strong id="audit-stat-recent">0</strong></div>
</div>
<div class="audit-logs-toolbar">
<div class="audit-logs-filters">
<label class="audit-filter-cascade-group">
<div class="audit-filter-card">
<div class="audit-time-presets" id="audit-time-presets">
<span class="audit-time-presets-label" data-i18n="settingsAudit.timePresets">快捷</span>
<button type="button" class="audit-time-preset-btn" data-preset="15m" data-i18n="settingsAudit.preset15m">最近15分钟</button>
<button type="button" class="audit-time-preset-btn" data-preset="1h" data-i18n="settingsAudit.preset1h">最近1小时</button>
<button type="button" class="audit-time-preset-btn" data-preset="24h" data-i18n="settingsAudit.preset24h">最近24小时</button>
<button type="button" class="audit-time-preset-btn" data-preset="7d" data-i18n="settingsAudit.preset7d">最近7天</button>
<button type="button" class="audit-time-preset-btn" data-preset="today" data-i18n="settingsAudit.presetToday">今天</button>
</div>
<div class="audit-filter-fields">
<div class="audit-filter-row">
<label class="audit-field audit-field--event">
<span data-i18n="settingsAudit.filterEvent">事件类型</span>
<div class="audit-filter-cascade">
<select id="audit-filter-category" onchange="onAuditCategoryFilterChange()" aria-label="类别">
@@ -3049,7 +3057,7 @@
</select>
</div>
</label>
<label>
<label class="audit-field audit-field--result">
<span data-i18n="settingsAudit.filterResult">结果</span>
<select id="audit-filter-result">
<option value="" data-i18n="settingsAudit.filterAll">全部</option>
@@ -3057,36 +3065,54 @@
<option value="failure">failure</option>
</select>
</label>
<label>
<div class="audit-filter-time-group">
<label class="audit-field audit-field--time">
<span data-i18n="settingsAudit.filterSince">开始时间</span>
<input type="datetime-local" id="audit-filter-since" />
<div class="audit-datetime-field" id="audit-filter-since-field">
<input type="text" id="audit-filter-since" class="audit-datetime-input" readonly autocomplete="off" data-i18n="settingsAudit.datetimePlaceholder" data-i18n-attr="placeholder" placeholder="选择日期时间" />
<button type="button" class="audit-datetime-btn audit-datetime-clear-btn" title="Clear" aria-label="Clear" hidden>&times;</button>
<button type="button" class="audit-datetime-btn audit-datetime-open-btn" title="Open" aria-label="Open">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/><path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
</label>
<label>
<label class="audit-field audit-field--time">
<span data-i18n="settingsAudit.filterUntil">结束时间</span>
<input type="datetime-local" id="audit-filter-until" />
<div class="audit-datetime-field" id="audit-filter-until-field">
<input type="text" id="audit-filter-until" class="audit-datetime-input" readonly autocomplete="off" data-i18n="settingsAudit.datetimePlaceholder" data-i18n-attr="placeholder" placeholder="选择日期时间" />
<button type="button" class="audit-datetime-btn audit-datetime-clear-btn" title="Clear" aria-label="Clear" hidden>&times;</button>
<button type="button" class="audit-datetime-btn audit-datetime-open-btn" title="Open" aria-label="Open">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/><path d="M16 2v4M8 2v4M3 10h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
</label>
<label>
</div>
</div>
</div>
<div class="audit-filter-bottom">
<label class="audit-field audit-field--keyword">
<span data-i18n="settingsAudit.filterQuery">关键词</span>
<input type="text" id="audit-filter-q" data-i18n="settingsAudit.filterQueryPlaceholder" data-i18n-attr="placeholder" placeholder="消息 / 资源 ID / 操作名" />
</label>
<button type="button" class="btn-secondary" onclick="filterAuditLogs()" data-i18n="settingsAudit.filterBtn">筛选</button>
<button type="button" class="btn-secondary" onclick="resetAuditLogFilters()" data-i18n="settingsAudit.resetBtn">重置</button>
</div>
<div class="audit-logs-actions">
<button type="button" class="btn-secondary" onclick="refreshAuditLogs()" data-i18n="common.refresh">刷新</button>
<div class="audit-export-dropdown">
<button type="button" class="btn-secondary audit-export-trigger" id="audit-export-trigger" onclick="toggleAuditExportMenu(event)" aria-haspopup="true" aria-expanded="false">
<span data-i18n="settingsAudit.exportBtn">导出</span>
<span class="audit-export-caret" aria-hidden="true"></span>
</button>
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
<div class="audit-logs-actions">
<button type="button" class="btn-primary" onclick="filterAuditLogs()" data-i18n="settingsAudit.filterBtn">筛选</button>
<button type="button" class="btn-secondary" onclick="resetAuditLogFilters()" data-i18n="settingsAudit.resetBtn">重置</button>
<button type="button" class="btn-secondary" onclick="refreshAuditLogs()" data-i18n="common.refresh">刷新</button>
<div class="audit-export-dropdown">
<button type="button" class="btn-secondary audit-export-trigger" id="audit-export-trigger" onclick="toggleAuditExportMenu(event)" aria-haspopup="true" aria-expanded="false">
<span data-i18n="settingsAudit.exportBtn">导出</span>
<span class="audit-export-caret" aria-hidden="true"></span>
</button>
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
</div>
</div>
</div>
</div>
<p id="audit-filter-timezone-hint" class="audit-timezone-hint" hidden></p>
</div>
<div id="audit-log-list" class="audit-log-list c2-event-list"></div>
<div id="audit-log-list" class="audit-log-list"></div>
<div id="audit-logs-pagination" class="pagination-container audit-logs-pagination"></div>
</div>
@@ -4301,6 +4327,7 @@
<script src="/static/js/chat.js"></script>
<script src="/static/js/hitl.js"></script>
<script src="/static/js/settings.js"></script>
<script src="/static/js/audit-datetime-picker.js"></script>
<script src="/static/js/audit.js"></script>
<script src="/static/js/wechat-robot.js"></script>
<script src="/static/vendor/xterm.js"></script>