fix: 全站时间统一为北京时间(Asia/Shanghai)

新增 src/utils/datetime.ts 共享 formatBjTime 工具,
13 个页面(11后台+2前台) 的 toLocaleString/formatDate
改为显式北京时间。Dashboard.tsx 保留图表标签的
formatDate(split) 不受影响。
This commit is contained in:
QihuAI Design committed 2026-07-16 12:39:35 +08:00
1 parent 82afe14a23
commit b3866f770c
12 files changed
+34 -19

No files matched your search

+2 -1
View File
@@ -11,6 +11,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import { formatBjTime } from '@/utils/datetime';
@@ -1012,7 +1013,7 @@ const Achievement: React.FC = () => {
<div style={{ fontSize: '12px', color: '#8892B0', marginTop: '8px' }}> <div style={{ fontSize: '12px', color: '#8892B0', marginTop: '8px' }}>
完成时间: {new Date(ach.completed_at).toLocaleString('zh-CN')} 完成时间: {formatBjTime(ach.completed_at)}
</div> </div>
+4 -3
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import './Apps.css'; import './Apps.css';
import { formatBjTime } from '@/utils/datetime';
interface AIApp { interface AIApp {
id: string; id: string;
@@ -779,9 +780,9 @@ function AppDetailDrawer({ app, analytics, onClose, onAction }: {
{/* Timestamps */} {/* Timestamps */}
<div style={{ marginTop: '24px', fontSize: '13px', color: 'var(--color-text-tertiary)' }}> <div style={{ marginTop: '24px', fontSize: '13px', color: 'var(--color-text-tertiary)' }}>
<div>创建时间: {app.created_at ? new Date(app.created_at).toLocaleString('zh-CN') : '-'}</div> <div>创建时间: {app.created_at ? formatBjTime(app.created_at) : '-'}</div>
<div>更新时间: {app.updated_at ? new Date(app.updated_at).toLocaleString('zh-CN') : '-'}</div> <div>更新时间: {app.updated_at ? formatBjTime(app.updated_at) : '-'}</div>
{app.published_at && <div>发布时间: {new Date(app.published_at).toLocaleString('zh-CN')}</div>} {app.published_at && <div>发布时间: {formatBjTime(app.published_at)}</div>}
</div> </div>
</div> </div>
+2 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import { formatBjTime } from '@/utils/datetime';
interface AutomationTask { interface AutomationTask {
id: string; id: string;
@@ -270,7 +271,7 @@ const Automation: React.FC = () => {
// Format time // Format time
const formatTime = (ts: string) => { const formatTime = (ts: string) => {
if (!ts) return '-'; if (!ts) return '-';
try { return new Date(ts).toLocaleString('zh-CN'); } try { return formatBjTime(ts); }
catch { return ts; } catch { return ts; }
}; };
+3 -7
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import { formatBjTime } from '@/utils/datetime';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => ( const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
@@ -226,12 +227,7 @@ const Dashboard: React.FC = () => {
fetchData(); fetchData();
}, []); }, []);
const formatDate = (d: string) => { const formatDate = (d: string | null) => formatBjTime(d);
if (!d) return '-';
const dt = new Date(d);
if (isNaN(dt.getTime())) return d.slice(0, 19) || '-';
return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
const formatTokens = (n: number): string => { const formatTokens = (n: number): string => {
if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿'; if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿';
@@ -336,7 +332,7 @@ const Dashboard: React.FC = () => {
</div> </div>
))} ))}
<div style={{ marginTop: 12, fontSize: 11, color: '#6B7280', textAlign: 'center' }}> <div style={{ marginTop: 12, fontSize: 11, color: '#6B7280', textAlign: 'center' }}>
服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'} 服务器时间: {stats.server_time ? formatBjTime(stats.server_time) : '-'}
</div> </div>
</div> </div>
</div> </div>
+2 -1
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import './Developer.css'; import './Developer.css';
import { formatBjTime } from '@/utils/datetime';
const API_BASE = '/app/api'; const API_BASE = '/app/api';
@@ -216,7 +217,7 @@ export default function AdminDeveloper() {
const fmtTime = (t: string | null) => { const fmtTime = (t: string | null) => {
if (!t) return '-'; if (!t) return '-';
try { return new Date(t).toLocaleString('zh-CN'); } catch { return t; } try { return formatBjTime(t); } catch { return t; }
}; };
const statusBadge = (status: string) => { const statusBadge = (status: string) => {
+2 -1
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import './Developer.css'; import './Developer.css';
import { formatBjTime } from '@/utils/datetime';
const API_BASE = '/app/api'; const API_BASE = '/app/api';
@@ -169,7 +170,7 @@ export default function AdminDeveloperTools() {
const fmtTime = (t: string | null) => { const fmtTime = (t: string | null) => {
if (!t) return '-'; if (!t) return '-';
try { return new Date(t).toLocaleString('zh-CN'); } catch { return t; } try { return formatBjTime(t); } catch { return t; }
}; };
const statusBadge = (status: string) => { const statusBadge = (status: string) => {
+2 -1
View File
@@ -3,6 +3,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { import {
adminAPI, adminAPI,
} from '@/api'; } from '@/api';
import { formatBjTime } from '@/utils/datetime';
// Platform icons (simple SVG representations) // Platform icons (simple SVG representations)
const platformIcons: Record<string, string> = { const platformIcons: Record<string, string> = {
@@ -329,7 +330,7 @@ const ImBots: React.FC = () => {
// Format time // Format time
const formatTime = (time: string) => { const formatTime = (time: string) => {
if (!time) return '-'; if (!time) return '-';
return new Date(time).toLocaleString('zh-CN'); return formatBjTime(time);
}; };
const tabs: { key: Tab; label: string; icon: string }[] = [ const tabs: { key: Tab; label: string; icon: string }[] = [
+2 -1
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import { formatBjTime } from '@/utils/datetime';
const styles: Record<string, React.CSSProperties> = { const styles: Record<string, React.CSSProperties> = {
container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' }, container: { padding: 24, backgroundColor: '#f5f7fa', minHeight: '100vh' },
@@ -277,7 +278,7 @@ export default function AdminMcp() {
<td style={styles.td}>{getTransportBadge(s.transport_type)}</td> <td style={styles.td}>{getTransportBadge(s.transport_type)}</td>
<td style={styles.td}>{getStatusBadge(s.status)}</td> <td style={styles.td}>{getStatusBadge(s.status)}</td>
<td style={styles.td}>{s.tools_count || '-'}</td> <td style={styles.td}>{s.tools_count || '-'}</td>
<td style={styles.td}>{s.last_connected_at ? new Date(s.last_connected_at).toLocaleString() : '从未'}</td> <td style={styles.td}>{s.last_connected_at ? formatBjTime(s.last_connected_at) : '从未'}</td>
<td style={styles.td}> <td style={styles.td}>
<span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}> <span style={{ ...styles.badge, backgroundColor: s.is_enabled ? '#dcfce7' : '#f3f4f6', color: s.is_enabled ? '#166534' : '#6b7280' }}>
{s.is_enabled ? '是' : '否'} {s.is_enabled ? '是' : '否'}
+2 -1
View File
@@ -3,6 +3,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
import { formatBjTime } from '@/utils/datetime';
@@ -464,7 +465,7 @@ export default function PrivacySettings() {
const formatDate = (d: string | null) => d ? new Date(d).toLocaleString('zh-CN') : '-'; const formatDate = (d: string | null) => d ? formatBjTime(d) : '-';
+2 -1
View File
@@ -3,6 +3,7 @@ import { adminAPI } from '@/api';
import { mockUsers, mockMembershipPlans } from '@/mock/data'; import { mockUsers, mockMembershipPlans } from '@/mock/data';
import type { User, RegisterSettings } from '@/types'; import type { User, RegisterSettings } from '@/types';
import './Users.css'; import './Users.css';
import { formatBjTime } from '@/utils/datetime';
// ===== SVG Icons ===== // ===== SVG Icons =====
const IconSearch = () => ( const IconSearch = () => (
@@ -141,7 +142,7 @@ const UserDetailModal: React.FC<UserDetailModalProps> = ({ user, onClose }) => {
</div> </div>
<div className="user-detail-item"> <div className="user-detail-item">
<span className="detail-label">注册时间</span> <span className="detail-label">注册时间</span>
<span className="detail-value">{new Date(user.createdAt).toLocaleString('zh-CN')}</span> <span className="detail-value">{formatBjTime(user.createdAt)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -4,6 +4,7 @@ import type { EvolutionSkill, EvolutionEvent, EvolutionProfile, SkillTreeData, S
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import './EvolutionDashboard.css'; import './EvolutionDashboard.css';
import { formatBjTime } from '@/utils/datetime';
// ==================== SVG Icons ==================== // ==================== SVG Icons ====================
const Icon: React.FC<{ children: React.ReactNode; size?: number; className?: string }> = ({ children, size = 16, className }) => ( const Icon: React.FC<{ children: React.ReactNode; size?: number; className?: string }> = ({ children, size = 16, className }) => (
@@ -486,7 +487,7 @@ const TimelinePanel: React.FC<{
<p className="evo-timeline-desc">{event.description}</p> <p className="evo-timeline-desc">{event.description}</p>
<div className="evo-timeline-time"> <div className="evo-timeline-time">
<Icon size={12}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></Icon> <Icon size={12}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></Icon>
{new Date(event.created_at).toLocaleString()} {formatBjTime(event.created_at)}
</div> </div>
</div> </div>
</div> </div>
+9
View File
@@ -0,0 +1,9 @@
// 后端按北京时间存储(无时区标记),此处显式按 Asia/Shanghai 渲染,
// 避免浏览器本地时区导致时间显示错乱。
export function formatBjTime(t?: string | null): string {
if (!t) return '-';
const iso = /[Z+]/.test(t) ? t : t + '+08:00';
const d = new Date(iso);
if (isNaN(d.getTime())) return t;
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
}