Files
timeline-frontend/src/pages/story/components/TimelineItemDrawer.tsx
jhao 5a0aa2b3c1
All checks were successful
test/timeline-frontend/pipeline/head This commit looks good
feat: 添加评论、反应、离线编辑及主题定制功能
- 实现评论系统,包括评论输入、列表展示和集成指南
- 添加反应功能组件(ReactionBar、ReactionButton、ReactionPicker)
- 实现离线编辑支持,包括同步状态管理和冲突解决
- 添加主题定制功能,支持多种配色方案和主题预览
- 新增多视图布局选项(时间线、分组、砌体视图)
- 实现个人资料编辑器,支持头像、简介和自定义字段编辑
- 添加统计页面,展示存储使用情况和上传趋势
- 新增相册管理功能,支持相册创建、编辑和照片管理
- 实现响应式设计和加载骨架屏组件
- 扩展国际化支持,新增孟加拉语、波斯语、印尼语、日语、葡萄牙语等语言
- 添加错误边界组件和离线指示器
- 更新配置文件、路由和依赖项
- 新增完整的文档、测试用例和集成指南
2026-02-25 15:02:05 +08:00

290 lines
9.6 KiB
TypeScript

// src/pages/story/components/TimelineItemDrawer.tsx
import TimelineImage from '@/components/TimelineImage';
import { Comments } from '@/components/Comments';
import { ReactionBar } from '@/components/Reactions';
import useComments from '@/hooks/useComments';
import useReactions from '@/hooks/useReactions';
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, theme } from 'antd';
import React, { useEffect } from 'react';
// 格式化时间数组为易读格式
const formatTimeArray = (time: string | number[] | undefined): string => {
if (!time) return '';
// 如果是数组格式 [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')}`;
}
// 如果已经是字符串格式,直接返回
return String(time);
};
interface Props {
storyItem: StoryItem;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDelete: () => void;
handOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
disableEdit?: boolean;
}
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) => {
return await queryStoryItemImages(itemId);
},
{
manual: true,
},
);
// Initialize comments for this story item
const {
comments,
loading: commentsLoading,
createLoading,
updateLoading,
deleteLoading,
addComment,
updateComment,
deleteComment,
} = useComments({
entityType: 'story',
entityId: storyItem.instanceId || '',
autoFetch: open,
autoSubscribe: open,
});
// Initialize reactions for this story item
const {
reactions,
userReaction,
addReaction,
updateReaction,
removeReaction,
actionLoading: reactionLoading,
} = useReactions('story', storyItem.instanceId || '', {
autoFetch: open,
autoSubscribe: open,
});
useEffect(() => {
if (open) {
run(storyItem.instanceId);
}
}, [open]);
const closeDrawer = () => {
setOpen(false);
};
// 格式化日期显示
const formatDate = (dateString: string | number[] | undefined) => {
if (!dateString) return '';
const formattedTime = formatTimeArray(dateString);
const date = new Date(formattedTime);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
return (
<>
{/* 只在打开时渲染抽屉 */}
<Drawer
width={800}
placement="right"
onClose={closeDrawer}
open={open}
zIndex={1000}
title={
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}>{storyItem.title}</h2>
<div style={{ display: 'flex', gap: '8px' }}>
{storyItem.createName && (
<Tag color="blue">
: {storyItem.createName}
</Tag>
)}
{storyItem.updateName && storyItem.updateName !== storyItem.createName && (
<Tag color="green">
: {storyItem.updateName}
</Tag>
)}
</div>
</div>
<div style={{ fontSize: '14px', color: '#888', marginTop: '4px' }}>
{formatTimeArray(storyItem.storyItemTime)} {storyItem.location ? `${storyItem.location}` : ''}
</div>
</div>
}
footer={ disableEdit &&
<div style={{ textAlign: 'right' }}>
<Space>
<Button
icon={<EditOutlined />}
onClick={() => {
return handOption(storyItem, 'edit');
}}
>
</Button>
<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
icon={<DeleteOutlined />}
danger
onClick={(e) => e.stopPropagation()}
aria-label={intl.formatMessage({ id: 'story.delete' })}
>
</Button>
</Popconfirm>
<Button onClick={closeDrawer}></Button>
</Space>
</div>
}>
<div style={{
padding: '0 24px',
backgroundColor: token.colorBgContainer,
color: token.colorText
}}>
<div style={{ marginBottom: '24px' }}>
<h3 style={{ color: token.colorText }}></h3>
<p style={{
fontSize: '16px',
lineHeight: '1.6',
color: token.colorTextSecondary
}}>
{storyItem.description}
</p>
</div>
<Divider style={{ borderColor: token.colorBorder }} />
{/* 时刻图库 */}
{imagesList && imagesList.length > 0 && (
<div style={{ marginBottom: '24px' }}>
<h3 style={{ color: token.colorText }}></h3>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '16px',
marginTop: '16px',
}}
>
{imagesList.map((imageInstanceId, index) => (
<div
key={imageInstanceId + index}
style={{
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
aspectRatio: '1/1',
}}
>
<TimelineImage
title={imageInstanceId}
imageInstanceId={imageInstanceId}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
))}
</div>
</div>
)}
<Divider />
{/* 创建和更新信息 */}
<div style={{ marginBottom: '24px' }}>
<h3></h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', marginTop: '16px' }}>
<div style={{ flex: '1', minWidth: '200px' }}>
<div style={{ color: '#888', marginBottom: '4px' }}></div>
<div style={{ fontSize: '16px' }}>{formatDate(storyItem.createTime)}</div>
</div>
<div style={{ flex: '1', minWidth: '200px' }}>
<div style={{ color: '#888', marginBottom: '4px' }}></div>
<div style={{ fontSize: '16px' }}>{formatDate(storyItem.updateTime)}</div>
</div>
<div style={{ flex: '1', minWidth: '200px' }}>
<div style={{ color: '#888', marginBottom: '4px' }}></div>
<div style={{ fontSize: '16px' }}>{storyItem.createName || '系统用户'}</div>
</div>
<div style={{ flex: '1', minWidth: '200px' }}>
<div style={{ color: '#888', marginBottom: '4px' }}></div>
<div style={{ fontSize: '16px' }}>{storyItem.updateName || storyItem.createName || '系统用户'}</div>
</div>
</div>
</div>
<Divider style={{ borderColor: token.colorBorder }} />
{/* Reactions Section */}
<div style={{ marginBottom: '24px' }}>
<h3 style={{ color: token.colorText }}></h3>
<ReactionBar
entityType="story"
entityId={storyItem.instanceId || ''}
reactionSummary={reactions || undefined}
onAdd={addReaction}
onRemove={removeReaction}
onChange={updateReaction}
loading={reactionLoading}
size="large"
/>
</div>
<Divider style={{ borderColor: token.colorBorder }} />
{/* Comments Section */}
<div style={{ marginBottom: '24px' }}>
<Comments
entityType="story"
entityId={storyItem.instanceId || ''}
comments={comments}
loading={commentsLoading}
onCreate={addComment}
onEdit={updateComment}
onDelete={deleteComment}
createLoading={createLoading}
editLoading={updateLoading}
deleteLoading={deleteLoading}
showCard={false}
title="评论"
/>
</div>
</div>
</Drawer>
</>
);
};
export default TimelineItemDrawer;