feat: 平台治理与对象存储增强,审批中心与运行日志整合
- 新增 storage/policy.py 落盘策略:按大小/类型决定文件存 MinIO 或内联数据库 - 数据处理源文件与生成结果写入 MinIO 并登记 storage_objects,支持失败回滚 - 算力节点训练产物按版本归档到 MinIO,登记 model_artifacts - 数据转换任务输入输出对象化,支持从 MinIO 读写 - 新增审批中心(申请/我的/策略)、组织与权限、运行日志整合页面 - schema 与 docker 配置、前端路由侧边栏、治理文档同步更新 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ export interface AuditQuery {
|
||||
actor_id?: string
|
||||
action?: string
|
||||
target_type?: string
|
||||
target_id?: string
|
||||
keyword?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
limit?: number
|
||||
|
||||
@@ -15,6 +15,10 @@ const activeMenu = computed(() => {
|
||||
if (seg === 'training-log') return 'fine-tune'
|
||||
// 维度管理归到模型评测
|
||||
if (route.path.includes('model-eval/dimension')) return 'model-eval'
|
||||
// 组织与权限承接用户、租户和历史治理入口
|
||||
if (route.path.startsWith('/organization') || route.path.startsWith('/user-settings') || route.path.startsWith('/tenants')) return 'organization'
|
||||
// 运行日志承接审计和操作诊断两个历史入口
|
||||
if (route.path.startsWith('/logs') || route.path.startsWith('/audit-logs') || route.path.startsWith('/operation-logs')) return 'logs'
|
||||
// 对比对话归到模型推理
|
||||
if (route.path.startsWith('/model-compare/chat')) return 'model-inference'
|
||||
// 合并权重归到模型管理
|
||||
@@ -77,21 +81,16 @@ const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
title: '平台治理',
|
||||
items: [
|
||||
{ key: 'tenants', label: '租户管理', icon: 'fa-building', to: '/tenants', permission: 'user-settings' },
|
||||
{ key: 'projects', label: '项目空间', icon: 'fa-folder', to: '/projects', permission: 'user-settings' },
|
||||
{ key: 'organization', label: '组织与权限', icon: 'fa-users', to: '/organization', permission: 'user-settings' },
|
||||
{ key: 'resource-acl', label: '资源授权', icon: 'fa-key', to: '/resource-acl', permission: 'user-settings' },
|
||||
{ key: 'audit-logs', label: '审计日志', icon: 'fa-history', to: '/audit-logs', permission: 'user-settings' },
|
||||
{ key: 'operation-logs', label: '操作日志', icon: 'fa-list', to: '/operation-logs', permission: 'user-settings' },
|
||||
{ key: 'approval-templates', label: '审批模板', icon: 'fa-list-alt', to: '/approval-templates', permission: 'user-settings' },
|
||||
{ key: 'approval-instances', label: '审批中心', icon: 'fa-check-square', to: '/approval-instances', permission: 'user-settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '系统设置',
|
||||
items: [
|
||||
{ key: 'user-settings', label: '用户设置', icon: 'fa-users', to: '/user-settings', permission: 'user-settings' },
|
||||
{ key: 'hardware', label: '平台性能', icon: 'fa-bar-chart', to: '/hardware', permission: 'hardware' },
|
||||
{ key: 'logs', label: '查看日志', icon: 'fa-file-text', to: '/logs', permission: 'logs' },
|
||||
{ key: 'logs', label: '运行日志', icon: 'fa-file-text', to: '/logs', permission: 'logs' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -101,9 +100,9 @@ const menuGroups: MenuGroup[] = [
|
||||
*
|
||||
* 1. admin 用户:可以看到所有菜单
|
||||
* 2. 非 admin 用户:
|
||||
* - 默认可见所有业务菜单(模型训练、评测、推理、数据集、数据处理、转换、性能、日志等)
|
||||
* - 默认可见所有业务菜单(模型训练、评测、推理、数据集、数据处理、转换、性能、运行日志等)
|
||||
* - 仅以下菜单对非 admin 不可见:
|
||||
* - user-settings(用户设置、租户管理、项目空间、审批模板/中心、审计日志)
|
||||
* - user-settings(组织与权限、资源授权、审批;运行日志中的审计/诊断页签)
|
||||
* - compute(算力节点/GPU 分配)
|
||||
*
|
||||
* 注意:移除了旧的权限码(permission code)过滤逻辑,
|
||||
|
||||
@@ -32,11 +32,17 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { title: '服务看板' },
|
||||
},
|
||||
// 平台治理
|
||||
{
|
||||
path: 'organization',
|
||||
name: 'organization',
|
||||
component: () => import('@/views/governance/OrganizationPermissionView.vue'),
|
||||
meta: { title: '组织与权限', pageSurface: 'self', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'tenants',
|
||||
name: 'tenants',
|
||||
component: () => import('@/views/tenants/TenantListView.vue'),
|
||||
meta: { title: '租户管理', permission: 'user-settings' },
|
||||
redirect: '/organization?tab=tenants',
|
||||
meta: { title: '租户与配额', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'tenants/:id',
|
||||
@@ -47,37 +53,37 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: 'projects',
|
||||
name: 'projects',
|
||||
component: () => import('@/views/projects/ProjectListView.vue'),
|
||||
meta: { title: '项目空间', permission: 'user-settings' },
|
||||
redirect: '/organization?tab=users',
|
||||
meta: { title: '组织与权限', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'projects/:id',
|
||||
name: 'project-detail',
|
||||
component: () => import('@/views/projects/ProjectDetailView.vue'),
|
||||
meta: { title: '项目详情', permission: 'user-settings' },
|
||||
redirect: '/organization?tab=users',
|
||||
meta: { title: '组织与权限', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'audit-logs',
|
||||
name: 'audit-logs',
|
||||
component: () => import('@/views/audit/AuditLogView.vue'),
|
||||
meta: { title: '审计日志', permission: 'user-settings' },
|
||||
redirect: '/logs?tab=audit',
|
||||
meta: { title: '运行日志', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'operation-logs',
|
||||
name: 'operation-logs',
|
||||
component: () => import('@/views/audit/OperationLogView.vue'),
|
||||
meta: { title: '操作日志', permission: 'user-settings' },
|
||||
redirect: '/logs?tab=operations',
|
||||
meta: { title: '运行日志', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'approval-templates',
|
||||
name: 'approval-templates',
|
||||
component: () => import('@/views/approvals/ApprovalTemplateView.vue'),
|
||||
meta: { title: '审批模板', permission: 'user-settings' },
|
||||
redirect: '/approval-instances?tab=strategies',
|
||||
meta: { title: '审批中心', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'approval-instances',
|
||||
name: 'approval-instances',
|
||||
component: () => import('@/views/approvals/ApprovalInstanceView.vue'),
|
||||
component: () => import('@/views/approvals/ApprovalCenterView.vue'),
|
||||
meta: { title: '审批中心', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
@@ -302,14 +308,14 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: 'logs',
|
||||
name: 'logs',
|
||||
component: () => import('@/views/system/LogsView.vue'),
|
||||
meta: { title: '查看日志' },
|
||||
component: () => import('@/views/system/RuntimeLogsView.vue'),
|
||||
meta: { title: '运行日志', pageSurface: 'self', permission: 'logs' },
|
||||
},
|
||||
{
|
||||
path: 'user-settings',
|
||||
name: 'user-settings',
|
||||
component: () => import('@/views/system/UserSettingsView.vue'),
|
||||
meta: { title: '用户设置', pageSurface: 'self', permission: 'user-settings' },
|
||||
redirect: '/organization?tab=users',
|
||||
meta: { title: '组织与权限', pageSurface: 'self', permission: 'user-settings' },
|
||||
},
|
||||
{
|
||||
path: 'user-settings/create',
|
||||
@@ -357,6 +363,7 @@ const permissionBySegment: Record<string, PermissionCode> = {
|
||||
tools: 'data-convert',
|
||||
hardware: 'hardware',
|
||||
logs: 'logs',
|
||||
organization: 'user-settings',
|
||||
'user-settings': 'user-settings',
|
||||
tenants: 'user-settings',
|
||||
projects: 'user-settings',
|
||||
@@ -379,7 +386,7 @@ function requiredPermission(path: string, explicit?: unknown) {
|
||||
// 权限控制规则(基于 governance-user-guide.md 设计):
|
||||
// - admin 用户:可以访问所有页面
|
||||
// - 非 admin 用户:默认可访问所有业务页面(训练、评测、推理、数据等)
|
||||
// 仅以下页面限制 admin 访问:user-settings、compute(算力节点)
|
||||
// 仅治理与资源管理页面限制 admin 访问:organization、user-settings、compute
|
||||
router.beforeEach((to, _from, next) => {
|
||||
if (!to.meta.public) routeLoading.value = true
|
||||
const auth = useAuthStore()
|
||||
@@ -404,7 +411,7 @@ router.beforeEach((to, _from, next) => {
|
||||
if (!to.meta.skipPermission) {
|
||||
const permission = requiredPermission(to.path, to.meta.permission)
|
||||
// 仅限制管理员专属页面的访问权限
|
||||
// user-settings(用户设置、租户管理、项目空间、审批、审计日志)仅 admin 可访问
|
||||
// user-settings(组织与权限、资源授权、审批中心、运行日志)仅 admin 可访问
|
||||
if (permission === 'user-settings' && !auth.isAdmin) {
|
||||
next({ name: 'permission-denied', replace: true })
|
||||
return
|
||||
|
||||
66
frontend/src/views/approvals/ApprovalCenterView.vue
Normal file
66
frontend/src/views/approvals/ApprovalCenterView.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ApprovalInstanceView from './ApprovalInstanceView.vue'
|
||||
import ApprovalTemplateView from './ApprovalTemplateView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = computed<'instances' | 'mine' | 'strategies'>({
|
||||
get: () => route.query.tab === 'strategies' ? 'strategies' : route.query.tab === 'mine' ? 'mine' : 'instances',
|
||||
set: (value: string) => {
|
||||
void router.replace({ query: value === 'instances' ? {} : { tab: value } })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="approval-center">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h2>审批中心</h2>
|
||||
<p>集中处理审批申请、审批历史和审批策略。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="审批申请" name="instances">
|
||||
<ApprovalInstanceView v-if="activeTab === 'instances'" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="我的申请" name="mine">
|
||||
<ApprovalInstanceView v-if="activeTab === 'mine'" mine />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="审批策略" name="strategies">
|
||||
<ApprovalTemplateView v-if="activeTab === 'strategies'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.approval-center {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 4px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.page) {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataTablePage from '@/components/DataTablePage.vue'
|
||||
import { getApprovalInstances, decideApproval, type ApprovalInstance } from '@/api/modules/approval'
|
||||
import { getUsers } from '@/api/modules/system'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { SystemUser } from '@/types'
|
||||
|
||||
const props = defineProps<{ mine?: boolean }>()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const instances = ref<ApprovalInstance[]>([])
|
||||
const users = ref<SystemUser[]>([])
|
||||
@@ -14,6 +18,14 @@ const showDecide = ref(false)
|
||||
const current = ref<ApprovalInstance | null>(null)
|
||||
const decision = ref({ step_index: 0, approver_id: '', approved: true, comment: '' })
|
||||
|
||||
const visibleInstances = computed(() => {
|
||||
if (!props.mine) return instances.value
|
||||
const currentUserId = auth.currentUser?.id
|
||||
return currentUserId
|
||||
? instances.value.filter((item) => item.applicant_id === currentUserId)
|
||||
: []
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待审批', value: 'pending' },
|
||||
{ label: '已通过', value: 'approved' },
|
||||
@@ -77,7 +89,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<DataTablePage title="审批实例" :data="instances" :loading="loading" searchable :search-fields="['resource_type', 'resource_id']">
|
||||
<DataTablePage :title="props.mine ? '我的申请' : '审批申请'" :data="visibleInstances" :loading="loading" searchable :search-fields="['resource_type', 'resource_id']">
|
||||
<template #toolbar-extra>
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 140px" @change="load">
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAuditLogs, exportAuditLogs, type AuditLog, type AuditQuery } from '@/api/modules/audit'
|
||||
import { getUsers, type SystemUser } from '@/api/modules/system'
|
||||
import { getTenants, type Tenant } from '@/api/modules/tenant'
|
||||
|
||||
const loading = ref(false)
|
||||
const logs = ref<AuditLog[]>([])
|
||||
const total = ref(0)
|
||||
const users = ref<SystemUser[]>([])
|
||||
const tenants = ref<Tenant[]>([])
|
||||
const query = reactive<AuditQuery>({
|
||||
tenant_id: '',
|
||||
project_id: '',
|
||||
actor_id: '',
|
||||
action: '',
|
||||
target_type: '',
|
||||
target_id: '',
|
||||
keyword: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
limit: 50,
|
||||
@@ -21,6 +26,87 @@ const query = reactive<AuditQuery>({
|
||||
// 时间范围(el-date-picker 双向绑定数组 [start, end])
|
||||
const timeRange = ref<[string, string] | null>(null)
|
||||
|
||||
const actionOptions = [
|
||||
{ value: 'create_dataset', label: '创建数据集' },
|
||||
{ value: 'update_dataset', label: '修改数据集' },
|
||||
{ value: 'delete_dataset', label: '删除数据集' },
|
||||
{ value: 'create_model', label: '创建模型' },
|
||||
{ value: 'update_model', label: '修改模型' },
|
||||
{ value: 'delete_model', label: '删除模型' },
|
||||
{ value: 'create_fine_tune', label: '创建训练任务' },
|
||||
{ value: 'update_fine_tune', label: '修改训练任务' },
|
||||
{ value: 'delete_fine_tune', label: '删除训练任务' },
|
||||
{ value: 'create_inference', label: '创建推理任务' },
|
||||
{ value: 'update_inference', label: '修改推理任务' },
|
||||
{ value: 'delete_inference', label: '删除推理任务' },
|
||||
{ value: 'create_user', label: '创建用户' },
|
||||
{ value: 'update_user', label: '修改用户' },
|
||||
{ value: 'delete_user', label: '删除用户' },
|
||||
{ value: 'tenant.create', label: '创建租户' },
|
||||
{ value: 'tenant.update', label: '修改租户' },
|
||||
{ value: 'tenant.delete', label: '删除租户' },
|
||||
{ value: 'tenant.quota.set', label: '设置租户配额' },
|
||||
{ value: 'tenant.retention.set', label: '设置留存策略' },
|
||||
{ value: 'grant_acl', label: '授予资源权限' },
|
||||
{ value: 'revoke_acl', label: '撤销资源权限' },
|
||||
{ value: 'gpu.assign', label: '分配算力卡' },
|
||||
{ value: 'gpu.release', label: '释放算力卡' },
|
||||
{ value: 'create', label: '创建' },
|
||||
{ value: 'update', label: '修改' },
|
||||
{ value: 'delete', label: '删除' },
|
||||
{ value: 'start', label: '启动' },
|
||||
{ value: 'stop', label: '停止' },
|
||||
{ value: 'upload', label: '上传' },
|
||||
{ value: 'download', label: '下载' },
|
||||
{ value: 'convert', label: '转换' },
|
||||
{ value: 'merge', label: '合并权重' },
|
||||
{ value: 'publish', label: '发布' },
|
||||
{ value: 'retry', label: '重试' },
|
||||
{ value: 'login', label: '登录' },
|
||||
{ value: 'logout', label: '退出登录' },
|
||||
]
|
||||
|
||||
const targetTypeOptions = [
|
||||
{ value: 'dataset', label: '数据集' },
|
||||
{ value: 'model', label: '模型' },
|
||||
{ value: 'fine_tune_task', label: '训练任务' },
|
||||
{ value: 'fine_tune', label: '训练任务' },
|
||||
{ value: 'inference_task', label: '推理任务' },
|
||||
{ value: 'inference', label: '推理任务' },
|
||||
{ value: 'eval_task', label: '评测任务' },
|
||||
{ value: 'trained_model', label: '训练模型' },
|
||||
{ value: 'convert_task', label: '数据转换任务' },
|
||||
{ value: 'tenant', label: '租户' },
|
||||
{ value: 'user', label: '用户' },
|
||||
{ value: 'resource_acl', label: '资源权限' },
|
||||
{ value: 'gpu', label: '算力卡' },
|
||||
{ value: 'retention_policy', label: '留存策略' },
|
||||
{ value: 'module', label: '业务模块' },
|
||||
{ value: 'api', label: '接口' },
|
||||
]
|
||||
|
||||
const actionLabels = Object.fromEntries(actionOptions.map((item) => [item.value, item.label]))
|
||||
const targetTypeLabels = Object.fromEntries(targetTypeOptions.map((item) => [item.value, item.label]))
|
||||
|
||||
function userName(id?: string) {
|
||||
if (!id) return '系统'
|
||||
const user = users.value.find((item) => item.id === id)
|
||||
return user ? `${user.display_name || user.username}(${user.username})` : id
|
||||
}
|
||||
|
||||
function tenantName(id?: string) {
|
||||
if (!id) return '未关联租户'
|
||||
return tenants.value.find((item) => item.id === id)?.name || id
|
||||
}
|
||||
|
||||
function actionName(action?: string) {
|
||||
return action ? actionLabels[action] || action : '未记录'
|
||||
}
|
||||
|
||||
function targetTypeName(type?: string) {
|
||||
return type ? targetTypeLabels[type] || type : '未指定'
|
||||
}
|
||||
|
||||
function applyTimeRange() {
|
||||
if (timeRange.value && timeRange.value.length === 2) {
|
||||
query.start_time = timeRange.value[0]
|
||||
@@ -42,6 +128,25 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFilterOptions() {
|
||||
const [userResult, tenantResult] = await Promise.allSettled([getUsers(), getTenants()])
|
||||
if (userResult.status === 'fulfilled') users.value = userResult.value
|
||||
if (tenantResult.status === 'fulfilled') tenants.value = tenantResult.value
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
query.tenant_id = ''
|
||||
query.actor_id = ''
|
||||
query.action = ''
|
||||
query.target_type = ''
|
||||
query.target_id = ''
|
||||
query.keyword = ''
|
||||
query.start_time = ''
|
||||
query.end_time = ''
|
||||
timeRange.value = null
|
||||
void load()
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
try {
|
||||
const blob = await exportAuditLogs({ ...query, limit: 10000, offset: 0 })
|
||||
@@ -56,7 +161,9 @@ async function handleExport() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onMounted(() => {
|
||||
void Promise.all([loadFilterOptions(), load()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,21 +173,32 @@ onMounted(load)
|
||||
<el-button @click="handleExport">导出 CSV</el-button>
|
||||
</div>
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true">
|
||||
<el-form :inline="true" class="filter-form">
|
||||
<el-form-item label="租户">
|
||||
<el-input v-model="query.tenant_id" placeholder="tenant_id" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目">
|
||||
<el-input v-model="query.project_id" placeholder="project_id" clearable />
|
||||
<el-select v-model="query.tenant_id" placeholder="全部租户" clearable filterable style="width: 190px">
|
||||
<el-option v-for="tenant in tenants" :key="tenant.id" :label="tenant.name" :value="tenant.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人">
|
||||
<el-input v-model="query.actor_id" placeholder="actor_id" clearable />
|
||||
<el-select v-model="query.actor_id" placeholder="全部用户" clearable filterable style="width: 210px">
|
||||
<el-option v-for="user in users" :key="user.id" :label="`${user.display_name || user.username}(${user.username})`" :value="user.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="动作">
|
||||
<el-input v-model="query.action" placeholder="action" clearable />
|
||||
<el-select v-model="query.action" placeholder="全部动作" clearable filterable style="width: 180px">
|
||||
<el-option v-for="item in actionOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标类型">
|
||||
<el-input v-model="query.target_type" placeholder="target_type" clearable />
|
||||
<el-select v-model="query.target_type" placeholder="全部资源" clearable filterable style="width: 160px">
|
||||
<el-option v-for="item in targetTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="query.keyword" placeholder="资源 ID 或详情" clearable style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标 ID">
|
||||
<el-input v-model="query.target_id" placeholder="精确查询,可选" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
@@ -97,16 +215,24 @@ onMounted(load)
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-table :data="logs" v-loading="loading" border stripe class="log-table">
|
||||
<el-table-column prop="time" label="时间" min-width="180" />
|
||||
<el-table-column prop="tenant_id" label="租户" min-width="120" />
|
||||
<el-table-column prop="project_id" label="项目" min-width="120" />
|
||||
<el-table-column prop="actor_id" label="操作人" min-width="120" />
|
||||
<el-table-column prop="action" label="动作" min-width="140" />
|
||||
<el-table-column prop="target_type" label="目标类型" min-width="120" />
|
||||
<el-table-column label="租户" min-width="140">
|
||||
<template #default="{ row }">{{ tenantName(row.tenant_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" min-width="180">
|
||||
<template #default="{ row }">{{ userName(row.actor_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="动作" min-width="140">
|
||||
<template #default="{ row }">{{ actionName(row.action) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标类型" min-width="120">
|
||||
<template #default="{ row }">{{ targetTypeName(row.target_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_id" label="目标 ID" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="detail" label="详情" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="client_ip" label="IP" min-width="120" />
|
||||
@@ -120,6 +246,7 @@ onMounted(load)
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||
.page-title { margin: 0; font-size: 18px; }
|
||||
.filter-card { margin-bottom: 16px; }
|
||||
.filter-form { display: flex; flex-wrap: wrap; }
|
||||
.log-table { margin-top: 8px; }
|
||||
.pager { margin-top: 12px; text-align: right; color: #909399; }
|
||||
</style>
|
||||
|
||||
68
frontend/src/views/governance/OrganizationPermissionView.vue
Normal file
68
frontend/src/views/governance/OrganizationPermissionView.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import UserSettingsView from '@/views/system/UserSettingsView.vue'
|
||||
import TenantListView from '@/views/tenants/TenantListView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = computed({
|
||||
get: () => route.query.tab === 'tenants' ? 'tenants' : 'users',
|
||||
set: (value: string) => {
|
||||
void router.replace({ query: { tab: value === 'tenants' ? 'tenants' : 'users' } })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="organization-page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h2>组织与权限</h2>
|
||||
<p>统一管理平台用户、角色、租户和资源配额。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="用户与角色" name="users">
|
||||
<UserSettingsView v-if="activeTab === 'users'" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="租户与配额" name="tenants">
|
||||
<TenantListView v-if="activeTab === 'tenants'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.organization-page {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 4px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.user-settings),
|
||||
:deep(.page) {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
:deep(.user-settings .page-header > div:first-child) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
78
frontend/src/views/system/RuntimeLogsView.vue
Normal file
78
frontend/src/views/system/RuntimeLogsView.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import LogsView from './LogsView.vue'
|
||||
import AuditLogView from '@/views/audit/AuditLogView.vue'
|
||||
import OperationLogView from '@/views/audit/OperationLogView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
if (!auth.isAdmin) return 'runtime'
|
||||
if (route.query.tab === 'audit') return 'audit'
|
||||
if (route.query.tab === 'operations') return 'operations'
|
||||
return 'runtime'
|
||||
},
|
||||
set: (value: string) => {
|
||||
void router.replace({ query: value === 'runtime' ? {} : { tab: value } })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="runtime-logs">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h2>运行日志</h2>
|
||||
<p>查看系统运行、训练任务、审计记录和操作诊断信息。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="运行日志" name="runtime">
|
||||
<LogsView v-if="activeTab === 'runtime'" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="auth.isAdmin" label="审计记录" name="audit">
|
||||
<AuditLogView v-if="activeTab === 'audit'" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="auth.isAdmin" label="操作诊断" name="operations">
|
||||
<OperationLogView v-if="activeTab === 'operations'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.runtime-logs {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 4px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.page) {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
:deep(.page-header .page-title) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user