Sfoglia il codice sorgente

Fix: 修复时区显示问题

iaun 3 mesi fa
parent
commit
d12d2f1f35

+ 13 - 3
backend/app/models/credential.py

@@ -1,8 +1,18 @@
-from datetime import datetime
+from datetime import datetime, timezone
 from app import db
 from app.utils.encryption import encrypt_value, decrypt_value
 
 
+def format_datetime(dt: datetime) -> str:
+    """Format datetime to ISO format with UTC timezone indicator"""
+    if dt is None:
+        return None
+    # If datetime is naive (no timezone), assume it's UTC
+    if dt.tzinfo is None:
+        dt = dt.replace(tzinfo=timezone.utc)
+    return dt.isoformat()
+
+
 class AWSCredential(db.Model):
     """AWS Credential model for storing IAM Role or Access Key credentials"""
     __tablename__ = 'aws_credentials'
@@ -46,7 +56,7 @@ class AWSCredential(db.Model):
             'role_arn': self.role_arn,
             'external_id': self.external_id,
             'access_key_id': self.access_key_id,
-            'created_at': self.created_at.isoformat() if self.created_at else None,
+            'created_at': format_datetime(self.created_at),
             'is_active': self.is_active
         }
         
@@ -106,7 +116,7 @@ class BaseAssumeRoleConfig(db.Model):
         result = {
             'id': self.id,
             'access_key_id': self.access_key_id,
-            'updated_at': self.updated_at.isoformat() if self.updated_at else None
+            'updated_at': format_datetime(self.updated_at)
         }
         
         if mask_sensitive:

+ 12 - 2
backend/app/models/report.py

@@ -1,7 +1,17 @@
-from datetime import datetime
+from datetime import datetime, timezone
 from app import db
 
 
+def format_datetime(dt: datetime) -> str:
+    """Format datetime to ISO format with UTC timezone indicator"""
+    if dt is None:
+        return None
+    # If datetime is naive (no timezone), assume it's UTC
+    if dt.tzinfo is None:
+        dt = dt.replace(tzinfo=timezone.utc)
+    return dt.isoformat()
+
+
 class Report(db.Model):
     """Report model for generated Word documents"""
     __tablename__ = 'reports'
@@ -23,7 +33,7 @@ class Report(db.Model):
             'task_id': self.task_id,
             'file_name': self.file_name,
             'file_size': self.file_size,
-            'created_at': self.created_at.isoformat() if self.created_at else None,
+            'created_at': format_datetime(self.created_at),
             'download_url': f'/api/reports/download?id={self.id}'
         }
     

+ 15 - 5
backend/app/models/task.py

@@ -1,8 +1,18 @@
-from datetime import datetime
+from datetime import datetime, timezone
 import json
 from app import db
 
 
+def format_datetime(dt: datetime) -> str:
+    """Format datetime to ISO format with UTC timezone indicator"""
+    if dt is None:
+        return None
+    # If datetime is naive (no timezone), assume it's UTC
+    if dt.tzinfo is None:
+        dt = dt.replace(tzinfo=timezone.utc)
+    return dt.isoformat()
+
+
 class Task(db.Model):
     """Task model for scan tasks"""
     __tablename__ = 'tasks'
@@ -75,9 +85,9 @@ class Task(db.Model):
             'status': self.status,
             'progress': self.progress,
             'created_by': self.created_by,
-            'created_at': self.created_at.isoformat() if self.created_at else None,
-            'started_at': self.started_at.isoformat() if self.started_at else None,
-            'completed_at': self.completed_at.isoformat() if self.completed_at else None,
+            'created_at': format_datetime(self.created_at),
+            'started_at': format_datetime(self.started_at),
+            'completed_at': format_datetime(self.completed_at),
             'credential_ids': self.credential_ids,
             'regions': self.regions,
             'project_metadata': self.project_metadata
@@ -108,7 +118,7 @@ class TaskLog(db.Model):
             'task_id': self.task_id,
             'level': self.level,
             'message': self.message,
-            'created_at': self.created_at.isoformat() if self.created_at else None
+            'created_at': format_datetime(self.created_at)
         }
         if self.details:
             result['details'] = json.loads(self.details)

+ 11 - 1
backend/app/models/user.py

@@ -8,6 +8,16 @@ def utc_now():
     return datetime.now(timezone.utc)
 
 
+def format_datetime(dt: datetime) -> str:
+    """Format datetime to ISO format with UTC timezone indicator"""
+    if dt is None:
+        return None
+    # If datetime is naive (no timezone), assume it's UTC
+    if dt.tzinfo is None:
+        dt = dt.replace(tzinfo=timezone.utc)
+    return dt.isoformat()
+
+
 class User(db.Model):
     """User model for authentication and authorization"""
     __tablename__ = 'users'
