feat(ops): 统一弹窗组件并优化分页设置

- OpsErrorDetailsModal和OpsRequestDetailsModal改用BaseDialog统一弹窗组件
- 分页默认值从50改为20条,减少单页数据量
- OpsAlertEventsCard表格添加sticky表头,优化滚动体验
- 移除自定义Teleport和Transition实现,使用统一组件
This commit is contained in:
IanShaw027
2026-01-11 20:41:39 +08:00
parent dd59e872ff
commit 1b938b2003
3 changed files with 196 additions and 274 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { onKeyStroke } from '@vueuse/core'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Pagination from '@/components/common/Pagination.vue'
import { useClipboard } from '@/composables/useClipboard'
import { useAppStore } from '@/stores'
@@ -38,18 +38,10 @@ const loading = ref(false)
const items = ref<OpsRequestDetail[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(50)
const pageSize = ref(20)
const close = () => emit('update:modelValue', false)
onKeyStroke('Escape', () => {
if (props.modelValue) close()
})
const handleOverlayClick = (e: MouseEvent) => {
if (e.target === e.currentTarget) close()
}
const rangeLabel = computed(() => {
const minutes = parseTimeRangeMinutes(props.timeRange)
if (minutes >= 60) return t('admin.ops.requestDetails.rangeHours', { n: Math.round(minutes / 60) })
@@ -156,154 +148,135 @@ const kindBadgeClass = (kind: string) => {
</script>
<template>
<Teleport to="body">
<div v-if="modelValue" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" @click="handleOverlayClick">
<div class="relative max-h-[90vh] w-full max-w-5xl overflow-y-auto rounded-2xl bg-white shadow-2xl dark:bg-dark-800" @click.stop>
<!-- Header -->
<div class="sticky top-0 z-10 flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4 dark:border-dark-700 dark:bg-dark-800">
<div>
<div class="text-sm font-bold text-gray-900 dark:text-white">
{{ props.preset.title || t('admin.ops.requestDetails.title') }}
</div>
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.rangeLabel', { range: rangeLabel }) }}
</div>
</div>
<div class="flex items-center gap-3">
<button
type="button"
class="rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-bold text-gray-600 hover:bg-gray-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
@click="fetchData"
>
{{ t('common.refresh') }}
</button>
<button
type="button"
class="rounded-lg bg-gray-100 p-2 text-gray-600 hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-200 dark:hover:bg-dark-600"
@click="close"
:title="t('common.close')"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<BaseDialog :show="modelValue" :title="props.preset.title || t('admin.ops.requestDetails.title')" width="full" @close="close">
<template #default>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-4">
{{ t('admin.ops.requestDetails.rangeLabel', { range: rangeLabel }) }}
</div>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-16">
<div class="flex flex-col items-center gap-3">
<svg class="h-8 w-8 animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('common.loading') }}</span>
</div>
</div>
<div class="flex justify-end mb-4">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="fetchData"
>
{{ t('common.refresh') }}
</button>
</div>
<!-- Table -->
<div v-else class="p-6">
<div v-if="items.length === 0" class="rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700">
<div class="text-sm font-medium text-gray-600 dark:text-gray-300">{{ t('admin.ops.requestDetails.empty') }}</div>
<div class="mt-1 text-xs text-gray-400">{{ t('admin.ops.requestDetails.emptyHint') }}</div>
</div>
<div v-else class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
<thead class="bg-gray-50 dark:bg-dark-900">
<tr>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.time') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.kind') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.platform') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.model') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.duration') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.status') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.requestId') }}
</th>
<th class="px-4 py-3 text-right text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.actions') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-800">
<tr v-for="(row, idx) in items" :key="idx" class="hover:bg-gray-50 dark:hover:bg-dark-700/50">
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ formatDateTime(row.created_at) }}
</td>
<td class="whitespace-nowrap px-4 py-3">
<span class="rounded-full px-2 py-1 text-[10px] font-bold" :class="kindBadgeClass(row.kind)">
{{ row.kind === 'error' ? t('admin.ops.requestDetails.kind.error') : t('admin.ops.requestDetails.kind.success') }}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-200">
{{ (row.platform || 'unknown').toUpperCase() }}
</td>
<td class="max-w-[240px] truncate px-4 py-3 text-xs text-gray-600 dark:text-gray-300" :title="row.model || ''">
{{ row.model || '-' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ typeof row.duration_ms === 'number' ? `${row.duration_ms} ms` : '-' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ row.status_code ?? '-' }}
</td>
<td class="px-4 py-3">
<div v-if="row.request_id" class="flex items-center gap-2">
<span class="max-w-[220px] truncate font-mono text-[11px] text-gray-700 dark:text-gray-200" :title="row.request_id">
{{ row.request_id }}
</span>
<button
class="rounded-md bg-gray-100 px-2 py-1 text-[10px] font-bold text-gray-600 hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
@click="handleCopyRequestId(row.request_id)"
>
{{ t('admin.ops.requestDetails.copy') }}
</button>
</div>
<span v-else class="text-xs text-gray-400">-</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
<button
v-if="row.kind === 'error' && row.error_id"
class="rounded-lg bg-red-50 px-3 py-1.5 text-xs font-bold text-red-600 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-300 dark:hover:bg-red-900/30"
@click="openErrorDetail(row.error_id)"
>
{{ t('admin.ops.requestDetails.viewError') }}
</button>
<span v-else class="text-xs text-gray-400">-</span>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
:total="total"
:page="page"
:page-size="pageSize"
@update:page="handlePageChange"
@update:pageSize="handlePageSizeChange"
/>
</div>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-16">
<div class="flex flex-col items-center gap-3">
<svg class="h-8 w-8 animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('common.loading') }}</span>
</div>
</div>
</div>
</Teleport>
<!-- Table -->
<div v-else>
<div v-if="items.length === 0" class="rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700">
<div class="text-sm font-medium text-gray-600 dark:text-gray-300">{{ t('admin.ops.requestDetails.empty') }}</div>
<div class="mt-1 text-xs text-gray-400">{{ t('admin.ops.requestDetails.emptyHint') }}</div>
</div>
<div v-else class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
<thead class="bg-gray-50 dark:bg-dark-900">
<tr>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.time') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.kind') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.platform') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.model') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.duration') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.status') }}
</th>
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.requestId') }}
</th>
<th class="px-4 py-3 text-right text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{{ t('admin.ops.requestDetails.table.actions') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-800">
<tr v-for="(row, idx) in items" :key="idx" class="hover:bg-gray-50 dark:hover:bg-dark-700/50">
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ formatDateTime(row.created_at) }}
</td>
<td class="whitespace-nowrap px-4 py-3">
<span class="rounded-full px-2 py-1 text-[10px] font-bold" :class="kindBadgeClass(row.kind)">
{{ row.kind === 'error' ? t('admin.ops.requestDetails.kind.error') : t('admin.ops.requestDetails.kind.success') }}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-200">
{{ (row.platform || 'unknown').toUpperCase() }}
</td>
<td class="max-w-[240px] truncate px-4 py-3 text-xs text-gray-600 dark:text-gray-300" :title="row.model || ''">
{{ row.model || '-' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ typeof row.duration_ms === 'number' ? `${row.duration_ms} ms` : '-' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-xs text-gray-600 dark:text-gray-300">
{{ row.status_code ?? '-' }}
</td>
<td class="px-4 py-3">
<div v-if="row.request_id" class="flex items-center gap-2">
<span class="max-w-[220px] truncate font-mono text-[11px] text-gray-700 dark:text-gray-200" :title="row.request_id">
{{ row.request_id }}
</span>
<button
class="rounded-md bg-gray-100 px-2 py-1 text-[10px] font-bold text-gray-600 hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
@click="handleCopyRequestId(row.request_id)"
>
{{ t('admin.ops.requestDetails.copy') }}
</button>
</div>
<span v-else class="text-xs text-gray-400">-</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
<button
v-if="row.kind === 'error' && row.error_id"
class="rounded-lg bg-red-50 px-3 py-1.5 text-xs font-bold text-red-600 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-300 dark:hover:bg-red-900/30"
@click="openErrorDetail(row.error_id)"
>
{{ t('admin.ops.requestDetails.viewError') }}
</button>
<span v-else class="text-xs text-gray-400">-</span>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
:total="total"
:page="page"
:page-size="pageSize"
@update:page="handlePageChange"
@update:pageSize="handlePageSizeChange"
/>
</div>
</div>
</template>
</BaseDialog>
</template>