修改普通用户的数据类型转换在数据集管理看不见的问题

This commit is contained in:
wangjiming
2026-08-18 14:49:12 +08:00
parent 4e27b98a84
commit 04c3c1412c
12 changed files with 1221 additions and 37 deletions

View File

@@ -0,0 +1,58 @@
import { get } from '../request'
export interface OperationLog {
id: string
user_id?: string
username?: string
module?: string
action?: string
target_type?: string
target_id?: string
target_name?: string
status: string
error_message?: string
error_type?: string
error_traceback?: string
func_name?: string
detail?: string
client_ip?: string
request_method?: string
request_path?: string
trace_id?: string
duration_ms?: number
create_time?: string
}
export interface OperationLogQuery {
user_id?: string
module?: string
action?: string
status?: string
keyword?: string
start_time?: string
end_time?: string
limit?: number
offset?: number
}
export interface OperationLogStats {
total: number
success: number
failure: number
failure_rate: number
module_failures: { module: string; count: number }[]
error_types: { type: string; count: number }[]
recent_errors: OperationLog[]
}
/** 操作日志查询 */
export const getOperationLogs = (query: OperationLogQuery = {}) =>
get<{ items: OperationLog[]; total: number }>('/system/operation-logs', query)
/** 操作日志统计 */
export const getOperationLogStats = (params?: { start_time?: string; end_time?: string }) =>
get<OperationLogStats>('/system/operation-logs/stats', params)
/** 获取操作日志中出现的模块列表 */
export const getOperationLogModules = () =>
get<{ value: string; label: string }[]>('/system/operation-logs/modules')

View File

