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

@@ -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') }}

View File

@@ -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,53 +173,9 @@ watch(
</script> </script>
<template> <template>
<Teleport to="body"> <BaseDialog :show="show" :title="modalTitle" width="full" @close="close">
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<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">
<Transition
enter-active-class="transition duration-300 ease-out"
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" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Filters --> <!-- Filters -->
<div class="border-b border-gray-200 px-6 py-4 dark:border-dark-700"> <div class="border-b border-gray-200 pb-4 mb-4 dark:border-dark-700">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-12"> <div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
<div class="lg:col-span-5"> <div class="lg:col-span-5">
<div class="relative group"> <div class="relative group">
@@ -273,7 +225,9 @@ watch(
</div> </div>
<!-- Body --> <!-- Body -->
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-6"> <div class="text-xs text-gray-500 dark:text-gray-400 mb-2">
{{ t('admin.ops.errorDetails.total') }} {{ total }}
</div>
<OpsErrorLogTable <OpsErrorLogTable
:rows="rows" :rows="rows"
:total="total" :total="total"
@@ -284,10 +238,5 @@ watch(
@update:page="page = $event" @update:page="page = $event"
@update:pageSize="pageSize = $event" @update:pageSize="pageSize = $event"
/> />
</div> </BaseDialog>
</div>
</Transition>
</div>
</Transition>
</Teleport>
</template> </template>

View File

@@ -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,38 +148,20 @@ 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 -->
<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 }) }} {{ t('admin.ops.requestDetails.rangeLabel', { range: rangeLabel }) }}
</div> </div>
</div>
<div class="flex items-center gap-3"> <div class="flex justify-end mb-4">
<button <button
type="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" class="btn btn-secondary btn-sm"
@click="fetchData" @click="fetchData"
> >
{{ t('common.refresh') }} {{ t('common.refresh') }}
</button> </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> </div>
<!-- Loading --> <!-- Loading -->
@@ -206,7 +180,7 @@ const kindBadgeClass = (kind: string) => {
</div> </div>
<!-- Table --> <!-- Table -->
<div v-else class="p-6"> <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 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="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 class="mt-1 text-xs text-gray-400">{{ t('admin.ops.requestDetails.emptyHint') }}</div>
@@ -303,7 +277,6 @@ const kindBadgeClass = (kind: string) => {
/> />
</div> </div>
</div> </div>
</div> </template>
</div> </BaseDialog>
</Teleport>
</template> </template>