时间线详情展示重构
This commit is contained in:
217
src/pages/story/components/TimelineGridItem.tsx
Normal file
217
src/pages/story/components/TimelineGridItem.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
// TimelineGridItem.tsx - Grid card layout for timeline items
|
||||
import TimelineImage from '@/components/TimelineImage';
|
||||
import { StoryItem } from '@/pages/story/data';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useIntl, useRequest } from '@umijs/max';
|
||||
import { Button, message, Popconfirm, Tag, theme } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { queryStoryItemImages, removeStoryItem } from '../service';
|
||||
|
||||
// 格式化时间数组为易读格式
|
||||
const formatTimeArray = (time: string | number[] | undefined): string => {
|
||||
if (!time) return '';
|
||||
|
||||
// 如果是数组格式 [2025, 12, 23, 8, 55, 39]
|
||||
if (Array.isArray(time)) {
|
||||
const [, , , hour, minute] = time;
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 如果已经是字符串格式,提取时间部分
|
||||
const timeStr = String(time);
|
||||
const timePart = timeStr.split(' ')[1];
|
||||
if (timePart) {
|
||||
const [hour, minute] = timePart.split(':');
|
||||
return `${hour}:${minute}`;
|
||||
}
|
||||
|
||||
return timeStr;
|
||||
};
|
||||
|
||||
const TimelineGridItem: React.FC<{
|
||||
item: StoryItem;
|
||||
handleOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
|
||||
onOpenDetail: (item: StoryItem) => void;
|
||||
refresh: () => void;
|
||||
disableEdit?: boolean;
|
||||
}> = ({ item, handleOption, onOpenDetail, refresh, disableEdit }) => {
|
||||
const intl = useIntl();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 动态设置CSS变量以适配主题
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// 根据Ant Design的token设置主题变量
|
||||
root.style.setProperty('--timeline-bg', token.colorBgContainer);
|
||||
root.style.setProperty('--timeline-card-bg', token.colorBgElevated);
|
||||
root.style.setProperty('--timeline-card-border', token.colorBorder);
|
||||
root.style.setProperty('--timeline-card-shadow', `0 2px 8px ${token.colorBgMask}`);
|
||||
root.style.setProperty('--timeline-text-primary', token.colorText);
|
||||
root.style.setProperty('--timeline-text-secondary', token.colorTextSecondary);
|
||||
root.style.setProperty('--timeline-text-tertiary', token.colorTextTertiary);
|
||||
root.style.setProperty('--timeline-header-color', token.colorPrimary);
|
||||
root.style.setProperty('--timeline-location-bg', `${token.colorSuccess}1A`); // 10% opacity
|
||||
root.style.setProperty('--timeline-location-border', `${token.colorSuccess}4D`); // 30% opacity
|
||||
root.style.setProperty('--timeline-location-color', token.colorSuccess);
|
||||
root.style.setProperty('--timeline-image-border', token.colorBorder);
|
||||
root.style.setProperty('--timeline-more-bg', token.colorBgMask);
|
||||
root.style.setProperty('--timeline-more-color', token.colorWhite);
|
||||
}, [token]);
|
||||
|
||||
const { data: imagesList } = useRequest(async () => {
|
||||
return await queryStoryItemImages(item.instanceId);
|
||||
}, {
|
||||
refreshDeps: [item.instanceId]
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!item.instanceId) return;
|
||||
const response = await removeStoryItem(item.instanceId);
|
||||
if (response.code === 200) {
|
||||
message.success(intl.formatMessage({ id: 'story.deleteSuccess' }));
|
||||
refresh();
|
||||
} else {
|
||||
message.error(intl.formatMessage({ id: 'story.deleteFailed' }));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(intl.formatMessage({ id: 'story.deleteFailed' }));
|
||||
}
|
||||
};
|
||||
|
||||
// 动态计算卡片大小(1-4格)
|
||||
const calculateCardSize = () => {
|
||||
const imageCount = imagesList?.length || 0;
|
||||
const descriptionLength = item.description?.length || 0;
|
||||
|
||||
// 根据图片数量和描述长度决定卡片大小
|
||||
if (imageCount >= 4 || descriptionLength > 200) {
|
||||
return 4; // 占据整行
|
||||
} else if (imageCount >= 2 || descriptionLength > 100) {
|
||||
return 2; // 占据2格
|
||||
} else if (imageCount >= 1 && descriptionLength > 50) {
|
||||
return 2; // 有图片且描述较长,占据2格
|
||||
} else {
|
||||
return 1; // 默认占据1格
|
||||
}
|
||||
};
|
||||
|
||||
const cardSize = calculateCardSize();
|
||||
|
||||
// 统一的文本长度 - 根据卡片大小调整
|
||||
const getDescriptionMaxLength = (size: number) => {
|
||||
switch (size) {
|
||||
case 1: return 80;
|
||||
case 2: return 150;
|
||||
case 3: return 200;
|
||||
case 4: return 300;
|
||||
default: return 100;
|
||||
}
|
||||
};
|
||||
|
||||
const descriptionMaxLength = getDescriptionMaxLength(cardSize);
|
||||
|
||||
// 截断描述文本
|
||||
const truncateText = (text: string | undefined, maxLength: number) => {
|
||||
if (!text) return '';
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
};
|
||||
|
||||
// 只返回article元素,不包含任何其他元素
|
||||
return (
|
||||
<article
|
||||
className={`timeline-grid-item size-${cardSize}`}
|
||||
onClick={() => onOpenDetail(item)}
|
||||
>
|
||||
{/* Action buttons */}
|
||||
{!disableEdit && (
|
||||
<div className="item-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOption(item, 'edit');
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.edit' })}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={intl.formatMessage({ id: 'story.deleteConfirm' })}
|
||||
description={intl.formatMessage({ id: 'story.deleteConfirmDescription' })}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
okText={intl.formatMessage({ id: 'story.yes' })}
|
||||
cancelText={intl.formatMessage({ id: 'story.no' })}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={intl.formatMessage({ id: 'story.delete' })}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time header */}
|
||||
<h3>{formatTimeArray(item.storyItemTime)}</h3>
|
||||
|
||||
{/* Title */}
|
||||
<div className="item-title">{item.title}</div>
|
||||
|
||||
{/* Description */}
|
||||
<p>{truncateText(item.description, descriptionMaxLength)}</p>
|
||||
|
||||
{/* Images preview - 固定间隔,单行展示,多余折叠 */}
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<div className="item-images-container">
|
||||
<div className="item-images-row">
|
||||
{imagesList
|
||||
.filter(imageInstanceId => imageInstanceId && imageInstanceId.trim() !== '')
|
||||
.slice(0, 6) // 最多显示6张图片
|
||||
.map((imageInstanceId, index) => (
|
||||
<div key={imageInstanceId + index} className="item-image-wrapper">
|
||||
<TimelineImage
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{imagesList.length > 6 && (
|
||||
<div className="more-images-indicator">
|
||||
+{imagesList.length - 6}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location badge */}
|
||||
{item.location && (
|
||||
<span className="timeline-location-badge">📍 {item.location}</span>
|
||||
)}
|
||||
|
||||
{/* Creator/Updater tags */}
|
||||
<div className="item-tags">
|
||||
{item.createName && (
|
||||
<Tag color="blue" style={{ fontSize: '11px', padding: '2px 8px' }}>
|
||||
{item.createName}
|
||||
</Tag>
|
||||
)}
|
||||
{item.updateName && item.updateName !== item.createName && (
|
||||
<Tag color="green" style={{ fontSize: '11px', padding: '2px 8px' }}>
|
||||
{item.updateName}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineGridItem;
|
||||
@@ -109,15 +109,6 @@ const TimelineItem: React.FC<{
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.edit' })}
|
||||
/>
|
||||
{/*<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOption(item, 'addSubItem');
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.addSubItem' })}
|
||||
/>*/}
|
||||
<Popconfirm
|
||||
title={intl.formatMessage({ id: 'story.deleteConfirm' })}
|
||||
description={intl.formatMessage({ id: 'story.deleteConfirmDescription' })}
|
||||
@@ -145,8 +136,8 @@ const TimelineItem: React.FC<{
|
||||
<div className={styles.content}>
|
||||
<div className={styles.date} onClick={() => setOpenDetail(true)}>
|
||||
<Space size="small" className={styles.dateInfo}>
|
||||
<span className={styles.time}>{formatTimeArray(item.storyItemTime)}</span>
|
||||
{item.location && <span className={styles.location}>📍 {item.location}</span>}
|
||||
<span className="timeline-date-badge">{formatTimeArray(item.storyItemTime)}</span>
|
||||
{item.location && <span className="timeline-location-badge">📍 {item.location}</span>}
|
||||
</Space>
|
||||
</div>
|
||||
<div className={styles.description} onClick={() => setOpenDetail(true)}>
|
||||
@@ -166,18 +157,16 @@ const TimelineItem: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<>
|
||||
<div className={styles.timelineItemImages}>
|
||||
{imagesList.map((imageInstanceId, index) => (
|
||||
<TimelineImage
|
||||
key={imageInstanceId + index}
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
className={styles.timelineImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<div className="timeline-images-grid">
|
||||
{imagesList.map((imageInstanceId, index) => (
|
||||
<TimelineImage
|
||||
key={imageInstanceId + index}
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
className={styles.timelineImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.subItems && item.subItems.length > 0 && (
|
||||
<div className={styles.subItems}>
|
||||
|
||||
84
src/pages/story/components/TimelineItem/timeline.html
Normal file
84
src/pages/story/components/TimelineItem/timeline.html
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
<main>
|
||||
<h1>Events</h1>
|
||||
<section>
|
||||
<h2>2 December</h2>
|
||||
<div class="grid-wrapper">
|
||||
<article>
|
||||
<h3>9:00 AM</h3>
|
||||
<p>Life finds a way. You know what? It is beets. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>10:00 AM</h3>
|
||||
<p>I've crashed into a beet truck </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>12:30 AM</h3>
|
||||
<p>I was part of something special. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>13:30 AM</h3>
|
||||
<p>Yeah, but your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should. </p>
|
||||
<img src="https://images.fineartamerica.com/images-medium-large-5/maroon-bells-aspen-colorado-black-and-white-photography-by-sai.jpg" alt="Black and white photo of a lake">
|
||||
|
||||
</article>
|
||||
<article>
|
||||
<h3>14:30 AM</h3>
|
||||
<p>Just my luck, no ice. God help us, we're in the hands of engineers. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>15:30 AM</h3>
|
||||
<p>I gave it a cold? I gave it a virus. A computer virus. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>16:30 AM</h3>
|
||||
<p>God creates dinosaurs. God destroys dinosaurs. God creates Man. Man destroys God. Man creates Dinosaurs. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>17:30 AM</h3>
|
||||
<p>What do they got in there? King Kong? </p>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Eiffel_tower_at_Exposition_Universelle%2C_Paris%2C_1889.jpg/1200px-Eiffel_tower_at_Exposition_Universelle%2C_Paris%2C_1889.jpg" alt="Black and White Eiffel Tower"" alt="Black and white Mountian view">
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>3 Jan</h2>
|
||||
<div class="grid-wrapper">
|
||||
<article>
|
||||
<h3>9:00 AM</h3>
|
||||
<p>Life finds a way. You know what? It is beets. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>10:00 AM</h3>
|
||||
<p>I've crashed into a beet truck </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>12:30 AM</h3>
|
||||
<p>I was part of something special. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>13:30 AM</h3>
|
||||
<p>Yeah, but your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should. </p>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Eiffel_tower_at_Exposition_Universelle%2C_Paris%2C_1889.jpg/1200px-Eiffel_tower_at_Exposition_Universelle%2C_Paris%2C_1889.jpg" alt="Black and White Eiffel Tower">
|
||||
</article>
|
||||
<article>
|
||||
<h3>14:30 AM</h3>
|
||||
<p>Just my luck, no ice. God help us, we're in the hands of engineers. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>15:30 AM</h3>
|
||||
<p>I gave it a cold? I gave it a virus. A computer virus. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>16:30 AM</h3>
|
||||
<p>God creates dinosaurs. God destroys dinosaurs. God creates Man. Man destroys God. Man creates Dinosaurs. </p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>17:30 AM</h3>
|
||||
<p>What do they got in there? King Kong? </p>
|
||||
<img src="https://images.fineartamerica.com/images-medium-large-5/maroon-bells-aspen-colorado-black-and-white-photography-by-sai.jpg" alt="Black and white Mountian view">
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<p class="footer-note">Design by <a href="https://dribbble.com/shots/8576480-Book-Festival-Responsive-Website">tubik</a></p>
|
||||
</main>
|
||||
109
src/pages/story/components/TimelineItem/timeline.scss
Normal file
109
src/pages/story/components/TimelineItem/timeline.scss
Normal file
@@ -0,0 +1,109 @@
|
||||
.timeline-container {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(to bottom, #1890ff, #52c41a);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 30px;
|
||||
padding-left: 70px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: 21px;
|
||||
top: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 0 0 2px #1890ff;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
margin: 10px 0;
|
||||
color: #595959;
|
||||
line-height: 1.6;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-location {
|
||||
margin: 5px 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.timeline-images {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.timeline-image {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.timeline-tags {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.timeline-actions {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.timeline-item:hover .timeline-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { StoryItem } from '@/pages/story/data';
|
||||
import { queryStoryItemImages } from '@/pages/story/service';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useIntl, useRequest } from '@umijs/max';
|
||||
import { Button, Divider, Drawer, Popconfirm, Space, Tag } from 'antd';
|
||||
import { Button, Divider, Drawer, Popconfirm, Space, Tag, theme } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
// 格式化时间数组为易读格式
|
||||
@@ -33,6 +33,7 @@ interface Props {
|
||||
const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
const { storyItem, open, setOpen, handleDelete, handOption, disableEdit } = props;
|
||||
const intl = useIntl();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const { data: imagesList, run } = useRequest(
|
||||
async (itemId) => {
|
||||
@@ -70,8 +71,8 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 主时间点详情抽屉 */}
|
||||
<>
|
||||
{/* 只在打开时渲染抽屉 */}
|
||||
<Drawer
|
||||
width={800}
|
||||
placement="right"
|
||||
@@ -134,18 +135,28 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
</Space>
|
||||
</div>
|
||||
}>
|
||||
<div style={{ padding: '0 24px' }}>
|
||||
<div style={{
|
||||
padding: '0 24px',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
color: token.colorText
|
||||
}}>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3>描述</h3>
|
||||
<p style={{ fontSize: '16px', lineHeight: '1.6' }}>{storyItem.description}</p>
|
||||
<h3 style={{ color: token.colorText }}>描述</h3>
|
||||
<p style={{
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: token.colorTextSecondary
|
||||
}}>
|
||||
{storyItem.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<Divider style={{ borderColor: token.colorBorder }} />
|
||||
|
||||
{/* 时刻图库 */}
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3>时刻图库</h3>
|
||||
<h3 style={{ color: token.colorText }}>时刻图库</h3>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
@@ -200,7 +211,7 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user