From 632edee7dc6bbcc44c4e9adec60668237af87a75 Mon Sep 17 00:00:00 2001 From: Zyy <571092077@qq.com> Date: Fri, 23 Oct 2026 20:08:29 +0800 Subject: [PATCH] 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 --- .../src/admin/accounts/presenter.rs | 1 + .../gateway-api/src/admin/accounts/wire.rs | 1 + .../src/admin/observability/presenter.rs | 10 +- .../src/admin/observability/wire.rs | 14 ++- .../tests/admin/observability/response.rs | 102 +++++++++++++++++- docs/api.md | 13 ++- frontend/src/api/modules/accounts.ts | 1 + frontend/src/api/modules/dashboard.ts | 3 + frontend/src/api/modules/usage.ts | 1 + frontend/src/i18n/locales/en/accounts.json | 1 - frontend/src/i18n/locales/en/dashboard.json | 1 + frontend/src/i18n/locales/en/keys.json | 1 - .../src/i18n/locales/zh-Hans/accounts.json | 1 - .../src/i18n/locales/zh-Hans/dashboard.json | 1 + frontend/src/i18n/locales/zh-Hans/keys.json | 1 - .../src/i18n/locales/zh-Hant/accounts.json | 1 - .../src/i18n/locales/zh-Hant/dashboard.json | 1 + frontend/src/i18n/locales/zh-Hant/keys.json | 1 - .../components/AccountBatchEditModal.vue | 2 +- .../accounts/components/AccountEditModal.vue | 2 +- .../components/account-quota-panel/index.vue | 11 +- frontend/src/views/accounts/index.vue | 1 - .../components/AccountOverviewCard.vue | 8 +- frontend/src/views/groups/index.vue | 1 - .../views/keys/components/ApiKeyFormModal.vue | 2 +- frontend/src/views/keys/index.vue | 1 - frontend/src/views/proxies/index.vue | 1 - .../components/AppearanceSettingsSection.vue | 2 +- .../src/views/theme/fixtures/dashboard.ts | 9 ++ .../usage/components/UsageSummaryCards.vue | 19 +++- .../usage/composables/useUsageRecordsTable.ts | 1 + 31 files changed, 184 insertions(+), 31 deletions(-) diff --git a/backend/crates/gateway-api/src/admin/accounts/presenter.rs b/backend/crates/gateway-api/src/admin/accounts/presenter.rs index 869fa5c14..130f9c719 100644 --- a/backend/crates/gateway-api/src/admin/accounts/presenter.rs +++ b/backend/crates/gateway-api/src/admin/accounts/presenter.rs @@ -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, diff --git a/backend/crates/gateway-api/src/admin/accounts/wire.rs b/backend/crates/gateway-api/src/admin/accounts/wire.rs index 49accbee1..24c6c94b0 100644 --- a/backend/crates/gateway-api/src/admin/accounts/wire.rs +++ b/backend/crates/gateway-api/src/admin/accounts/wire.rs @@ -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, pub refreshed_at_display: String, pub limit_reached: bool, /// 冷却到期或可开始恢复探测的时间;非限流中为 `null` diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index 10d5e3e2d..7347e53c5 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -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), } diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs index eb8c40365..1980b946b 100644 --- a/backend/crates/gateway-api/src/admin/observability/wire.rs +++ b/backend/crates/gateway-api/src/admin/observability/wire.rs @@ -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, pub quota_used_percent: Option, pub usage_window: Option, + pub metric: DashboardAccountMetric, + pub metric_value_raw: Option, pub metric_label: String, pub metric_value: String, + pub last_used_at: Option>, 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, pub logical_requests: RequestMetricsView, pub attempts: AttemptMetricsView, } diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index 40bb3987e..1616cb694 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -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) { diff --git a/docs/api.md b/docs/api.md index 6242f7e37..7d549306f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 投影。套餐缺失不表示免费套餐,显示时舍入的百分比不用于判断触顶 diff --git a/frontend/src/api/modules/accounts.ts b/frontend/src/api/modules/accounts.ts index 50b65a13e..e2c21fa86 100644 --- a/frontend/src/api/modules/accounts.ts +++ b/frontend/src/api/modules/accounts.ts @@ -35,6 +35,7 @@ export interface AccountQuotaCredits { } export interface AccountQuota { + refreshedAt: string | null refreshedAtDisplay: string limitReached: boolean // 429 临时限流(Redis 冷却)到期时间;非限流中为 null。 diff --git a/frontend/src/api/modules/dashboard.ts b/frontend/src/api/modules/dashboard.ts index 306d3769a..b83bafb80 100644 --- a/frontend/src/api/modules/dashboard.ts +++ b/frontend/src/api/modules/dashboard.ts @@ -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 } diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts index 322fb4ab7..7bd585d81 100644 --- a/frontend/src/api/modules/usage.ts +++ b/frontend/src/api/modules/usage.ts @@ -229,6 +229,7 @@ export interface UsageSummaryResponse { cacheWriteInputTokens: string totalTokens: string averageLatencyMs: string + averageLatencyMsValue: number | null } export interface UsageOverviewHealthPoint { diff --git a/frontend/src/i18n/locales/en/accounts.json b/frontend/src/i18n/locales/en/accounts.json index ae162c67b..fc8651374 100644 --- a/frontend/src/i18n/locales/en/accounts.json +++ b/frontend/src/i18n/locales/en/accounts.json @@ -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" diff --git a/frontend/src/i18n/locales/en/dashboard.json b/frontend/src/i18n/locales/en/dashboard.json index dee2232a8..524cd236c 100644 --- a/frontend/src/i18n/locales/en/dashboard.json +++ b/frontend/src/i18n/locales/en/dashboard.json @@ -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", diff --git a/frontend/src/i18n/locales/en/keys.json b/frontend/src/i18n/locales/en/keys.json index 5b6c14185..ace82bf3e 100644 --- a/frontend/src/i18n/locales/en/keys.json +++ b/frontend/src/i18n/locales/en/keys.json @@ -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": { diff --git a/frontend/src/i18n/locales/zh-Hans/accounts.json b/frontend/src/i18n/locales/zh-Hans/accounts.json index b4f452419..e05183e7b 100644 --- a/frontend/src/i18n/locales/zh-Hans/accounts.json +++ b/frontend/src/i18n/locales/zh-Hans/accounts.json @@ -141,7 +141,6 @@ "batchEdit": { "title": "批量编辑账号", "selectionTitle": "编辑 {count} 个账号", - "saveChanges": "保存更改", "proxyRequired": "请选择已通过测试的代理", "saved": "已更新 {count} 个账号", "staleAccount": "账号 {accountId} 的页面数据已失效,请重新选择" diff --git a/frontend/src/i18n/locales/zh-Hans/dashboard.json b/frontend/src/i18n/locales/zh-Hans/dashboard.json index ff8abf633..4c9f39c9a 100644 --- a/frontend/src/i18n/locales/zh-Hans/dashboard.json +++ b/frontend/src/i18n/locales/zh-Hans/dashboard.json @@ -29,6 +29,7 @@ "unavailableCount": "不可用 {value}" }, "activeAccounts": { + "todayTokens": "今日 Token", "title": "活跃账号用量", "sortByRecent": "最近使用排序", "empty": "暂无账号请求记录", diff --git a/frontend/src/i18n/locales/zh-Hans/keys.json b/frontend/src/i18n/locales/zh-Hans/keys.json index fe6fe84f1..095b9010d 100644 --- a/frontend/src/i18n/locales/zh-Hans/keys.json +++ b/frontend/src/i18n/locales/zh-Hans/keys.json @@ -68,7 +68,6 @@ "weeklyLimitUsd": "周限额(USD)", "maximumConcurrency": "最大并发", "requestsPerMinuteRpm": "每分钟请求数(RPM)", - "saveChanges": "保存更改", "updated": "API Key 已更新", "created": "API Key 已创建", "validation": { diff --git a/frontend/src/i18n/locales/zh-Hant/accounts.json b/frontend/src/i18n/locales/zh-Hant/accounts.json index 993d6cee2..54ca6f5aa 100644 --- a/frontend/src/i18n/locales/zh-Hant/accounts.json +++ b/frontend/src/i18n/locales/zh-Hant/accounts.json @@ -141,7 +141,6 @@ "batchEdit": { "title": "批量編輯賬號", "selectionTitle": "編輯 {count} 個賬號", - "saveChanges": "保存更改", "proxyRequired": "請選擇已通過測試的代理", "saved": "已更新 {count} 個賬號", "staleAccount": "賬號 {accountId} 的頁面數據已失效,請重新選擇" diff --git a/frontend/src/i18n/locales/zh-Hant/dashboard.json b/frontend/src/i18n/locales/zh-Hant/dashboard.json index f5d487ba7..cbb067027 100644 --- a/frontend/src/i18n/locales/zh-Hant/dashboard.json +++ b/frontend/src/i18n/locales/zh-Hant/dashboard.json @@ -29,6 +29,7 @@ "unavailableCount": "不可用 {value}" }, "activeAccounts": { + "todayTokens": "今日 Token", "title": "活躍賬號用量", "sortByRecent": "最近使用排序", "empty": "暫無賬號請求記錄", diff --git a/frontend/src/i18n/locales/zh-Hant/keys.json b/frontend/src/i18n/locales/zh-Hant/keys.json index 09d6909ca..ec27cf237 100644 --- a/frontend/src/i18n/locales/zh-Hant/keys.json +++ b/frontend/src/i18n/locales/zh-Hant/keys.json @@ -68,7 +68,6 @@ "weeklyLimitUsd": "周限額(USD)", "maximumConcurrency": "最大併發", "requestsPerMinuteRpm": "每分鐘請求數(RPM)", - "saveChanges": "保存更改", "updated": "API Key 已更新", "created": "API Key 已創建", "validation": { diff --git a/frontend/src/views/accounts/components/AccountBatchEditModal.vue b/frontend/src/views/accounts/components/AccountBatchEditModal.vue index 9c8b7a776..236d5fb14 100644 --- a/frontend/src/views/accounts/components/AccountBatchEditModal.vue +++ b/frontend/src/views/accounts/components/AccountBatchEditModal.vue @@ -65,7 +65,7 @@ const selectedGroupIds = defineModel('selectedGroupIds', { required: t :disabled="selectedCount === 0 || groupsLoading" @click="emit('save')" > - {{ t('accounts.batchEdit.saveChanges') }} + {{ t('common.actions.save') }} diff --git a/frontend/src/views/accounts/components/AccountEditModal.vue b/frontend/src/views/accounts/components/AccountEditModal.vue index 04004768f..24112da95 100644 --- a/frontend/src/views/accounts/components/AccountEditModal.vue +++ b/frontend/src/views/accounts/components/AccountEditModal.vue @@ -118,7 +118,7 @@ const selectedGroupIds = defineModel('selectedGroupIds', { required: t :disabled="!account || groupsLoading || (isOpenAiApiKeyAccount(account) && !configurationReady)" @click="emit('save')" > - {{ t('accounts.batchEdit.saveChanges') }} + {{ t('common.actions.save') }} diff --git a/frontend/src/views/accounts/components/account-quota-panel/index.vue b/frontend/src/views/accounts/components/account-quota-panel/index.vue index 99a862533..dad15460a 100644 --- a/frontend/src/views/accounts/components/account-quota-panel/index.vue +++ b/frontend/src/views/accounts/components/account-quota-panel/index.vue @@ -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) => { · - {{ t('accounts.quota.lastRefreshed', { refreshedAtDisplay: account.quota.refreshedAtDisplay }) }} + {{ t('accounts.quota.lastRefreshed', { refreshedAtDisplay: account.quota.refreshedAt ? refreshedAtDisplay : '—' }) }}

diff --git a/frontend/src/views/accounts/index.vue b/frontend/src/views/accounts/index.vue index 34b7d6eb7..affdcfce2 100644 --- a/frontend/src/views/accounts/index.vue +++ b/frontend/src/views/accounts/index.vue @@ -207,7 +207,6 @@ const {