release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="page page--flush">
|
||||
<div class="page page--flush audit-logs-page">
|
||||
<div class="main-content-flat unified-shell">
|
||||
<!-- 统计概览 -->
|
||||
<div class="stats-row">
|
||||
<section class="audit-overview" aria-labelledby="audit-overview-title">
|
||||
<div class="overview-heading">
|
||||
<h2 id="audit-overview-title">审计概览</h2>
|
||||
<span class="overview-status"><i></i>日志汇总</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-card stat-card--total">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
@@ -35,19 +40,34 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<span class="stat-value">{{ operatorCount }}</span>
|
||||
<span class="stat-label">操作人数</span>
|
||||
<span class="stat-value">{{ uniqueIpCount }}</span>
|
||||
<span class="stat-label">来源 IP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="audit-toolbar unified-action-bar">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<div class="filter-item-form">
|
||||
<div class="filter-heading">
|
||||
<el-icon><Filter /></el-icon>
|
||||
<span>筛选</span>
|
||||
</div>
|
||||
<div class="filter-item-form project-filter-group">
|
||||
<el-select v-model="selectedStudyId" filterable :placeholder="TEXT.common.fields.projectName" @change="onProjectChange" class="filter-select-project">
|
||||
<el-option v-for="project in studies" :key="project.id" :label="project.name" :value="project.id" />
|
||||
</el-select>
|
||||
<el-tooltip content="导出当前项目日志" placement="top">
|
||||
<el-button
|
||||
v-if="canProjectExport"
|
||||
:icon="Download"
|
||||
class="project-export-button"
|
||||
:loading="exportLoading"
|
||||
aria-label="导出当前项目日志"
|
||||
@click="confirmExport"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="filter-item-form">
|
||||
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="onServerFilterChange" class="filter-select-comp">
|
||||
@@ -78,42 +98,32 @@
|
||||
@change="onLocalFilterChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-dropdown trigger="click" @command="handleExportCommand" class="audit-export-dropdown">
|
||||
<el-button plain class="audit-export-btn" :loading="exportLoading">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
导出 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="canProjectExport" command="project">{{ TEXT.modules.adminAuditLogs.exportProject }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button v-if="hasActiveFilters" text :icon="RefreshLeft" class="reset-filter-button" @click="resetFilters">重置</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 日志表格 -->
|
||||
<div class="unified-section table-section">
|
||||
<el-table :data="logs" v-loading="loading" class="audit-table" style="width: 100%" table-layout="fixed">
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="150">
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="128">
|
||||
<template #default="scope">
|
||||
<span class="time-text">{{ displayDateTime(scope.row.timestamp) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="120">
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="155">
|
||||
<template #default="scope">
|
||||
<div class="actor-cell">
|
||||
<span class="actor-name">{{ scope.row.actorName }}</span>
|
||||
<span v-if="scope.row.actorAccount" class="actor-account">{{ scope.row.actorAccount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" width="145">
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" width="170">
|
||||
<template #default="scope">
|
||||
<span class="event-tag">{{ scope.row.eventLabel }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" min-width="240">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" width="280">
|
||||
<template #default="scope">
|
||||
<div class="target-cell">
|
||||
<span v-if="scope.row.targetTypeLabel" class="target-text">
|
||||
@@ -135,7 +145,7 @@
|
||||
<span v-else class="text-muted">{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="76" align="center">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="72" align="center" class-name="result-column">
|
||||
<template #default="scope">
|
||||
<span class="result-badge" :class="scope.row.result === 'SUCCESS' ? 'result--success' : 'result--fail'">
|
||||
{{ scope.row.resultLabel }}
|
||||
@@ -211,7 +221,6 @@
|
||||
<span class="meta-value">{{ selectedLog.actionText || TEXT.common.fallback }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 变更明细 -->
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">
|
||||
@@ -256,7 +265,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { Download, Filter, RefreshLeft } from "@element-plus/icons-vue";
|
||||
import { createAuditEvent, fetchAuditLogs } from "../../api/auditLogs";
|
||||
import { fetchStudies } from "../../api/studies";
|
||||
import { fetchApiEndpointPermissions } from "../../api/projectPermissions";
|
||||
@@ -265,7 +274,6 @@ import { listMembers } from "../../api/members";
|
||||
import { auditDict, normalizeAuditEvent } from "../../audit";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { roleDict, getDictLabel } from "../../dictionaries";
|
||||
import { exportAuditCsv } from "../../audit/export/auditExportService";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
|
||||
@@ -302,7 +310,13 @@ const filters = ref({
|
||||
|
||||
const successCount = computed(() => allLogs.value.filter(l => (l.result || 'SUCCESS') === 'SUCCESS').length);
|
||||
const failCount = computed(() => allLogs.value.filter(l => l.result === 'FAIL').length);
|
||||
const operatorCount = computed(() => new Set(allLogs.value.map(l => l.actorId || l.actorName)).size);
|
||||
const uniqueIpCount = computed(() => new Set(allLogs.value.map(l => l.clientIp || "未知 IP")).size);
|
||||
const hasActiveFilters = computed(() => Boolean(
|
||||
filters.value.eventType ||
|
||||
filters.value.operatorId ||
|
||||
filters.value.result ||
|
||||
filters.value.range?.length
|
||||
));
|
||||
|
||||
const formatDiffLine = (line: any): string => {
|
||||
if (typeof line === 'string') return line;
|
||||
@@ -470,6 +484,13 @@ const fetchAuditLogPages = async (baseParams: Record<string, any> = {}) => {
|
||||
return allItems;
|
||||
};
|
||||
|
||||
const buildAuditServerParams = () => {
|
||||
return {
|
||||
action: filters.value.eventType || undefined,
|
||||
operator_id: filters.value.operatorId || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const loadLogs = async () => {
|
||||
if (!selectedStudyId.value) return;
|
||||
loading.value = true;
|
||||
@@ -477,10 +498,7 @@ const loadLogs = async () => {
|
||||
if (!permissionMatrix.value) {
|
||||
await loadPermissionMatrix();
|
||||
}
|
||||
const allItems = await fetchAuditLogPages({
|
||||
action: filters.value.eventType || undefined,
|
||||
operator_id: filters.value.operatorId || undefined,
|
||||
});
|
||||
const allItems = await fetchAuditLogPages(buildAuditServerParams());
|
||||
enrichLogs(allItems);
|
||||
refreshPagedLogs();
|
||||
} catch (e: any) {
|
||||
@@ -498,15 +516,13 @@ const enrichLogs = (items: any[]) => {
|
||||
allLogs.value = items
|
||||
.map((log) => normalizeAuditEvent(log, userMap))
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
if (users.value.length === 0) {
|
||||
const byActor = new Map<string, string>();
|
||||
allLogs.value.forEach((log) => {
|
||||
if (!byActor.has(log.actorId)) {
|
||||
byActor.set(log.actorId, log.actorName || log.actorId);
|
||||
}
|
||||
});
|
||||
users.value = Array.from(byActor.entries()).map(([id, full_name]) => ({ id, full_name }));
|
||||
}
|
||||
const byActor = new Map(users.value.map((user) => [user.id, resolveUserDisplayName(user)]));
|
||||
allLogs.value.forEach((log) => {
|
||||
if (!byActor.has(log.actorId)) {
|
||||
byActor.set(log.actorId, log.actorName || log.actorAccount || log.actorId);
|
||||
}
|
||||
});
|
||||
users.value = Array.from(byActor.entries()).map(([id, full_name]) => ({ id, full_name }));
|
||||
};
|
||||
|
||||
const filterLogs = (items: any[]) =>
|
||||
@@ -540,6 +556,14 @@ const onLocalFilterChange = () => {
|
||||
refreshPagedLogs();
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.value.eventType = "";
|
||||
filters.value.operatorId = "";
|
||||
filters.value.result = "";
|
||||
filters.value.range = [];
|
||||
onServerFilterChange();
|
||||
};
|
||||
|
||||
const onProjectChange = async () => {
|
||||
permissionMatrix.value = null;
|
||||
users.value = [];
|
||||
@@ -582,24 +606,12 @@ const fetchAllForExport = async () => {
|
||||
if (!selectedStudyId.value) return [];
|
||||
exportLoading.value = true;
|
||||
try {
|
||||
const items = await fetchAuditLogPages({
|
||||
action: filters.value.eventType || undefined,
|
||||
operator_id: filters.value.operatorId || undefined,
|
||||
});
|
||||
const items = await fetchAuditLogPages(buildAuditServerParams());
|
||||
const userMap = users.value.reduce<Record<string, string>>((acc, cur) => {
|
||||
acc[cur.id] = resolveUserDisplayName(cur);
|
||||
return acc;
|
||||
}, {});
|
||||
const filtered = items.filter((log: any) => {
|
||||
if (filters.value.range?.length === 2) {
|
||||
const ts = new Date(log.created_at);
|
||||
const start = new Date(filters.value.range[0]);
|
||||
const end = new Date(filters.value.range[1]);
|
||||
if (ts < start || ts > end) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return filtered.map((log: any) => normalizeAuditEvent(log, userMap));
|
||||
return filterLogs(items.map((log: any) => normalizeAuditEvent(log, userMap)));
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.exportLoadFailed);
|
||||
return [];
|
||||
@@ -608,10 +620,6 @@ const fetchAllForExport = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCommand = (command: string) => {
|
||||
if (command === "project") confirmExport();
|
||||
};
|
||||
|
||||
const confirmExport = async () => {
|
||||
const currentStudy = selectedStudy.value;
|
||||
if (!currentStudy) return;
|
||||
@@ -647,33 +655,77 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:global(.web-layout-container .content-wrapper:has(.audit-logs-page)) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.audit-overview {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
background: linear-gradient(180deg, var(--ctms-bg-card) 0%, color-mix(in srgb, var(--ctms-neutral-100) 55%, var(--ctms-bg-card)) 100%);
|
||||
}
|
||||
|
||||
.overview-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 38px;
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
.overview-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.overview-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid rgba(63, 143, 107, 0.18);
|
||||
border-radius: 999px;
|
||||
background: rgba(63, 143, 107, 0.07);
|
||||
color: var(--ctms-success);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overview-status i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--unified-shell-divider);
|
||||
background: var(--ctms-bg-card);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--ctms-neutral-100);
|
||||
transition: var(--ctms-transition);
|
||||
min-height: 48px;
|
||||
padding: 6px 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
.stat-card + .stat-card {
|
||||
border-left: 1px solid var(--unified-shell-divider);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -681,19 +733,27 @@ onMounted(async () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon svg { width: 18px; height: 18px; }
|
||||
.stat-icon svg { width: 17px; height: 17px; }
|
||||
.stat-card--total .stat-icon { background: #e8edf3; color: var(--ctms-primary); }
|
||||
.stat-card--success .stat-icon { background: #e6f4ed; color: var(--ctms-success); }
|
||||
.stat-card--fail .stat-icon { background: #fde8e8; color: var(--ctms-danger); }
|
||||
.stat-card--operators .stat-icon { background: #eef2f6; color: var(--ctms-info); }
|
||||
|
||||
.stat-body { display: flex; flex-direction: column; }
|
||||
.stat-value { font-size: 20px; font-weight: 700; line-height: 1.2; color: var(--ctms-text-main); }
|
||||
.stat-label { font-size: 11px; color: var(--ctms-text-secondary); margin-top: 2px; }
|
||||
.stat-value { font-size: 18px; font-weight: 700; line-height: 1.2; color: var(--ctms-text-main); }
|
||||
.stat-label { font-size: 10px; color: var(--ctms-text-secondary); margin-top: 2px; }
|
||||
|
||||
/* 筛选栏 */
|
||||
.audit-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 8;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
background: color-mix(in srgb, var(--ctms-bg-card) 95%, transparent);
|
||||
box-shadow: 0 5px 14px rgba(15, 23, 42, 0.05);
|
||||
backdrop-filter: blur(12px);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
@@ -701,7 +761,7 @@ onMounted(async () => {
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item-form {
|
||||
@@ -710,29 +770,65 @@ onMounted(async () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-filter-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.filter-select-project :deep(.el-select__wrapper) {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.project-export-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
margin-left: -1px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: var(--ctms-primary);
|
||||
border-color: color-mix(in srgb, var(--ctms-primary) 24%, var(--ctms-border-color));
|
||||
background: color-mix(in srgb, var(--ctms-primary) 7%, var(--ctms-bg-card));
|
||||
}
|
||||
|
||||
.filter-heading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-input__inner),
|
||||
.filter-form :deep(.el-select__placeholder),
|
||||
.filter-form :deep(.el-select__selected-item),
|
||||
.filter-form :deep(.el-range-input),
|
||||
.filter-form :deep(.el-button) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-select__wrapper),
|
||||
.filter-form :deep(.el-input__wrapper),
|
||||
.filter-form :deep(.el-date-editor) {
|
||||
min-height: 32px;
|
||||
box-shadow: 0 0 0 1px var(--ctms-border-color) inset;
|
||||
}
|
||||
|
||||
.reset-filter-button {
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.filter-select-comp { width: 132px; }
|
||||
.filter-select-project { width: 180px; }
|
||||
.filter-select-result { width: 96px; }
|
||||
.date-range-picker-comp { width: 248px !important; }
|
||||
.filter-spacer { flex: 1; }
|
||||
|
||||
.audit-export-dropdown { flex-shrink: 0; }
|
||||
.audit-export-btn {
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600;
|
||||
height: 32px;
|
||||
color: var(--ctms-primary) !important;
|
||||
border-color: #c0cdd7 !important;
|
||||
background: #f0f6ff !important;
|
||||
}
|
||||
.audit-export-btn:hover {
|
||||
color: var(--ctms-primary-hover) !important;
|
||||
border-color: #9bb1c2 !important;
|
||||
background: #e2edfa !important;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.table-section { padding: 0; }
|
||||
.table-section { padding: 0 !important; }
|
||||
.audit-table :deep(.el-table__inner-wrapper::before) { display: none; }
|
||||
.audit-table :deep(.el-table__cell) {
|
||||
padding: 8px 0;
|
||||
@@ -748,17 +844,29 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.actor-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
.actor-name,
|
||||
.actor-account {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actor-account {
|
||||
font-size: 11px;
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.event-tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
@@ -807,6 +915,12 @@ onMounted(async () => {
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.audit-table :deep(.result-column .cell) {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.result--success { background: #e6f4ed; color: #166534; }
|
||||
@@ -1057,6 +1171,9 @@ onMounted(async () => {
|
||||
@media (max-width: 768px) {
|
||||
.stats-row { grid-template-columns: repeat(2, 1fr); }
|
||||
.filter-form { flex-wrap: wrap; }
|
||||
.stat-card:nth-child(3) { border-left: 0; }
|
||||
.stat-card:nth-child(n + 3) { border-top: 1px solid var(--unified-shell-divider); }
|
||||
.filter-heading { width: 100%; }
|
||||
.diff-item-row { grid-template-columns: 1fr; gap: 4px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user