立项配置数据入库、审计日志数据入库、编辑页UI界面美化、设备管理内容补充、审计日志显示优化

This commit is contained in:
Cheng Zhou
2026-02-27 16:16:26 +08:00
parent fd7e3fc948
commit db2d38edbc
48 changed files with 2936 additions and 909 deletions
+22 -45
View File
@@ -148,7 +148,7 @@ import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { ArrowDown } from "@element-plus/icons-vue";
import { fetchAuditLogs } from "../../api/auditLogs";
import { createAuditEvent, fetchAuditLogs } from "../../api/auditLogs";
import { fetchUsers } from "../../api/users";
import { listMembers } from "../../api/members";
import { auditDict, normalizeAuditEvent } from "../../audit";
@@ -156,27 +156,9 @@ import { useStudyStore } from "../../store/study";
import { useAuthStore } from "../../store/auth";
import { roleDict, getDictLabel } from "../../dictionaries";
import { exportAuditCsv } from "../../audit/export/auditExportService";
import { logAudit } from "../../audit";
import { displayDateTime } from "../../utils/display";
import { TEXT } from "../../locales";
const buildLogKey = (log: any) => {
if (log.id) return `id:${log.id}`;
return `${log.action || log.eventType || "event"}-${log.entity_id || log.entityId || ""}-${log.created_at || log.timestamp || ""}`;
};
const dedupeLogs = (list: any[]) => {
const seen = new Set<string>();
const result: any[] = [];
list.forEach((log) => {
const key = buildLogKey(log);
if (seen.has(key)) return;
seen.add(key);
result.push(log);
});
return result;
};
const study = useStudyStore();
const auth = useAuthStore();
const router = useRouter();
@@ -250,10 +232,8 @@ const loadLogs = async () => {
};
const { data } = await fetchAuditLogs(study.currentStudy.id, params);
const items = Array.isArray(data) ? data : (data as any).items || [];
const local = loadLocalLogs();
const merged = dedupeLogs([...items, ...local]);
rawLogs.value = merged;
total.value = (data as any).total || merged.length;
rawLogs.value = items;
total.value = (data as any).total || items.length;
enrichLogs();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.loadFailed);
@@ -262,18 +242,6 @@ const loadLogs = async () => {
}
};
const loadLocalLogs = () => {
try {
const key = `audit_local_${study.currentStudy?.id || "global"}`;
const raw = localStorage.getItem(key);
if (!raw) return [];
const list = JSON.parse(raw);
return Array.isArray(list) ? list : [];
} catch {
return [];
}
};
const enrichLogs = () => {
const userMap = users.value.reduce<Record<string, string>>((acc, cur) => {
acc[cur.id] = resolveUserDisplayName(cur);
@@ -333,13 +301,11 @@ const fetchAllForExport = async () => {
};
const { data } = await fetchAuditLogs(study.currentStudy.id, params);
const items = Array.isArray(data) ? data : (data as any).items || [];
const local = loadLocalLogs();
const merged = dedupeLogs([...items, ...local]);
const userMap = users.value.reduce<Record<string, string>>((acc, cur) => {
acc[cur.id] = resolveUserDisplayName(cur);
return acc;
}, {});
const filtered = merged.filter((log) => {
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]);
@@ -348,7 +314,7 @@ const fetchAllForExport = async () => {
}
return true;
});
return filtered.map((log) => normalizeAuditEvent(log, userMap));
return filtered.map((log: any) => normalizeAuditEvent(log, userMap));
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.exportLoadFailed);
return [];
@@ -362,6 +328,8 @@ const handleExportCommand = (command: string) => {
};
const confirmExport = async (scope: "system" | "project") => {
const currentStudy = study.currentStudy;
if (!currentStudy) return;
const ok = await ElMessageBox.confirm(
TEXT.modules.adminAuditLogs.exportConfirm,
TEXT.modules.adminAuditLogs.exportConfirmTitle,
@@ -376,13 +344,22 @@ const confirmExport = async (scope: "system" | "project") => {
const fileName =
scope === "system"
? `${TEXT.modules.adminAuditLogs.exportSystemName}_${new Date().toISOString().slice(0, 10)}`
: `${TEXT.modules.adminAuditLogs.exportProjectName}_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
: `${TEXT.modules.adminAuditLogs.exportProjectName}_${currentStudy.code || ""}_${new Date().toISOString().slice(0, 10)}`;
exportAuditCsv(events, { fileName });
logAudit(scope === "system" ? "AUDIT_EXPORT_SYSTEM" : "AUDIT_EXPORT_PROJECT", {
targetId: study.currentStudy?.id,
targetName: study.currentStudy?.name,
severity: "normal",
});
await createAuditEvent(currentStudy.id, {
action: scope === "system" ? "AUDIT_EXPORT_SYSTEM" : "AUDIT_EXPORT_PROJECT",
entity_type: "audit_log",
entity_id: null,
detail: JSON.stringify(
{
targetName: currentStudy.name,
scope,
exportedCount: events.length,
},
null,
0
),
}).catch(() => null);
};
onMounted(async () => {
File diff suppressed because it is too large Load Diff
@@ -105,7 +105,6 @@ import { fetchStudyDetail } from "../../api/studies";
import type { Study, StudyMember, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { displayDateTime, displayEnum } from "../../utils/display";
import { TEXT, requiredMessage } from "../../locales";
@@ -189,12 +188,6 @@ const submitAdd = async () => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
await addFormRef.value?.validate();
@@ -204,21 +197,8 @@ const submitAdd = async () => {
ElMessage.success(TEXT.modules.adminProjectMembers.addSuccess);
addVisible.value = false;
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
after: { user_id: newMember.user_id, role_in_study: newMember.role_in_study },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.addFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
after: { user_id: newMember.user_id, role_in_study: newMember.role_in_study },
severity: "warning",
reason: e?.response?.data?.message,
});
} finally {
adding.value = false;
}
@@ -233,34 +213,15 @@ const updateRole = async (memberId: string, role: string) => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
try {
await updateMember(projectId.value, memberId, { role_in_study: role });
ElMessage.success(TEXT.modules.adminProjectMembers.roleUpdated);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
after: { member_id: memberId, role_in_study: role },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.updateFailed);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
after: { member_id: memberId, role_in_study: role },
severity: "warning",
reason: e?.response?.data?.message,
});
}
};
@@ -273,12 +234,6 @@ const toggleActive = async (row: StudyMember) => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
if (row.is_active) {
@@ -291,46 +246,16 @@ const toggleActive = async (row: StudyMember) => {
await updateMember(projectId.value, row.id, { is_active: false });
ElMessage.success(TEXT.modules.adminProjectMembers.disableSuccess);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { is_active: false },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { is_active: false },
severity: "warning",
reason: e?.response?.data?.message,
});
}
} else {
try {
await updateMember(projectId.value, row.id, { is_active: true });
ElMessage.success(TEXT.modules.adminProjectMembers.enableSuccess);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { is_active: true },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { is_active: true },
severity: "warning",
reason: e?.response?.data?.message,
});
}
}
};
@@ -344,12 +269,6 @@ const onDelete = async (row: StudyMember) => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.removeConfirm, TEXT.modules.adminProjectMembers.removeTitle, {
@@ -362,23 +281,8 @@ const onDelete = async (row: StudyMember) => {
await removeMember(projectId.value, row.id);
members.value = members.value.filter((m) => m.id !== row.id);
ElMessage.success(TEXT.modules.adminProjectMembers.removeSuccess);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { removed_member_id: row.id },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
before: { is_active: row.is_active },
after: { removed_member_id: row.id },
severity: "warning",
reason: e?.response?.data?.message,
});
}
};
@@ -23,7 +23,6 @@ import { updateSite } from "../../api/sites";
import type { Site, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT } from "../../locales";
const props = defineProps<{
@@ -85,12 +84,6 @@ const onSave = async () => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: props.site.id,
targetName: props.site.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
saving.value = true;
@@ -102,23 +95,8 @@ const onSave = async () => {
ElMessage.success(TEXT.modules.adminSites.craSaveSuccess);
emit("saved");
visibleProxy.value = false;
logAudit("SITE_CRA_BOUND", {
targetId: props.site.id,
targetName: props.site.name,
before: { contact: props.site.contact },
after: { contact: names.join(",") },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
logAudit("SITE_CRA_BOUND", {
targetId: props.site.id,
targetName: props.site.name,
before: { contact: props.site.contact },
after: { contact: selectedCras.value.join(",") },
severity: "warning",
reason: e?.response?.data?.message,
});
} finally {
saving.value = false;
}
-26
View File
@@ -43,7 +43,6 @@ import { createSite, updateSite } from "../../api/sites";
import type { Site } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
@@ -137,12 +136,6 @@ const onSubmit = async () => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
await formRef.value.validate();
@@ -158,12 +151,6 @@ const onSubmit = async () => {
is_active: form.is_active,
});
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site.id,
targetName: props.site.name,
after: { name: form.name, is_active: form.is_active },
severity: "normal",
});
} else {
await createSite(props.studyId, {
name: form.name,
@@ -172,25 +159,12 @@ const onSubmit = async () => {
contact,
});
ElMessage.success(TEXT.modules.adminSites.createSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.studyId,
targetName: form.name,
after: { name: form.name },
severity: "normal",
});
}
emit("saved");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name || form.name,
after: { name: form.name },
severity: "warning",
reason: e?.response?.data?.message,
});
} finally {
submitting.value = false;
}
-33
View File
@@ -86,7 +86,6 @@ import SiteForm from "./SiteForm.vue";
import type { Site, Study, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT } from "../../locales";
const route = useRoute();
@@ -217,12 +216,6 @@ const toggleSite = async (row: Site) => {
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: row.id,
targetName: row.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
if (row.is_active) {
@@ -237,23 +230,8 @@ const toggleSite = async (row: Site) => {
await updateSite(projectId.value, row.id, { is_active: !row.is_active });
ElMessage.success(row.is_active ? "已锁定" : "已解锁");
loadSites();
logAudit("SITE_STATUS_CHANGED", {
targetId: row.id,
targetName: row.name,
before: { is_active: row.is_active },
after: { is_active: !row.is_active },
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("SITE_STATUS_CHANGED", {
targetId: row.id,
targetName: row.name,
before: { is_active: row.is_active },
after: { is_active: !row.is_active },
severity: "warning",
reason: e?.response?.data?.message,
});
}
};
@@ -277,19 +255,8 @@ const confirmDelete = async (row: Site) => {
await deleteSite(projectId.value, row.id);
ElMessage.success(TEXT.common.messages.deleteSuccess);
loadSites();
logAudit("SITE_DELETED", {
targetId: row.id,
targetName: row.name,
severity: "high",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
logAudit("SITE_DELETED", {
targetId: row.id,
targetName: row.name,
severity: "warning",
reason: e?.response?.data?.message,
});
}
};