refactor(frontend): improve mobile overflow and proxy node table

This commit is contained in:
ZheFox
2026-07-03 12:25:53 +08:00
parent 1dcee77055
commit 26d85681f0
3 changed files with 203 additions and 32 deletions
+8 -8
View File
@@ -158,18 +158,18 @@
<!-- Mobile Dropdown Menu -->
<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-[calc(100dvh-5rem)]"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
leave-from-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-to-class="opacity-0 max-h-0"
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div
v-if="mobileMenuOpen"
class="max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
class="fixed inset-x-0 bottom-0 top-[73px] overflow-y-scroll overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl [-webkit-overflow-scrolling:touch] touch-pan-y"
>
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="mx-auto max-w-7xl px-6 py-4 pb-28">
<!-- Navigation Groups -->
<div class="space-y-4">
<div
@@ -53,21 +53,177 @@
</TableRow>
</TableHeader>
<TableBody>
<ProxyNodeTableRow
<template
v-for="node in nodes"
:key="node.id"
:node="node"
:expanded="expandedNodeIds.has(node.id)"
:testing="testingNodeIds.has(node.id)"
:detail-state="nodeDetails[node.id]"
@toggle-details="$emit('toggle-details', node)"
@refresh-details="$emit('refresh-details', node)"
@test="$emit('test', node)"
@edit="$emit('edit', node)"
@config="$emit('config', node)"
@view-events="$emit('view-events', node)"
@delete="$emit('delete', node)"
/>
>
<TableRow
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
:class="expandedNodeIds.has(node.id) ? 'bg-muted/20' : ''"
>
<TableCell class="w-[28px] min-w-[28px] max-w-[28px] p-0 pl-2 text-center">
<button
type="button"
class="inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
:title="expandedNodeIds.has(node.id) ? legacyT('收起数据') : legacyT('展开数据')"
@click="$emit('toggle-details', node)"
>
<ChevronDown
v-if="expandedNodeIds.has(node.id)"
class="h-3.5 w-3.5"
/>
<ChevronRight
v-else
class="h-3.5 w-3.5"
/>
</button>
</TableCell>
<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"
class="text-[10px] px-1.5 py-0"
>
{{ legacyT('手动') }}
</Badge>
<Badge
v-if="node.tunnel_mode"
variant="outline"
class="text-[10px] px-1.5 py-0"
>
Tunnel
</Badge>
<Badge
v-if="proxyNodeSchedulingBadge(node)"
:variant="proxyNodeSchedulingBadge(node)?.variant"
class="text-[10px] px-1.5 py-0"
>
{{ legacyT(proxyNodeSchedulingBadge(node)?.label || '') }}
</Badge>
<HardwareTooltip :node="node" />
</div>
</TableCell>
<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 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
:variant="proxyNodeStatusVariant(node.status)"
:title="legacyT(proxyNodeStatusTitle(node))"
class="font-medium px-2.5 py-0.5 text-xs"
>
{{ legacyT(proxyNodeStatusLabel(node)) }}
</Badge>
</TableCell>
<TableCell class="py-4 text-center">
<span class="text-sm tabular-nums">{{ formatProxyNodeNumber(node.total_requests) }}</span>
</TableCell>
<TableCell class="py-4 text-center">
<span
class="text-sm tabular-nums"
:class="proxyNodeFailureRate(node) > 5 ? 'text-destructive font-medium' : ''"
>{{ formatProxyNodeFailureRate(node) }}</span>
</TableCell>
<TableCell class="py-4 text-center">
<span class="text-sm tabular-nums">{{ node.avg_latency_ms != null ? `${node.avg_latency_ms.toFixed(0)}ms` : '-' }}</span>
</TableCell>
<TableCell class="py-4 text-center">
<span class="text-sm tabular-nums">{{ node.is_manual ? '-' : proxyNodeVersion(node) }}</span>
</TableCell>
<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">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:title="testingNodeIds.has(node.id) ? legacyT('测试中...') : legacyT('测试连通性')"
:disabled="testingNodeIds.has(node.id)"
@click="$emit('test', node)"
>
<Loader2
v-if="testingNodeIds.has(node.id)"
class="h-4 w-4 animate-spin"
/>
<Activity
v-else
class="h-4 w-4"
/>
</Button>
<Button
v-if="node.is_manual"
variant="ghost"
size="icon"
class="h-8 w-8"
:title="legacyT('编辑')"
@click="$emit('edit', node)"
>
<SquarePen class="h-4 w-4" />
</Button>
<Button
v-if="!node.is_manual"
variant="ghost"
size="icon"
class="h-8 w-8"
:title="legacyT('远程配置')"
@click="$emit('config', node)"
>
<Settings class="h-4 w-4" />
</Button>
<Button
v-if="!node.is_manual"
variant="ghost"
size="icon"
class="h-8 w-8"
:title="legacyT('连接事件')"
@click="$emit('view-events', node)"
>
<History class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:title="legacyT('删除')"
@click="$emit('delete', node)"
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
<TableRow
v-if="expandedNodeIds.has(node.id)"
class="border-b border-border/40 hover:bg-transparent"
>
<TableCell
colspan="11"
class="p-0"
>
<ProxyNodeDataPanel
:node="node"
:state="nodeDetails[node.id]"
@refresh="$emit('refresh-details', node)"
/>
</TableCell>
</TableRow>
</template>
<TableRow v-if="nodes.length === 0">
<TableCell
colspan="11"
@@ -114,11 +270,26 @@
</template>
<script setup lang="ts">
import { Pagination, SortableTableHead, Table, TableBody, TableCell, TableFilterMenu, TableHead, TableHeader, TableRow } from '@/components/ui'
import { Activity, ChevronDown, ChevronRight, History, Loader2, Settings, SquarePen, Trash2 } from 'lucide-vue-next'
import { Badge, Button, Pagination, SortableTableHead, Table, TableBody, TableCell, TableFilterMenu, TableHead, TableHeader, TableRow } from '@/components/ui'
import type { ProxyNode } from '@/api/proxy-nodes'
import { useI18n } from '@/i18n'
import HardwareTooltip from './HardwareTooltip.vue'
import ProxyNodeMobileCard from './ProxyNodeMobileCard.vue'
import ProxyNodeTableRow from './ProxyNodeTableRow.vue'
import ProxyNodeDataPanel from './ProxyNodeDataPanel.vue'
import {
formatProxyNodeFailureRate,
formatProxyNodeNumber,
formatProxyNodeRegion,
formatProxyNodeTime,
proxyNodeAddress,
proxyNodeFailureRate,
proxyNodeSchedulingBadge,
proxyNodeStatusLabel,
proxyNodeStatusTitle,
proxyNodeStatusVariant,
proxyNodeVersion,
} from './proxy-node-display'
import type { ProxyNodeDetailState, ProxyNodeStatusFilterOption } from './proxy-node-types'
defineProps<{
@@ -147,5 +318,5 @@ defineEmits<{
delete: [node: ProxyNode]
}>()
const { legacyT } = useI18n()
const { legacyT, locale } = useI18n()
</script>
@@ -158,18 +158,18 @@
<!-- Mobile Dropdown Menu -->
<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-[calc(100dvh-5rem)]"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
leave-from-class="opacity-100 max-h-[calc(100dvh-5rem)]"
leave-to-class="opacity-0 max-h-0"
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div
v-if="mobileMenuOpen"
class="max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl"
class="fixed inset-x-0 bottom-0 top-[73px] overflow-y-scroll overscroll-contain border-t border-[var(--shell-border)] bg-[var(--shell-glass)] backdrop-blur-xl [-webkit-overflow-scrolling:touch] touch-pan-y"
>
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="mx-auto max-w-7xl px-6 py-4 pb-28">
<div class="space-y-4">
<div
v-for="group in navigation"