feat(ops): 统一弹窗组件并优化分页设置
- OpsErrorDetailsModal和OpsRequestDetailsModal改用BaseDialog统一弹窗组件 - 分页默认值从50改为20条,减少单页数据量 - OpsAlertEventsCard表格添加sticky表头,优化滚动体验 - 移除自定义Teleport和Transition实现,使用统一组件
This commit is contained in:
@@ -96,9 +96,9 @@ const empty = computed(() => events.value.length === 0 && !loading.value)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
|
<div v-else class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
|
||||||
<div class="max-h-[600px] overflow-y-auto overflow-x-auto">
|
<div class="max-h-[600px] overflow-y-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
|
||||||
<thead class="bg-gray-50 dark:bg-dark-900">
|
<thead class="sticky top-0 z-10 bg-gray-50 dark:bg-dark-900">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
<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.alertEvents.table.time') }}
|
{{ t('admin.ops.alertEvents.table.time') }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { onKeyStroke } from '@vueuse/core'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import OpsErrorLogTable from './OpsErrorLogTable.vue'
|
import OpsErrorLogTable from './OpsErrorLogTable.vue'
|
||||||
import { opsAPI, type OpsErrorLog } from '@/api/admin/ops'
|
import { opsAPI, type OpsErrorLog } from '@/api/admin/ops'
|
||||||
@@ -26,7 +26,7 @@ const loading = ref(false)
|
|||||||
const rows = ref<OpsErrorLog[]>([])
|
const rows = ref<OpsErrorLog[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(20)
|
||||||
|
|
||||||
const q = ref('')
|
const q = ref('')
|
||||||
const statusCode = ref<number | null>(null)
|
const statusCode = ref<number | null>(null)
|
||||||
@@ -70,10 +70,6 @@ function close() {
|
|||||||
emit('update:show', false)
|
emit('update:show', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyStroke('Escape', () => {
|
|
||||||
if (props.show) close()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchErrorLogs() {
|
async function fetchErrorLogs() {
|
||||||
if (!props.show) return
|
if (!props.show) return
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ watch(
|
|||||||
(open) => {
|
(open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
page.value = 1
|
page.value = 1
|
||||||
pageSize.value = 50
|
pageSize.value = 20
|
||||||
resetFilters()
|
resetFilters()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -177,117 +173,70 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<BaseDialog :show="show" :title="modalTitle" width="full" @close="close">
|
||||||
<Transition
|
<!-- Filters -->
|
||||||
enter-active-class="transition duration-300 ease-out"
|
<div class="border-b border-gray-200 pb-4 mb-4 dark:border-dark-700">
|
||||||
enter-from-class="opacity-0"
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
|
||||||
enter-to-class="opacity-100"
|
<div class="lg:col-span-5">
|
||||||
leave-active-class="transition duration-200 ease-in"
|
<div class="relative group">
|
||||||
leave-from-class="opacity-100"
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
|
||||||
leave-to-class="opacity-0"
|
<svg
|
||||||
>
|
class="h-4 w-4 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||||
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4" @click.self="close">
|
fill="none"
|
||||||
<Transition
|
viewBox="0 0 24 24"
|
||||||
enter-active-class="transition duration-300 ease-out"
|
stroke="currentColor"
|
||||||
enter-from-class="opacity-0 scale-95"
|
|
||||||
enter-to-class="opacity-100 scale-100"
|
|
||||||
leave-active-class="transition duration-200 ease-in"
|
|
||||||
leave-from-class="opacity-100 scale-100"
|
|
||||||
leave-to-class="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<div v-if="show" class="relative flex max-h-[90vh] w-full max-w-7xl flex-col overflow-hidden rounded-3xl bg-white shadow-2xl dark:bg-dark-800">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-5 dark:border-dark-700">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-2xl bg-orange-50 dark:bg-orange-900/20">
|
|
||||||
<svg class="h-6 w-6 text-orange-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-black text-gray-900 dark:text-white">{{ modalTitle }}</h3>
|
|
||||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
{{ t('admin.ops.errorDetails.total') }} {{ total }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700"
|
|
||||||
@click="close"
|
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" 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" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
</svg>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="border-b border-gray-200 px-6 py-4 dark:border-dark-700">
|
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
|
|
||||||
<div class="lg:col-span-5">
|
|
||||||
<div class="relative group">
|
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
|
|
||||||
<svg
|
|
||||||
class="h-4 w-4 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-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-4 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="lg:col-span-2">
|
|
||||||
<Select :model-value="statusCode" :options="statusCodeSelectOptions" class="w-full" @update:model-value="statusCode = $event as any" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<Select :model-value="phase" :options="phaseSelectOptions" class="w-full" @update:model-value="phase = String($event ?? '')" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<input
|
|
||||||
v-model="accountIdInput"
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
class="input w-full text-sm"
|
|
||||||
:placeholder="t('admin.ops.errorDetails.accountIdPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-1 flex items-center justify-end">
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm" @click="resetFilters">
|
|
||||||
{{ t('common.reset') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Body -->
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-6">
|
|
||||||
<OpsErrorLogTable
|
|
||||||
: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>
|
||||||
|
<input
|
||||||
|
v-model="q"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-4 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>
|
||||||
</Transition>
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<Select :model-value="statusCode" :options="statusCodeSelectOptions" class="w-full" @update:model-value="statusCode = $event as any" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<Select :model-value="phase" :options="phaseSelectOptions" class="w-full" @update:model-value="phase = String($event ?? '')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<input
|
||||||
|
v-model="accountIdInput"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
class="input w-full text-sm"
|
||||||
|
:placeholder="t('admin.ops.errorDetails.accountIdPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-1 flex items-center justify-end">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" @click="resetFilters">
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</div>
|
||||||
</Teleport>
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
{{ t('admin.ops.errorDetails.total') }} {{ total }}
|
||||||
|
</div>
|
||||||
|
<OpsErrorLogTable
|
||||||
|
:rows="rows"
|
||||||
|
:total="total"
|
||||||
|
:loading="loading"
|
||||||
|
:page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
|
@openErrorDetail="emit('openErrorDetail', $event)"
|
||||||
|
@update:page="page = $event"
|
||||||
|
@update:pageSize="pageSize = $event"
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 Pagination from '@/components/common/Pagination.vue'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
@@ -38,18 +38,10 @@ const loading = ref(false)
|
|||||||
const items = ref<OpsRequestDetail[]>([])
|
const items = ref<OpsRequestDetail[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(20)
|
||||||
|
|
||||||
const close = () => emit('update:modelValue', false)
|
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 rangeLabel = computed(() => {
|
||||||
const minutes = parseTimeRangeMinutes(props.timeRange)
|
const minutes = parseTimeRangeMinutes(props.timeRange)
|
||||||
if (minutes >= 60) return t('admin.ops.requestDetails.rangeHours', { n: Math.round(minutes / 60) })
|
if (minutes >= 60) return t('admin.ops.requestDetails.rangeHours', { n: Math.round(minutes / 60) })
|
||||||
@@ -156,154 +148,135 @@ const kindBadgeClass = (kind: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<BaseDialog :show="modelValue" :title="props.preset.title || t('admin.ops.requestDetails.title')" width="full" @close="close">
|
||||||
<div v-if="modelValue" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" @click="handleOverlayClick">
|
<template #default>
|
||||||
<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>
|
<div class="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
||||||
<!-- Header -->
|
{{ t('admin.ops.requestDetails.rangeLabel', { range: rangeLabel }) }}
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Loading -->
|
<div class="flex justify-end mb-4">
|
||||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
<button
|
||||||
<div class="flex flex-col items-center gap-3">
|
type="button"
|
||||||
<svg class="h-8 w-8 animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
|
class="btn btn-secondary btn-sm"
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
@click="fetchData"
|
||||||
<path
|
>
|
||||||
class="opacity-75"
|
{{ t('common.refresh') }}
|
||||||
fill="currentColor"
|
</button>
|
||||||
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"
|
</div>
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('common.loading') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table -->
|
<!-- Loading -->
|
||||||
<div v-else class="p-6">
|
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||||
<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="flex flex-col items-center gap-3">
|
||||||
<div class="text-sm font-medium text-gray-600 dark:text-gray-300">{{ t('admin.ops.requestDetails.empty') }}</div>
|
<svg class="h-8 w-8 animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
|
||||||
<div class="mt-1 text-xs text-gray-400">{{ t('admin.ops.requestDetails.emptyHint') }}</div>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
</div>
|
<path
|
||||||
|
class="opacity-75"
|
||||||
<div v-else class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
|
fill="currentColor"
|
||||||
<div class="overflow-x-auto">
|
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"
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-dark-700">
|
></path>
|
||||||
<thead class="bg-gray-50 dark:bg-dark-900">
|
</svg>
|
||||||
<tr>
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('common.loading') }}</span>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user