fix(mobile): improve usage and pool management layouts

- Fix pool account batch dialog scrolling on mobile
- Rework usage records mobile filters into clearer rows
- Align user filter styling with other select filters
- Improve request detail drawer metric layout on mobile
This commit is contained in:
AAEE86
2026-05-27 09:20:10 +08:00
parent cf8372c8cb
commit 049f26c03b
5 changed files with 82 additions and 61 deletions
@@ -3,7 +3,7 @@
<Select
v-model="selectedPreset"
>
<SelectTrigger class="h-8 w-32 text-xs border-border/60">
<SelectTrigger :class="['h-8 w-32 text-xs border-border/60', presetTriggerClass]">
<SelectValue placeholder="选择时间段" />
</SelectTrigger>
<SelectContent :searchable="false">
@@ -91,6 +91,7 @@ const props = withDefaults(defineProps<{
showGranularity?: boolean
allowHourly?: boolean
presetOptions?: SelectablePreset[]
presetTriggerClass?: string
}>(), {
presetOptions: () => ['today', 'yesterday', 'last7days', 'last30days', 'last90days', 'custom']
})
@@ -7,7 +7,7 @@
persistent
@update:model-value="emit('update:modelValue', $event)"
>
<div class="space-y-4">
<div class="max-h-[calc(100dvh-13rem)] space-y-4 overflow-y-auto overscroll-contain pr-1 sm:max-h-[min(72vh,44rem)] sm:pr-2">
<div class="space-y-3 rounded-lg border bg-muted/20 px-3 py-2.5">
<div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium text-foreground">快捷多选</span>
@@ -98,7 +98,7 @@
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_19rem]">
<div class="min-w-0 space-y-3">
<div class="max-h-[420px] overflow-y-auto rounded-lg border">
<div class="rounded-lg border lg:max-h-[420px] lg:overflow-y-auto">
<div
v-if="loading"
class="py-10 text-center text-sm text-muted-foreground"
@@ -18,13 +18,13 @@
<!-- 固定头部 - 整合基本信息 -->
<div class="sticky top-0 z-10 bg-background border-b px-3 sm:px-6 py-3 sm:py-4 flex-shrink-0">
<!-- 第一行标题模型状态操作按钮 -->
<div class="flex items-center justify-between gap-4 mb-3">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex items-center justify-between gap-2 sm:gap-4 mb-3">
<div class="flex min-w-0 flex-1 items-center gap-2 sm:gap-3 flex-wrap">
<h3 class="text-lg font-semibold">
请求详情
</h3>
<div class="flex items-center gap-1 text-sm font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">
<span>{{ detail?.model || '-' }}</span>
<div class="flex min-w-0 max-w-[10rem] items-center gap-1 text-sm font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded sm:max-w-none">
<span class="truncate">{{ detail?.model || '-' }}</span>
<template v-if="detail?.target_model && detail.target_model !== detail.model">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -38,7 +38,7 @@
clip-rule="evenodd"
/>
</svg>
<span>{{ detail.target_model }}</span>
<span class="truncate">{{ detail.target_model }}</span>
</template>
</div>
<Badge
@@ -112,20 +112,20 @@
<!-- 第二行关键元信息 -->
<div
v-if="detail"
class="flex items-center flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground"
class="flex items-center flex-wrap gap-x-2 gap-y-1 text-xs text-muted-foreground sm:gap-x-4"
>
<span class="flex items-center gap-1">
<span class="flex min-w-0 items-center gap-1">
<span class="font-medium text-foreground">ID:</span>
<span
class="font-mono"
:title="fullRequestId"
>{{ displayRequestId }}</span>
</span>
<span class="opacity-40">|</span>
<span class="hidden opacity-40 sm:inline">|</span>
<span>{{ formatDateTime(detail.created_at) }}</span>
<span class="opacity-40">|</span>
<span class="hidden opacity-40 sm:inline">|</span>
<span>{{ formatApiFormat(detail.api_format) }}</span>
<span class="opacity-40">|</span>
<span class="hidden opacity-40 sm:inline">|</span>
<span>用户: {{ detail.user?.username || 'Unknown' }}</span>
</div>
</div>
@@ -204,60 +204,55 @@
<Card>
<div class="p-3 sm:p-4">
<!-- 总费用和响应时间独立显示 -->
<div class="flex items-center mb-4">
<div class="flex items-center">
<span class="text-xs text-muted-foreground w-[56px]">总费用</span>
<span class="text-lg font-bold text-green-600 dark:text-green-400">
<div
class="grid gap-1.5 mb-4 sm:gap-2"
:class="detailOutputRate != null ? 'grid-cols-3' : 'grid-cols-2'"
>
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<span class="mb-0.5 block truncate text-[10px] text-muted-foreground sm:text-xs">总费用</span>
<span class="block truncate text-sm font-bold text-green-600 dark:text-green-400 sm:text-lg">
${{ ((typeof detail.cost === 'object' ? detail.cost?.total : detail.cost) || detail.total_cost || 0).toFixed(6) }}
</span>
</div>
<Separator
orientation="vertical"
class="h-6 mx-6"
/>
<div class="flex items-center">
<span class="text-xs text-muted-foreground w-[56px]">响应时间</span>
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
<span class="text-sm text-muted-foreground ml-1">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).unit : '' }}</span>
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<span class="mb-0.5 block truncate text-[10px] text-muted-foreground sm:text-xs">响应时间</span>
<span class="text-sm font-bold sm:text-lg">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
<span class="ml-0.5 text-[10px] text-muted-foreground sm:ml-1 sm:text-sm">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).unit : '' }}</span>
</div>
<template v-if="detailOutputRate != null">
<Separator
orientation="vertical"
class="h-6 mx-6"
/>
<div class="flex items-center">
<span class="text-xs text-muted-foreground w-[56px]">输出速度</span>
<span class="text-lg font-bold text-primary">{{ formatOutputRateValue(detailOutputRate) }}</span>
<span class="text-sm text-muted-foreground ml-1">tps</span>
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<span class="mb-0.5 block truncate text-[10px] text-muted-foreground sm:text-xs">输出速度</span>
<span class="text-sm font-bold text-primary sm:text-lg">{{ formatOutputRateValue(detailOutputRate) }}</span>
<span class="ml-0.5 text-[10px] text-muted-foreground sm:ml-1 sm:text-sm">tps</span>
</div>
</template>
</div>
<div
v-if="hasDetailPerformanceBreakdown"
class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 text-xs"
class="grid grid-cols-3 gap-1.5 mb-4 text-xs sm:gap-2"
>
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
<div class="text-muted-foreground mb-1">
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<div class="mb-1 truncate text-[10px] text-muted-foreground sm:text-xs">
首字时间
</div>
<div class="font-mono text-foreground">
<div class="truncate font-mono text-xs text-foreground sm:text-sm">
{{ formatDurationMs(detail.first_byte_time_ms) }}
</div>
</div>
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
<div class="text-muted-foreground mb-1">
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<div class="mb-1 truncate text-[10px] text-muted-foreground sm:text-xs">
生成耗时
</div>
<div class="font-mono text-foreground">
<div class="truncate font-mono text-xs text-foreground sm:text-sm">
{{ formatDurationMs(detailGenerationTimeMs) }}
</div>
</div>
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
<div class="text-muted-foreground mb-1">
<div class="min-w-0 rounded-md border border-border/50 bg-muted/20 px-2 py-2 sm:px-3">
<div class="mb-1 truncate text-[10px] text-muted-foreground sm:text-xs">
输出 Tokens
</div>
<div class="font-mono text-foreground">
<div class="truncate font-mono text-xs text-foreground sm:text-sm">
{{ formatNumber(detailOutputTokens) }}
</div>
</div>
@@ -6,11 +6,11 @@
<button
v-if="dropdown"
type="button"
class="flex h-8 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-border/60 bg-background px-3 text-left text-xs"
class="flex h-8 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-2xl border border-border/60 bg-card/80 px-4 py-2 text-left text-xs text-foreground shadow-sm backdrop-blur transition-all focus:border-primary/60 focus:outline-none focus:ring-2 focus:ring-primary/40"
@click="toggleOpen"
>
<span class="truncate">{{ selectedLabel }}</span>
<ChevronDown class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<ChevronDown class="h-4 w-4 shrink-0 text-muted-foreground opacity-50" />
</button>
<div
@@ -5,27 +5,51 @@
<TimeRangePicker
v-model="timeRangeModel"
:show-granularity="false"
class="hidden shrink-0 md:flex"
/>
<!-- 分隔线 -->
<div class="hidden sm:block h-4 w-px bg-border" />
<!-- 通用搜索 -->
<div class="relative">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
<Input
id="usage-records-search"
v-model="localSearch"
:placeholder="isAdmin ? '搜索用户/密钥' : '搜索密钥/模型'"
class="w-[7.5rem] sm:w-48 h-8 text-xs border-border/60 pl-8"
/>
<div class="order-1 flex w-full items-center gap-2 md:order-none md:w-auto">
<div class="relative min-w-0 flex-1 md:w-48 md:flex-none">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
<Input
id="usage-records-search"
v-model="localSearch"
:placeholder="isAdmin ? '搜索用户/密钥' : '搜索密钥/模型'"
class="h-8 w-full text-xs border-border/60 pl-8"
/>
</div>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 shrink-0 md:hidden"
:class="autoRefresh ? 'text-primary' : ''"
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
@click="$emit('update:autoRefresh', !autoRefresh)"
>
<RefreshCcw
class="w-3.5 h-3.5"
:class="autoRefresh ? 'animate-spin' : ''"
/>
</Button>
</div>
<div class="contents md:hidden">
<div class="order-3 grid w-full grid-cols-2 gap-2 md:hidden">
<!-- 时间范围筛选 -->
<TimeRangePicker
v-model="timeRangeModel"
:show-granularity="false"
class="min-w-0"
preset-trigger-class="!w-full"
/>
<!-- 用户筛选仅管理员可见 -->
<ServerUserSelector
v-if="isAdmin"
class="flex-1 min-w-0 sm:flex-none sm:w-40"
class="min-w-0"
:model-value="filterUser"
:initial-users="availableUsers"
dropdown
@@ -37,7 +61,7 @@
:model-value="filterModel"
@update:model-value="$emit('update:filterModel', $event)"
>
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-40 h-8 text-xs border-border/60">
<SelectTrigger class="h-8 w-full min-w-0 text-xs border-border/60">
<SelectValue placeholder="模型" />
</SelectTrigger>
<SelectContent>
@@ -60,7 +84,7 @@
:model-value="filterProvider"
@update:model-value="$emit('update:filterProvider', $event)"
>
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
<SelectTrigger class="h-8 w-full min-w-0 text-xs border-border/60">
<SelectValue placeholder="提供商" />
</SelectTrigger>
<SelectContent>
@@ -82,7 +106,7 @@
:model-value="filterApiFormat"
@update:model-value="$emit('update:filterApiFormat', $event)"
>
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
<SelectTrigger class="h-8 w-full min-w-0 text-xs border-border/60">
<SelectValue placeholder="格式" />
</SelectTrigger>
<SelectContent>
@@ -104,7 +128,7 @@
:model-value="filterStatus"
@update:model-value="$emit('update:filterStatus', $event)"
>
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-28 h-8 text-xs border-border/60">
<SelectTrigger class="h-8 w-full min-w-0 text-xs border-border/60">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
@@ -142,9 +166,10 @@
<!-- 列显示配置桌面端 -->
<MultiSelect
v-model="visibleColumnIds"
class="hidden md:block"
:options="columnSelectOptions"
placeholder="显示列"
trigger-class="hidden md:flex w-40 h-8 text-xs border-border/60"
trigger-class="w-40 h-8 text-xs border-border/60"
dropdown-min-width="14rem"
:searchable="false"
/>
@@ -156,7 +181,7 @@
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="hidden h-8 w-8 shrink-0 md:inline-flex"
:class="autoRefresh ? 'text-primary' : ''"
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
@click="$emit('update:autoRefresh', !autoRefresh)"