@@ -81,6 +81,7 @@ const menuGroups: MenuGroup[] = [
{ key: 'projects', label: '项目空间', icon: 'fa-folder', to: '/projects', 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' },
],

View File

@@ -62,6 +62,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/audit/AuditLogView.vue'),
meta: { title: '审计日志', permission: 'user-settings' },
},
{
path: 'operation-logs',
name: 'operation-logs',
component: () => import('@/views/audit/OperationLogView.vue'),
meta: { title: '操作日志', permission: 'user-settings' },
},
{
path: 'approval-templates',
name: 'approval-templates',
@@ -355,6 +361,7 @@ const permissionBySegment: Record<string, PermissionCode> = {
tenants: 'user-settings',
projects: 'user-settings',
'audit-logs': 'user-settings',
'operation-logs': 'user-settings',
'approval-templates': 'user-settings',
'approval-instances': 'user-settings',
'resource-acl': 'user-settings',

View File

@@ -0,0 +1,437 @@
<script setup lang="ts">
import { onMounted, reactive, ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import {
getOperationLogs,
getOperationLogStats,
type OperationLog,
type OperationLogQuery,
type OperationLogStats,
} from '@/api/modules/operation-log'
const loading = ref(false)
const statsLoading = ref(false)
const logs = ref<OperationLog[]>([])
const total = ref(0)
const stats = ref<OperationLogStats | null>(null)
// 默认筛选:只看失败
const query = reactive<OperationLogQuery>({
user_id: '',
module: '',
action: '',
status: 'failure',
keyword: '',
start_time: '',
end_time: '',
limit: 50,
offset: 0,
})
const timeRange = ref<[string, string] | null>(null)
// 模块选项
const moduleOptions = [
{ value: 'fine-tune', label: '模型训练' },
{ value: 'model-eval', label: '模型评测' },
{ value: 'model-inference', label: '模型推理' },
{ value: 'model-manage', label: '模型管理' },
{ value: 'dataset', label: '数据集' },
{ value: 'data-process', label: '数据处理' },
{ value: 'data-convert', label: '数据类型转换' },
{ value: 'compute', label: '算力节点' },
{ value: 'system', label: '系统' },
]
// 动作选项
const actionOptions = [
{ value: 'create', label: '创建' },
{ value: 'start', label: '启动' },
{ value: 'stop', label: '停止' },
{ value: 'delete', label: '删除' },
{ value: 'upload', label: '上传' },
{ value: 'convert', label: '转换' },
{ value: 'merge', label: '合并' },
{ value: 'download', label: '导出' },
{ value: 'import', label: '导入' },
{ value: 'publish', label: '发布' },
{ value: 'request', label: '系统请求' },
]
// 状态选项
const statusOptions = [
{ value: '', label: '全部' },
{ value: 'failure', label: '仅失败' },
{ value: 'success', label: '仅成功' },
]
// 详情弹窗
const detailVisible = ref(false)
const detailLog = ref<OperationLog | null>(null)
// 是否只看失败
const onlyFailures = computed(() => query.status === 'failure')
function applyTimeRange() {
if (timeRange.value && timeRange.value.length === 2) {
query.start_time = timeRange.value[0]
query.end_time = timeRange.value[1]
} else {
query.start_time = ''
query.end_time = ''
}
}
async function load() {
loading.value = true
try {
const params: OperationLogQuery = { ...query }
Object.keys(params).forEach((k) => {
if (params[k as keyof OperationLogQuery] === '' || params[k as keyof OperationLogQuery] === null) {
delete params[k as keyof OperationLogQuery]
}
})
const res = await getOperationLogs(params)
logs.value = res.items
total.value = res.total
} catch {
ElMessage.error('加载操作日志失败')
} finally {
loading.value = false
}
}
async function loadStats() {
statsLoading.value = true
try {
const params: { start_time?: string; end_time?: string } = {}
if (query.start_time) params.start_time = query.start_time
if (query.end_time) params.end_time = query.end_time
stats.value = await getOperationLogStats(params)
} catch {
// 静默失败
} finally {
statsLoading.value = false
}
}
function handleSearch() {
query.offset = 0
load()
loadStats()
}
function handlePageChange(page: number) {
query.offset = (page - 1) * (query.limit || 50)
load()
}
function showDetail(row: OperationLog) {
detailLog.value = row
detailVisible.value = true
}
function statusTagType(status: string) {
return status === 'success' ? 'success' : 'danger'
}
function moduleLabel(module?: string) {
return moduleOptions.find((m) => m.value === module)?.label || module || '-'
}
function actionLabel(action?: string) {
return actionOptions.find((a) => a.value === action)?.label || action || '-'
}
function toggleOnlyFailures() {
query.status = onlyFailures.value ? '' : 'failure'
handleSearch()
}
onMounted(() => {
load()
loadStats()
})
</script>
<template>
<div class="page">
<div class="page-header">
<h2 class="page-title">系统操作日志</h2>
<el-button :type="onlyFailures ? 'danger' : 'default'" @click="toggleOnlyFailures">
{{ onlyFailures ? '只看失败 ' : '显示全部' }}
</el-button>
</div>
<!-- 统计卡片 -->
<el-row :gutter="12" class="stats-row" v-loading="statsLoading">
<el-col :span="4">
<el-card class="stat-card" shadow="hover">
<div class="stat-value">{{ stats?.total ?? '-' }}</div>
<div class="stat-label">总操作数</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card class="stat-card" shadow="hover">
<div class="stat-value" style="color: #67c23a">{{ stats?.success ?? '-' }}</div>
<div class="stat-label">成功</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card class="stat-card" shadow="hover">
<div class="stat-value" style="color: #f56c6c">{{ stats?.failure ?? '-' }}</div>
<div class="stat-label">失败</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card class="stat-card" shadow="hover">
<div class="stat-value" :style="{ color: (stats?.failure_rate ?? 0) > 5 ? '#f56c6c' : '#909399' }">
{{ stats?.failure_rate ?? '-' }}%
</div>
<div class="stat-label">失败率</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="stat-card" shadow="hover">
<div class="stat-label" style="margin-bottom: 4px">各模块失败分布</div>
<div class="stat-modules">
<el-tag
v-for="m in stats?.module_failures || []"
:key="m.module"
type="danger"
size="small"
style="margin-right: 4px; margin-bottom: 4px"
>
{{ moduleOptions.find((opt) => opt.value === m.module)?.label || m.module }}: {{ m.count }}
</el-tag>
<span v-if="!stats?.module_failures?.length" style="color: #c0c4cc">暂无失败</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选区域 -->
<el-card class="filter-card">
<el-form :inline="true">
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部状态" clearable style="width: 120px" @change="handleSearch">
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="模块">
<el-select v-model="query.module" placeholder="全部模块" clearable style="width: 150px" @change="handleSearch">
<el-option v-for="m in moduleOptions" :key="m.value" :label="m.label" :value="m.value" />
</el-select>
</el-form-item>
<el-form-item label="动作">
<el-select v-model="query.action" placeholder="全部动作" clearable style="width: 120px" @change="handleSearch">
<el-option v-for="a in actionOptions" :key="a.value" :label="a.label" :value="a.value" />
</el-select>
</el-form-item>
<el-form-item label="报错关键字">
<el-input
v-model="query.keyword"
placeholder="搜索报错信息/异常类型"
clearable
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="用户">
<el-input v-model="query.user_id" placeholder="用户ID/用户名" clearable style="width: 140px" @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="timeRange"
type="datetimerange"
value-format="YYYY-MM-DDTHH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
clearable
style="width: 360px"
@change="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 日志列表 -->
<el-table
:data="logs"
v-loading="loading"
border
stripe
class="log-table"
:row-class-name="({ row }) => row.status === 'failure' ? 'failure-row' : ''"
>
<el-table-column prop="create_time" label="时间" width="170" align="center">
<template #default="{ row }">
{{ row.create_time ? new Date(row.create_time).toLocaleString('zh-CN') : '-' }}
</template>
</el-table-column>
<el-table-column label="用户" width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.username || row.user_id || '-' }}</template>
</el-table-column>
<el-table-column label="模块" width="110" align="center">
<template #default="{ row }">{{ moduleLabel(row.module) }}</template>
</el-table-column>
<el-table-column label="动作" width="90" align="center">
<template #default="{ row }">{{ actionLabel(row.action) }}</template>
</el-table-column>
<el-table-column label="目标" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.target_name || row.target_id || '-' }}</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small" effect="dark">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常类型" width="160" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag v-if="row.error_type" type="danger" size="small" effect="plain">{{ row.error_type }}</el-tag>
<span v-else style="color: #c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="报错信息" min-width="250" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.error_message" class="error-text">{{ row.error_message }}</span>
<span v-else style="color: #c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="耗时(ms)" width="90" align="center">
<template #default="{ row }">
<span :style="{ color: row.duration_ms > 3000 ? '#e6a23c' : '' }">
{{ row.duration_ms ? row.duration_ms : '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="showDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pager">
<el-pagination
background
layout="total, prev, pager, next"
:total="total"
:page-size="query.limit"
:current-page="Math.floor((query.offset || 0) / (query.limit || 50)) + 1"
@current-change="handlePageChange"
/>
</div>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="操作日志详情" width="850px" top="5vh">
<el-descriptions :column="2" border v-if="detailLog">
<el-descriptions-item label="时间">
{{ detailLog.create_time ? new Date(detailLog.create_time).toLocaleString('zh-CN') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detailLog.status)" size="small" effect="dark">
{{ detailLog.status === 'success' ? '成功' : '失败' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用户">{{ detailLog.username || detailLog.user_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="IP">{{ detailLog.client_ip || '-' }}</el-descriptions-item>
<el-descriptions-item label="模块">{{ moduleLabel(detailLog.module) }}</el-descriptions-item>
<el-descriptions-item label="动作">{{ actionLabel(detailLog.action) }}</el-descriptions-item>
<el-descriptions-item label="目标类型">{{ detailLog.target_type || '-' }}</el-descriptions-item>
<el-descriptions-item label="目标ID">{{ detailLog.target_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="目标名称">{{ detailLog.target_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detailLog.duration_ms ? detailLog.duration_ms + ' ms' : '-' }}</el-descriptions-item>
<el-descriptions-item label="出错函数" :span="2">
<span v-if="detailLog.func_name" class="func-name">{{ detailLog.func_name }}</span>
<span v-else style="color: #c0c4cc">-</span>
</el-descriptions-item>
<el-descriptions-item label="请求方法">{{ detailLog.request_method || '-' }}</el-descriptions-item>
<el-descriptions-item label="请求路径">{{ detailLog.request_path || '-' }}</el-descriptions-item>
<el-descriptions-item label="Trace ID" :span="2">{{ detailLog.trace_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="异常类型" :span="2">
<el-tag v-if="detailLog.error_type" type="danger" size="small" effect="dark">{{ detailLog.error_type }}</el-tag>
<span v-else style="color: #c0c4cc">-</span>
</el-descriptions-item>
<el-descriptions-item label="报错信息" :span="2">
<div v-if="detailLog.error_message" class="error-box">{{ detailLog.error_message }}</div>
<span v-else style="color: #c0c4cc">-</span>
</el-descriptions-item>
<el-descriptions-item label="异常堆栈 (Traceback)" :span="2">
<pre v-if="detailLog.error_traceback" class="traceback-box">{{ detailLog.error_traceback }}</pre>
<span v-else style="color: #c0c4cc">无堆栈信息</span>
</el-descriptions-item>
<el-descriptions-item label="操作详情" :span="2">
<pre v-if="detailLog.detail" class="detail-box">{{ detailLog.detail }}</pre>
<span v-else style="color: #c0c4cc">-</span>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.page { padding: 16px; }
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.page-title { margin: 0; font-size: 18px; }
.stats-row { margin-bottom: 16px; }
.stat-card {
text-align: center;
.stat-value { font-size: 24px; font-weight: bold; }
.stat-label { font-size: 12px; color: #909399; }
.stat-modules { text-align: left; min-height: 40px; }
}
.filter-card { margin-bottom: 16px; }
.log-table { margin-top: 8px; }
.pager { margin-top: 12px; text-align: right; }
.error-text { color: #f56c6c; font-weight: 500; }
.func-name {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #e6a23c;
background: #fdf6ec;
padding: 2px 6px;
border-radius: 3px;
}
.error-box {
color: #f56c6c;
background: #fef0f0;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
word-break: break-all;
}
.traceback-box {
background: #2d2d2d;
color: #f48771;
padding: 12px;
border-radius: 4px;
font-size: 12px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.detail-box {
background: #f5f7fa;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
:deep(.failure-row) {
background-color: #fef0f0 !important;
}
:deep(.failure-row:hover > td) {
background-color: #fde2e2 !important;
}
</style>