修复时区展示
This commit is contained in:
@@ -286,8 +286,17 @@ pub fn parse_jsonl_file(
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| encoded_project_name.to_string());
|
.unwrap_or_else(|| encoded_project_name.to_string());
|
||||||
|
|
||||||
|
// 转换时间戳为本地时间格式
|
||||||
|
let local_timestamp = if let Ok(dt) = DateTime::parse_from_rfc3339(&entry.timestamp) {
|
||||||
|
// 转换为本地时区并格式化为 ISO 格式
|
||||||
|
dt.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S%.3f").to_string()
|
||||||
|
} else {
|
||||||
|
// 如果解析失败,保留原始时间戳
|
||||||
|
entry.timestamp.clone()
|
||||||
|
};
|
||||||
|
|
||||||
entries.push(UsageEntry {
|
entries.push(UsageEntry {
|
||||||
timestamp: entry.timestamp,
|
timestamp: local_timestamp,
|
||||||
model: message
|
model: message
|
||||||
.model
|
.model
|
||||||
.clone()
|
.clone()
|
||||||
@@ -398,17 +407,23 @@ pub fn get_usage_stats(days: Option<u32>) -> Result<UsageStats, String> {
|
|||||||
// Filter by days if specified
|
// Filter by days if specified
|
||||||
let filtered_entries = if let Some(days) = days {
|
let filtered_entries = if let Some(days) = days {
|
||||||
// Convert 'now' to local date for consistent comparison
|
// Convert 'now' to local date for consistent comparison
|
||||||
let cutoff = Local::now().with_timezone(&Local).date_naive() - chrono::Duration::days(days as i64);
|
let cutoff = Local::now().date_naive() - chrono::Duration::days(days as i64);
|
||||||
all_entries
|
all_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
// Convert each entry timestamp to local time, then compare dates
|
let date = if e.timestamp.contains(' ') {
|
||||||
let local_date = dt.with_timezone(&Local).date_naive();
|
// 新格式:直接解析日期部分
|
||||||
local_date >= cutoff
|
e.timestamp.split(' ').next()
|
||||||
|
.and_then(|date_str| NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok())
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
|
Some(dt.with_timezone(&Local).date_naive())
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
}
|
};
|
||||||
|
|
||||||
|
date.map_or(false, |d| d >= cutoff)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -469,7 +484,12 @@ pub fn get_usage_stats(days: Option<u32>) -> Result<UsageStats, String> {
|
|||||||
.insert(entry.session_id.clone());
|
.insert(entry.session_id.clone());
|
||||||
|
|
||||||
// Update daily stats (use local timezone date)
|
// Update daily stats (use local timezone date)
|
||||||
let date = if let Ok(dt) = DateTime::parse_from_rfc3339(&entry.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
|
let date = if entry.timestamp.contains(' ') {
|
||||||
|
// 新格式:直接提取日期部分
|
||||||
|
entry.timestamp.split(' ').next().unwrap_or(&entry.timestamp).to_string()
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&entry.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
dt.with_timezone(&Local).date_naive().to_string()
|
dt.with_timezone(&Local).date_naive().to_string()
|
||||||
} else {
|
} else {
|
||||||
// Fallback to raw prefix if parse fails
|
// Fallback to raw prefix if parse fails
|
||||||
@@ -608,12 +628,19 @@ pub fn get_usage_by_date_range(start_date: String, end_date: String) -> Result<U
|
|||||||
let filtered_entries: Vec<_> = all_entries
|
let filtered_entries: Vec<_> = all_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
let date = dt.with_timezone(&Local).date_naive();
|
let date = if e.timestamp.contains(' ') {
|
||||||
date >= start && date <= end
|
// 新格式:直接解析日期部分
|
||||||
|
e.timestamp.split(' ').next()
|
||||||
|
.and_then(|date_str| NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok())
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
|
Some(dt.with_timezone(&Local).date_naive())
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
}
|
};
|
||||||
|
|
||||||
|
date.map_or(false, |d| d >= start && d <= end)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -686,9 +713,15 @@ pub fn get_usage_by_date_range(start_date: String, end_date: String) -> Result<U
|
|||||||
.insert(entry.session_id.clone());
|
.insert(entry.session_id.clone());
|
||||||
|
|
||||||
// Update daily stats (use local timezone date)
|
// Update daily stats (use local timezone date)
|
||||||
let date = if let Ok(dt) = DateTime::parse_from_rfc3339(&entry.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
|
let date = if entry.timestamp.contains(' ') {
|
||||||
|
// 新格式:直接提取日期部分
|
||||||
|
entry.timestamp.split(' ').next().unwrap_or(&entry.timestamp).to_string()
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&entry.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
dt.with_timezone(&Local).date_naive().to_string()
|
dt.with_timezone(&Local).date_naive().to_string()
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback to raw prefix if parse fails
|
||||||
entry
|
entry
|
||||||
.timestamp
|
.timestamp
|
||||||
.split('T')
|
.split('T')
|
||||||
@@ -817,12 +850,18 @@ pub fn get_usage_details(
|
|||||||
// Filter by date if specified (compare against local date string YYYY-MM-DD)
|
// Filter by date if specified (compare against local date string YYYY-MM-DD)
|
||||||
if let Some(date) = date {
|
if let Some(date) = date {
|
||||||
all_entries.retain(|e| {
|
all_entries.retain(|e| {
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
let local_date_str = dt.with_timezone(&Local).date_naive().to_string();
|
let entry_date = if e.timestamp.contains(' ') {
|
||||||
local_date_str == date
|
// 新格式:直接提取日期部分
|
||||||
|
e.timestamp.split(' ').next().map(|s| s.to_string())
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
|
Some(dt.with_timezone(&Local).date_naive().to_string())
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
}
|
};
|
||||||
|
|
||||||
|
entry_date.map_or(false, |d| d == date)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,10 +886,21 @@ pub fn get_session_stats(
|
|||||||
let filtered_entries: Vec<_> = all_entries
|
let filtered_entries: Vec<_> = all_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
// 处理新的本地时间格式 "YYYY-MM-DD HH:MM:SS.sss"
|
||||||
let date = dt.with_timezone(&Local).date_naive();
|
let date = if e.timestamp.contains(' ') {
|
||||||
let is_after_since = since_date.map_or(true, |s| date >= s);
|
// 新格式:直接解析日期部分
|
||||||
let is_before_until = until_date.map_or(true, |u| date <= u);
|
e.timestamp.split(' ').next()
|
||||||
|
.and_then(|date_str| NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok())
|
||||||
|
} else if let Ok(dt) = DateTime::parse_from_rfc3339(&e.timestamp) {
|
||||||
|
// 旧格式:RFC3339 格式
|
||||||
|
Some(dt.with_timezone(&Local).date_naive())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(d) = date {
|
||||||
|
let is_after_since = since_date.map_or(true, |s| d >= s);
|
||||||
|
let is_before_until = until_date.map_or(true, |u| d <= u);
|
||||||
is_after_since && is_before_until
|
is_after_since && is_before_until
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@@ -1,40 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* Formats a Unix timestamp to a human-readable date string
|
* Formats a Unix timestamp to a human-readable date string
|
||||||
* @param timestamp - Unix timestamp in seconds
|
* @param timestamp - Unix timestamp in seconds
|
||||||
|
* @param locale - Optional locale string (e.g., 'en-US', 'zh-CN')
|
||||||
* @returns Formatted date string
|
* @returns Formatted date string
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* formatUnixTimestamp(1735555200) // "Dec 30, 2024"
|
* formatUnixTimestamp(1735555200) // "Dec 30, 2024"
|
||||||
|
* formatUnixTimestamp(1735555200, "zh-CN") // "12月30日, 2024年"
|
||||||
*/
|
*/
|
||||||
export function formatUnixTimestamp(timestamp: number): string {
|
export function formatUnixTimestamp(timestamp: number, locale?: string): string {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const effectiveLocale = locale || navigator.language || 'en-US';
|
||||||
|
const isZhCN = effectiveLocale.startsWith('zh');
|
||||||
|
|
||||||
// If it's today, show time
|
// If it's today, show time
|
||||||
if (isToday(date)) {
|
if (isToday(date)) {
|
||||||
return formatTime(date);
|
return formatTime(date, effectiveLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's yesterday
|
// If it's yesterday
|
||||||
if (isYesterday(date)) {
|
if (isYesterday(date)) {
|
||||||
return `Yesterday, ${formatTime(date)}`;
|
const yesterdayLabel = isZhCN ? '昨天' : 'Yesterday';
|
||||||
|
return `${yesterdayLabel}, ${formatTime(date, effectiveLocale)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's within the last week, show day of week
|
// If it's within the last week, show day of week
|
||||||
if (isWithinWeek(date)) {
|
if (isWithinWeek(date)) {
|
||||||
return `${getDayName(date)}, ${formatTime(date)}`;
|
return `${getDayName(date, effectiveLocale)}, ${formatTime(date, effectiveLocale)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's this year, don't show year
|
// If it's this year, don't show year
|
||||||
if (date.getFullYear() === now.getFullYear()) {
|
if (date.getFullYear() === now.getFullYear()) {
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString(effectiveLocale, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise show full date
|
// Otherwise show full date
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString(effectiveLocale, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
@@ -44,14 +49,17 @@ export function formatUnixTimestamp(timestamp: number): string {
|
|||||||
/**
|
/**
|
||||||
* Formats an ISO timestamp string to a human-readable date
|
* Formats an ISO timestamp string to a human-readable date
|
||||||
* @param isoString - ISO timestamp string
|
* @param isoString - ISO timestamp string
|
||||||
|
* @param locale - Optional locale string (e.g., 'en-US', 'zh-CN')
|
||||||
* @returns Formatted date string
|
* @returns Formatted date string
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* formatISOTimestamp("2025-01-04T10:13:29.000Z") // "Jan 4, 2025"
|
* formatISOTimestamp("2025-01-04T10:13:29.000Z") // "Jan 4, 2025"
|
||||||
|
* formatISOTimestamp("2025-01-04T10:13:29.000Z", "zh-CN") // "1月4日, 2025"
|
||||||
*/
|
*/
|
||||||
export function formatISOTimestamp(isoString: string): string {
|
export function formatISOTimestamp(isoString: string, locale?: string): string {
|
||||||
const date = new Date(isoString);
|
const date = new Date(isoString);
|
||||||
return formatUnixTimestamp(Math.floor(date.getTime() / 1000));
|
const effectiveLocale = locale || navigator.language || 'en-US';
|
||||||
|
return formatUnixTimestamp(Math.floor(date.getTime() / 1000), effectiveLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,11 +84,14 @@ export function getFirstLine(text: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function formatTime(date: Date): string {
|
function formatTime(date: Date, locale?: string): string {
|
||||||
return date.toLocaleTimeString('en-US', {
|
const effectiveLocale = locale || navigator.language || 'en-US';
|
||||||
|
const isZhCN = effectiveLocale.startsWith('zh');
|
||||||
|
|
||||||
|
return date.toLocaleTimeString(effectiveLocale, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: true
|
hour12: !isZhCN // Chinese typically uses 24-hour format
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,22 +112,26 @@ function isWithinWeek(date: Date): boolean {
|
|||||||
return date > weekAgo;
|
return date > weekAgo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDayName(date: Date): string {
|
function getDayName(date: Date, locale?: string): string {
|
||||||
return date.toLocaleDateString('en-US', { weekday: 'long' });
|
const effectiveLocale = locale || navigator.language || 'en-US';
|
||||||
|
return date.toLocaleDateString(effectiveLocale, { weekday: 'long' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a timestamp to a relative time string (e.g., "2 hours ago", "3 days ago")
|
* Formats a timestamp to a relative time string (e.g., "2 hours ago", "3 days ago")
|
||||||
* @param timestamp - Unix timestamp in milliseconds
|
* @param timestamp - Unix timestamp in milliseconds
|
||||||
|
* @param locale - Optional locale string (e.g., 'en-US', 'zh-CN')
|
||||||
* @returns Relative time string
|
* @returns Relative time string
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* formatTimeAgo(Date.now() - 3600000) // "1 hour ago"
|
* formatTimeAgo(Date.now() - 3600000) // "1 hour ago"
|
||||||
* formatTimeAgo(Date.now() - 86400000) // "1 day ago"
|
* formatTimeAgo(Date.now() - 86400000, "zh-CN") // "1小时前"
|
||||||
*/
|
*/
|
||||||
export function formatTimeAgo(timestamp: number): string {
|
export function formatTimeAgo(timestamp: number, locale?: string): string {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const diff = now - timestamp;
|
const diff = now - timestamp;
|
||||||
|
const effectiveLocale = locale || navigator.language || 'en-US';
|
||||||
|
const isZhCN = effectiveLocale.startsWith('zh');
|
||||||
|
|
||||||
const seconds = Math.floor(diff / 1000);
|
const seconds = Math.floor(diff / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
@@ -126,27 +141,51 @@ export function formatTimeAgo(timestamp: number): string {
|
|||||||
const months = Math.floor(days / 30);
|
const months = Math.floor(days / 30);
|
||||||
const years = Math.floor(days / 365);
|
const years = Math.floor(days / 365);
|
||||||
|
|
||||||
if (years > 0) {
|
if (isZhCN) {
|
||||||
return years === 1 ? '1 year ago' : `${years} years ago`;
|
if (years > 0) {
|
||||||
|
return `${years}年前`;
|
||||||
|
}
|
||||||
|
if (months > 0) {
|
||||||
|
return `${months}个月前`;
|
||||||
|
}
|
||||||
|
if (weeks > 0) {
|
||||||
|
return `${weeks}周前`;
|
||||||
|
}
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}天前`;
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时前`;
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}分钟前`;
|
||||||
|
}
|
||||||
|
if (seconds > 0) {
|
||||||
|
return `${seconds}秒前`;
|
||||||
|
}
|
||||||
|
return '刚刚';
|
||||||
|
} else {
|
||||||
|
if (years > 0) {
|
||||||
|
return years === 1 ? '1 year ago' : `${years} years ago`;
|
||||||
|
}
|
||||||
|
if (months > 0) {
|
||||||
|
return months === 1 ? '1 month ago' : `${months} months ago`;
|
||||||
|
}
|
||||||
|
if (weeks > 0) {
|
||||||
|
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;
|
||||||
|
}
|
||||||
|
if (days > 0) {
|
||||||
|
return days === 1 ? '1 day ago' : `${days} days ago`;
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
return hours === 1 ? '1 hour ago' : `${hours} hours ago`;
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`;
|
||||||
|
}
|
||||||
|
if (seconds > 0) {
|
||||||
|
return seconds === 1 ? '1 second ago' : `${seconds} seconds ago`;
|
||||||
|
}
|
||||||
|
return 'just now';
|
||||||
}
|
}
|
||||||
if (months > 0) {
|
|
||||||
return months === 1 ? '1 month ago' : `${months} months ago`;
|
|
||||||
}
|
|
||||||
if (weeks > 0) {
|
|
||||||
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;
|
|
||||||
}
|
|
||||||
if (days > 0) {
|
|
||||||
return days === 1 ? '1 day ago' : `${days} days ago`;
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
return hours === 1 ? '1 hour ago' : `${hours} hours ago`;
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`;
|
|
||||||
}
|
|
||||||
if (seconds > 0) {
|
|
||||||
return seconds === 1 ? '1 second ago' : `${seconds} seconds ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'just now';
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user