fix: move admin time display to backend

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-06-25 12:44:51 +08:00
co-authored by Codex
parent 11f0308f5f
commit da64ece0c0
19 changed files with 264 additions and 150 deletions
+11 -2
View File
@@ -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<String>,
pub status: String,
pub access_token_expires_at: Option<String>,
pub access_token_expires_at_display: Option<String>,
pub added_at: String,
pub added_at_display: String,
pub updated_at: String,
pub updated_at_display: String,
}
impl From<AdminAccountMetadata> 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<AdminAccountMetadata> 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),
}
}
}
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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,
};
+9 -1
View File
@@ -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<String>,
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<String>,
pub last_used_at_display: String,
pub plaintext: String,
}
@@ -114,7 +118,9 @@ impl From<AdminStoredClientApiKey> 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<AdminCreatedClientApiKey> 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,
}
+31 -5
View File
@@ -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<DashboardAccountUsageData>,
pub service_statuses: Vec<DashboardServiceStatusData>,
pub event_logs: Vec<EventLog>,
pub event_logs: Vec<DashboardEventLogData>,
pub pool_summary: AccountPoolDiagnostics,
pub capacity_info: AccountCapacityDiagnostics,
pub rotation_strategy: Option<String>,
@@ -151,10 +151,23 @@ pub struct DashboardAccountUsageData {
pub requests: u64,
pub tokens: u64,
pub quota_used_percent: Option<f64>,
pub last_used_at: Option<String>,
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<String>,
pub route: Option<String>,
pub model: Option<String>,
pub status_code: Option<i64>,
pub latency_ms: Option<i64>,
}
#[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],
+1 -1
View File
@@ -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<Utc>,
}
+48 -11
View File
@@ -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<String>) -> Option<String> {
.filter(|value| !value.is_empty())
}
impl From<EventLog> for LogData {
fn from(log: EventLog) -> Self {
Self {
created_at_display: china_datetime(&log.created_at),
log,
}
}
}
impl From<AdminLogState> for LogStateData {
fn from(state: AdminLogState) -> Self {
Self {
+1 -1
View File
@@ -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,
};
+2 -41
View File
@@ -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<Utc>) -> 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<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
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;
+99
View File
@@ -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<Utc>) -> 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<Utc>) -> String {
value
.with_timezone(&china_offset())
.format("%Y-%m-%d %H:%M:%S")
.to_string()
}
/// 将 UTC 时间输出为中国时区日期。
pub fn china_date(value: &DateTime<Utc>) -> String {
value
.with_timezone(&china_offset())
.format("%Y-%m-%d")
.to_string()
}
/// 将 UTC 时间输出为中国时区时间。
pub fn china_time(value: &DateTime<Utc>) -> String {
value
.with_timezone(&china_offset())
.format("%H:%M:%S")
.to_string()
}
/// 将 UTC 时间输出为相对时间,超过 7 天时返回中国时区日期。
pub fn china_relative_time(value: Option<DateTime<Utc>>, now: DateTime<Utc>) -> 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<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&china_rfc3339(value))
}
fn china_offset() -> FixedOffset {
FixedOffset::east_opt(CHINA_OFFSET_SECONDS).expect("valid China timezone offset")
}
+33 -1
View File
@@ -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();
+3
View File
@@ -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 {
+2
View File
@@ -9,7 +9,9 @@ export interface ClientApiKey {
prefix: string
enabled: boolean
createdAt: string
createdAtDisplay: string
lastUsedAt?: string
lastUsedAtDisplay: string
}
export interface CreateApiKeyPayload {
+13 -3
View File
@@ -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
+1
View File
@@ -20,6 +20,7 @@ export interface EventLog {
message: string
metadata?: any
createdAt: string
createdAtDisplay: string
}
export interface LogsQuery {
+2 -21
View File
@@ -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(() => {
<template #updatedAt="{ row }">
<span class="text-(--cp-text-secondary)">
{{ formatDate(row.updatedAt) }}
{{ row.updatedAtDisplay }}
</span>
</template>
<template #accessTokenExpiresAt="{ row }">
<span class="text-(--cp-text-secondary)">
{{ formatDate(row.accessTokenExpiresAt) }}
{{ row.accessTokenExpiresAtDisplay || '—' }}
</span>
</template>
+2 -18
View File
@@ -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, () => {
<template #createdAt="{ row }">
<span class="text-(--cp-text-secondary)">
{{ formatDate(row.createdAt) }}
{{ row.createdAtDisplay }}
</span>
</template>
<template #lastUsedAt="{ row }">
<span class="text-(--cp-text-secondary)">
{{ formatDate(row.lastUsedAt) }}
{{ row.lastUsedAtDisplay }}
</span>
</template>
@@ -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(() => {
+2 -15
View File
@@ -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<string, string> = {
info: '信息',
@@ -249,7 +236,7 @@ onBeforeUnmount(() => {
>
<template #createdAt="{ row }">
<span class="font-mono text-(--cp-text-secondary)">
{{ formatTime(row.createdAt) }}
{{ row.createdAtDisplay }}
</span>
</template>
@@ -345,7 +332,7 @@ onBeforeUnmount(() => {
<div>
<label class="block text-[11px] font-bold text-(--cp-text-muted) mb-1">时间</label>
<p class="m-0 text-[13px] text-(--cp-text-primary)">
{{ formatTime(selectedLog.createdAt) }}
{{ selectedLog.createdAtDisplay }}
</p>
</div>
<div>