@@ -45,7 +55,7 @@ class User(db.Model):
             'username': self.username,
             'email': self.email,
             'role': self.role,
-            'created_at': self.created_at.isoformat() if self.created_at else None,
+            'created_at': format_datetime(self.created_at),
             'is_active': self.is_active
         }
     

+ 2 - 7
frontend/src/pages/Credentials.tsx

@@ -31,6 +31,7 @@ import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
 import type { AWSCredential } from '../types';
 import { credentialService, type CreateCredentialRequest, type BaseRoleConfig } from '../services/credentials';
 import { usePagination } from '../hooks/usePagination';
+import { formatDateTime } from '../utils';
 
 const { Title, Text } = Typography;
 const { Option } = Select;
@@ -278,12 +279,6 @@ export default function Credentials() {
     }
   };
 
-  // Format date
-  const formatDate = (dateString?: string): string => {
-    if (!dateString) return '-';
-    return new Date(dateString).toLocaleString();
-  };
-
   // Table columns
   const columns: ColumnsType<AWSCredential> = [
     {
@@ -330,7 +325,7 @@ export default function Credentials() {
       dataIndex: 'created_at',
       key: 'created_at',
       width: 180,
-      render: (date: string) => formatDate(date),
+      render: (date: string) => formatDateTime(date),
     },
     {
       title: 'Actions',

+ 2 - 8
frontend/src/pages/Dashboard.tsx

@@ -13,6 +13,7 @@ import {
 import type { ColumnsType } from 'antd/es/table';
 import { dashboardService, DashboardStats, RecentReport } from '../services/dashboard';
 import { reportService } from '../services/reports';
+import { formatDateTime } from '../utils';
 
 const { Title } = Typography;
 
@@ -65,13 +66,6 @@ export default function Dashboard() {
     return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
   };
 
-  const formatDate = (dateString: string): string => {
-    if (!dateString) return '-';
-    const date = new Date(dateString);
-    if (isNaN(date.getTime())) return '-';
-    return date.toLocaleString();
-  };
-
   const columns: ColumnsType<RecentReport> = [
     {
       title: 'File Name',
@@ -91,7 +85,7 @@ export default function Dashboard() {
       dataIndex: 'created_at',
       key: 'created_at',
       width: 180,
-      render: (date: string) => formatDate(date),
+      render: (date: string) => formatDateTime(date),
     },
     {
       title: 'Action',

+ 3 - 8
frontend/src/pages/Reports.tsx

@@ -25,6 +25,7 @@ import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
 import type { Report } from '../types';
 import { reportService } from '../services/reports';
 import { usePagination } from '../hooks/usePagination';
+import { formatDateTime } from '../utils';
 
 const { Title, Text } = Typography;
 
@@ -90,12 +91,6 @@ export default function Reports() {
     return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
   };
 
-  // Format date
-  const formatDate = (dateString?: string): string => {
-    if (!dateString) return '-';
-    return new Date(dateString).toLocaleString();
-  };
-
   // Handle view report detail
   const handleViewReport = async (report: Report) => {
     try {
@@ -187,7 +182,7 @@ export default function Reports() {
       dataIndex: 'created_at', 
       key: 'created_at',
       width: 180,
-      render: (date: string) => formatDate(date),
+      render: (date: string) => formatDateTime(date),
     },
     {
       title: 'Actions',
@@ -326,7 +321,7 @@ export default function Reports() {
                 </Text>
               </Descriptions.Item>
               <Descriptions.Item label="Created At">
-                {formatDate(selectedReport.created_at)}
+                {formatDateTime(selectedReport.created_at)}
               </Descriptions.Item>
               <Descriptions.Item label="Status">
                 <Tag color="success" icon={<CheckCircleOutlined />}>

+ 5 - 10
frontend/src/pages/Tasks.tsx

@@ -39,6 +39,7 @@ import type { ScanTask, AWSCredential, ErrorLog } from '../types';
 import { taskService } from '../services/tasks';
 import { credentialService } from '../services/credentials';
 import { usePagination } from '../hooks/usePagination';
+import { formatDateTime } from '../utils';
 
 const { Title, Text } = Typography;
 const { Option } = Select;
@@ -298,12 +299,6 @@ export default function Tasks() {
     }
   };
 
-  // Format date
-  const formatDate = (dateString?: string): string => {
-    if (!dateString) return '-';
-    return new Date(dateString).toLocaleString();
-  };
-
   // Table columns
   const columns: ColumnsType<ScanTask> = [
     { 
@@ -347,7 +342,7 @@ export default function Tasks() {
       dataIndex: 'created_at', 
       key: 'created_at',
       width: 180,
-      render: (date: string) => formatDate(date),
+      render: (date: string) => formatDateTime(date),
     },
     {
       title: 'Actions',
@@ -647,8 +642,8 @@ export default function Tasks() {
                     status={selectedTask.status === 'failed' ? 'exception' : selectedTask.status === 'completed' ? 'success' : 'active'}
                   />
                 </Descriptions.Item>
-                <Descriptions.Item label="Created At">{formatDate(selectedTask.created_at)}</Descriptions.Item>
-                <Descriptions.Item label="Completed At">{formatDate(selectedTask.completed_at)}</Descriptions.Item>
+                <Descriptions.Item label="Created At">{formatDateTime(selectedTask.created_at)}</Descriptions.Item>
+                <Descriptions.Item label="Completed At">{formatDateTime(selectedTask.completed_at)}</Descriptions.Item>
                 <Descriptions.Item label="Regions" span={2}>
                   <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
                     {selectedTask.regions?.map((r: string) => <Tag key={r}>{r}</Tag>) || '-'}
@@ -719,7 +714,7 @@ export default function Tasks() {
                         description={
                           <Space direction="vertical" size={0}>
                             <Text type="secondary" style={{ fontSize: 12 }}>
-                              {formatDate(log.created_at)}
+                              {formatDateTime(log.created_at)}
                             </Text>
                             {log.details && (
                               <Text type="secondary" style={{ fontSize: 12 }}>

+ 2 - 7
frontend/src/pages/Users.tsx

@@ -28,6 +28,7 @@ import type { User, AWSCredential } from '../types';
 import { userService } from '../services/users';
 import { credentialService } from '../services/credentials';
 import { usePagination } from '../hooks/usePagination';
+import { formatDateTime } from '../utils';
 
 const { Title } = Typography;
 const { Option } = Select;
@@ -250,12 +251,6 @@ export default function Users() {
     }
   };
 
-  // Format date
-  const formatDate = (dateString?: string): string => {
-    if (!dateString) return '-';
-    return new Date(dateString).toLocaleString();
-  };
-
   // Table columns
   const columns: ColumnsType<UserWithCredentials> = [
     {
@@ -288,7 +283,7 @@ export default function Users() {
       dataIndex: 'created_at',
       key: 'created_at',
       width: 180,
-      render: (date: string) => formatDate(date),
+      render: (date: string) => formatDateTime(date),
     },
     {
       title: 'Actions',

+ 65 - 0
frontend/src/utils/date.ts

@@ -0,0 +1,65 @@
+/**
+ * Date formatting utilities
+ * All dates are displayed in user's local timezone
+ */
+
+/**
+ * Format a date string to local date and time
+ * @param dateString - ISO date string from backend (UTC)
+ * @returns Formatted date string in user's local timezone
+ */
+export function formatDateTime(dateString?: string | null): string {
+  if (!dateString) return '-';
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return '-';
+  return date.toLocaleString();
+}
+
+/**
+ * Format a date string to local date only
+ * @param dateString - ISO date string from backend (UTC)
+ * @returns Formatted date string in user's local timezone (date only)
+ */
+export function formatDate(dateString?: string | null): string {
+  if (!dateString) return '-';
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return '-';
+  return date.toLocaleDateString();
+}
+
+/**
+ * Format a date string to local time only
+ * @param dateString - ISO date string from backend (UTC)
+ * @returns Formatted time string in user's local timezone (time only)
+ */
+export function formatTime(dateString?: string | null): string {
+  if (!dateString) return '-';
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return '-';
+  return date.toLocaleTimeString();
+}
+
+/**
+ * Format a date string to relative time (e.g., "2 hours ago")
+ * @param dateString - ISO date string from backend (UTC)
+ * @returns Relative time string
+ */
+export function formatRelativeTime(dateString?: string | null): string {
+  if (!dateString) return '-';
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return '-';
+  
+  const now = new Date();
+  const diffMs = now.getTime() - date.getTime();
+  const diffSec = Math.floor(diffMs / 1000);
+  const diffMin = Math.floor(diffSec / 60);
+  const diffHour = Math.floor(diffMin / 60);
+  const diffDay = Math.floor(diffHour / 24);
+  
+  if (diffSec < 60) return 'just now';
+  if (diffMin < 60) return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`;
+  if (diffHour < 24) return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`;
+  if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`;
+  
+  return date.toLocaleDateString();
+}

+ 1 - 0
frontend/src/utils/index.ts

@@ -0,0 +1 @@
+export * from './date';