diff --git a/src/admin/accounts/routes.rs b/src/admin/accounts/routes.rs index fcf3c8064..43a806c3e 100644 --- a/src/admin/accounts/routes.rs +++ b/src/admin/accounts/routes.rs @@ -17,8 +17,8 @@ use crate::{ admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, http::middleware::request_id::RequestId, infra::{ - china_rfc3339, china_rfc3339_str, json::{clamp_limit, clamp_page, Page}, + time::{china_datetime, china_rfc3339, china_rfc3339_str}, }, runtime::state::AppState, upstream::accounts::store::StoredAccount, @@ -100,12 +100,18 @@ pub struct AdminAccountData { pub plan_type: Option, pub status: String, pub access_token_expires_at: Option, + pub access_token_expires_at_display: Option, pub added_at: String, + pub added_at_display: String, pub updated_at: String, + pub updated_at_display: String, } impl From for AdminAccountData { fn from(a: AdminAccountMetadata) -> Self { + let access_token_expires_at = a.access_token_expires_at.as_ref().map(china_rfc3339); + let access_token_expires_at_display = + a.access_token_expires_at.as_ref().map(china_datetime); Self { id: a.id, email: a.email, @@ -114,9 +120,12 @@ impl From for AdminAccountData { label: a.label, plan_type: a.plan_type, status: account_status_str(a.status).to_string(), - access_token_expires_at: a.access_token_expires_at.map(|dt| china_rfc3339(&dt)), + access_token_expires_at, + access_token_expires_at_display, added_at: china_rfc3339(&a.added_at), + added_at_display: china_datetime(&a.added_at), updated_at: china_rfc3339(&a.updated_at), + updated_at_display: china_datetime(&a.updated_at), } } } diff --git a/src/admin/accounts/service/quota.rs b/src/admin/accounts/service/quota.rs index d13aea81b..9560b1296 100644 --- a/src/admin/accounts/service/quota.rs +++ b/src/admin/accounts/service/quota.rs @@ -1,7 +1,7 @@ use chrono::Utc; use secrecy::ExposeSecret; -use crate::infra::china_rfc3339; +use crate::infra::time::china_rfc3339; use super::{types::AdminAccountError, AdminAccountService}; diff --git a/src/admin/auth/session.rs b/src/admin/auth/session.rs index cb3760849..2052fe9e7 100644 --- a/src/admin/auth/session.rs +++ b/src/admin/auth/session.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{ admin::response::{AdminEnvelope, AdminError, AdminResponse}, http::middleware::request_id::RequestId, - infra::china_rfc3339, + infra::time::china_rfc3339, runtime::state::AppState, }; diff --git a/src/admin/keys/routes.rs b/src/admin/keys/routes.rs index 0c694f5cf..c0374cd6b 100644 --- a/src/admin/keys/routes.rs +++ b/src/admin/keys/routes.rs @@ -17,8 +17,8 @@ use crate::{ admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, http::middleware::request_id::RequestId, infra::{ - china_rfc3339_str, json::{clamp_limit, Page}, + time::{china_relative_time_str, china_rfc3339_str}, }, runtime::state::AppState, }; @@ -61,7 +61,9 @@ pub struct ClientApiKeyData { pub prefix: String, pub enabled: bool, pub created_at: String, + pub created_at_display: String, pub last_used_at: Option, + pub last_used_at_display: String, } #[derive(Debug, Serialize)] @@ -73,7 +75,9 @@ pub struct CreatedClientApiKeyData { pub prefix: String, pub enabled: bool, pub created_at: String, + pub created_at_display: String, pub last_used_at: Option, + pub last_used_at_display: String, pub plaintext: String, } @@ -114,7 +118,9 @@ impl From for ClientApiKeyData { label: k.label, prefix: k.prefix, enabled: k.enabled, + created_at_display: china_relative_time_str(Some(&k.created_at)), created_at: china_rfc3339_str(&k.created_at), + last_used_at_display: china_relative_time_str(k.last_used_at.as_deref()), last_used_at: k.last_used_at.as_deref().map(china_rfc3339_str), } } @@ -142,7 +148,9 @@ impl From for CreatedClientApiKeyData { label: k.label, prefix: k.prefix, enabled: k.enabled, + created_at_display: china_relative_time_str(Some(&k.created_at)), created_at: china_rfc3339_str(&k.created_at), + last_used_at_display: china_relative_time_str(k.last_used_at.as_deref()), last_used_at: k.last_used_at.as_deref().map(china_rfc3339_str), plaintext: k.plaintext, } diff --git a/src/admin/monitoring/dashboard.rs b/src/admin/monitoring/dashboard.rs index f0c1e951b..6dbda90f4 100644 --- a/src/admin/monitoring/dashboard.rs +++ b/src/admin/monitoring/dashboard.rs @@ -27,7 +27,7 @@ use crate::{ response::{AdminEnvelope, AdminError, AdminResponse}, }, http::middleware::request_id::RequestId, - infra::{china_datetime_rfc3339_str, china_rfc3339}, + infra::time::{china_datetime_rfc3339_str, china_relative_time, china_time}, runtime::state::AppState, upstream::accounts::model::{Account, AccountStatus}, }; @@ -56,7 +56,7 @@ pub struct DashboardSummaryData { pub trend: DashboardTrendData, pub account_usage: Vec, pub service_statuses: Vec, - pub event_logs: Vec, + pub event_logs: Vec, pub pool_summary: AccountPoolDiagnostics, pub capacity_info: AccountCapacityDiagnostics, pub rotation_strategy: Option, @@ -151,10 +151,23 @@ pub struct DashboardAccountUsageData { pub requests: u64, pub tokens: u64, pub quota_used_percent: Option, - pub last_used_at: Option, + pub last_used: String, pub status: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DashboardEventLogData { + pub id: String, + pub time: String, + pub level: EventLevel, + pub request_id: Option, + pub route: Option, + pub model: Option, + pub status_code: Option, + pub latency_ms: Option, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardServiceStatusData { @@ -246,7 +259,7 @@ pub async fn dashboard_summary( .unwrap_or_default(); let pool_summary = account_pool_summary(&accounts); - let dashboard_logs = logs.iter().take(10).cloned().collect(); + let dashboard_logs = logs.iter().take(10).map(dashboard_event_log_data).collect(); let trend = dashboard_trend_data(&logs, DashboardTrendKind::Usage); let quota_used_by_account = account_quota_used_percent_by_id(&state, &usage_records).await; let settings = state.services.settings.current(); @@ -737,7 +750,7 @@ fn account_usage_data( requests: nonnegative_i64_to_u64(usage.request_count), tokens: nonnegative_i64_to_u64(usage.input_tokens + usage.output_tokens), quota_used_percent, - last_used_at: usage.last_used_at.map(|value| china_rfc3339(&value)), + last_used: china_relative_time(usage.last_used_at, Utc::now()), status, } }) @@ -747,6 +760,19 @@ fn account_usage_data( rows } +fn dashboard_event_log_data(log: &EventLog) -> DashboardEventLogData { + DashboardEventLogData { + id: log.id.clone(), + time: china_time(&log.created_at), + level: log.level, + request_id: log.request_id.clone(), + route: log.route.clone(), + model: log.model.clone(), + status_code: log.status_code, + latency_ms: log.latency_ms, + } +} + async fn account_quota_used_percent_by_id( state: &AppState, usage_records: &[AdminUsageRecord], diff --git a/src/admin/monitoring/events.rs b/src/admin/monitoring/events.rs index 30e0c2015..35c37e9c3 100644 --- a/src/admin/monitoring/events.rs +++ b/src/admin/monitoring/events.rs @@ -60,7 +60,7 @@ pub struct EventLog { /// 结构化元数据。 pub metadata: Value, /// 创建时间。 - #[serde(serialize_with = "crate::infra::serialize_china_rfc3339")] + #[serde(serialize_with = "crate::infra::time::serialize_china_rfc3339")] pub created_at: DateTime, } diff --git a/src/admin/monitoring/logs.rs b/src/admin/monitoring/logs.rs index 70d878097..383356090 100644 --- a/src/admin/monitoring/logs.rs +++ b/src/admin/monitoring/logs.rs @@ -11,14 +11,17 @@ use serde::{Deserialize, Serialize}; use crate::{ admin::monitoring::{ event_store::{AdminLogError, AdminLogFilter, AdminLogState, AdminLogStateUpdate}, - events::EventLevel, + events::{EventLevel, EventLog}, }, admin::{ auth::session::require_admin_session, response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, }, http::middleware::request_id::RequestId, - infra::json::{clamp_limit, clamp_page}, + infra::{ + json::{clamp_limit, clamp_page, NumberedPage, Page}, + time::china_datetime, + }, runtime::state::AppState, }; @@ -94,6 +97,17 @@ pub struct ClearLogsData { pub cleared: u64, } +/// 管理端日志响应。 +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogData { + /// 原始日志字段。 + #[serde(flatten)] + pub log: EventLog, + /// 中国时区展示时间。 + pub created_at_display: String, +} + /// 更新日志状态请求。 #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -128,19 +142,33 @@ pub async fn logs( .list_page(clamp_page(page.unwrap_or(1)), limit, filter) .await { - Ok(page) => Ok(AdminResponse::new( - StatusCode::OK, - AdminPageEnvelope::numbered(page, request_id), - )), + Ok(page) => { + let page = NumberedPage { + items: page.items.into_iter().map(LogData::from).collect(), + total: page.total, + page: page.page, + page_size: page.page_size, + }; + Ok(AdminResponse::new( + StatusCode::OK, + AdminPageEnvelope::numbered(page, request_id), + )) + } Err(error) => Err(log_error(error, request_id)), }; } match state.services.logs.list(cursor, limit, filter).await { - Ok(page) => Ok(AdminResponse::new( - StatusCode::OK, - AdminPageEnvelope::ok(page, limit, request_id), - )), + Ok(page) => { + let page = Page { + items: page.items.into_iter().map(LogData::from).collect(), + next_cursor: page.next_cursor, + }; + Ok(AdminResponse::new( + StatusCode::OK, + AdminPageEnvelope::ok(page, limit, request_id), + )) + } Err(error) => Err(log_error(error, request_id)), } } @@ -157,7 +185,7 @@ pub async fn log_detail( match state.services.logs.get(&query.id).await { Ok(Some(log)) => Ok(AdminResponse::new( StatusCode::OK, - AdminEnvelope::ok(log, request_id), + AdminEnvelope::ok(LogData::from(log), request_id), )), Ok(None) => Err(AdminError::new( StatusCode::NOT_FOUND, @@ -288,6 +316,15 @@ fn non_empty(value: Option) -> Option { .filter(|value| !value.is_empty()) } +impl From for LogData { + fn from(log: EventLog) -> Self { + Self { + created_at_display: china_datetime(&log.created_at), + log, + } + } +} + impl From for LogStateData { fn from(state: AdminLogState) -> Self { Self { diff --git a/src/admin/monitoring/usage.rs b/src/admin/monitoring/usage.rs index 22bc1551f..d98cce18d 100644 --- a/src/admin/monitoring/usage.rs +++ b/src/admin/monitoring/usage.rs @@ -14,8 +14,8 @@ use crate::{ admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse}, http::middleware::request_id::RequestId, infra::{ - china_rfc3339, json::{clamp_limit, clamp_page, Page}, + time::china_rfc3339, }, runtime::state::AppState, }; diff --git a/src/infra/mod.rs b/src/infra/mod.rs index 4fcd8cc55..9423daef3 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -2,11 +2,6 @@ //! 平台基础设施层,承载加密、身份、存储、日志和 JSON 原语。 -use chrono::{DateTime, FixedOffset, Utc}; -use serde::Serializer; - -const CHINA_OFFSET_SECONDS: i32 = 8 * 60 * 60; - /// 加密原语。 pub mod crypto; /// 数据库连接与 SQLite 管理。 @@ -19,39 +14,5 @@ pub mod json; pub mod logging; /// 路径和安装 ID 辅助。 pub mod paths; - -/// 将 UTC 时间输出为中国时区 RFC3339 字符串。 -pub fn china_rfc3339(value: &DateTime) -> String { - value.with_timezone(&china_offset()).to_rfc3339() -} - -/// 将 RFC3339 字符串输出为中国时区 RFC3339 字符串。 -pub fn china_rfc3339_str(value: &str) -> String { - DateTime::parse_from_rfc3339(value) - .map(|datetime| datetime.with_timezone(&china_offset()).to_rfc3339()) - .unwrap_or_else(|_| value.to_string()) -} - -/// 将 RFC3339 字符串输出为中国时区日期时间。 -pub fn china_datetime_rfc3339_str(value: &str) -> String { - DateTime::parse_from_rfc3339(value) - .map(|datetime| { - datetime - .with_timezone(&china_offset()) - .format("%Y-%m-%d %H:%M:%S") - .to_string() - }) - .unwrap_or_else(|_| value.to_string()) -} - -/// Serde 序列化 UTC 时间为中国时区 RFC3339 字符串。 -pub fn serialize_china_rfc3339(value: &DateTime, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&china_rfc3339(value)) -} - -fn china_offset() -> FixedOffset { - FixedOffset::east_opt(CHINA_OFFSET_SECONDS).expect("valid China timezone offset") -} +/// 时间格式化辅助。 +pub mod time; diff --git a/src/infra/time.rs b/src/infra/time.rs new file mode 100644 index 000000000..9d7c3fb66 --- /dev/null +++ b/src/infra/time.rs @@ -0,0 +1,99 @@ +//! 时间格式化辅助。 + +use chrono::{DateTime, FixedOffset, Utc}; +use serde::Serializer; + +const CHINA_OFFSET_SECONDS: i32 = 8 * 60 * 60; + +/// 将 UTC 时间输出为中国时区 RFC3339 字符串。 +pub fn china_rfc3339(value: &DateTime) -> String { + value.with_timezone(&china_offset()).to_rfc3339() +} + +/// 将 RFC3339 字符串输出为中国时区 RFC3339 字符串。 +pub fn china_rfc3339_str(value: &str) -> String { + DateTime::parse_from_rfc3339(value) + .map(|datetime| datetime.with_timezone(&china_offset()).to_rfc3339()) + .unwrap_or_else(|_| value.to_string()) +} + +/// 将 RFC3339 字符串输出为中国时区日期时间。 +pub fn china_datetime_rfc3339_str(value: &str) -> String { + DateTime::parse_from_rfc3339(value) + .map(|datetime| { + datetime + .with_timezone(&china_offset()) + .format("%Y-%m-%d %H:%M:%S") + .to_string() + }) + .unwrap_or_else(|_| value.to_string()) +} + +/// 将 UTC 时间输出为中国时区日期时间。 +pub fn china_datetime(value: &DateTime) -> String { + value + .with_timezone(&china_offset()) + .format("%Y-%m-%d %H:%M:%S") + .to_string() +} + +/// 将 UTC 时间输出为中国时区日期。 +pub fn china_date(value: &DateTime) -> String { + value + .with_timezone(&china_offset()) + .format("%Y-%m-%d") + .to_string() +} + +/// 将 UTC 时间输出为中国时区时间。 +pub fn china_time(value: &DateTime) -> String { + value + .with_timezone(&china_offset()) + .format("%H:%M:%S") + .to_string() +} + +/// 将 UTC 时间输出为相对时间,超过 7 天时返回中国时区日期。 +pub fn china_relative_time(value: Option>, now: DateTime) -> String { + let Some(value) = value else { + return "-".to_string(); + }; + let diff = now.signed_duration_since(value); + let minutes = diff.num_minutes(); + let hours = diff.num_hours(); + let days = diff.num_days(); + + if minutes < 1 { + "刚刚".to_string() + } else if minutes < 60 { + format!("{minutes}分钟前") + } else if hours < 24 { + format!("{hours}小时前") + } else if days < 7 { + format!("{days}天前") + } else { + china_date(&value) + } +} + +/// 将 RFC3339 字符串输出为相对时间,超过 7 天时返回中国时区日期。 +pub fn china_relative_time_str(value: Option<&str>) -> String { + let Some(value) = value else { + return "-".to_string(); + }; + DateTime::parse_from_rfc3339(value) + .map(|datetime| china_relative_time(Some(datetime.with_timezone(&Utc)), Utc::now())) + .unwrap_or_else(|_| value.to_string()) +} + +/// Serde 序列化 UTC 时间为中国时区 RFC3339 字符串。 +pub fn serialize_china_rfc3339(value: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&china_rfc3339(value)) +} + +fn china_offset() -> FixedOffset { + FixedOffset::east_opt(CHINA_OFFSET_SECONDS).expect("valid China timezone offset") +} diff --git a/tests/admin/monitoring/dashboard.rs b/tests/admin/monitoring/dashboard.rs index 3129d3d2b..462d32093 100644 --- a/tests/admin/monitoring/dashboard.rs +++ b/tests/admin/monitoring/dashboard.rs @@ -191,6 +191,28 @@ async fn dashboard_summary_should_fallback_total_cost_to_usage_summary_when_logs ); } +#[tokio::test] +async fn dashboard_summary_should_return_backend_formatted_time_fields() { + let (app, store, pool, _dir) = dashboard_test_app( + "dashboard-backend-formatted-time.sqlite", + crate::support::fingerprint::test_fingerprint(), + ) + .await; + store + .append(&usage_log_with_tokens( + "2026-06-18T12:34:56Z".parse().unwrap(), + 10, + )) + .await + .unwrap(); + seed_usage_summary_with_last_used(&pool, 1, 1, "2000-01-01T00:00:00Z").await; + + let body = dashboard_summary(app).await; + + assert_eq!(body["data"]["eventLogs"][0]["time"], "20:34:56"); + assert_eq!(body["data"]["accountUsage"][0]["lastUsed"], "2000-01-01"); +} + async fn dashboard_summary_total_cost_for_log(db_name: &str, model: &str, metadata: Value) -> f64 { let (app, store, _pool, _dir) = dashboard_test_app(db_name, crate::support::fingerprint::test_fingerprint()).await; @@ -286,6 +308,16 @@ async fn seed_admin_session(pool: &SqlitePool, session_id: &str) { } async fn seed_usage_summary(pool: &SqlitePool, input_tokens: u64, output_tokens: u64) { + seed_usage_summary_with_last_used(pool, input_tokens, output_tokens, "2026-06-18T00:00:00Z") + .await; +} + +async fn seed_usage_summary_with_last_used( + pool: &SqlitePool, + input_tokens: u64, + output_tokens: u64, + last_used_at: &str, +) { sqlx::query( "insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?)", ) @@ -305,7 +337,7 @@ async fn seed_usage_summary(pool: &SqlitePool, input_tokens: u64, output_tokens: .bind(input_tokens as i64) .bind(output_tokens as i64) .bind((input_tokens + output_tokens) as i64) - .bind("2026-06-18T00:00:00Z") + .bind(last_used_at) .execute(pool) .await .unwrap(); diff --git a/web/src/api/modules/accounts.ts b/web/src/api/modules/accounts.ts index 4b6fc9bb2..5e41ebafd 100644 --- a/web/src/api/modules/accounts.ts +++ b/web/src/api/modules/accounts.ts @@ -10,8 +10,11 @@ export interface Account { status: 'active' | 'expired' | 'disabled' | 'banned' | 'quota_exhausted' | 'refreshing' label?: string accessTokenExpiresAt?: string + accessTokenExpiresAtDisplay?: string addedAt: string + addedAtDisplay: string updatedAt: string + updatedAtDisplay: string } export interface AccountsQuery { diff --git a/web/src/api/modules/api-keys.ts b/web/src/api/modules/api-keys.ts index bd3f6cace..f24899388 100644 --- a/web/src/api/modules/api-keys.ts +++ b/web/src/api/modules/api-keys.ts @@ -9,7 +9,9 @@ export interface ClientApiKey { prefix: string enabled: boolean createdAt: string + createdAtDisplay: string lastUsedAt?: string + lastUsedAtDisplay: string } export interface CreateApiKeyPayload { diff --git a/web/src/api/modules/dashboard.ts b/web/src/api/modules/dashboard.ts index b9de79614..217d81f56 100644 --- a/web/src/api/modules/dashboard.ts +++ b/web/src/api/modules/dashboard.ts @@ -1,5 +1,4 @@ import { requestJson } from '../request' -import type { EventLog } from './logs' export type DashboardTrendKind = 'usage' | 'latency' | 'errors' @@ -8,7 +7,7 @@ export interface DashboardSummary { trend: DashboardTrend accountUsage: DashboardAccountUsage[] serviceStatuses: DashboardServiceStatus[] - eventLogs: EventLog[] + eventLogs: DashboardEventLog[] poolSummary: DashboardAccountPoolSummary capacityInfo: DashboardAccountCapacityInfo rotationStrategy?: string @@ -85,10 +84,21 @@ export interface DashboardAccountUsage { requests: number tokens: number quotaUsedPercent?: number | null - lastUsedAt?: string | null + lastUsed: string status: 'active' | 'expired' | 'disabled' | 'banned' | 'quota_exhausted' | 'refreshing' } +export interface DashboardEventLog { + id: string + time: string + level: 'debug' | 'info' | 'warn' | 'error' + requestId?: string + route?: string + model?: string + statusCode?: number + latencyMs?: number +} + export interface DashboardServiceStatus { label: string value: string diff --git a/web/src/api/modules/logs.ts b/web/src/api/modules/logs.ts index cf4a97126..3063b0675 100644 --- a/web/src/api/modules/logs.ts +++ b/web/src/api/modules/logs.ts @@ -20,6 +20,7 @@ export interface EventLog { message: string metadata?: any createdAt: string + createdAtDisplay: string } export interface LogsQuery { diff --git a/web/src/views/accounts/AccountsView.vue b/web/src/views/accounts/AccountsView.vue index a0189cc34..3a5604cba 100644 --- a/web/src/views/accounts/AccountsView.vue +++ b/web/src/views/accounts/AccountsView.vue @@ -231,25 +231,6 @@ function handlePageSizeChange(nextPageSize: number) { void loadAccounts() } -function formatDate(dateStr?: string): string { - if (!dateStr) return '—' - const date = new Date(dateStr) - if (Number.isNaN(date.getTime())) return dateStr - - return new Intl.DateTimeFormat('zh-CN', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }) - .format(date) - .replaceAll('/', '-') -} - onMounted(() => { loadAccounts() }) @@ -399,13 +380,13 @@ onBeforeUnmount(() => { diff --git a/web/src/views/api-keys/ApiKeysView.vue b/web/src/views/api-keys/ApiKeysView.vue index 6664f5aec..044e8c313 100644 --- a/web/src/views/api-keys/ApiKeysView.vue +++ b/web/src/views/api-keys/ApiKeysView.vue @@ -256,22 +256,6 @@ function copyToClipboard(text: string) { }) } -function formatDate(dateStr?: string): string { - if (!dateStr) return '—' - const date = new Date(dateStr) - const now = new Date() - const diff = now.getTime() - date.getTime() - const minutes = Math.floor(diff / 60000) - const hours = Math.floor(diff / 3600000) - const days = Math.floor(diff / 86400000) - - if (minutes < 1) return '刚刚' - if (minutes < 60) return `${minutes}分钟前` - if (hours < 24) return `${hours}小时前` - if (days < 7) return `${days}天前` - return date.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' }) -} - function maskKey(prefix: string): string { return `${prefix}••••••••••••••••` } @@ -449,13 +433,13 @@ watch(filteredKeys, () => { diff --git a/web/src/views/dashboard/composables/useDashboard.ts b/web/src/views/dashboard/composables/useDashboard.ts index 45df3e01a..4898fc4a4 100644 --- a/web/src/views/dashboard/composables/useDashboard.ts +++ b/web/src/views/dashboard/composables/useDashboard.ts @@ -97,7 +97,7 @@ export function useDashboard(): { serviceStatuses.value = summary.serviceStatuses.map(serviceStatusItem) eventLogs.value = summary.eventLogs.map((log) => ({ id: log.id, - time: formatTime(log.createdAt), + time: log.time, level: levelLabel(log.level), requestId: log.requestId || '-', route: log.route || '-', @@ -270,7 +270,7 @@ export function useDashboard(): { plan: item.plan || 'free', requests: formatNumber(item.requests), tokens: formatTokens(item.tokens), - lastUsed: formatRelativeTime(item.lastUsedAt ?? undefined), + lastUsed: item.lastUsed || '-', tone, quotaPercent, quotaTone: quotaTone(quotaPercent), @@ -385,32 +385,6 @@ export function useDashboard(): { return `${ms}ms` } - function formatRelativeTime(dateStr: string | undefined): string { - if (!dateStr) return '-' - const date = new Date(dateStr) - const now = new Date() - const diff = now.getTime() - date.getTime() - const minutes = Math.floor(diff / 60000) - const hours = Math.floor(diff / 3600000) - const days = Math.floor(diff / 86400000) - - if (minutes < 1) return '刚刚' - if (minutes < 60) return `${minutes}分钟前` - if (hours < 24) return `${hours}小时前` - if (days < 7) return `${days}天前` - return date.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' }) - } - - function formatTime(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleTimeString('zh-CN', { - timeZone: 'Asia/Shanghai', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }) - } - function startAutoRefresh() { stopAutoRefresh() autoRefreshTimer.value = setInterval(() => { diff --git a/web/src/views/logs/LogsView.vue b/web/src/views/logs/LogsView.vue index bc9a82575..b3fd698c2 100644 --- a/web/src/views/logs/LogsView.vue +++ b/web/src/views/logs/LogsView.vue @@ -143,19 +143,6 @@ async function handleViewDetail(log: EventLog) { } } -function formatTime(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleString('zh-CN', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }) -} - function getLevelLabel(level: string): string { const labels: Record = { info: '信息', @@ -249,7 +236,7 @@ onBeforeUnmount(() => { > @@ -345,7 +332,7 @@ onBeforeUnmount(() => {

- {{ formatTime(selectedLog.createdAt) }} + {{ selectedLog.createdAtDisplay }}