- 新增 GetErrorLogByID 接口用于获取单个错误日志详情 - 优化 GetErrorLogs 过滤逻辑,简化参数处理 - 简化前端错误详情模态框代码,提升可维护性 - 更新相关 API 接口和 i18n 翻译
271 lines
8.3 KiB
Vue
271 lines
8.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
|
import Select from '@/components/common/Select.vue'
|
|
import OpsErrorLogTable from './OpsErrorLogTable.vue'
|
|
import { opsAPI, type OpsErrorLog } from '@/api/admin/ops'
|
|
|
|
interface Props {
|
|
show: boolean
|
|
timeRange: string
|
|
platform?: string
|
|
groupId?: number | null
|
|
errorType: 'request' | 'upstream'
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<{
|
|
(e: 'update:show', value: boolean): void
|
|
(e: 'openErrorDetail', errorId: number): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
const loading = ref(false)
|
|
const rows = ref<OpsErrorLog[]>([])
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const pageSize = ref(10)
|
|
|
|
const q = ref('')
|
|
const statusCode = ref<number | 'other' | null>(null)
|
|
const phase = ref<string>('')
|
|
const errorOwner = ref<string>('')
|
|
const viewMode = ref<'errors' | 'excluded' | 'all'>('errors')
|
|
|
|
|
|
const modalTitle = computed(() => {
|
|
return props.errorType === 'upstream' ? t('admin.ops.errorDetails.upstreamErrors') : t('admin.ops.errorDetails.requestErrors')
|
|
})
|
|
|
|
const statusCodeSelectOptions = computed(() => {
|
|
const codes = [400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504, 529]
|
|
return [
|
|
{ value: null, label: t('common.all') },
|
|
...codes.map((c) => ({ value: c, label: String(c) })),
|
|
{ value: 'other', label: t('admin.ops.errorDetails.statusCodeOther') || 'Other' }
|
|
]
|
|
})
|
|
|
|
const ownerSelectOptions = computed(() => {
|
|
return [
|
|
{ value: '', label: t('common.all') },
|
|
{ value: 'provider', label: t('admin.ops.errorDetails.owner.provider') || 'provider' },
|
|
{ value: 'client', label: t('admin.ops.errorDetails.owner.client') || 'client' },
|
|
{ value: 'platform', label: t('admin.ops.errorDetails.owner.platform') || 'platform' }
|
|
]
|
|
})
|
|
|
|
|
|
const viewModeSelectOptions = computed(() => {
|
|
return [
|
|
{ value: 'errors', label: t('admin.ops.errorDetails.viewErrors') || 'errors' },
|
|
{ value: 'excluded', label: t('admin.ops.errorDetails.viewExcluded') || 'excluded' },
|
|
{ value: 'all', label: t('common.all') }
|
|
]
|
|
})
|
|
|
|
const phaseSelectOptions = computed(() => {
|
|
const options = [
|
|
{ value: '', label: t('common.all') },
|
|
{ value: 'request', label: t('admin.ops.errorDetails.phase.request') || 'request' },
|
|
{ value: 'auth', label: t('admin.ops.errorDetails.phase.auth') || 'auth' },
|
|
{ value: 'routing', label: t('admin.ops.errorDetails.phase.routing') || 'routing' },
|
|
{ value: 'upstream', label: t('admin.ops.errorDetails.phase.upstream') || 'upstream' },
|
|
{ value: 'network', label: t('admin.ops.errorDetails.phase.network') || 'network' },
|
|
{ value: 'internal', label: t('admin.ops.errorDetails.phase.internal') || 'internal' }
|
|
]
|
|
return options
|
|
})
|
|
|
|
function close() {
|
|
emit('update:show', false)
|
|
}
|
|
|
|
async function fetchErrorLogs() {
|
|
if (!props.show) return
|
|
|
|
loading.value = true
|
|
try {
|
|
const params: Record<string, any> = {
|
|
page: page.value,
|
|
page_size: pageSize.value,
|
|
time_range: props.timeRange,
|
|
view: viewMode.value
|
|
}
|
|
|
|
const platform = String(props.platform || '').trim()
|
|
if (platform) params.platform = platform
|
|
if (typeof props.groupId === 'number' && props.groupId > 0) params.group_id = props.groupId
|
|
|
|
if (q.value.trim()) params.q = q.value.trim()
|
|
if (statusCode.value === 'other') params.status_codes_other = '1'
|
|
else if (typeof statusCode.value === 'number') params.status_codes = String(statusCode.value)
|
|
|
|
const phaseVal = String(phase.value || '').trim()
|
|
if (phaseVal) params.phase = phaseVal
|
|
|
|
const ownerVal = String(errorOwner.value || '').trim()
|
|
if (ownerVal) params.error_owner = ownerVal
|
|
|
|
|
|
const res = props.errorType === 'upstream'
|
|
? await opsAPI.listUpstreamErrors(params)
|
|
: await opsAPI.listRequestErrors(params)
|
|
rows.value = res.items || []
|
|
total.value = res.total || 0
|
|
} catch (err) {
|
|
console.error('[OpsErrorDetailsModal] Failed to fetch error logs', err)
|
|
rows.value = []
|
|
total.value = 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function resetFilters() {
|
|
q.value = ''
|
|
statusCode.value = null
|
|
phase.value = props.errorType === 'upstream' ? 'upstream' : ''
|
|
errorOwner.value = ''
|
|
viewMode.value = 'errors'
|
|
page.value = 1
|
|
fetchErrorLogs()
|
|
}
|
|
|
|
|
|
watch(
|
|
() => props.show,
|
|
(open) => {
|
|
if (!open) return
|
|
page.value = 1
|
|
pageSize.value = 10
|
|
resetFilters()
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => [props.timeRange, props.platform, props.groupId] as const,
|
|
() => {
|
|
if (!props.show) return
|
|
page.value = 1
|
|
fetchErrorLogs()
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => [page.value, pageSize.value] as const,
|
|
() => {
|
|
if (!props.show) return
|
|
fetchErrorLogs()
|
|
}
|
|
)
|
|
|
|
let searchTimeout: number | null = null
|
|
watch(
|
|
() => q.value,
|
|
() => {
|
|
if (!props.show) return
|
|
if (searchTimeout) window.clearTimeout(searchTimeout)
|
|
searchTimeout = window.setTimeout(() => {
|
|
page.value = 1
|
|
fetchErrorLogs()
|
|
}, 350)
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => [statusCode.value, phase.value, errorOwner.value, viewMode.value] as const,
|
|
() => {
|
|
if (!props.show) return
|
|
page.value = 1
|
|
fetchErrorLogs()
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<BaseDialog :show="show" :title="modalTitle" width="full" @close="close">
|
|
<div class="flex h-full min-h-0 flex-col">
|
|
<!-- Filters -->
|
|
<div class="mb-4 flex-shrink-0 border-b border-gray-200 pb-4 dark:border-dark-700">
|
|
<div class="grid grid-cols-8 gap-2">
|
|
<div class="col-span-2 compact-select">
|
|
<div class="relative group">
|
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<svg
|
|
class="h-3.5 w-3.5 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
v-model="q"
|
|
type="text"
|
|
class="w-full rounded-lg border-gray-200 bg-gray-50/50 py-1.5 pl-9 pr-3 text-xs font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/10 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:focus:bg-dark-800"
|
|
:placeholder="t('admin.ops.errorDetails.searchPlaceholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="compact-select">
|
|
<Select :model-value="statusCode" :options="statusCodeSelectOptions" @update:model-value="statusCode = $event as any" />
|
|
</div>
|
|
|
|
<div class="compact-select">
|
|
<Select :model-value="phase" :options="phaseSelectOptions" @update:model-value="phase = String($event ?? '')" />
|
|
</div>
|
|
|
|
<div class="compact-select">
|
|
<Select :model-value="errorOwner" :options="ownerSelectOptions" @update:model-value="errorOwner = String($event ?? '')" />
|
|
</div>
|
|
|
|
|
|
|
|
<div class="compact-select">
|
|
<Select :model-value="viewMode" :options="viewModeSelectOptions" @update:model-value="viewMode = $event as any" />
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end">
|
|
<button type="button" class="rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600" @click="resetFilters">
|
|
{{ t('common.reset') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="flex min-h-0 flex-1 flex-col">
|
|
<div class="mb-2 flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">
|
|
{{ t('admin.ops.errorDetails.total') }} {{ total }}
|
|
</div>
|
|
|
|
<OpsErrorLogTable
|
|
class="min-h-0 flex-1"
|
|
:rows="rows"
|
|
:total="total"
|
|
:loading="loading"
|
|
:page="page"
|
|
:page-size="pageSize"
|
|
@openErrorDetail="emit('openErrorDetail', $event)"
|
|
|
|
@update:page="page = $event"
|
|
@update:pageSize="pageSize = $event"
|
|
/>
|
|
|
|
</div>
|
|
</div>
|
|
</BaseDialog>
|
|
</template>
|
|
|
|
<style>
|
|
.compact-select .select-trigger {
|
|
@apply py-1.5 px-3 text-xs rounded-lg;
|
|
}
|
|
</style>
|