refactor(frontend): 优化功能模块组件

- 更新 api-keys 模块: StandaloneKeyFormDialog
- 改进 auth 模块: LoginDialog
- 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor
- 重构 providers 模块: 多个表单和对话框组件
- 更新 usage 模块: 时间线、表格和详情组件
- 调整 users 模块: UserFormDialog
This commit is contained in:
fawney19
2025-12-12 16:15:36 +08:00
parent e9a6233655
commit 06c0a47b21
29 changed files with 2572 additions and 1051 deletions

View File

@@ -3,19 +3,29 @@
<template #actions>
<!-- 时间段筛选 -->
<Select
:model-value="selectedPeriod"
v-model:open="periodSelectOpen"
:model-value="selectedPeriod"
@update:model-value="$emit('update:selectedPeriod', $event)"
>
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="选择时间段" />
</SelectTrigger>
<SelectContent>
<SelectItem value="today">今天</SelectItem>
<SelectItem value="yesterday">昨天</SelectItem>
<SelectItem value="last7days">最近7天</SelectItem>
<SelectItem value="last30days">最近30天</SelectItem>
<SelectItem value="last90days">最近90天</SelectItem>
<SelectItem value="today">
今天
</SelectItem>
<SelectItem value="yesterday">
昨天
</SelectItem>
<SelectItem value="last7days">
最近7天
</SelectItem>
<SelectItem value="last30days">
最近30天
</SelectItem>
<SelectItem value="last90days">
最近90天
</SelectItem>
</SelectContent>
</Select>
@@ -25,16 +35,22 @@
<!-- 用户筛选仅管理员可见 -->
<Select
v-if="isAdmin && availableUsers.length > 0"
:model-value="filterUser"
v-model:open="filterUserSelectOpen"
:model-value="filterUser"
@update:model-value="$emit('update:filterUser', $event)"
>
<SelectTrigger class="w-36 h-8 text-xs border-border/60">
<SelectValue placeholder="全部用户" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">全部用户</SelectItem>
<SelectItem v-for="user in availableUsers" :key="user.id" :value="user.id">
<SelectItem value="__all__">
全部用户
</SelectItem>
<SelectItem
v-for="user in availableUsers"
:key="user.id"
:value="user.id"
>
{{ user.username || user.email }}
</SelectItem>
</SelectContent>
@@ -42,16 +58,22 @@
<!-- 模型筛选 -->
<Select
:model-value="filterModel"
v-model:open="filterModelSelectOpen"
:model-value="filterModel"
@update:model-value="$emit('update:filterModel', $event)"
>
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
<SelectValue placeholder="全部模型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">全部模型</SelectItem>
<SelectItem v-for="model in availableModels" :key="model" :value="model">
<SelectItem value="__all__">
全部模型
</SelectItem>
<SelectItem
v-for="model in availableModels"
:key="model"
:value="model"
>
{{ model.replace('claude-', '') }}
</SelectItem>
</SelectContent>
@@ -59,16 +81,22 @@
<!-- 提供商筛选 -->
<Select
:model-value="filterProvider"
v-model:open="filterProviderSelectOpen"
:model-value="filterProvider"
@update:model-value="$emit('update:filterProvider', $event)"
>
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="全部提供商" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">全部提供商</SelectItem>
<SelectItem v-for="provider in availableProviders" :key="provider" :value="provider">
<SelectItem value="__all__">
全部提供商
</SelectItem>
<SelectItem
v-for="provider in availableProviders"
:key="provider"
:value="provider"
>
{{ provider }}
</SelectItem>
</SelectContent>
@@ -76,20 +104,32 @@
<!-- 状态筛选 -->
<Select
:model-value="filterStatus"
v-model:open="filterStatusSelectOpen"
:model-value="filterStatus"
@update:model-value="$emit('update:filterStatus', $event)"
>
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">全部状态</SelectItem>
<SelectItem value="active">进行中</SelectItem>
<SelectItem value="pending">等待中</SelectItem>
<SelectItem value="streaming">流式传输</SelectItem>
<SelectItem value="completed">已完成</SelectItem>
<SelectItem value="failed">已失败</SelectItem>
<SelectItem value="__all__">
全部状态
</SelectItem>
<SelectItem value="active">
进行中
</SelectItem>
<SelectItem value="pending">
等待中
</SelectItem>
<SelectItem value="streaming">
流式传输
</SelectItem>
<SelectItem value="completed">
已完成
</SelectItem>
<SelectItem value="failed">
已失败
</SelectItem>
</SelectContent>
</Select>
@@ -97,58 +137,113 @@
<div class="h-4 w-px bg-border" />
<!-- 刷新按钮 -->
<RefreshButton :loading="loading" @click="$emit('refresh')" />
<RefreshButton
:loading="loading"
@click="$emit('refresh')"
/>
</template>
<Table>
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="h-12 font-semibold w-[70px]">时间</TableHead>
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">用户</TableHead>
<TableHead class="h-12 font-semibold w-[140px]">模型</TableHead>
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">提供商</TableHead>
<TableHead class="h-12 font-semibold w-[80px]">API格式</TableHead>
<TableHead class="h-12 font-semibold w-[50px] text-center">类型</TableHead>
<TableHead class="h-12 font-semibold w-[140px] text-right">Tokens</TableHead>
<TableHead class="h-12 font-semibold w-[100px] text-right">费用</TableHead>
<TableHead class="h-12 font-semibold w-[70px]">
时间
</TableHead>
<TableHead
v-if="isAdmin"
class="h-12 font-semibold w-[100px]"
>
用户
</TableHead>
<TableHead class="h-12 font-semibold w-[140px]">
模型
</TableHead>
<TableHead
v-if="isAdmin"
class="h-12 font-semibold w-[100px]"
>
提供商
</TableHead>
<TableHead class="h-12 font-semibold w-[80px]">
API格式
</TableHead>
<TableHead class="h-12 font-semibold w-[50px] text-center">
类型
</TableHead>
<TableHead class="h-12 font-semibold w-[140px] text-right">
Tokens
</TableHead>
<TableHead class="h-12 font-semibold w-[100px] text-right">
费用
</TableHead>
<TableHead class="h-12 font-semibold w-[70px] text-right">
<div class="inline-block max-w-[2rem] leading-tight">响应时间</div>
<div class="inline-block max-w-[2rem] leading-tight">
响应时间
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="records.length === 0">
<TableCell :colspan="isAdmin ? 9 : 7" class="text-center py-12 text-muted-foreground">
<TableCell
:colspan="isAdmin ? 9 : 7"
class="text-center py-12 text-muted-foreground"
>
暂无请求记录
</TableCell>
</TableRow>
<TableRow
v-else
v-for="record in records"
v-else
:key="record.id"
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
@mousedown="handleMouseDown"
@click="handleRowClick($event, record.id)"
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
>
<TableCell class="text-xs py-4 w-[70px]">
{{ formatDateTime(record.created_at) }}
</TableCell>
<TableCell v-if="isAdmin" class="py-4 w-[100px] truncate" :title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')">
<TableCell
v-if="isAdmin"
class="py-4 w-[100px] truncate"
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
>
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
</TableCell>
<TableCell class="font-medium py-4 w-[140px]" :title="getModelTooltip(record)">
<div v-if="getActualModel(record)" class="flex flex-col text-xs gap-0.5">
<TableCell
class="font-medium py-4 w-[140px]"
:title="getModelTooltip(record)"
>
<div
v-if="getActualModel(record)"
class="flex flex-col text-xs gap-0.5"
>
<div class="flex items-center gap-1 truncate">
<span class="truncate">{{ record.model }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 text-muted-foreground flex-shrink-0">
<path fill-rule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-3 h-3 text-muted-foreground flex-shrink-0"
>
<path
fill-rule="evenodd"
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<span class="text-muted-foreground truncate">{{ getActualModel(record) }}</span>
</div>
<span v-else class="truncate block">{{ record.model }}</span>
<span
v-else
class="truncate block"
>{{ record.model }}</span>
</TableCell>
<TableCell v-if="isAdmin" class="py-4 w-[60px]">
<TableCell
v-if="isAdmin"
class="py-4 w-[60px]"
>
<div class="flex flex-col text-xs gap-0.5">
<div class="flex items-center gap-1">
<span>{{ record.provider }}</span>
@@ -157,14 +252,30 @@
class="inline-flex items-center justify-center w-4 h-4 text-xs text-amber-600 dark:text-amber-400"
title="此请求发生了 Provider 切换"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</span>
</div>
<span v-if="record.api_key_name" class="text-muted-foreground truncate" :title="record.api_key_name">
<span
v-if="record.api_key_name"
class="text-muted-foreground truncate"
:title="record.api_key_name"
>
{{ record.api_key_name }}
<span v-if="record.rate_multiplier && record.rate_multiplier !== 1.0" class="text-foreground/60">({{ record.rate_multiplier }}x)</span>
<span
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
class="text-foreground/60"
>({{ record.rate_multiplier }}x)</span>
</span>
</div>
</TableCell>
@@ -176,23 +287,46 @@
>
{{ formatApiFormat(record.api_format) }}
</span>
<span v-else class="text-muted-foreground text-xs">-</span>
<span
v-else
class="text-muted-foreground text-xs"
>-</span>
</TableCell>
<TableCell class="text-center py-4 w-[50px]">
<!-- 优先显示请求状态 -->
<Badge v-if="record.status === 'pending'" variant="outline" class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground">
<Badge
v-if="record.status === 'pending'"
variant="outline"
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground"
>
等待中
</Badge>
<Badge v-else-if="record.status === 'streaming'" variant="outline" class="whitespace-nowrap animate-pulse border-primary/50 text-primary">
<Badge
v-else-if="record.status === 'streaming'"
variant="outline"
class="whitespace-nowrap animate-pulse border-primary/50 text-primary"
>
传输中
</Badge>
<Badge v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message" variant="destructive" class="whitespace-nowrap">
<Badge
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
variant="destructive"
class="whitespace-nowrap"
>
失败
</Badge>
<Badge v-else-if="record.is_stream" variant="secondary" class="whitespace-nowrap">
<Badge
v-else-if="record.is_stream"
variant="secondary"
class="whitespace-nowrap"
>
流式
</Badge>
<Badge v-else variant="outline" class="whitespace-nowrap border-border/60 text-muted-foreground">
<Badge
v-else
variant="outline"
class="whitespace-nowrap border-border/60 text-muted-foreground"
>
标准
</Badge>
</TableCell>
@@ -213,7 +347,10 @@
<TableCell class="text-right py-4 w-[100px]">
<div class="flex flex-col items-end text-xs gap-0.5">
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
<span v-if="showActualCost && record.actual_cost !== undefined" class="text-muted-foreground">
<span
v-if="showActualCost && record.actual_cost !== undefined"
class="text-muted-foreground"
>
{{ formatCurrency(record.actual_cost) }}
</span>
</div>
@@ -228,7 +365,10 @@
<span v-else-if="record.response_time_ms">
{{ (record.response_time_ms / 1000).toFixed(2) }}s
</span>
<span v-else class="text-muted-foreground">-</span>
<span
v-else
class="text-muted-foreground"
>-</span>
</TableCell>
</TableRow>
</TableBody>
@@ -251,21 +391,23 @@
<script setup lang="ts">
import { ref, computed, onUnmounted, watch } from 'vue'
import TableCard from '@/components/ui/table-card.vue'
import Button from '@/components/ui/button.vue'
import Badge from '@/components/ui/badge.vue'
import Select from '@/components/ui/select.vue'
import SelectTrigger from '@/components/ui/select-trigger.vue'
import SelectValue from '@/components/ui/select-value.vue'
import SelectContent from '@/components/ui/select-content.vue'
import SelectItem from '@/components/ui/select-item.vue'
import Table from '@/components/ui/table.vue'
import TableHeader from '@/components/ui/table-header.vue'
import TableBody from '@/components/ui/table-body.vue'
import TableRow from '@/components/ui/table-row.vue'
import TableHead from '@/components/ui/table-head.vue'
import TableCell from '@/components/ui/table-cell.vue'
import { Pagination, RefreshButton } from '@/components/ui'
import {
TableCard,
Badge,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
Pagination,
RefreshButton,
} from '@/components/ui'
import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables'
import { useRowClick } from '@/composables/useRowClick'