mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
refactor(frontend): improve dialog and mobile overflow handling
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user