refactor(frontend): improve dialog and mobile overflow handling

This commit is contained in:
ZheFox
2026-07-03 11:26:40 +08:00
parent 1ac16005f9
commit 1dcee77055
6 changed files with 130 additions and 33 deletions
+11 -6
View File
@@ -2,7 +2,7 @@
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 overflow-y-auto pointer-events-none"
class="fixed inset-0 overflow-hidden pointer-events-none"
:style="{ zIndex: containerZIndex }"
>
<!-- 背景遮罩 -->
@@ -22,7 +22,7 @@
/>
</Transition>
<div class="relative flex min-h-full items-end justify-center px-3 py-4 text-center sm:items-center sm:p-0 pointer-events-none">
<div class="relative flex h-full items-end justify-center overflow-hidden px-3 py-4 text-center sm:items-center sm:p-0 pointer-events-none">
<!-- 对话框内容 -->
<Transition
enter-active-class="duration-300 ease-out"
@@ -34,7 +34,7 @@
>
<div
v-if="isOpen"
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all w-full sm:my-8 sm:w-full border border-border pointer-events-auto"
class="relative flex max-h-[calc(100dvh-2rem)] w-full transform flex-col overflow-hidden rounded-lg border border-border bg-background text-left shadow-2xl transition-all pointer-events-auto sm:my-8 sm:w-full sm:max-h-[calc(100dvh-4rem)]"
:style="{ zIndex: contentZIndex }"
:class="maxWidthClass"
@click.stop
@@ -43,7 +43,7 @@
<slot name="header">
<div
v-if="title"
class="border-b border-border px-4 py-4 sm:px-6"
class="shrink-0 border-b border-border px-4 py-4 sm:px-6"
>
<div class="flex items-center gap-3">
<div
@@ -73,14 +73,14 @@
</slot>
<!-- 内容区域可选添加 padding -->
<div :class="noPadding ? '' : 'px-4 py-3 sm:px-6'">
<div :class="contentBodyClass">
<slot />
</div>
<!-- Footer 区域如果有 footer 插槽自动添加样式 -->
<div
v-if="slots.footer"
class="border-t border-border bg-muted/10 px-4 py-4 sm:px-6 flex flex-row-reverse gap-3"
class="shrink-0 border-t border-border bg-muted/10 px-4 py-4 sm:px-6 flex flex-row-reverse gap-3"
>
<slot name="footer" />
</div>
@@ -168,6 +168,11 @@ const maxWidthClass = computed(() => {
return sizes[sizeValue]
})
const contentBodyClass = computed(() => [
'min-h-0 overflow-y-auto overscroll-contain',
props.noPadding ? '' : 'px-4 py-3 sm:px-6',
].filter(Boolean).join(' '))
// Z-index computed values for nested dialogs support
const containerZIndex = computed(() => props.zIndex || 60)
const backdropZIndex = computed(() => props.zIndex || 60)
@@ -181,6 +181,7 @@
v-if="getCodexQuotaDisplay(key)?.primary_used_percent !== undefined"
:label="legacyT('周限额')"
:used-percent="getCodexQuotaDisplay(key)?.primary_used_percent || 0"
:remaining-percent="toCodexRemainingPercent(getCodexQuotaDisplay(key)?.primary_used_percent)"
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
:reset-text="(getCodexQuotaDisplay(key)?.primary_reset_at || getCodexQuotaDisplay(key)?.primary_reset_seconds) && shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.primary_used_percent || 0)
@@ -203,6 +204,7 @@
v-if="isCodexTeamPlan(key) && getCodexQuotaDisplay(key)?.secondary_used_percent !== undefined"
:label="legacyT('5H限额')"
:used-percent="getCodexQuotaDisplay(key)?.secondary_used_percent || 0"
:remaining-percent="toCodexRemainingPercent(getCodexQuotaDisplay(key)?.secondary_used_percent)"
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)
@@ -234,6 +236,7 @@
v-if="getCodexQuotaDisplay(key)?.spark_secondary_used_percent !== undefined"
:label="legacyT('Spark 周')"
:used-percent="getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0"
:remaining-percent="toCodexRemainingPercent(getCodexQuotaDisplay(key)?.spark_secondary_used_percent)"
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)
@@ -255,6 +258,7 @@
v-if="getCodexQuotaDisplay(key)?.spark_primary_used_percent !== undefined"
:label="legacyT('Spark 5H')"
:used-percent="getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0"
:remaining-percent="toCodexRemainingPercent(getCodexQuotaDisplay(key)?.spark_primary_used_percent)"
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)
@@ -1717,8 +1721,54 @@ function getQuotaWindowLiveResetSeconds(
return null
}
function getCodexQuotaDisplay(key: EndpointAPIKey): CodexUpstreamMetadata | null {
const quota = getQuotaSnapshotForProvider(key, 'codex')
function copyCodexNumberField(
target: CodexUpstreamMetadata,
source: CodexUpstreamMetadata,
field: keyof CodexUpstreamMetadata,
) {
const value = source[field]
if (typeof value === 'number' && Number.isFinite(value)) {
;(target[field] as number | undefined) = value
}
}
function getCodexQuotaDisplayFromMetadata(metadata: CodexUpstreamMetadata | null | undefined): CodexUpstreamMetadata | null {
if (!metadata) return null
const display: CodexUpstreamMetadata = {}
if (metadata.plan_type) display.plan_type = metadata.plan_type
const numberFields: (keyof CodexUpstreamMetadata)[] = [
'updated_at',
'primary_used_percent',
'primary_reset_seconds',
'primary_reset_after_seconds',
'primary_reset_at',
'primary_window_minutes',
'secondary_used_percent',
'secondary_reset_seconds',
'secondary_reset_after_seconds',
'secondary_reset_at',
'secondary_window_minutes',
'spark_primary_used_percent',
'spark_primary_reset_seconds',
'spark_primary_reset_after_seconds',
'spark_primary_reset_at',
'spark_primary_window_minutes',
'spark_secondary_used_percent',
'spark_secondary_reset_seconds',
'spark_secondary_reset_after_seconds',
'spark_secondary_reset_at',
'spark_secondary_window_minutes',
'credits_balance',
]
numberFields.forEach(field => copyCodexNumberField(display, metadata, field))
if (metadata.has_credits !== undefined) display.has_credits = metadata.has_credits
return Object.keys(display).length > 0 ? display : null
}
function getCodexQuotaDisplayFromSnapshot(quota: QuotaStatusSnapshot | null | undefined): CodexUpstreamMetadata | null {
if (!quota) return null
const display: CodexUpstreamMetadata = {}
@@ -1773,6 +1823,39 @@ function getCodexQuotaDisplay(key: EndpointAPIKey): CodexUpstreamMetadata | null
return Object.keys(display).length > 0 ? display : null
}
function getCodexQuotaDisplayUpdatedAt(display: CodexUpstreamMetadata | null | undefined): number | null {
const updatedAt = Number(display?.updated_at)
return Number.isFinite(updatedAt) ? updatedAt : null
}
function codexDisplayHasUsage(display: CodexUpstreamMetadata | null | undefined): boolean {
return !!display && (
display.primary_used_percent !== undefined
|| display.secondary_used_percent !== undefined
|| display.spark_primary_used_percent !== undefined
|| display.spark_secondary_used_percent !== undefined
)
}
function getCodexQuotaDisplay(key: EndpointAPIKey): CodexUpstreamMetadata | null {
const snapshotDisplay = getCodexQuotaDisplayFromSnapshot(getQuotaSnapshotForProvider(key, 'codex'))
const metadataDisplay = getCodexQuotaDisplayFromMetadata(key.upstream_metadata?.codex)
if (!snapshotDisplay) return metadataDisplay
if (!metadataDisplay) return snapshotDisplay
if (!codexDisplayHasUsage(snapshotDisplay) && codexDisplayHasUsage(metadataDisplay)) {
return metadataDisplay
}
const snapshotUpdatedAt = getCodexQuotaDisplayUpdatedAt(snapshotDisplay)
const metadataUpdatedAt = getCodexQuotaDisplayUpdatedAt(metadataDisplay)
if (metadataUpdatedAt !== null && (snapshotUpdatedAt === null || metadataUpdatedAt > snapshotUpdatedAt)) {
return metadataDisplay
}
return snapshotDisplay
}
function hasCodexQuotaDisplayData(key: EndpointAPIKey): boolean {
const codex = getCodexQuotaDisplay(key)
return !!codex && (
+3 -3
View File
@@ -160,14 +160,14 @@
<Transition
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-[500px]"
enter-to-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
leave-from-class="opacity-100 max-h-[500px]"
leave-from-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="mobileMenuOpen"
class="border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
class="max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
>
<div class="mx-auto max-w-7xl px-6 py-4">
<!-- Navigation Groups -->
@@ -1,16 +1,16 @@
<template>
<div class="hidden xl:block overflow-x-auto">
<Table>
<Table class="min-w-[1040px] table-fixed">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="w-[28px] min-w-[28px] max-w-[28px] h-12 p-0 pl-2" />
<TableHead class="w-[160px] h-12 font-semibold">
<TableHead class="w-[150px] h-12 font-semibold">
{{ legacyT('名称') }}
</TableHead>
<TableHead class="w-[180px] h-12 font-semibold">
<TableHead class="w-[190px] h-12 font-semibold">
{{ legacyT('地址') }}
</TableHead>
<TableHead class="w-[100px] h-12 font-semibold">
<TableHead class="w-[90px] h-12 font-semibold">
{{ legacyT('区域') }}
</TableHead>
<SortableTableHead
@@ -32,22 +32,22 @@
/>
</template>
</SortableTableHead>
<TableHead class="w-[100px] h-12 font-semibold text-center">
<TableHead class="w-[86px] h-12 font-semibold text-center">
{{ legacyT('总请求') }}
</TableHead>
<TableHead class="w-[100px] h-12 font-semibold text-center">
<TableHead class="w-[86px] h-12 font-semibold text-center">
{{ legacyT('失败率') }}
</TableHead>
<TableHead class="w-[100px] h-12 font-semibold text-center">
<TableHead class="w-[86px] h-12 font-semibold text-center">
{{ legacyT('延迟') }}
</TableHead>
<TableHead class="w-[120px] h-12 font-semibold text-center">
<TableHead class="w-[90px] h-12 font-semibold text-center">
{{ legacyT('版本') }}
</TableHead>
<TableHead class="w-[160px] h-12 font-semibold">
<TableHead class="w-[130px] h-12 font-semibold">
{{ legacyT('最后心跳') }}
</TableHead>
<TableHead class="w-[140px] h-12 font-semibold text-center">
<TableHead class="w-[120px] h-12 font-semibold text-center">
{{ legacyT('操作') }}
</TableHead>
</TableRow>
@@ -21,9 +21,12 @@
/>
</button>
</TableCell>
<TableCell class="py-4">
<div class="flex items-center gap-1.5">
<span class="text-sm font-semibold">{{ node.name }}</span>
<TableCell class="py-4 min-w-0">
<div class="flex items-center gap-1.5 min-w-0">
<span
class="min-w-0 truncate text-sm font-semibold"
:title="node.name"
>{{ node.name }}</span>
<Badge
v-if="node.is_manual"
variant="outline"
@@ -48,11 +51,17 @@
<HardwareTooltip :node="node" />
</div>
</TableCell>
<TableCell class="py-4">
<code class="text-xs text-muted-foreground">{{ proxyNodeAddress(node) }}</code>
<TableCell class="py-4 min-w-0">
<code
class="block min-w-0 truncate text-xs text-muted-foreground"
:title="proxyNodeAddress(node)"
>{{ proxyNodeAddress(node) }}</code>
</TableCell>
<TableCell class="py-4">
<span class="text-sm text-muted-foreground">{{ legacyT(formatProxyNodeRegion(node.region)) }}</span>
<TableCell class="py-4 min-w-0">
<span
class="block min-w-0 truncate text-sm text-muted-foreground"
:title="legacyT(formatProxyNodeRegion(node.region))"
>{{ legacyT(formatProxyNodeRegion(node.region)) }}</span>
</TableCell>
<TableCell class="py-4 text-center">
<Badge
@@ -78,8 +87,8 @@
<TableCell class="py-4 text-center">
<span class="text-sm tabular-nums">{{ node.is_manual ? '-' : proxyNodeVersion(node) }}</span>
</TableCell>
<TableCell class="py-4">
<span class="text-xs text-muted-foreground">{{ formatProxyNodeTime(node.last_heartbeat_at, locale) }}</span>
<TableCell class="py-4 min-w-0">
<span class="block min-w-0 truncate text-xs text-muted-foreground">{{ formatProxyNodeTime(node.last_heartbeat_at, locale) }}</span>
</TableCell>
<TableCell class="py-4 text-center">
<div class="flex items-center justify-center gap-0.5">
@@ -160,14 +160,14 @@
<Transition
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-[500px]"
enter-to-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
leave-from-class="opacity-100 max-h-[500px]"
leave-from-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="mobileMenuOpen"
class="border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
class="max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
>
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="space-y-4">