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 = (
+ '' +
+ '
' +
+ '' +
+ '| 时间 | ' +
+ '说明 | ' +
+ '类别 | ' +
+ '操作 | ' +
+ '结果 | ' +
+ 'IP | ' +
+ '资源 ID | ' +
+ '
'
+ );
+ 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 : '') +
- '
'
+ '' +
+ '| ' + when + ' | ' +
+ '' + (msg || dash) + ' | ' +
+ '' + (catLabel ? '' + catLabel + '' : dash) + ' | ' +
+ '' + (actionLabel ? '' + actionLabel + '' : dash) + ' | ' +
+ '' + (res ? '' + res + '' : dash) + ' | ' +
+ '' + (ip || dash) + ' | ' +
+ '' + (rid || dash) + ' | ' +
+ '
'
);
}).join('');
+ listEl.innerHTML = head + rows + '
';
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 @@
-