mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-13 08:06:41 +00:00
fix: move admin time display to backend
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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,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};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,9 @@ export interface ClientApiKey {
|
||||
prefix: string
|
||||
enabled: boolean
|
||||
createdAt: string
|
||||
createdAtDisplay: string
|
||||
lastUsedAt?: string
|
||||
lastUsedAtDisplay: string
|
||||
}
|
||||
|
||||
export interface CreateApiKeyPayload {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface EventLog {
|
||||
message: string
|
||||
metadata?: any
|
||||
createdAt: string
|
||||
createdAtDisplay: string
|
||||
}
|
||||
|
||||
export interface LogsQuery {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user