feat: OAuth 账户管理、维护调度、端点健康检查增强及前端优化

- 新增 OAuth 账户管理对话框和提供商详情抽屉中的 OAuth 信息展示
- 新增维护调度器(maintenance_scheduler)支持定时清理和健康检查
- 增强端点健康检查器,支持更多检测策略
- 重构 codex 服务为 metadata_collectors 模块
- 优化 OpenAI CLI normalizer 代码结构
- 前端: 改进使用量表格、统计图表、指南页面和异步任务管理
- 扩展多个数据库字符串列为 TEXT 类型
- 新增倒计时 composable 和 provider OAuth API 端点
This commit is contained in:
fawney19
2026-02-04 23:59:45 +08:00
parent 24c9105628
commit 4d6e7c094f
64 changed files with 3885 additions and 930 deletions

View File

@@ -1,21 +1,46 @@
<template>
<div class="flex flex-wrap items-center gap-2">
<Select v-model:open="presetSelectOpen" v-model="selectedPreset">
<Select
v-model:open="presetSelectOpen"
v-model="selectedPreset"
>
<SelectTrigger class="h-8 w-32 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="this_week">本周</SelectItem>
<SelectItem value="last_week">上周</SelectItem>
<SelectItem value="this_month">本月</SelectItem>
<SelectItem value="last_month">上月</SelectItem>
<SelectItem value="this_year">今年</SelectItem>
<SelectItem value="custom">自定义</SelectItem>
<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="this_week">
本周
</SelectItem>
<SelectItem value="last_week">
上周
</SelectItem>
<SelectItem value="this_month">
本月
</SelectItem>
<SelectItem value="last_month">
上月
</SelectItem>
<SelectItem value="this_year">
今年
</SelectItem>
<SelectItem value="custom">
自定义
</SelectItem>
</SelectContent>
</Select>
@@ -36,15 +61,30 @@
/>
</div>
<Select v-if="showGranularity" v-model:open="granularitySelectOpen" v-model="selectedGranularity">
<Select
v-if="showGranularity"
v-model:open="granularitySelectOpen"
v-model="selectedGranularity"
>
<SelectTrigger class="h-8 w-24 text-xs border-border/60">
<SelectValue placeholder="粒度" />
</SelectTrigger>
<SelectContent>
<SelectItem v-if="allowHourly && canUseHourly" value="hour">小时</SelectItem>
<SelectItem value="day"></SelectItem>
<SelectItem value="week"></SelectItem>
<SelectItem value="month"></SelectItem>
<SelectItem
v-if="allowHourly && canUseHourly"
value="hour"
>
小时
</SelectItem>
<SelectItem value="day">
</SelectItem>
<SelectItem value="week">
</SelectItem>
<SelectItem value="month">
</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -1,8 +1,15 @@
<template>
<Card class="p-4 space-y-2">
<div class="text-xs text-muted-foreground">{{ label }}</div>
<div class="text-lg font-semibold">{{ value }}</div>
<div class="text-xs" :class="changeClass">
<div class="text-xs text-muted-foreground">
{{ label }}
</div>
<div class="text-lg font-semibold">
{{ value }}
</div>
<div
class="text-xs"
:class="changeClass"
>
<span v-if="changePercent !== null">{{ changePercent }}%</span>
<span v-else>--</span>
<span class="ml-1 text-muted-foreground">vs 对比期</span>

View File

@@ -1,14 +1,28 @@
<template>
<div class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">{{ title }}</h3>
<span class="text-xs text-muted-foreground" v-if="subtitle">{{ subtitle }}</span>
<h3 class="text-sm font-semibold">
{{ title }}
</h3>
<span
v-if="subtitle"
class="text-xs text-muted-foreground"
>{{ subtitle }}</span>
</div>
<div v-if="loading" class="p-6">
<div
v-if="loading"
class="p-6"
>
<LoadingState />
</div>
<div v-else class="h-[280px]">
<LineChart :data="chartData" :options="chartOptions" />
<div
v-else
class="h-[280px]"
>
<LineChart
:data="chartData"
:options="chartOptions"
/>
</div>
</div>
</template>

View File

@@ -1,14 +1,28 @@
<template>
<div class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">{{ title }}</h3>
<span class="text-xs text-muted-foreground" v-if="subtitle">{{ subtitle }}</span>
<h3 class="text-sm font-semibold">
{{ title }}
</h3>
<span
v-if="subtitle"
class="text-xs text-muted-foreground"
>{{ subtitle }}</span>
</div>
<div v-if="loading" class="p-6">
<div
v-if="loading"
class="p-6"
>
<LoadingState />
</div>
<div v-else class="h-[260px]">
<DoughnutChart :data="chartData" :options="chartOptions" />
<div
v-else
class="h-[260px]"
>
<DoughnutChart
:data="chartData"
:options="chartOptions"
/>
</div>
</div>
</template>

