feat(frontend): 优化ops监控UI组件
This commit is contained in:
@@ -1,61 +1,48 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div class="flex h-full min-h-0 flex-col bg-white dark:bg-dark-900">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex flex-1 items-center justify-center py-10">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- Table Container -->
|
||||
<div v-else class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
|
||||
<thead class="sticky top-0 z-10 bg-gray-50/50 dark:bg-dark-800/50">
|
||||
<div class="min-h-0 flex-1 overflow-auto border-b border-gray-200 dark:border-dark-700">
|
||||
<table class="w-full border-separate border-spacing-0">
|
||||
<thead class="sticky top-0 z-10 bg-gray-50 dark:bg-dark-800">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
{{ t('admin.ops.errorLog.timeId') }}
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.time') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.type') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
{{ t('admin.ops.errorLog.context') }}
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.platform') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.model') }}
|
||||
</th>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.group') }}
|
||||
</th>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.account') }}
|
||||
</th>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.status') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.message') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
{{ t('admin.ops.errorLog.latency') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<th class="border-b border-gray-200 px-4 py-2.5 text-right text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:border-dark-700 dark:text-dark-400">
|
||||
{{ t('admin.ops.errorLog.action') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-dark-700">
|
||||
<tr v-if="rows.length === 0" class="bg-white dark:bg-dark-900">
|
||||
<td colspan="7" class="py-16 text-center text-sm text-gray-400 dark:text-dark-500">
|
||||
<tr v-if="rows.length === 0">
|
||||
<td colspan="9" class="py-12 text-center text-sm text-gray-400 dark:text-dark-500">
|
||||
{{ t('admin.ops.errorLog.noErrors') }}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -63,83 +50,73 @@
|
||||
<tr
|
||||
v-for="log in rows"
|
||||
:key="log.id"
|
||||
class="group cursor-pointer transition-all duration-200 hover:bg-gray-50/80 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:hover:bg-dark-800/50 dark:focus:ring-offset-dark-900"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="group cursor-pointer transition-colors hover:bg-gray-50/80 dark:hover:bg-dark-800/50"
|
||||
@click="emit('openErrorDetail', log.id)"
|
||||
@keydown.enter.prevent="emit('openErrorDetail', log.id)"
|
||||
@keydown.space.prevent="emit('openErrorDetail', log.id)"
|
||||
>
|
||||
<!-- Time & ID -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-mono text-xs font-bold text-gray-900 dark:text-gray-200">
|
||||
<!-- Time -->
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<el-tooltip :content="log.request_id || log.client_request_id" placement="top" :show-after="500">
|
||||
<span class="font-mono text-xs font-medium text-gray-900 dark:text-gray-200">
|
||||
{{ formatDateTime(log.created_at).split(' ')[1] }}
|
||||
</span>
|
||||
<span
|
||||
class="font-mono text-[10px] text-gray-400 transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400"
|
||||
:title="log.request_id || log.client_request_id"
|
||||
>
|
||||
{{ (log.request_id || log.client_request_id || '').substring(0, 12) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</td>
|
||||
|
||||
<!-- Type -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
|
||||
getTypeBadge(log).className
|
||||
]"
|
||||
>
|
||||
{{ getTypeBadge(log).label }}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
||||
<div v-if="(log as any).error_owner" class="flex items-center gap-1">
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300"></span>
|
||||
<span class="text-[9px] font-black uppercase tracking-tighter text-gray-400">{{ (log as any).error_owner }}</span>
|
||||
</div>
|
||||
<div v-if="(log as any).error_source" class="flex items-center gap-1">
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300"></span>
|
||||
<span class="text-[9px] font-black uppercase tracking-tighter text-gray-400">{{ (log as any).error_source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold ring-1 ring-inset',
|
||||
getTypeBadge(log).className
|
||||
]"
|
||||
>
|
||||
{{ getTypeBadge(log).label }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Context (Platform/Model) -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-col items-start gap-1.5">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-tight text-gray-600 dark:bg-dark-700 dark:text-gray-300"
|
||||
>
|
||||
{{ log.platform || '-' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="log.model"
|
||||
class="max-w-[160px] truncate font-mono text-[10px] text-gray-500 dark:text-dark-400"
|
||||
:title="log.model"
|
||||
>
|
||||
<!-- Platform -->
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<span class="inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-bold uppercase text-gray-600 dark:bg-dark-700 dark:text-gray-300">
|
||||
{{ log.platform || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Model -->
|
||||
<td class="px-4 py-2">
|
||||
<div class="max-w-[120px] truncate" :title="log.model">
|
||||
<span v-if="log.model" class="font-mono text-[11px] text-gray-700 dark:text-gray-300">
|
||||
{{ log.model }}
|
||||
</span>
|
||||
<div
|
||||
v-if="log.group_id || log.account_id"
|
||||
class="flex flex-wrap items-center gap-2 font-mono text-[10px] font-semibold text-gray-400 dark:text-dark-500"
|
||||
>
|
||||
<span v-if="log.group_id">{{ t('admin.ops.errorLog.grp') }} {{ log.group_id }}</span>
|
||||
<span v-if="log.account_id">{{ t('admin.ops.errorLog.acc') }} {{ log.account_id }}</span>
|
||||
</div>
|
||||
<span v-else class="text-xs text-gray-400">-</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Status & Severity -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Group -->
|
||||
<td class="px-4 py-2">
|
||||
<el-tooltip v-if="log.group_id" :content="'ID: ' + log.group_id" placement="top" :show-after="500">
|
||||
<span class="max-w-[100px] truncate text-xs font-medium text-gray-900 dark:text-gray-200">
|
||||
{{ log.group_name || '-' }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="text-xs text-gray-400">-</span>
|
||||
</td>
|
||||
|
||||
<!-- Account -->
|
||||
<td class="px-4 py-2">
|
||||
<el-tooltip v-if="log.account_id" :content="'ID: ' + log.account_id" placement="top" :show-after="500">
|
||||
<span class="max-w-[100px] truncate text-xs font-medium text-gray-900 dark:text-gray-200">
|
||||
{{ log.account_name || '-' }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="text-xs text-gray-400">-</span>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td class="whitespace-nowrap px-4 py-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
|
||||
'inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold ring-1 ring-inset',
|
||||
getStatusClass(log.status_code)
|
||||
]"
|
||||
>
|
||||
@@ -147,44 +124,25 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="log.severity"
|
||||
:class="['rounded-md px-2 py-0.5 text-[10px] font-black shadow-sm', getSeverityClass(log.severity)]"
|
||||
:class="['rounded px-1.5 py-0.5 text-[10px] font-bold', getSeverityClass(log.severity)]"
|
||||
>
|
||||
{{ log.severity }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Message -->
|
||||
<td class="px-6 py-4">
|
||||
<div class="max-w-md lg:max-w-2xl">
|
||||
<p class="truncate text-xs font-semibold text-gray-700 dark:text-gray-300" :title="log.message">
|
||||
<!-- Message (Response Content) -->
|
||||
<td class="px-4 py-2">
|
||||
<div class="max-w-[200px]">
|
||||
<p class="truncate text-[11px] font-medium text-gray-600 dark:text-gray-400" :title="log.message">
|
||||
{{ formatSmartMessage(log.message) || '-' }}
|
||||
</p>
|
||||
<div class="mt-1.5 flex flex-wrap gap-x-3 gap-y-1">
|
||||
<div v-if="log.phase" class="flex items-center gap-1">
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300"></span>
|
||||
<span class="text-[9px] font-black uppercase tracking-tighter text-gray-400">{{ log.phase }}</span>
|
||||
</div>
|
||||
<div v-if="log.client_ip" class="flex items-center gap-1">
|
||||
<span class="h-1 w-1 rounded-full bg-gray-300"></span>
|
||||
<span class="text-[9px] font-mono font-bold text-gray-400">{{ log.client_ip }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Latency -->
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="font-mono text-xs font-black" :class="getLatencyClass(log.latency_ms ?? null)">
|
||||
{{ log.latency_ms != null ? Math.round(log.latency_ms) + 'ms' : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-6 py-4 text-right" @click.stop>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="emit('openErrorDetail', log.id)">
|
||||
<td class="whitespace-nowrap px-4 py-2 text-right" @click.stop>
|
||||
<button type="button" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 text-xs font-bold" @click="emit('openErrorDetail', log.id)">
|
||||
{{ t('admin.ops.errorLog.details') }}
|
||||
</button>
|
||||
</td>
|
||||
@@ -193,15 +151,18 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="[10, 20, 50, 100, 200, 500]"
|
||||
@update:page="emit('update:page', $event)"
|
||||
@update:pageSize="emit('update:pageSize', $event)"
|
||||
/>
|
||||
<!-- Pagination -->
|
||||
<div class="bg-gray-50/50 dark:bg-dark-800/50">
|
||||
<Pagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="[10, 20, 50, 100, 200, 500]"
|
||||
@update:page="emit('update:page', $event)"
|
||||
@update:pageSize="emit('update:pageSize', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -212,39 +173,32 @@ import Pagination from '@/components/common/Pagination.vue'
|
||||
import type { OpsErrorLog } from '@/api/admin/ops'
|
||||
import { getSeverityClass, formatDateTime } from '../utils/opsFormatters'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function getTypeBadge(log: OpsErrorLog): { label: string; className: string } {
|
||||
const phase = String(log.phase || '').toLowerCase()
|
||||
const owner = String((log as any).error_owner || '').toLowerCase()
|
||||
const owner = String(log.error_owner || '').toLowerCase()
|
||||
|
||||
// Mapping aligned with the design:
|
||||
// - upstream/provider => 🔴 上游
|
||||
// - request/client => 🟡 请求
|
||||
// - auth/client => 🔵 认证
|
||||
// - routing/platform => 🟣 路由
|
||||
// - internal/platform => ⚫ 内部
|
||||
if (phase === 'upstream' && owner === 'provider') {
|
||||
return { label: '🔴 上游', className: 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/30' }
|
||||
return { label: t('admin.ops.errorLog.typeUpstream'), className: 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/30' }
|
||||
}
|
||||
if (phase === 'request' && owner === 'client') {
|
||||
return { label: '🟡 请求', className: 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-500/30' }
|
||||
return { label: t('admin.ops.errorLog.typeRequest'), className: 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-500/30' }
|
||||
}
|
||||
if (phase === 'auth' && owner === 'client') {
|
||||
return { label: '🔵 认证', className: 'bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-500/30' }
|
||||
return { label: t('admin.ops.errorLog.typeAuth'), className: 'bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-500/30' }
|
||||
}
|
||||
if (phase === 'routing' && owner === 'platform') {
|
||||
return { label: '🟣 路由', className: 'bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-400 dark:ring-purple-500/30' }
|
||||
return { label: t('admin.ops.errorLog.typeRouting'), className: 'bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-400 dark:ring-purple-500/30' }
|
||||
}
|
||||
if (phase === 'internal' && owner === 'platform') {
|
||||
return { label: '⚫ 内部', className: 'bg-gray-100 text-gray-800 ring-gray-600/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500/40' }
|
||||
return { label: t('admin.ops.errorLog.typeInternal'), className: 'bg-gray-100 text-gray-800 ring-gray-600/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500/40' }
|
||||
}
|
||||
|
||||
// Fallback: show phase/owner for unknown combos.
|
||||
const fallback = phase || owner || 'unknown'
|
||||
return { label: fallback, className: 'bg-gray-50 text-gray-700 ring-gray-600/10 dark:bg-dark-900 dark:text-gray-300 dark:ring-dark-700' }
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
rows: OpsErrorLog[]
|
||||
total: number
|
||||
@@ -269,14 +223,6 @@ function getStatusClass(code: number): string {
|
||||
return 'bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-gray-900/30 dark:text-gray-400 dark:ring-gray-500/30'
|
||||
}
|
||||
|
||||
function getLatencyClass(latency: number | null): string {
|
||||
if (!latency) return 'text-gray-400'
|
||||
if (latency > 10000) return 'text-red-600 font-black'
|
||||
if (latency > 5000) return 'text-red-500 font-bold'
|
||||
if (latency > 2000) return 'text-orange-500 font-medium'
|
||||
return 'text-gray-600 dark:text-gray-400'
|
||||
}
|
||||
|
||||
function formatSmartMessage(msg: string): string {
|
||||
if (!msg) return ''
|
||||
|
||||
@@ -298,4 +244,4 @@ function formatSmartMessage(msg: string): string {
|
||||
|
||||
return msg.length > 200 ? msg.substring(0, 200) + '...' : msg
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
Reference in New Issue
Block a user