mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(frontend): localize account metrics and polish responsive layouts
Move account metric and relative-time presentation to the frontend using raw API values. Tighten latency unit spacing and unify edit actions and page header sizing. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
Zyy
co-authored by
Codex
parent
22518d064c
commit
632edee7dc
@@ -241,6 +241,7 @@ pub(super) fn account_quota_view(
|
||||
let recovery_probe_required = cooldown.is_some_and(|value| value.kind.requires_probe());
|
||||
(
|
||||
AccountQuotaView {
|
||||
refreshed_at: quota.observed_at.map(|value| time.rfc3339(&value)),
|
||||
refreshed_at_display,
|
||||
limit_reached: quota.limit_reached,
|
||||
rate_limited_until,
|
||||
|
||||
@@ -385,6 +385,7 @@ pub use crate::admin::account_groups::AccountGroupRefView;
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountQuotaView {
|
||||
pub refreshed_at: Option<String>,
|
||||
pub refreshed_at_display: String,
|
||||
pub limit_reached: bool,
|
||||
/// 冷却到期或可开始恢复探测的时间;非限流中为 `null`
|
||||
|
||||
@@ -928,8 +928,10 @@ pub(crate) fn dashboard_view(
|
||||
.filter(|window| window.used_percent.is_none())
|
||||
.and_then(|window| window.local_usage.as_ref())
|
||||
.map(|usage| usage.request_count);
|
||||
let (metric_label, metric_value) = match local_requests {
|
||||
let (metric, metric_value_raw, metric_label, metric_value) = match local_requests {
|
||||
Some(count) => (
|
||||
DashboardAccountMetric::Requests,
|
||||
Some(count),
|
||||
"次数".to_owned(),
|
||||
if count > 0 {
|
||||
format_compact_number(count)
|
||||
@@ -938,6 +940,8 @@ pub(crate) fn dashboard_view(
|
||||
},
|
||||
),
|
||||
None => (
|
||||
DashboardAccountMetric::Tokens,
|
||||
credential.total_tokens,
|
||||
"今日 Token".to_owned(),
|
||||
credential
|
||||
.total_tokens
|
||||
@@ -971,8 +975,11 @@ pub(crate) fn dashboard_view(
|
||||
usage_window: credential
|
||||
.quota_window
|
||||
.map(|window| crate::admin::accounts::quota_window_view(window, time)),
|
||||
metric,
|
||||
metric_value_raw,
|
||||
metric_label,
|
||||
metric_value,
|
||||
last_used_at: credential.last_used_at,
|
||||
last_used: time.relative_optional(credential.last_used_at, range.end),
|
||||
});
|
||||
}
|
||||
@@ -1060,6 +1067,7 @@ pub(crate) fn usage_summary_view(
|
||||
cache_write_input_tokens: format_compact_number(overview.requests.cache_write_input_tokens),
|
||||
total_tokens: format_compact_number(overview.requests.total_tokens),
|
||||
average_latency_ms: display_duration(summary.average_latency_ms),
|
||||
average_latency_ms_value: summary.average_latency_ms,
|
||||
logical_requests: request_summary_metrics_view(&overview.requests),
|
||||
attempts: attempt_metrics_view(&overview.attempts),
|
||||
}
|
||||
|
||||
@@ -401,7 +401,15 @@ pub struct DashboardCacheCardView {
|
||||
pub average_first_token_latency_ms: String,
|
||||
}
|
||||
|
||||
/// 旧 Dashboard 账号概览卡片所需的 Provider 安全投影
|
||||
/// Dashboard 账号概览指标的稳定标识
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DashboardAccountMetric {
|
||||
Requests,
|
||||
Tokens,
|
||||
}
|
||||
|
||||
/// Dashboard 账号概览卡片所需的 Provider 安全投影
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DashboardAccountUsageView {
|
||||
@@ -416,8 +424,11 @@ pub struct DashboardAccountUsageView {
|
||||
pub request_buckets: Vec<crate::time::RequestBucketView>,
|
||||
pub quota_used_percent: Option<f64>,
|
||||
pub usage_window: Option<crate::admin::accounts::AccountQuotaWindowView>,
|
||||
pub metric: DashboardAccountMetric,
|
||||
pub metric_value_raw: Option<u64>,
|
||||
pub metric_label: String,
|
||||
pub metric_value: String,
|
||||
pub last_used_at: Option<DateTime<Utc>>,
|
||||
pub last_used: String,
|
||||
}
|
||||
|
||||
@@ -610,6 +621,7 @@ pub struct UsageSummaryView {
|
||||
pub cache_write_input_tokens: String,
|
||||
pub total_tokens: String,
|
||||
pub average_latency_ms: String,
|
||||
pub average_latency_ms_value: Option<u64>,
|
||||
pub logical_requests: RequestMetricsView,
|
||||
pub attempts: AttemptMetricsView,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ async fn usage_summary_keeps_request_attempt_cost_and_average_latency_wire_field
|
||||
use gateway_api::admin::observability;
|
||||
use tower::ServiceExt as _;
|
||||
|
||||
for (sum, count, display) in [(404, 3, "134 ms"), (0, 1, "0 ms"), (0, 0, "—")] {
|
||||
for (sum, count, display, raw) in [
|
||||
(404, 3, "134 ms", Some(134)),
|
||||
(0, 1, "0 ms", Some(0)),
|
||||
(0, 0, "—", None),
|
||||
] {
|
||||
let fixture = AdminTestFixture::new().await;
|
||||
fixture.auth.insert_session("valid-session");
|
||||
let now = Utc::now();
|
||||
@@ -84,6 +88,7 @@ async fn usage_summary_keeps_request_attempt_cost_and_average_latency_wire_field
|
||||
"totalRequests": "5", "inputTokens": "400", "outputTokens": "80",
|
||||
"cachedInputTokens": "30", "cacheWriteInputTokens": "7", "totalTokens": "480",
|
||||
"averageLatencyMs": display,
|
||||
"averageLatencyMsValue": raw,
|
||||
"logicalRequests": {"requestCount":5,"successCount":4,"failureCount":0,"cancelledCount":1,"incompleteCount":0,"callerErrorCount":1,"inputTokens":400,"outputTokens":80,"cachedInputTokens":30,"cacheWriteInputTokens":7,"reasoningOutputTokens":9,"totalTokens":480},
|
||||
"attempts": {"attemptCount":6,"successCount":4,"failureCount":1,"cancelledCount":1,"incompleteCount":0,"rateLimitedCount":1,"authFailureCount":0,"provider5xxCount":0,"costCoverage":{"known":3,"partial":0,"unknown":1,"notBillable":0},"costs":[{"currency":"USD","estimatedAmount":"1.25"}]}
|
||||
})
|
||||
@@ -1472,7 +1477,11 @@ async fn dashboard_summary_should_identify_metrics_without_localized_labels() {
|
||||
http::{Request, StatusCode, header},
|
||||
};
|
||||
use chrono::Duration;
|
||||
use gateway_admin::model::observability::{DashboardObservation, TimeRange};
|
||||
use gateway_admin::model::{
|
||||
accounts::AccountUsage,
|
||||
observability::{DashboardAccountUsage, DashboardObservation, TimeRange},
|
||||
provider_credentials::{ProviderQuotaWindow, QuotaLocalUsageAttribution},
|
||||
};
|
||||
use gateway_api::admin::observability;
|
||||
use tower::ServiceExt as _;
|
||||
|
||||
@@ -1489,7 +1498,80 @@ async fn dashboard_summary_should_identify_metrics_without_localized_labels() {
|
||||
totals: Default::default(),
|
||||
provider_accounts: Default::default(),
|
||||
trend: Vec::new(),
|
||||
account_usage: Vec::new(),
|
||||
account_usage: [
|
||||
(None, Some(2_000), Some(now)),
|
||||
(None, Some(0), None),
|
||||
(None, None, None),
|
||||
(Some(9), Some(2_000), Some(now)),
|
||||
(Some(0), Some(2_000), None),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (requests, tokens, last_used_at))| {
|
||||
let account_id = format!("acct_metric_{index}");
|
||||
DashboardAccountUsage {
|
||||
account_id: account_id.clone(),
|
||||
provider_kind: "openai".to_owned(),
|
||||
authentication_kind: "oauth".to_owned(),
|
||||
name: account_id.clone(),
|
||||
email: None,
|
||||
plan_type: None,
|
||||
plan_type_display: None,
|
||||
request_count: 10,
|
||||
success_count: 10,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
cache_write_input_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
image_input_tokens: None,
|
||||
image_output_tokens: None,
|
||||
image_request_count: 0,
|
||||
image_request_failed_count: 0,
|
||||
total_tokens: tokens,
|
||||
cost_coverage: Default::default(),
|
||||
costs: Vec::new(),
|
||||
last_used_at,
|
||||
request_buckets: Vec::new(),
|
||||
quota_used_percent: None,
|
||||
quota_window: requests.map(|request_count| ProviderQuotaWindow {
|
||||
key: "rolling".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "滚动窗口".to_owned(),
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(86_400),
|
||||
used_percent: None,
|
||||
reset_at: None,
|
||||
limit_reached: false,
|
||||
local_usage: Some(AccountUsage {
|
||||
account_id,
|
||||
request_count,
|
||||
success_count: request_count,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
cached_input_tokens: None,
|
||||
cache_write_input_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
image_input_tokens: None,
|
||||
image_output_tokens: None,
|
||||
image_request_count: 0,
|
||||
image_request_failed_count: 0,
|
||||
total_tokens: None,
|
||||
cost_coverage: Default::default(),
|
||||
costs: Vec::new(),
|
||||
last_used_at,
|
||||
request_buckets: Vec::new(),
|
||||
models: Vec::new(),
|
||||
}),
|
||||
provider_data: None,
|
||||
}),
|
||||
models: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
recent_requests: Vec::new(),
|
||||
});
|
||||
|
||||
@@ -1521,6 +1603,20 @@ async fn dashboard_summary_should_identify_metrics_without_localized_labels() {
|
||||
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(status, StatusCode::OK, "{kind}: {value}");
|
||||
let accounts = value["data"]["accountUsage"].as_array().unwrap();
|
||||
assert_eq!(accounts.len(), 5);
|
||||
for (account, (metric, raw, display, last_used_at)) in accounts.iter().zip([
|
||||
("tokens", Some(2_000), "2K", Some(now)),
|
||||
("tokens", Some(0), "0", None),
|
||||
("tokens", None, "—", None),
|
||||
("requests", Some(9), "9", Some(now)),
|
||||
("requests", Some(0), "—", None),
|
||||
]) {
|
||||
assert_eq!(account["metric"], metric);
|
||||
assert_eq!(account["metricValueRaw"], json!(raw));
|
||||
assert_eq!(account["metricValue"], display);
|
||||
assert_eq!(account["lastUsedAt"], json!(last_used_at));
|
||||
}
|
||||
let summary = value["data"]["trend"]["summary"].as_array().unwrap();
|
||||
assert_eq!(summary.len(), metrics.len());
|
||||
for (item, metric) in summary.iter().zip(metrics) {
|
||||
|
||||
+11
-2
@@ -158,7 +158,10 @@ Codex PAT 验证服务不可用和身份响应无效分别返回 `50301`、`5020
|
||||
`YYYY-MM-DD HH:mm:ss`,短时间、相对时间和图表标签按对应视图返回;缺失时间的展示字段为 `null` 或占位文本。
|
||||
调用者直接显示文本,使用原始时间点进行排序、比较和输入,不把展示文本转回请求时间
|
||||
|
||||
账号、模型用量和 Key 列表的最近使用时间由前端根据原始 RFC3339 时间点,通过 VueUse `useTimeAgoIntl` 按界面语言显示短相对时间,并随时间更新;悬停的完整日期仍使用后端 `lastUsedAtFullDisplay`,不使用浏览器本地时区重算
|
||||
账号、模型用量和 Key 列表以及 Dashboard 的最近使用时间由前端根据原始 RFC3339 `lastUsedAt`,
|
||||
通过 VueUse `useTimeAgoIntl` 按界面语言显示短相对时间,并随时间更新。账号额度的最近刷新时间
|
||||
同样使用 `quota.refreshedAt`,缺失时显示占位文本。悬停的完整日期仍使用后端
|
||||
`lastUsedAtFullDisplay`,不使用浏览器本地时区重算
|
||||
|
||||
账号冷却返回 RFC3339 `rateLimitedUntil` 和 `rateLimitRecoveryDisplay`,后者包含剩余时长、
|
||||
到期时间或等待恢复探测的提示,随账号数据刷新;前端不解析展示文本或自行计算恢复提示
|
||||
@@ -1997,6 +2000,9 @@ errorCode, errorMessage, startedAt, completedAt, expiresAt, createdAt, updatedAt
|
||||
| `GET` | `/api/admin/operations/errors` | 运维错误分页列表 |
|
||||
| `GET` | `/api/admin/diagnostics/export` | 按 `requestId` 或 `errorId` 下载诊断 ZIP 附件 |
|
||||
|
||||
用量汇总的 `averageLatencyMsValue` 为可空毫秒数,前端按持续时长格式化;无延迟样本时为 `null`,
|
||||
`averageLatencyMs` 保留为兼容展示字段
|
||||
|
||||
### Dashboard 趋势指标
|
||||
|
||||
趋势摘要 `summary[]` 返回稳定的 `metric`、展示数值 `value` 和可空的 `ratio`,指标名称由前端本地化
|
||||
@@ -2026,7 +2032,10 @@ Dashboard 的 `capacityInfo` 统计普通容量,不含独立审批池;`maxCo
|
||||
`capacityInfo.totalSlots` 为 `number | null`;可用账号池含无限并发账号时为 `null`,此时 `availableSlots` 也为 `null`。
|
||||
`usedSlots` 仍表示实际在途数,Redis 不可用时为 `null`;没有可用账号时 `totalSlots` 为 `0`
|
||||
|
||||
Dashboard 的 `accountUsage[]` 由后端提供 `usageWindow`、`metricLabel`、`metricValue`
|
||||
Dashboard 的 `accountUsage[]` 返回稳定指标 `metric=requests|tokens`、可空原始数值 `metricValueRaw` 和
|
||||
可空 RFC3339 `lastUsedAt`,前端负责指标名称、数字和相对时间的本地化。
|
||||
`requests` 使用代表性额度窗口的本地请求数,`tokens` 使用今日 Token;未知数值为 `null`,零值保留 `0`。
|
||||
`metricLabel`、`metricValue` 和 `lastUsed` 保留给兼容调用方,当前管理端不解析或显示这些字段
|
||||
|
||||
`usageWindow` 复用账号额度窗口合同,缺失额度事实时为 `null`;窗口标签、百分比、触顶状态、重置时间
|
||||
和本地用量由 Provider/Admin 投影。套餐缺失不表示免费套餐,显示时舍入的百分比不用于判断触顶
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface AccountQuotaCredits {
|
||||
}
|
||||
|
||||
export interface AccountQuota {
|
||||
refreshedAt: string | null
|
||||
refreshedAtDisplay: string
|
||||
limitReached: boolean
|
||||
// 429 临时限流(Redis 冷却)到期时间;非限流中为 null。
|
||||
|
||||
@@ -160,8 +160,11 @@ export interface DashboardAccountUsage {
|
||||
requestBuckets: DashboardAccountRequestBucket[]
|
||||
quotaUsedPercent: number | null
|
||||
usageWindow: AccountQuotaWindow | null
|
||||
metric: 'requests' | 'tokens'
|
||||
metricValueRaw: number | null
|
||||
metricLabel: string
|
||||
metricValue: string
|
||||
lastUsedAt: string | null
|
||||
lastUsed: string
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ export interface UsageSummaryResponse {
|
||||
cacheWriteInputTokens: string
|
||||
totalTokens: string
|
||||
averageLatencyMs: string
|
||||
averageLatencyMsValue: number | null
|
||||
}
|
||||
|
||||
export interface UsageOverviewHealthPoint {
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"batchEdit": {
|
||||
"title": "Edit accounts",
|
||||
"selectionTitle": "Edit {count} account | Edit {count} accounts",
|
||||
"saveChanges": "Save changes",
|
||||
"proxyRequired": "Select a proxy that passed the connection test",
|
||||
"saved": "Updated {count} account | Updated {count} accounts",
|
||||
"staleAccount": "Account {accountId} is out of date, select it again"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"unavailableCount": "Unavailable: {value}"
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "Today’s tokens",
|
||||
"title": "Active account usage",
|
||||
"sortByRecent": "Sorted by last used",
|
||||
"empty": "No recent account requests",
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
"weeklyLimitUsd": "Weekly limit (USD)",
|
||||
"maximumConcurrency": "Maximum concurrency",
|
||||
"requestsPerMinuteRpm": "Requests per minute (RPM)",
|
||||
"saveChanges": "Save changes",
|
||||
"updated": "API key updated",
|
||||
"created": "API key created",
|
||||
"validation": {
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"batchEdit": {
|
||||
"title": "批量编辑账号",
|
||||
"selectionTitle": "编辑 {count} 个账号",
|
||||
"saveChanges": "保存更改",
|
||||
"proxyRequired": "请选择已通过测试的代理",
|
||||
"saved": "已更新 {count} 个账号",
|
||||
"staleAccount": "账号 {accountId} 的页面数据已失效,请重新选择"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"unavailableCount": "不可用 {value}"
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "今日 Token",
|
||||
"title": "活跃账号用量",
|
||||
"sortByRecent": "最近使用排序",
|
||||
"empty": "暂无账号请求记录",
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
"weeklyLimitUsd": "周限额(USD)",
|
||||
"maximumConcurrency": "最大并发",
|
||||
"requestsPerMinuteRpm": "每分钟请求数(RPM)",
|
||||
"saveChanges": "保存更改",
|
||||
"updated": "API Key 已更新",
|
||||
"created": "API Key 已创建",
|
||||
"validation": {
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"batchEdit": {
|
||||
"title": "批量編輯賬號",
|
||||
"selectionTitle": "編輯 {count} 個賬號",
|
||||
"saveChanges": "保存更改",
|
||||
"proxyRequired": "請選擇已通過測試的代理",
|
||||
"saved": "已更新 {count} 個賬號",
|
||||
"staleAccount": "賬號 {accountId} 的頁面數據已失效,請重新選擇"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"unavailableCount": "不可用 {value}"
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "今日 Token",
|
||||
"title": "活躍賬號用量",
|
||||
"sortByRecent": "最近使用排序",
|
||||
"empty": "暫無賬號請求記錄",
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
"weeklyLimitUsd": "周限額(USD)",
|
||||
"maximumConcurrency": "最大併發",
|
||||
"requestsPerMinuteRpm": "每分鐘請求數(RPM)",
|
||||
"saveChanges": "保存更改",
|
||||
"updated": "API Key 已更新",
|
||||
"created": "API Key 已創建",
|
||||
"validation": {
|
||||
|
||||
@@ -65,7 +65,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:disabled="selectedCount === 0 || groupsLoading"
|
||||
@click="emit('save')"
|
||||
>
|
||||
{{ t('accounts.batchEdit.saveChanges') }}
|
||||
{{ t('common.actions.save') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
</ZDialog>
|
||||
|
||||
@@ -118,7 +118,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:disabled="!account || groupsLoading || (isOpenAiApiKeyAccount(account) && !configurationReady)"
|
||||
@click="emit('save')"
|
||||
>
|
||||
{{ t('accounts.batchEdit.saveChanges') }}
|
||||
{{ t('common.actions.save') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
</ZDialog>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Account } from '@/api'
|
||||
import { ZEmpty, ZIconButton } from '@codex-proxy/ui'
|
||||
|
||||
import { ChartNoAxesCombined, RefreshCw, UserRound } from '@lucide/vue'
|
||||
import { useTimeAgoIntl } from '@vueuse/core'
|
||||
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -26,7 +27,13 @@ const emit = defineEmits<{
|
||||
quotaReset: [accountId: string]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
const { t, locale } = useI18n({ useScope: 'global' })
|
||||
|
||||
const refreshedAtDisplay = useTimeAgoIntl(() => props.account.quota.refreshedAt ?? 0, {
|
||||
get locale() { return locale.value },
|
||||
relativeTimeFormatOptions: { numeric: 'auto', style: 'short' },
|
||||
insertSpace: false,
|
||||
})
|
||||
|
||||
const quotaEntries = computed(() => groupedAccountQuotaWindows(
|
||||
orderedPanelQuotaWindows(props.account.quota.windows),
|
||||
@@ -59,7 +66,7 @@ watch(hasPersonalInfo, (available) => {
|
||||
<AccountPlanBadge :plan-type="account.planType" :plan-type-display="account.planTypeDisplay" size="sm" />
|
||||
</template>
|
||||
<span>·</span>
|
||||
<span>{{ t('accounts.quota.lastRefreshed', { refreshedAtDisplay: account.quota.refreshedAtDisplay }) }}</span>
|
||||
<span>{{ t('accounts.quota.lastRefreshed', { refreshedAtDisplay: account.quota.refreshedAt ? refreshedAtDisplay : '—' }) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="hasActions" class="flex shrink-0 items-center gap-0.5 [&_svg]:size-3.5 [&_svg]:stroke-2">
|
||||
|
||||
@@ -207,7 +207,6 @@ const {
|
||||
<template>
|
||||
<div class="flex min-h-0 w-full flex-col xl:h-full xl:overflow-hidden">
|
||||
<ZPageHeader
|
||||
class="h-17"
|
||||
:title="t('navigation.accounts')"
|
||||
:description="t('accounts.page.description')"
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CircleCheck, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AccountUsageWindow from '@/components/account/account-usage-window/index.vue'
|
||||
import LastUsedAtCell from '@/components/LastUsedAtCell.vue'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
import { metricToneIconClasses, metricToneValueClasses } from '../constants'
|
||||
import DashboardAccountIdentity from './DashboardAccountIdentity.vue'
|
||||
@@ -251,12 +252,12 @@ const statusBars = computed(() => {
|
||||
>
|
||||
<span class="grid min-w-0 gap-1">
|
||||
<span class="text-cp-xs leading-none font-bold text-cp-text-quaternary">
|
||||
{{ account.metricLabel }}
|
||||
{{ account.metric === 'requests' ? t('dashboard.metrics.requestCount') : t('dashboard.activeAccounts.todayTokens') }}
|
||||
</span>
|
||||
<strong
|
||||
class="font-mono text-sm leading-[1.15] font-heavy tabular-nums text-cp-text"
|
||||
>
|
||||
{{ account.metricValue }}
|
||||
{{ account.metricValueRaw == null || (account.metric === 'requests' && account.metricValueRaw === 0) ? '—' : formatCompactNumber(account.metricValueRaw) }}
|
||||
</strong>
|
||||
</span>
|
||||
|
||||
@@ -265,7 +266,8 @@ const statusBars = computed(() => {
|
||||
<span
|
||||
class="min-w-0 truncate text-xs leading-[1.15] font-bold text-cp-text-secondary xl:whitespace-nowrap"
|
||||
>
|
||||
{{ account.lastUsed }}
|
||||
<LastUsedAtCell v-if="account.lastUsedAt" :value="account.lastUsedAt" :full-display="null" />
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ const { allSelected, indeterminate, selectedRowKeys, toggleSelection, toggleAll
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 w-full flex-col overflow-hidden">
|
||||
<ZPageHeader
|
||||
class="h-17"
|
||||
:title="t('navigation.groups')"
|
||||
:description="t('groups.page.description')"
|
||||
/>
|
||||
|
||||
@@ -170,7 +170,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
:disabled="!form.name.trim()"
|
||||
@click="emit('save')"
|
||||
>
|
||||
{{ editing ? t('keys.editor.saveChanges') : t('common.actions.create') }}
|
||||
{{ editing ? t('common.actions.save') : t('common.actions.create') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
</ZDialog>
|
||||
|
||||
@@ -114,7 +114,6 @@ watch(
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 w-full flex-col overflow-hidden">
|
||||
<ZPageHeader
|
||||
class="h-17"
|
||||
:title="t('navigation.apiKeys')"
|
||||
:description="t('keys.page.description')"
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,6 @@ watch(search, () => {
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 w-full flex-col overflow-hidden">
|
||||
<ZPageHeader
|
||||
class="h-17"
|
||||
:title="t('navigation.proxies')"
|
||||
:description="t('proxies.page.description')"
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,7 @@ function saveTheme(event: MouseEvent) {
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-0 w-full flex-col">
|
||||
<ZPageHeader class="xl:h-17" :title="t('navigation.appearance')">
|
||||
<ZPageHeader :title="t('navigation.appearance')">
|
||||
<template #description>
|
||||
<span class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span>{{ t('appearance.settings.description') }}</span>
|
||||
|
||||
@@ -337,6 +337,8 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
quotaUsedPercent: 32,
|
||||
metricLabel: t('appearance.preview.dashboard.todaySToken'),
|
||||
metricValue: '926K',
|
||||
metric: 'tokens',
|
||||
metricValueRaw: 926_000,
|
||||
usageWindow: {
|
||||
key: 'short',
|
||||
group: 'shortTerm',
|
||||
@@ -352,6 +354,7 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
resetAtDisplay: '—',
|
||||
},
|
||||
lastUsed: t('appearance.preview.components.justNow'),
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'preview-xai-1',
|
||||
@@ -370,6 +373,8 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
quotaUsedPercent: null,
|
||||
metricLabel: t('appearance.preview.dashboard.times'),
|
||||
metricValue: '894',
|
||||
metric: 'requests',
|
||||
metricValueRaw: 894,
|
||||
usageWindow: {
|
||||
key: 'free-rolling-24h',
|
||||
group: 'shortTerm',
|
||||
@@ -394,6 +399,7 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
},
|
||||
},
|
||||
lastUsed: t('common.time.minutesAgo', { count: 5 }),
|
||||
lastUsedAt: new Date(Date.now() - 5 * 60_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'preview-openai-2',
|
||||
@@ -408,6 +414,8 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
quotaUsedPercent: 68,
|
||||
metricLabel: t('appearance.preview.dashboard.todaySToken'),
|
||||
metricValue: '512K',
|
||||
metric: 'tokens',
|
||||
metricValueRaw: 512_000,
|
||||
usageWindow: {
|
||||
key: 'short',
|
||||
group: 'shortTerm',
|
||||
@@ -423,6 +431,7 @@ export const themeDashboardSummary = computed<DashboardSummaryResponse>(() => ({
|
||||
resetAtDisplay: '—',
|
||||
},
|
||||
lastUsed: t('common.time.minutesAgo', { count: 8 }),
|
||||
lastUsedAt: new Date(Date.now() - 8 * 60_000).toISOString(),
|
||||
},
|
||||
],
|
||||
usageRecords: [
|
||||
|
||||
@@ -13,9 +13,17 @@ const props = defineProps<{
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
function averageLatencyDisplay(value: string) {
|
||||
return !value || value === '—' || value === '-' ? '0 ms' : value
|
||||
}
|
||||
const averageLatency = computed(() => {
|
||||
const raw = props.summary.averageLatencyMsValue
|
||||
const value = raw == null || !Number.isFinite(raw) || raw < 0 ? 0 : raw
|
||||
if (value < 1_000)
|
||||
return { value: String(Math.round(value)), unit: 'ms' }
|
||||
if (value < 60_000)
|
||||
return { value: (value / 1_000).toFixed(2), unit: 's' }
|
||||
if (value < 3_600_000)
|
||||
return { value: (value / 60_000).toFixed(1), unit: 'min' }
|
||||
return { value: (value / 3_600_000).toFixed(1), unit: 'h' }
|
||||
})
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
@@ -46,7 +54,8 @@ const items = computed(() => [
|
||||
key: 'latency',
|
||||
label: t('usage.averageTimeTaken'),
|
||||
icon: Timer,
|
||||
value: averageLatencyDisplay(props.summary.averageLatencyMs),
|
||||
value: averageLatency.value.value,
|
||||
unit: averageLatency.value.unit,
|
||||
detail: t('usage.successfulRequestAverage'),
|
||||
tone: 'bg-cp-cyan-container text-cp-cyan-on-container',
|
||||
},
|
||||
@@ -70,7 +79,7 @@ const items = computed(() => [
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<strong class="my-2 block truncate font-mono text-[24px] leading-none font-extrabold text-cp-text">
|
||||
{{ item.value }}
|
||||
{{ item.value }}<span v-if="item.unit" class="ml-0.5 font-sans text-cp font-semibold text-cp-text-secondary">{{ item.unit }}</span>
|
||||
</strong>
|
||||
<span class="block truncate text-cp-sm leading-none font-emphasis text-cp-text-quaternary">
|
||||
{{ item.detail }}
|
||||
|
||||
@@ -267,6 +267,7 @@ function emptySummary() {
|
||||
cacheWriteInputTokens: '0',
|
||||
totalTokens: '0',
|
||||
averageLatencyMs: '0 ms',
|
||||
averageLatencyMsValue: null,
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user