🕒 feat(ui): standardize Timelines to left mode and unify time display

- Switch Semi UI Timeline to mode="left" in:
  - web/src/components/layout/NoticeModal.jsx
  - web/src/components/dashboard/AnnouncementsPanel.jsx
- Show both relative and absolute time in the `time` prop (e.g. "3 days ago 2025-02-18 10:30")
- Move auxiliary description to the `extra` prop and remove duplicate rendering from content area
- Keep original `extra` data intact; compute and pass:
  - `time`: absolute time (yyyy-MM-dd HH:mm)
  - `relative`: relative time (e.g., "3 days ago")
- Update data assembly to expose `time` and `relative` without overwriting `extra`:
  - web/src/components/dashboard/index.jsx
- No i18n changes; no linter errors introduced

Why: Aligns Timeline layout across the app and clarifies time context by combining relative and absolute timestamps while preserving auxiliary notes via `extra`.
This commit is contained in:
t0ng7u
2025-08-24 17:23:03 +08:00
parent 1e3621833f
commit 6dcf954bfe
7 changed files with 60 additions and 43 deletions

View File

@@ -68,26 +68,29 @@ const AnnouncementsPanel = ({
> >
<ScrollableContainer maxHeight="24rem"> <ScrollableContainer maxHeight="24rem">
{announcementData.length > 0 ? ( {announcementData.length > 0 ? (
<Timeline mode="alternate"> <Timeline mode="left">
{announcementData.map((item, idx) => ( {announcementData.map((item, idx) => {
<Timeline.Item const htmlExtra = item.extra ? marked.parse(item.extra) : '';
key={idx} return (
type={item.type || 'default'} <Timeline.Item
time={item.time} key={idx}
> type={item.type || 'default'}
<div> time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
<div extra={item.extra ? (
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
/>
{item.extra && (
<div <div
className="text-xs text-gray-500" className="text-xs text-gray-500"
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }} dangerouslySetInnerHTML={{ __html: htmlExtra }}
/> />
)} ) : null}
</div> >
</Timeline.Item> <div>
))} <div
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
/>
</div>
</Timeline.Item>
);
})}
</Timeline> </Timeline>
) : ( ) : (
<div className="flex justify-center items-center py-8"> <div className="flex justify-center items-center py-8">

View File

@@ -108,10 +108,18 @@ const Dashboard = () => {
// ========== 数据准备 ========== // ========== 数据准备 ==========
const apiInfoData = statusState?.status?.api_info || []; const apiInfoData = statusState?.status?.api_info || [];
const announcementData = (statusState?.status?.announcements || []).map(item => ({ const announcementData = (statusState?.status?.announcements || []).map(item => {
...item, const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
time: getRelativeTime(item.publishDate) const absoluteTime = pubDate && !isNaN(pubDate.getTime())
})); ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
: (item?.publishDate || '');
const relativeTime = getRelativeTime(item.publishDate);
return ({
...item,
time: absoluteTime,
relative: relativeTime
});
});
const faqData = statusState?.status?.faq || []; const faqData = statusState?.status?.faq || [];
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({ const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({

View File

@@ -41,14 +41,21 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`; const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
const processedAnnouncements = useMemo(() => { const processedAnnouncements = useMemo(() => {
return (announcements || []).slice(0, 20).map(item => ({ return (announcements || []).slice(0, 20).map(item => {
key: getKeyForItem(item), const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
type: item.type || 'default', const absoluteTime = pubDate && !isNaN(pubDate.getTime())
time: getRelativeTime(item.publishDate), ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
content: item.content, : (item?.publishDate || '');
extra: item.extra, return ({
isUnread: unreadSet.has(getKeyForItem(item)) key: getKeyForItem(item),
})); type: item.type || 'default',
time: absoluteTime,
content: item.content,
extra: item.extra,
relative: getRelativeTime(item.publishDate),
isUnread: unreadSet.has(getKeyForItem(item))
});
});
}, [announcements, unreadSet]); }, [announcements, unreadSet]);
const handleCloseTodayNotice = () => { const handleCloseTodayNotice = () => {
@@ -131,7 +138,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
return ( return (
<div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll"> <div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
<Timeline mode="alternate"> <Timeline mode="left">
{processedAnnouncements.map((item, idx) => { {processedAnnouncements.map((item, idx) => {
const htmlContent = marked.parse(item.content || ''); const htmlContent = marked.parse(item.content || '');
const htmlExtra = item.extra ? marked.parse(item.extra) : ''; const htmlExtra = item.extra ? marked.parse(item.extra) : '';
@@ -139,7 +146,13 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
<Timeline.Item <Timeline.Item
key={idx} key={idx}
type={item.type} type={item.type}
time={item.time} time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
extra={item.extra ? (
<div
className="text-xs text-gray-500"
dangerouslySetInnerHTML={{ __html: htmlExtra }}
/>
) : null}
className={item.isUnread ? '' : ''} className={item.isUnread ? '' : ''}
> >
<div> <div>
@@ -147,12 +160,6 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
className={item.isUnread ? 'shine-text' : ''} className={item.isUnread ? 'shine-text' : ''}
dangerouslySetInnerHTML={{ __html: htmlContent }} dangerouslySetInnerHTML={{ __html: htmlContent }}
/> />
{item.extra && (
<div
className="text-xs text-gray-500"
dangerouslySetInnerHTML={{ __html: htmlExtra }}
/>
)}
</div> </div>
</Timeline.Item> </Timeline.Item>
); );
@@ -177,8 +184,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
type='card' type='button'
size='small'
> >
<TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' /> <TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
<TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' /> <TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />

View File

@@ -92,7 +92,7 @@ const PricingSidebar = ({
}); });
return ( return (
<div className="p-4"> <div className="p-2">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="text-lg font-semibold text-gray-800"> <div className="text-lg font-semibold text-gray-800">
{t('筛选')} {t('筛选')}

View File

@@ -26,7 +26,7 @@ const PricingCardSkeleton = ({
showRatio = false showRatio = false
}) => { }) => {
const placeholder = ( const placeholder = (
<div className="px-4"> <div className="px-2">
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
{Array.from({ length: skeletonCount }).map((_, index) => ( {Array.from({ length: skeletonCount }).map((_, index) => (
<Card <Card

View File

@@ -202,7 +202,7 @@ const PricingCardView = ({
} }
return ( return (
<div className="px-4"> <div className="px-2">
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
{paginatedModels.map((model, index) => { {paginatedModels.map((model, index) => {
const modelKey = getModelKey(model); const modelKey = getModelKey(model);

View File

@@ -754,7 +754,7 @@ html.dark .with-pastel-balls::before {
} }
.pricing-search-header { .pricing-search-header {
padding: 1rem; padding: 0.5rem;
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
flex-shrink: 0; flex-shrink: 0;
position: sticky; position: sticky;