audit fixes: align all frontend timestamps to Beijing time + debounce MarketPage search (vv2026.07.20.1)
CI / gate (push) Has been cancelled
CI / gate (push) Has been cancelled
This commit is contained in:
1 parent
d55a496ff4
commit
612ce37518
9 files changed
+46
-28
No files matched your search
@@ -1121,7 +1121,7 @@ const Achievement: React.FC = () => {
|
|||||||
|
|
||||||
<td style={{ color: 'rgba(0,0,0,0.4)' }}>
|
<td style={{ color: 'rgba(0,0,0,0.4)' }}>
|
||||||
|
|
||||||
{entry.last_completed ? new Date(entry.last_completed).toLocaleDateString('zh-CN') : '-'}
|
{entry.last_completed ? formatBjDate(entry.last_completed) : '-'}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { adminAPI, marketAPI } from '@/api';
|
import { adminAPI, marketAPI } from '@/api';
|
||||||
import type { AdBanner, MarketApp } from '@/types';
|
import type { AdBanner, MarketApp } from '@/types';
|
||||||
|
import { formatBjDate } from '@/utils/datetime';
|
||||||
import './Ads.css';
|
import './Ads.css';
|
||||||
|
|
||||||
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
|
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
|
||||||
@@ -281,7 +282,7 @@ const Ads: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>
|
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>
|
||||||
{new Date(banner.createdAt).toLocaleDateString()}
|
{formatBjDate(banner.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style={{ display: 'flex', gap: 'var(--space-xs)' }}>
|
<div style={{ display: 'flex', gap: 'var(--space-xs)' }}>
|
||||||
|
|||||||
@@ -1,7 +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';
|
import { formatBjTime, formatBjDate } from '@/utils/datetime';
|
||||||
|
|
||||||
interface AIApp {
|
interface AIApp {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -224,11 +224,7 @@ export default function AdminApps() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dt: string | null) => {
|
const formatDate = (dt: string | null) => {
|
||||||
if (!dt) return '-';
|
return formatBjTime(dt);
|
||||||
try {
|
|
||||||
const d = new Date(dt);
|
|
||||||
return d.toLocaleDateString('zh-CN') + ' ' + d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
} catch { return dt; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -738,7 +734,7 @@ function AppDetailDrawer({ app, analytics, onClose, onAction }: {
|
|||||||
<div key={v.id} className="version-item">
|
<div key={v.id} className="version-item">
|
||||||
<span className="version-num">v{v.version}</span>
|
<span className="version-num">v{v.version}</span>
|
||||||
<span className="version-log">{v.changelog}</span>
|
<span className="version-log">{v.changelog}</span>
|
||||||
<span className="version-date">{v.created_at ? new Date(v.created_at).toLocaleDateString('zh-CN') : '-'}</span>
|
<span className="version-date">{v.created_at ? formatBjDate(v.created_at) : '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { imUserAPI } from '@/api';
|
import { imUserAPI } from '@/api';
|
||||||
|
import { formatBjDate } from '@/utils/datetime';
|
||||||
|
|
||||||
const PRIMARY = '#5B5FED';
|
const PRIMARY = '#5B5FED';
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ const AppServices: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '13px', color: '#6B7280', lineHeight: 1.7, flex: 1, margin: '0 0 14px 0' }}>{app.description || '暂无描述'}</p>
|
<p style={{ fontSize: '13px', color: '#6B7280', lineHeight: 1.7, flex: 1, margin: '0 0 14px 0' }}>{app.description || '暂无描述'}</p>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span style={{ fontSize: '12px', color: '#9CA3AF' }}>{app.published_at ? '上架于 ' + new Date(app.published_at).toLocaleDateString('zh-CN') : ''}</span>
|
<span style={{ fontSize: '12px', color: '#9CA3AF' }}>{app.published_at ? '上架于 ' + formatBjDate(app.published_at) : ''}</span>
|
||||||
<button onClick={() => showToast('已加入「我的应用」,可在 AI 聊天中调用')} style={{ padding: '7px 16px', background: PRIMARY, border: 'none', borderRadius: '8px', color: '#fff', fontSize: '13px', fontWeight: 500, cursor: 'pointer' }}>使用</button>
|
<button onClick={() => showToast('已加入「我的应用」,可在 AI 聊天中调用')} style={{ padding: '7px 16px', background: PRIMARY, border: 'none', borderRadius: '8px', color: '#fff', fontSize: '13px', fontWeight: 500, cursor: 'pointer' }}>使用</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { Asset, AssetCategory, AssetCategoryDef, AssetFileType } from '@/types';
|
import type { Asset, AssetCategory, AssetCategoryDef, AssetFileType } from '@/types';
|
||||||
import { assetsAPI } from '@/api';
|
import { assetsAPI } from '@/api';
|
||||||
|
import { formatBjTime } from '@/utils/datetime';
|
||||||
import './AssetsPage.css';
|
import './AssetsPage.css';
|
||||||
|
|
||||||
const fileTypeLabels: Record<string, { label: string; bg: string; color: string }> = {
|
const fileTypeLabels: Record<string, { label: string; bg: string; color: string }> = {
|
||||||
@@ -46,7 +47,7 @@ function formatSize(bytes?: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr: string): string {
|
function formatDate(dateStr: string): string {
|
||||||
return new Date(dateStr).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
return formatBjTime(dateStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将文件扩展名或API返回的file_type映射到前端fileType标签
|
// 将文件扩展名或API返回的file_type映射到前端fileType标签
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { chatAPI, knowledgeAPI, marketAPI, evolutionAPI } from '@/api';
|
import { chatAPI, knowledgeAPI, marketAPI, evolutionAPI } from '@/api';
|
||||||
|
import { formatChatTime } from '@/utils/datetime';
|
||||||
import type { Conversation, Message } from '@/types';
|
import type { Conversation, Message } from '@/types';
|
||||||
import './ChatPage.css';
|
import './ChatPage.css';
|
||||||
|
|
||||||
@@ -396,21 +397,7 @@ function escapeHtml(text: string): string {
|
|||||||
|
|
||||||
// ===== Time Formatting =====
|
// ===== Time Formatting =====
|
||||||
function formatTime(iso: string): string {
|
function formatTime(iso: string): string {
|
||||||
const date = new Date(iso);
|
return formatChatTime(iso);
|
||||||
const now = new Date();
|
|
||||||
const diff = now.getTime() - date.getTime();
|
|
||||||
const oneDay = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (diff < oneDay) {
|
|
||||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
} else if (diff < 7 * oneDay) {
|
|
||||||
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
||||||
return days[date.getDay()];
|
|
||||||
} else if (date.getFullYear() === now.getFullYear()) {
|
|
||||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
||||||
} else {
|
|
||||||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Helpers: normalize API responses =====
|
// ===== Helpers: normalize API responses =====
|
||||||
|
|||||||
@@ -1596,6 +1596,7 @@ const MarketPage: React.FC = () => {
|
|||||||
const [localApps, setLocalApps] = useState<MarketApp[]>([]);
|
const [localApps, setLocalApps] = useState<MarketApp[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const searchTimerRef = useRef<number | null>(null);
|
||||||
const [capabilityFilter, setCapabilityFilter] = useState<string | null>(null);
|
const [capabilityFilter, setCapabilityFilter] = useState<string | null>(null);
|
||||||
const [installToast, setInstallToast] = useState<string | null>(null);
|
const [installToast, setInstallToast] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -1893,7 +1894,7 @@ const MarketPage: React.FC = () => {
|
|||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="搜索应用名称或描述..."
|
placeholder="搜索应用名称或描述..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => { setSearchQuery(e.target.value); loadApps(e.target.value); }}
|
onChange={e => { const v = e.target.value; setSearchQuery(v); if (searchTimerRef.current) clearTimeout(searchTimerRef.current); searchTimerRef.current = window.setTimeout(() => loadApps(v), 350); }}
|
||||||
style={{ paddingLeft: 32, flex: 1, maxWidth: 400 }}
|
style={{ paddingLeft: 32, flex: 1, maxWidth: 400 }}
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
|
|||||||
@@ -7,3 +7,34 @@ export function formatBjTime(t?: string | null): string {
|
|||||||
if (isNaN(d.getTime())) return t;
|
if (isNaN(d.getTime())) return t;
|
||||||
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仅日期(北京时间)
|
||||||
|
export function formatBjDate(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.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将任意 Date 换算为「北京墙钟」对应的 UTC 视图,便于按北京日历日比较
|
||||||
|
function toBjView(d: Date): Date {
|
||||||
|
return new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聊天消息时间:今天 HH:MM / 昨天 HH:MM / MM-DD HH:MM / YYYY-MM-DD HH:MM(北京时间)
|
||||||
|
export function formatChatTime(iso?: string | null): string {
|
||||||
|
if (!iso) return '';
|
||||||
|
const s = /[Z+]/.test(iso) ? iso : iso + '+08:00';
|
||||||
|
const d = new Date(s);
|
||||||
|
if (isNaN(d.getTime())) return '';
|
||||||
|
const bj = toBjView(d);
|
||||||
|
const nowBj = toBjView(new Date());
|
||||||
|
const dayDiff = Math.floor(nowBj.getTime() / 86400000) - Math.floor(bj.getTime() / 86400000);
|
||||||
|
const hhmm = d.toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit' });
|
||||||
|
if (dayDiff <= 0) return hhmm;
|
||||||
|
if (dayDiff === 1) return '昨天 ' + hhmm;
|
||||||
|
const md = d.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit' });
|
||||||
|
if (bj.getUTCFullYear() === nowBj.getUTCFullYear()) return md + ' ' + hhmm;
|
||||||
|
return d.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }) + ' ' + hhmm;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user