移除用户系统级角色,权限完全迁移至项目级管理
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态 - 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员 - PM(项目负责人)角色自动拥有全部项目权限且不可修改 - 系统管理员在项目中的成员身份不可被删除 - 管理界面全面美化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,56 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page page--flush">
|
||||
<div class="main-content-flat unified-shell">
|
||||
<div class="audit-toolbar">
|
||||
<!-- 统计概览 -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<span class="stat-value">{{ total }}</span>
|
||||
<span class="stat-label">日志总数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--success">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<span class="stat-value">{{ successCount }}</span>
|
||||
<span class="stat-label">{{ TEXT.audit.resultSuccess }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--fail">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<span class="stat-value">{{ failCount }}</span>
|
||||
<span class="stat-label">{{ TEXT.audit.resultFail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--operators">
|
||||
<div class="stat-icon">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="audit-toolbar unified-action-bar">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<div class="filter-item-form">
|
||||
<el-select
|
||||
v-model="filters.eventType"
|
||||
clearable
|
||||
:placeholder="TEXT.modules.adminAuditLogs.filterEvent"
|
||||
@change="onServerFilterChange"
|
||||
class="filter-select-comp"
|
||||
>
|
||||
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="onServerFilterChange" class="filter-select-comp">
|
||||
<el-option v-for="opt in eventTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item-form">
|
||||
<el-select
|
||||
v-model="filters.operatorId"
|
||||
clearable
|
||||
:placeholder="TEXT.modules.adminAuditLogs.filterOperator"
|
||||
filterable
|
||||
@change="onServerFilterChange"
|
||||
class="filter-select-comp"
|
||||
>
|
||||
<el-select v-model="filters.operatorId" clearable :placeholder="TEXT.modules.adminAuditLogs.filterOperator" filterable @change="onServerFilterChange" class="filter-select-comp">
|
||||
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
@@ -48,6 +76,7 @@
|
||||
<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>
|
||||
@@ -59,46 +88,56 @@
|
||||
</el-dropdown>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 日志表格 -->
|
||||
<div class="unified-section table-section">
|
||||
<el-table :data="logs" v-loading="loading" class="audit-table" style="width: 100%" stripe table-layout="fixed">
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" align="center" show-overflow-tooltip>
|
||||
<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="155">
|
||||
<template #default="scope">
|
||||
<span class="text-gray-500">{{ displayDateTime(scope.row.timestamp) }}</span>
|
||||
<span class="time-text">{{ displayDateTime(scope.row.timestamp) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" show-overflow-tooltip />
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" show-overflow-tooltip>
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="130">
|
||||
<template #default="scope">
|
||||
<el-tag effect="plain">{{ scope.row.eventLabel }}</el-tag>
|
||||
<div class="actor-cell">
|
||||
<div class="actor-avatar">{{ getInitials(scope.row.actorName) }}</div>
|
||||
<span class="actor-name">{{ scope.row.actorName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" width="140">
|
||||
<template #default="scope">
|
||||
<span class="event-tag">{{ scope.row.eventLabel }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actionText" :label="TEXT.modules.adminAuditLogs.columns.action" show-overflow-tooltip />
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" show-overflow-tooltip>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.targetTypeLabel">
|
||||
<strong>{{ scope.row.targetTypeLabel }}</strong>
|
||||
<span class="text-gray-500" v-if="scope.row.targetName">:{{ scope.row.targetName }}</span>
|
||||
</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
<div class="target-cell">
|
||||
<span v-if="scope.row.targetTypeLabel" class="target-text">
|
||||
<strong>{{ scope.row.targetTypeLabel }}</strong><span v-if="scope.row.targetName" class="text-muted">:{{ scope.row.targetName }}</span>
|
||||
</span>
|
||||
<span v-else class="text-muted">{{ TEXT.common.fallback }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" show-overflow-tooltip>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.diffText?.length" class="diff-container">
|
||||
<div v-for="(line, idx) in getDiffPreview(scope.row)" :key="idx" class="diff-line">{{ line }}</div>
|
||||
<div v-for="(line, idx) in getDiffPreview(scope.row)" :key="idx" class="diff-line">{{ formatDiffLine(line) }}</div>
|
||||
<div class="diff-more-row" v-if="scope.row.diffText.length > DIFF_PREVIEW_LIMIT">
|
||||
<span class="diff-more-text">其余 {{ scope.row.diffText.length - DIFF_PREVIEW_LIMIT }} 项</span>
|
||||
<el-button link type="primary" @click="openDetail(scope.row)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="openDetail(scope.row)">{{ TEXT.common.actions.view }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">{{ TEXT.audit.emptyValue }}</span>
|
||||
<span v-else class="text-muted">{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="110" align="center">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'" effect="dark" size="small">
|
||||
<span class="result-badge" :class="scope.row.result === 'SUCCESS' ? 'result--success' : 'result--fail'">
|
||||
{{ scope.row.resultLabel }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -117,43 +156,88 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<el-drawer
|
||||
v-if="detailVisible"
|
||||
v-model="detailVisible"
|
||||
direction="rtl"
|
||||
size="560px"
|
||||
size="580px"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
class="audit-detail-drawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<div class="drawer-title">审计日志详情</div>
|
||||
<el-button @click="detailVisible = false">{{ TEXT.common.actions.close }}</el-button>
|
||||
<div class="drawer-title-row">
|
||||
<div class="drawer-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="18" height="18"><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"/></svg>
|
||||
</div>
|
||||
<span class="drawer-title">审计详情</span>
|
||||
</div>
|
||||
<el-button @click="detailVisible = false" size="small">{{ TEXT.common.actions.close }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="selectedLog">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-meta-grid">
|
||||
<div class="meta-item"><span class="meta-label">时间</span><span>{{ displayDateTime(selectedLog.timestamp) }}</span></div>
|
||||
<div class="meta-item"><span class="meta-label">操作人</span><span>{{ selectedLog.actorName || TEXT.common.fallback }}</span></div>
|
||||
<div class="meta-item"><span class="meta-label">事件</span><span>{{ selectedLog.eventLabel || TEXT.common.fallback }}</span></div>
|
||||
<div class="meta-item"><span class="meta-label">结果</span><span>{{ selectedLog.resultLabel || TEXT.common.fallback }}</span></div>
|
||||
<div class="meta-item meta-item-full">
|
||||
<span class="meta-label">对象</span>
|
||||
<span>{{ selectedLog.targetTypeLabel || TEXT.common.fallback }}<span v-if="selectedLog.targetName">:{{ selectedLog.targetName }}</span></span>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">时间</span>
|
||||
<span class="meta-value">{{ displayDateTime(selectedLog.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">操作人</span>
|
||||
<span class="meta-value">{{ selectedLog.actorName || TEXT.common.fallback }}</span>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">事件</span>
|
||||
<span class="meta-value">{{ selectedLog.eventLabel || TEXT.common.fallback }}</span>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">结果</span>
|
||||
<span class="meta-value">
|
||||
<span class="result-badge" :class="selectedLog.result === 'SUCCESS' ? 'result--success' : 'result--fail'">{{ selectedLog.resultLabel }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-item meta-item-full"><span class="meta-label">动作</span><span>{{ selectedLog.actionText || TEXT.common.fallback }}</span></div>
|
||||
</div>
|
||||
<div class="detail-meta-grid detail-meta-full">
|
||||
<div class="meta-card meta-card-full">
|
||||
<span class="meta-label">对象</span>
|
||||
<span class="meta-value">{{ selectedLog.targetTypeLabel || TEXT.common.fallback }}<span v-if="selectedLog.targetName">:{{ selectedLog.targetName }}</span></span>
|
||||
</div>
|
||||
<div class="meta-card meta-card-full">
|
||||
<span class="meta-label">动作</span>
|
||||
<span class="meta-value">{{ selectedLog.actionText || TEXT.common.fallback }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 变更明细 -->
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">变更明细</div>
|
||||
<el-scrollbar max-height="58vh">
|
||||
<div class="detail-section-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||
变更明细
|
||||
<span class="diff-count" v-if="selectedLog.diffText?.length">{{ selectedLog.diffText.length }} 条</span>
|
||||
</div>
|
||||
<el-scrollbar max-height="55vh">
|
||||
<div v-if="selectedLog.diffText?.length" class="detail-lines">
|
||||
<div v-for="(line, idx) in selectedLog.diffText" :key="idx" class="detail-line-row">
|
||||
<span class="detail-line-index">{{ Number(idx) + 1 }}.</span>
|
||||
<span class="detail-line-text">{{ line }}</span>
|
||||
<span class="detail-line-index">{{ Number(idx) + 1 }}</span>
|
||||
<div class="detail-line-content">
|
||||
<template v-if="parseDiffLine(formatDiffLine(line))">
|
||||
<span class="diff-field">{{ parseDiffLine(formatDiffLine(line))!.field }}</span>
|
||||
<div class="diff-values">
|
||||
<span class="diff-old">{{ parseDiffLine(formatDiffLine(line))!.oldVal }}</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12" class="diff-arrow"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||
<span class="diff-new">{{ parseDiffLine(formatDiffLine(line))!.newVal }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="detail-line-text">{{ formatDiffLine(line) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-400">{{ TEXT.audit.emptyValue }}</div>
|
||||
<div v-else class="empty-diff">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32" style="color:var(--ctms-text-disabled)"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<span class="text-muted">无变更记录</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -204,6 +288,33 @@ const filters = ref({
|
||||
range: [],
|
||||
});
|
||||
|
||||
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 getInitials = (name: string) => {
|
||||
if (!name) return '?';
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
const formatDiffLine = (line: any): string => {
|
||||
if (typeof line === 'string') return line;
|
||||
if (line === null || line === undefined) return '-';
|
||||
try {
|
||||
return JSON.stringify(line, null, 0);
|
||||
} catch {
|
||||
return String(line);
|
||||
}
|
||||
};
|
||||
|
||||
const parseDiffLine = (line: string): { field: string; oldVal: string; newVal: string } | null => {
|
||||
const match = line.match(/^(.+?):(.+?)\s*->\s*(.+)$/);
|
||||
if (!match) return null;
|
||||
return { field: match[1], oldVal: match[2], newVal: match[3] };
|
||||
};
|
||||
|
||||
const eventTypeOptions = Object.entries(auditDict).map(([value, cfg]) => ({
|
||||
value,
|
||||
label: cfg.label,
|
||||
@@ -242,7 +353,6 @@ const loadUsers = async () => {
|
||||
const { data } = await fetchUsers({ limit: 500 });
|
||||
users.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} else if (study.currentStudy) {
|
||||
// 非管理员,尝试加载项目成员作为备选
|
||||
const { data } = await listMembers(study.currentStudy.id, { limit: 500 });
|
||||
const items = Array.isArray(data) ? data : (data as any).items || [];
|
||||
users.value = items.map((m: any) => ({
|
||||
@@ -421,11 +531,7 @@ const confirmExport = async (scope: "system" | "project") => {
|
||||
entity_type: "audit_log",
|
||||
entity_id: null,
|
||||
detail: JSON.stringify(
|
||||
{
|
||||
targetName: currentStudy.name,
|
||||
scope,
|
||||
exportedCount: events.length,
|
||||
},
|
||||
{ targetName: currentStudy.name, scope, exportedCount: events.length },
|
||||
null,
|
||||
0
|
||||
),
|
||||
@@ -444,31 +550,52 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
/* 统计卡片 */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background: var(--ctms-neutral-100);
|
||||
transition: var(--ctms-transition);
|
||||
}
|
||||
|
||||
.main-content-flat {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
.stat-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
}
|
||||
|
||||
.main-content-flat :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
.stat-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding: 0;
|
||||
}
|
||||
.stat-icon svg { width: 20px; height: 20px; }
|
||||
.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: 22px; font-weight: 700; line-height: 1.2; color: var(--ctms-text-main); }
|
||||
.stat-label { font-size: 12px; color: var(--ctms-text-secondary); margin-top: 2px; }
|
||||
|
||||
/* 筛选栏 */
|
||||
.audit-toolbar {
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
}
|
||||
|
||||
@@ -486,112 +613,195 @@ onMounted(async () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-select-comp {
|
||||
width: 132px;
|
||||
}
|
||||
|
||||
.filter-select-result {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.date-range-picker-comp {
|
||||
width: 248px !important;
|
||||
}
|
||||
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.audit-export-dropdown {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.filter-select-comp { width: 132px; }
|
||||
.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: #3b6cb5 !important;
|
||||
border-color: #b3cce6 !important;
|
||||
color: var(--ctms-primary) !important;
|
||||
border-color: #c0cdd7 !important;
|
||||
background: #f0f6ff !important;
|
||||
}
|
||||
|
||||
.audit-export-btn:hover,
|
||||
.audit-export-btn:focus {
|
||||
color: #2a5699 !important;
|
||||
border-color: #8cb4de !important;
|
||||
.audit-export-btn:hover {
|
||||
color: var(--ctms-primary-hover) !important;
|
||||
border-color: #9bb1c2 !important;
|
||||
background: #e2edfa !important;
|
||||
}
|
||||
|
||||
.diff-container {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
line-height: 1.5;
|
||||
/* 表格 */
|
||||
.table-section { padding: 0; }
|
||||
.audit-table :deep(.el-table__inner-wrapper::before) { display: none; }
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
margin-bottom: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.diff-more-row {
|
||||
.actor-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diff-more-text {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
.actor-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, var(--ctms-primary), var(--ctms-primary-active));
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.event-tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: #f0f4f8;
|
||||
color: var(--ctms-text-regular);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.target-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.text-muted { color: var(--ctms-text-secondary); font-size: 12px; }
|
||||
|
||||
.result-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result--success { background: #e6f4ed; color: #166534; }
|
||||
.result--fail { background: #fde8e8; color: #991b1b; }
|
||||
|
||||
.diff-container {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-regular);
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.diff-more-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.diff-more-text { font-size: 11px; color: var(--ctms-text-disabled); }
|
||||
|
||||
.pagination-wrap {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
|
||||
.audit-table :deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.drawer-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 9px;
|
||||
background: var(--ctms-primary-light);
|
||||
color: var(--ctms-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 20px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #11264d;
|
||||
color: var(--ctms-text-main);
|
||||
}
|
||||
|
||||
.detail-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 14px;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
.detail-meta-full {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e6edf7;
|
||||
border-radius: 8px;
|
||||
background: #fbfdff;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--ctms-neutral-100);
|
||||
}
|
||||
|
||||
.meta-item-full {
|
||||
.meta-card-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 13px;
|
||||
color: var(--ctms-text-main);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
@@ -599,34 +809,131 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e3a5f;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ctms-text-main);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--ctms-border-color);
|
||||
}
|
||||
|
||||
.detail-section-title svg {
|
||||
color: var(--ctms-primary);
|
||||
}
|
||||
|
||||
.diff-count {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-secondary);
|
||||
background: var(--ctms-neutral-100);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.detail-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.detail-line-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
background: #fff;
|
||||
transition: var(--ctms-transition);
|
||||
}
|
||||
|
||||
.detail-line-row:hover {
|
||||
border-color: var(--ctms-border-color-hover);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
}
|
||||
|
||||
.detail-line-index {
|
||||
color: var(--ctms-text-disabled);
|
||||
min-width: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
background: var(--ctms-neutral-100);
|
||||
border-radius: 4px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.detail-line-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.diff-field {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.diff-values {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.diff-old {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-danger);
|
||||
background: #fef2f2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
max-width: 45%;
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: rgba(194, 75, 75, 0.3);
|
||||
}
|
||||
|
||||
.diff-arrow {
|
||||
color: var(--ctms-text-disabled);
|
||||
flex-shrink: 0;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.diff-new {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-success);
|
||||
background: #f0fdf4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.detail-line-text {
|
||||
color: var(--ctms-text-regular);
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-line-index {
|
||||
color: #94a3b8;
|
||||
min-width: 20px;
|
||||
.empty-diff {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.detail-line-text {
|
||||
color: #475569;
|
||||
word-break: break-word;
|
||||
@media (max-width: 768px) {
|
||||
.stats-row { grid-template-columns: repeat(2, 1fr); }
|
||||
.filter-form { flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user