View File

@@ -11,36 +11,70 @@
<SelectValue placeholder="指标" />
</SelectTrigger>
<SelectContent>
<SelectItem value="requests">请求数</SelectItem>
<SelectItem value="tokens">Tokens</SelectItem>
<SelectItem value="cost">成本</SelectItem>
<SelectItem value="requests">
请求数
</SelectItem>
<SelectItem value="tokens">
Tokens
</SelectItem>
<SelectItem value="cost">
成本
</SelectItem>
</SelectContent>
</Select>
</template>
<div v-if="loading" class="p-6">
<div
v-if="loading"
class="p-6"
>
<LoadingState />
</div>
<div v-else-if="items.length === 0" class="p-6">
<EmptyState title="暂无数据" description="当前时间范围内没有统计结果" />
<div
v-else-if="items.length === 0"
class="p-6"
>
<EmptyState
title="暂无数据"
description="当前时间范围内没有统计结果"
/>
</div>
<Table v-else>
<TableHeader>
<TableRow>
<TableHead class="w-16">排名</TableHead>
<TableHead class="w-16">
排名
</TableHead>
<TableHead>名称</TableHead>
<TableHead class="text-right">请求数</TableHead>
<TableHead class="text-right">Tokens</TableHead>
<TableHead class="text-right">成本</TableHead>
<TableHead class="text-right">
请求数
</TableHead>
<TableHead class="text-right">
Tokens
</TableHead>
<TableHead class="text-right">
成本
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="item in items" :key="item.id">
<TableCell class="font-medium">{{ item.rank }}</TableCell>
<TableRow
v-for="item in items"
:key="item.id"
>
<TableCell class="font-medium">
{{ item.rank }}
</TableCell>
<TableCell>{{ item.name }}</TableCell>
<TableCell class="text-right">{{ item.requests }}</TableCell>
<TableCell class="text-right">{{ formatTokens(item.tokens) }}</TableCell>
<TableCell class="text-right">{{ formatCurrency(item.cost) }}</TableCell>
<TableCell class="text-right">
{{ item.requests }}
</TableCell>
<TableCell class="text-right">
{{ formatTokens(item.tokens) }}
</TableCell>
<TableCell class="text-right">
{{ formatCurrency(item.cost) }}
</TableCell>
</TableRow>
</TableBody>
</Table>

View File

@@ -1,14 +1,28 @@
<template>
<div class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">{{ title }}</h3>
<span class="text-xs text-muted-foreground" v-if="subtitle">{{ subtitle }}</span>
<h3 class="text-sm font-semibold">
{{ title }}
</h3>
<span
v-if="subtitle"
class="text-xs text-muted-foreground"
>{{ subtitle }}</span>
</div>
<div v-if="loading" class="p-6">
<div
v-if="loading"
class="p-6"
>
<LoadingState />
</div>
<div v-else class="h-[260px]">
<LineChart :data="chartData" :options="chartOptions" />
<div
v-else
class="h-[260px]"
>
<LineChart
:data="chartData"
:options="chartOptions"
/>
</div>
</div>
</template>

View File

@@ -1,18 +1,39 @@
<template>
<Card class="p-4 space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">{{ title }}</h3>
<span class="text-xs text-muted-foreground" v-if="subtitle">{{ subtitle }}</span>
<h3 class="text-sm font-semibold">
{{ title }}
</h3>
<span
v-if="subtitle"
class="text-xs text-muted-foreground"
>{{ subtitle }}</span>
</div>
<div v-if="loading" class="p-4">
<div
v-if="loading"
class="p-4"
>
<LoadingState />
</div>
<div v-else-if="providers.length === 0" class="p-4">
<EmptyState title="暂无数据" description="暂无月卡配额数据" />
<div
v-else-if="providers.length === 0"
class="p-4"
>
<EmptyState
title="暂无数据"
description="暂无月卡配额数据"
/>
</div>
<div v-else class="space-y-4">
<div v-for="provider in providers" :key="provider.id" class="space-y-2">
<div
v-else
class="space-y-4"
>
<div
v-for="provider in providers"
:key="provider.id"
class="space-y-2"
>
<div class="flex items-center justify-between text-xs">
<span class="font-medium">{{ provider.name }}</span>
<span class="text-muted-foreground">