2025-08-08 17:42:07 +08:00
|
|
|
// TimelineItem.tsx
|
2025-08-06 18:41:32 +08:00
|
|
|
import TimelineImage from '@/components/TimelineImage';
|
|
|
|
|
import { StoryItem } from '@/pages/story/data';
|
2025-08-08 17:42:07 +08:00
|
|
|
import { DeleteOutlined, DownOutlined, EditOutlined, UpOutlined } from '@ant-design/icons';
|
2025-08-06 18:41:32 +08:00
|
|
|
import { useIntl, useRequest } from '@umijs/max';
|
2025-12-26 15:12:49 +08:00
|
|
|
import { Button, Card, message, Popconfirm, Tag, Space } from 'antd';
|
2025-08-06 18:41:32 +08:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { queryStoryItemImages, removeStoryItem } from '../../service';
|
2025-08-05 19:02:14 +08:00
|
|
|
import TimelineItemDrawer from '../TimelineItemDrawer';
|
2025-08-06 18:41:32 +08:00
|
|
|
import useStyles from './index.style';
|
2026-02-12 16:55:05 +08:00
|
|
|
import ResponsiveGrid from '@/components/ResponsiveGrid';
|
2025-08-05 19:02:14 +08:00
|
|
|
|
2025-12-26 15:12:49 +08:00
|
|
|
// 格式化时间数组为易读格式
|
|
|
|
|
const formatTimeArray = (time: string | number[] | undefined): string => {
|
|
|
|
|
if (!time) return '';
|
2025-12-31 14:30:03 +08:00
|
|
|
|
2025-12-26 15:12:49 +08:00
|
|
|
// 如果是数组格式 [2025, 12, 23, 8, 55, 39]
|
|
|
|
|
if (Array.isArray(time)) {
|
|
|
|
|
const [year, month, day, hour, minute, second] = time;
|
|
|
|
|
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
|
|
|
|
}
|
2025-12-31 14:30:03 +08:00
|
|
|
|
2025-12-26 15:12:49 +08:00
|
|
|
// 如果已经是字符串格式,直接返回
|
|
|
|
|
return String(time);
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-05 19:02:14 +08:00
|
|
|
const TimelineItem: React.FC<{
|
|
|
|
|
item: StoryItem;
|
|
|
|
|
handleOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
|
|
|
|
|
refresh: () => void;
|
2025-12-31 14:30:03 +08:00
|
|
|
disableEdit?: boolean;
|
|
|
|
|
}> = ({ item, handleOption, refresh, disableEdit }) => {
|
2026-02-12 16:55:05 +08:00
|
|
|
const { styles, cx, isMobile } = useStyles();
|
2025-08-05 19:02:14 +08:00
|
|
|
const intl = useIntl();
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
|
const [showActions, setShowActions] = useState(false);
|
|
|
|
|
const [subItemsExpanded, setSubItemsExpanded] = useState(false);
|
2025-08-06 18:41:32 +08:00
|
|
|
const [openDetail, setOpenDetail] = useState(false);
|
2025-12-26 15:12:49 +08:00
|
|
|
const [hovered, setHovered] = useState(false);
|
2025-08-06 18:41:32 +08:00
|
|
|
const { data: imagesList } = useRequest(async () => {
|
|
|
|
|
return await queryStoryItemImages(item.instanceId);
|
2025-12-26 15:12:49 +08:00
|
|
|
}, {
|
|
|
|
|
refreshDeps: [item.instanceId]
|
2025-08-06 18:41:32 +08:00
|
|
|
});
|
2025-08-05 19:02:14 +08:00
|
|
|
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' }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const toggleDescription = () => {
|
|
|
|
|
setExpanded(!expanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleSubItems = () => {
|
|
|
|
|
setSubItemsExpanded(!subItemsExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const displayedDescription = expanded
|
|
|
|
|
? item.description
|
2025-08-06 18:41:32 +08:00
|
|
|
: item.description?.substring(0, 100) +
|
|
|
|
|
(item.description && item.description.length > 100 ? '...' : '');
|
2025-08-05 19:02:14 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card
|
2025-12-26 15:12:49 +08:00
|
|
|
className={`${styles.timelineItem} ${hovered ? styles.timelineItemHover : ''}`}
|
|
|
|
|
title={
|
|
|
|
|
<div className={styles.timelineItemTitle}>
|
|
|
|
|
<div className={styles.timelineItemTitleText}>{item.title}</div>
|
|
|
|
|
<Space className={styles.timelineItemTags}>
|
|
|
|
|
{item.createName && (
|
|
|
|
|
<Tag color="blue" className={styles.creatorTag}>
|
|
|
|
|
创建: {item.createName}
|
|
|
|
|
</Tag>
|
|
|
|
|
)}
|
|
|
|
|
{item.updateName && item.updateName !== item.createName && (
|
|
|
|
|
<Tag color="green" className={styles.updaterTag}>
|
|
|
|
|
更新: {item.updateName}
|
|
|
|
|
</Tag>
|
|
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
onMouseEnter={() => {
|
|
|
|
|
setShowActions(true);
|
|
|
|
|
setHovered(true);
|
|
|
|
|
}}
|
|
|
|
|
onMouseLeave={() => {
|
|
|
|
|
setShowActions(false);
|
|
|
|
|
setHovered(false);
|
|
|
|
|
}}
|
2025-08-05 19:02:14 +08:00
|
|
|
extra={
|
2025-08-06 18:41:32 +08:00
|
|
|
<div className={styles.actions}>
|
2026-02-12 16:55:05 +08:00
|
|
|
{(showActions || isMobile) && !disableEdit && (
|
2025-08-05 19:02:14 +08:00
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
2025-08-06 18:41:32 +08:00
|
|
|
handleOption(item, 'edit');
|
2025-08-05 19:02:14 +08:00
|
|
|
}}
|
|
|
|
|
aria-label={intl.formatMessage({ id: 'story.edit' })}
|
|
|
|
|
/>
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title={intl.formatMessage({ id: 'story.deleteConfirm' })}
|
|
|
|
|
description={intl.formatMessage({ id: 'story.deleteConfirmDescription' })}
|
|
|
|
|
onConfirm={(e) => {
|
|
|
|
|
e?.stopPropagation();
|
2025-08-06 18:41:32 +08:00
|
|
|
handleDelete();
|
2025-08-05 19:02:14 +08:00
|
|
|
}}
|
|
|
|
|
okText={intl.formatMessage({ id: 'story.yes' })}
|
|
|
|
|
cancelText={intl.formatMessage({ id: 'story.no' })}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
danger
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
aria-label={intl.formatMessage({ id: 'story.delete' })}
|
|
|
|
|
/>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
hoverable
|
|
|
|
|
>
|
|
|
|
|
<div className={styles.content}>
|
2025-08-06 18:41:32 +08:00
|
|
|
<div className={styles.date} onClick={() => setOpenDetail(true)}>
|
2025-12-26 15:12:49 +08:00
|
|
|
<Space size="small" className={styles.dateInfo}>
|
2026-01-19 18:09:37 +08:00
|
|
|
<span className="timeline-date-badge">{formatTimeArray(item.storyItemTime)}</span>
|
|
|
|
|
{item.location && <span className="timeline-location-badge">📍 {item.location}</span>}
|
2025-12-26 15:12:49 +08:00
|
|
|
</Space>
|
2025-08-05 19:02:14 +08:00
|
|
|
</div>
|
2025-08-06 18:41:32 +08:00
|
|
|
<div className={styles.description} onClick={() => setOpenDetail(true)}>
|
2025-08-05 19:02:14 +08:00
|
|
|
{displayedDescription}
|
|
|
|
|
{item.description && item.description.length > 100 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleDescription();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{expanded
|
|
|
|
|
? intl.formatMessage({ id: 'story.showLess' })
|
|
|
|
|
: intl.formatMessage({ id: 'story.showMore' })}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{imagesList && imagesList.length > 0 && (
|
2026-01-19 18:09:37 +08:00
|
|
|
<div className="timeline-images-grid">
|
2026-02-12 16:55:05 +08:00
|
|
|
<ResponsiveGrid>
|
|
|
|
|
{imagesList.map((imageInstanceId, index) => (
|
|
|
|
|
<TimelineImage
|
|
|
|
|
key={imageInstanceId + index}
|
|
|
|
|
title={imageInstanceId}
|
|
|
|
|
imageInstanceId={imageInstanceId}
|
|
|
|
|
className={styles.timelineImage}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</ResponsiveGrid>
|
2026-01-19 18:09:37 +08:00
|
|
|
</div>
|
2025-08-05 19:02:14 +08:00
|
|
|
)}
|
|
|
|
|
{item.subItems && item.subItems.length > 0 && (
|
|
|
|
|
<div className={styles.subItems}>
|
|
|
|
|
<div
|
|
|
|
|
className={styles.subItemsHeader}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleSubItems();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span>
|
|
|
|
|
{intl.formatMessage({ id: 'story.subItems' })} ({item.subItems.length})
|
|
|
|
|
</span>
|
|
|
|
|
{subItemsExpanded ? <UpOutlined /> : <DownOutlined />}
|
|
|
|
|
</div>
|
|
|
|
|
{subItemsExpanded && (
|
|
|
|
|
<div className={styles.subItemsList}>
|
|
|
|
|
{item.subItems.map((subItem) => (
|
|
|
|
|
<div key={subItem.id} className={styles.subItem}>
|
|
|
|
|
<div className={styles.subItemDate}>
|
2025-12-26 15:12:49 +08:00
|
|
|
{formatTimeArray(item.storyItemTime)} {item.location ? `创建于${item.location}` : ''}
|
2025-08-05 19:02:14 +08:00
|
|
|
</div>
|
2025-08-06 18:41:32 +08:00
|
|
|
<div className={styles.subItemContent}>{subItem.description}</div>
|
2025-08-05 19:02:14 +08:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-08-08 17:42:07 +08:00
|
|
|
<TimelineItemDrawer
|
|
|
|
|
storyItem={item}
|
|
|
|
|
open={openDetail}
|
|
|
|
|
setOpen={setOpenDetail}
|
|
|
|
|
handleDelete={handleDelete}
|
2025-12-31 14:30:03 +08:00
|
|
|
disableEdit={disableEdit}
|
2025-08-08 17:42:07 +08:00
|
|
|
handOption={handleOption}
|
|
|
|
|
/>
|
2025-08-05 19:02:14 +08:00
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-31 14:30:03 +08:00
|
|
|
export default TimelineItem;
|