<template>
|
<div class="app-container">
|
<el-tabs v-model="activeTab" type="border-card">
|
<!-- ==================== Tab1: 审计追踪 ==================== -->
|
<el-tab-pane label="审计追踪" name="audit">
|
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
<template #title>
|
记录敏感数据的增删改与关键业务操作(样品出库、实验资源分配、告警处理等),
|
包含操作人、IP、字段变更前后值,满足合规追溯要求。
|
</template>
|
</el-alert>
|
<div class="search_form">
|
<div>
|
<span class="search_title">模块:</span>
|
<el-input v-model="auditSearch.module" style="width: 140px" placeholder="如 样品管理" clearable @change="loadAudit" />
|
<span class="search_title" style="margin-left: 14px">动作:</span>
|
<el-select v-model="auditSearch.action" style="width: 150px" placeholder="全部" clearable @change="loadAudit">
|
<el-option label="新增 create" value="create" />
|
<el-option label="修改 update" value="update" />
|
<el-option label="出库 stock_out" value="stock_out" />
|
<el-option label="入库 stock_in" value="stock_in" />
|
<el-option label="分配资源 assign_resource" value="assign_resource" />
|
<el-option label="处理告警 handle_alert" value="handle_alert" />
|
</el-select>
|
<span class="search_title" style="margin-left: 14px">操作人:</span>
|
<el-input v-model="auditSearch.operator" style="width: 130px" placeholder="请输入" clearable @change="loadAudit" />
|
<span class="search_title" style="margin-left: 14px">结果:</span>
|
<el-select v-model="auditSearch.execResult" style="width: 110px" placeholder="全部" clearable @change="loadAudit">
|
<el-option label="成功" value="success" />
|
<el-option label="失败" value="fail" />
|
</el-select>
|
<el-button type="primary" style="margin-left: 10px" @click="loadAudit">搜索</el-button>
|
</div>
|
<div>
|
<el-button @click="loadAudit">刷新</el-button>
|
</div>
|
</div>
|
<PIMTable rowKey="id" :column="auditColumns" :tableData="auditData" :page="auditPage"
|
:tableLoading="auditLoading" @pagination="paginationAudit" />
|
</el-tab-pane>
|
|
<!-- ==================== Tab2: 访问控制 ==================== -->
|
<el-tab-pane label="访问控制" name="access">
|
<el-alert type="success" :closable="false" style="margin-bottom: 16px">
|
<template #title>
|
基于 RuoYi 权限体系:用户 → 角色 → 菜单/按钮权限。所有接口都有 @PreAuthorize 校验,
|
未授权访问返回 401/403;登录密码用 BCrypt 加密存储,不落明文。
|
</template>
|
</el-alert>
|
|
<el-row :gutter="16">
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">用户管理</span>
|
<el-button link type="primary" @click="goSystem('user')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px; line-height: 1.8">
|
维护系统使用者账号,分配角色与部门。<br>
|
当前用户:<b>{{ stat.userCount ?? '-' }}</b> 人
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">角色管理</span>
|
<el-button link type="primary" @click="goSystem('role')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px; line-height: 1.8">
|
按岗位划分角色,配置可访问的菜单和数据范围。<br>
|
当前角色:<b>{{ stat.roleCount ?? '-' }}</b> 个
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">菜单权限</span>
|
<el-button link type="primary" @click="goSystem('menu')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px; line-height: 1.8">
|
控制每个角色能看到哪些菜单、能点哪些按钮。<br>
|
当前菜单:<b>{{ stat.menuCount ?? '-' }}</b> 项
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<el-divider content-position="left">登录与操作审计</el-divider>
|
<el-row :gutter="16">
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">登录日志</span>
|
<el-button link type="primary" @click="goMonitor('logininfor')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px">记录每次登录的 IP、地点、浏览器、成功/失败</div>
|
</el-card>
|
</el-col>
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">操作日志</span>
|
<el-button link type="primary" @click="goMonitor('operlog')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px">框架级操作日志(所有带 @Log 注解的接口)</div>
|
</el-card>
|
</el-col>
|
<el-col :span="8">
|
<el-card shadow="hover">
|
<template #header>
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
<span style="font-weight: 600">在线用户</span>
|
<el-button link type="primary" @click="goMonitor('online')">前往 →</el-button>
|
</div>
|
</template>
|
<div style="color: #606266; font-size: 13px">当前登录会话,可强制下线</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<el-divider content-position="left">安全措施说明</el-divider>
|
<el-table :data="securityMeasures" border size="small">
|
<el-table-column prop="item" label="措施" width="180" />
|
<el-table-column prop="impl" label="实现方式" />
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
<template #default="{ row }">
|
<el-tag :type="row.status === '已实施' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-tab-pane>
|
|
<!-- ==================== Tab3: 安全策略 ==================== -->
|
<el-tab-pane label="安全策略" name="policy">
|
<el-alert type="warning" :closable="false" style="margin-bottom: 12px">
|
<template #title>
|
以下参数控制安全相关行为,修改后立即生效(审计与参数服务有本地缓存,改完刷新页面即可)。
|
</template>
|
</el-alert>
|
<div class="search_form">
|
<div>
|
<span class="search_title">参数名:</span>
|
<el-input v-model="secSearch.settingName" style="width: 180px" placeholder="请输入" clearable @change="loadSecurity" />
|
<el-button type="primary" style="margin-left: 10px" @click="loadSecurity">搜索</el-button>
|
</div>
|
<div>
|
<el-button @click="loadSecurity">刷新</el-button>
|
</div>
|
</div>
|
<el-table :data="secData" :loading="secLoading" border size="small">
|
<el-table-column prop="settingKey" label="参数键" width="230" />
|
<el-table-column prop="settingName" label="参数名" width="160" />
|
<el-table-column prop="settingValue" label="当前值" width="140" />
|
<el-table-column prop="defaultValue" label="默认值" width="110" />
|
<el-table-column prop="description" label="说明" show-overflow-tooltip />
|
<el-table-column label="操作" width="90" align="center">
|
<template #default="{ row }">
|
<el-button link type="primary" @click="openEdit(row)">修改</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-tab-pane>
|
</el-tabs>
|
|
<!-- ===== 参数编辑弹窗 ===== -->
|
<el-dialog v-model="editVisible" title="修改安全策略" width="560px">
|
<el-form :model="editForm" label-position="top">
|
<el-form-item label="参数键"><el-input v-model="editForm.settingKey" disabled /></el-form-item>
|
<el-form-item :label="editForm.settingName">
|
<el-input v-model="editForm.settingValue" placeholder="请输入" />
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">{{ editForm.description }}</div>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="editVisible = false">取消</el-button>
|
<el-button type="primary" @click="submitEdit">保存</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
|
import { useRouter } from "vue-router";
|
import { ElMessage } from "element-plus";
|
import { listAuditLog } from "@/api/lims/auditLog";
|
import { listSetting, updateSetting } from "@/api/lims/setting";
|
|
const { proxy } = getCurrentInstance();
|
const router = useRouter();
|
const activeTab = ref("audit");
|
|
/* ==================== 审计追踪 ==================== */
|
const auditSearch = ref({});
|
const auditData = ref([]);
|
const auditLoading = ref(false);
|
const auditPage = reactive({ pageNum: 1, pageSize: 20, total: 0 });
|
|
const auditColumns = ref([
|
{ label: "时间", prop: "operateTime", width: 160 },
|
{ label: "模块", prop: "module", width: 110 },
|
{ label: "动作", prop: "action", width: 140 },
|
{ label: "业务编号", prop: "bizCode", width: 130 },
|
{ label: "说明", prop: "description", showOverflowTooltip: true },
|
{ label: "字段", prop: "fieldName", width: 100 },
|
{ label: "旧值", prop: "oldValue", width: 100 },
|
{ label: "新值", prop: "newValue", width: 100 },
|
{ label: "操作人", prop: "operator", width: 100 },
|
{ label: "IP", prop: "operatorIp", width: 130 },
|
{ label: "结果", prop: "execResult", width: 80 },
|
]);
|
|
const loadAudit = () => {
|
auditLoading.value = true;
|
listAuditLog({ ...auditSearch.value, ...auditPage }).then((res) => {
|
auditLoading.value = false;
|
auditData.value = res.data.records || [];
|
auditPage.total = res.data.total || 0;
|
}).catch(() => (auditLoading.value = false));
|
};
|
const paginationAudit = (p) => { auditPage.pageNum = p.pageNum; auditPage.pageSize = p.pageSize; loadAudit(); };
|
|
/* ==================== 访问控制 ==================== */
|
const stat = ref({});
|
const securityMeasures = ref([
|
{ item: "密码存储", impl: "BCrypt 加盐哈希(不可逆),数据库中不落明文", status: "已实施" },
|
{ item: "接口鉴权", impl: "JWT Token + Spring Security 拦截,未授权返回 401", status: "已实施" },
|
{ item: "菜单/按钮权限", impl: "用户→角色→菜单三级,按钮级 perms 校验", status: "已实施" },
|
{ item: "数据隔离", impl: "多租户插件自动拼接 tenant_id 条件", status: "已实施" },
|
{ item: "敏感操作审计", impl: "样品出入库、实验资源分配、告警处理写 lims_audit_log", status: "已实施" },
|
{ item: "审计留痕内容", impl: "操作人、IP、字段变更前后值、操作结果", status: "已实施" },
|
{ item: "登录日志", impl: "记录每次登录的 IP、地点、浏览器、成功失败", status: "已实施" },
|
{ item: "字段级加密", impl: "未做,如客户有等保要求需另行开发", status: "待定" },
|
{ item: "列表脱敏显示", impl: "参数 security.maskSensitive 已预留,前端展示未实现", status: "待定" },
|
]);
|
|
const goSystem = (path) => { router.push("/system/" + path); };
|
const goMonitor = (path) => { router.push("/monitor/" + path); };
|
|
// 统计(用已有的接口顺手取数,失败不影响展示)
|
const loadStat = () => {
|
Promise.all([
|
import("@/api/system/user").then((m) => m.listUser({ pageNum: 1, pageSize: 1 })).catch(() => null),
|
import("@/api/system/role").then((m) => m.listRole({ pageNum: 1, pageSize: 1 })).catch(() => null),
|
import("@/api/system/menu").then((m) => m.listMenu({})).catch(() => null),
|
]).then(([u, r, m]) => {
|
stat.value = {
|
userCount: u && u.total !== undefined ? u.total : (u && u.data && u.data.total),
|
roleCount: r && r.total !== undefined ? r.total : (r && r.data && r.data.total),
|
menuCount: Array.isArray(m) ? m.length : (m && m.data ? m.data.length : undefined),
|
};
|
}).catch(() => {});
|
};
|
|
/* ==================== 安全策略 ==================== */
|
const secSearch = ref({});
|
const secData = ref([]);
|
const secLoading = ref(false);
|
const editVisible = ref(false);
|
const editForm = ref({});
|
|
const loadSecurity = () => {
|
secLoading.value = true;
|
listSetting({ ...secSearch.value, category: "security", pageNum: 1, pageSize: 50 }).then((res) => {
|
secLoading.value = false;
|
secData.value = res.data.records || [];
|
}).catch(() => (secLoading.value = false));
|
};
|
|
const openEdit = (row) => {
|
editForm.value = { ...row };
|
editVisible.value = true;
|
};
|
const submitEdit = () => {
|
updateSetting(editForm.value).then(() => {
|
ElMessage.success("已保存,刷新页面后生效");
|
editVisible.value = false;
|
loadSecurity();
|
});
|
};
|
|
onMounted(() => {
|
loadAudit();
|
loadStat();
|
loadSecurity();
|
});
|
</script>
|
|
<style scoped>
|
.search_form {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 12px;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
.search_title {
|
font-size: 14px;
|
color: #606266;
|
}
|
</style>
|