mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-04 00:32:26 +08:00
refactor(frontend): 优化功能模块组件
- 更新 api-keys 模块: StandaloneKeyFormDialog - 改进 auth 模块: LoginDialog - 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor - 重构 providers 模块: 多个表单和对话框组件 - 更新 usage 模块: 时间线、表格和详情组件 - 调整 users 模块: UserFormDialog
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user