Story排版修改

This commit is contained in:
jiangh277
2025-08-05 19:02:14 +08:00
parent 63ae33288d
commit 141e8d9818
68 changed files with 536 additions and 3902 deletions

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { Form, Modal, Input, TimePicker, Button } from 'antd';
interface SubItem {
time: string;
title: string;
description: string;
cover?: string | null;
}
interface ModalProps {
visible: boolean;
onCancel: () => void;
onOk: (values: SubItem) => void;
}
const AddSubTimeLineItemModal: React.FC<ModalProps> = ({ visible, onCancel, onOk }) => {
const [form] = Form.useForm();
const handleOk = async () => {
try {
const values = await form.validateFields();
onOk(values);
} catch (error) {
console.error('表单校验失败:', error);
}
};
return (
<Modal
open={visible}
title="添加子时间点"
onCancel={() => {
form.resetFields();
onCancel();
}}
footer={[
<Button key="back" onClick={onCancel}>
</Button>,
<Button key="submit" type="primary" onClick={handleOk}>
</Button>,
]}
>
<Form form={form} layout="vertical" name="subTimeLineItemForm">
<Form.Item label="时间HH:mm" name="time" rules={[{ required: true, message: '请输入时间' }]}>
<Input placeholder="例如14:30" />
</Form.Item>
<Form.Item label="子标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
<Input placeholder="请输入子标题" />
</Form.Item>
<Form.Item label="子描述" name="description">
<Input.TextArea rows={4} placeholder="请输入子时间点描述" />
</Form.Item>
</Form>
</Modal>
);
};
export default AddSubTimeLineItemModal;

View File

@@ -0,0 +1,200 @@
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
import { addStoryItem } from '@/pages/story/service';
import { UploadOutlined } from '@ant-design/icons';
import { useRequest } from '@umijs/max';
import { Button, Cascader, DatePicker, Form, Input, message, Modal, Upload } from 'antd';
import ImgCrop from 'antd-img-crop';
import dayjs from 'dayjs';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
interface ModalProps {
visible: boolean;
onCancel: () => void;
onOk: () => void;
storyId: string | number | undefined;
initialValues?: any;
storyItemId?: string; // 是否根节点
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem';
}
const AddTimeLineItemModal: React.FC<ModalProps> = ({
visible,
onCancel,
onOk,
storyId,
initialValues,
storyItemId,
option,
}) => {
const [form] = Form.useForm();
const [fileList, setFileList] = useState<any[]>([]);
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
useEffect(() => {
if (initialValues && option === 'edit') {
form.setFieldsValue({
title: initialValues.title,
storyItemTime: initialValues.date ? moment(initialValues.date) : undefined,
location: initialValues.location,
description: initialValues.description,
cover: initialValues.cover ? [{ url: initialValues.cover }] : [],
images: initialValues.images?.map((url) => ({ url })) || [],
});
}
}, [initialValues, option]);
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
manual: true,
onSuccess: (data) => {
console.log(data);
if (data.code === 200) {
onOk();
message.success(initialValues ? '时间点已更新' : '时间点已保存');
}
},
onError: (error) => {
message.error('保存失败');
console.error('保存失败:', error);
},
});
const handleOk = async () => {
try {
const values = await form.validateFields();
const location = code2Location(values.location);
const newItem = {
...values,
id: initialValues?.id || Date.now(),
storyItemTime: dayjs(values.date).format('YYYY-MM-DDTHH:mm:ss'),
masterItemId: initialValues.masterItemId,
subItems: initialValues?.subItems || [],
storyInstanceId: storyId,
location,
};
delete newItem.cover;
delete newItem.images;
// 构建 FormData
const formData = new FormData();
// 添加 storyItem 作为 JSON 字符串
formData.append('storyItem', JSON.stringify(newItem));
// 添加封面文件
if (fileList.length > 0 && fileList[0].originFileObj instanceof File) {
formData.append('cover', fileList[0].originFileObj);
}
console.log(imageList);
if (imageList.length > 0) {
imageList.forEach((file) => {
if (file.originFileObj && file.originFileObj instanceof File) {
formData.append('images', file.originFileObj);
}
});
}
// 提交
submitItem(formData);
} catch (error) {
console.error('表单验证失败:', error);
message.error('请检查表单内容');
}
};
const uploadCoverProps = {
beforeUpload: (file) => {
// 确保 originFileObj 是真正的 File 对象
return false; // 阻止自动上传
},
onChange: ({ fileList }) => {
// 确保 originFileObj 是真正的 File 对象
const updatedFileList = fileList.map((file) => {
if (file.originFileObj && !(file.originFileObj instanceof File)) {
file.originFileObj = new File([file.originFileObj], file.name, { type: file.type });
}
return file;
});
setFileList(updatedFileList);
},
listType: 'picture',
maxCount: 1,
defaultFileList: initialValues?.cover ? [{ url: initialValues.cover }] : [],
};
const uploadImagesProps = {
beforeUpload: () => false,
onChange: ({ fileList }) => {
const updatedFileList = fileList.map((file) => {
if (file.originFileObj && !(file.originFileObj instanceof File)) {
file.originFileObj = new File([file.originFileObj], file.name, { type: file.type });
}
return file;
});
setImageList(updatedFileList);
},
listType: 'picture-card',
multiple: true,
defaultFileList: initialValues?.images?.map((url) => ({ url })),
};
return (
<Modal
open={visible}
title={initialValues ? '编辑时间点' : '添加时间点'}
onCancel={() => {
form.resetFields();
onCancel();
}}
footer={[
<Button key="back" onClick={onCancel}>
</Button>,
<Button key="submit" type="primary" onClick={handleOk} loading={loading}>
</Button>,
]}
>
<Form form={form} layout="vertical">
{['editSubItem', 'addSubItem'].includes( option) && <Form.Item label={'主时间点'}>{storyItemId}</Form.Item>}
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
<Input placeholder="请输入标题" />
</Form.Item>
<Form.Item label="时间" name="date" rules={[{ required: true, message: '请选择时间' }]}>
<DatePicker
showTime={{ defaultValue: dayjs('00:00:00', 'HH:mm:ss') }}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item label="位置" name="location" rules={[{ required: false }]}>
<Cascader
options={chinaRegion}
fieldNames={{ label: 'title', value: 'value', children: 'children' }}
placeholder="请选择省/市/区"
/>
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={4} placeholder="请输入时间点描述" />
</Form.Item>
<Form.Item label="封面图" name="cover">
<ImgCrop aspect={1} quality={0.5} rotationSlider>
<Upload {...uploadCoverProps} maxCount={1}>
<Button icon={<UploadOutlined />}></Button>
</Upload>
</ImgCrop>
</Form.Item>
{/* 新增:时刻图库 */}
<Form.Item label="时刻图库(多图)" name="images">
<Upload {...uploadImagesProps} maxCount={5}>
<Button size={'small'} icon={<UploadOutlined />}>
</Button>
</Upload>
</Form.Item>
</Form>
</Modal>
);
};
export default AddTimeLineItemModal;

View File

@@ -0,0 +1,348 @@
import {
ModalForm,
ProFormDateTimePicker,
ProFormSelect,
ProFormText,
ProFormTextArea,
} from '@ant-design/pro-components';
import { Button, message, Result, Upload, Radio } from 'antd';
import React, { FC, useState } from 'react';
import ImgCrop from 'antd-img-crop';
import type { StoryType } from '../data.d';
import useStyles from '../style.style';
import { defaultIcons } from '@/commonConstant/commonConstant';
type OperationModalProps = {
done: boolean;
open: boolean;
current: Partial<StoryType> | undefined;
onDone: () => void;
onSubmit: (values: StoryType) => void;
children?: React.ReactNode;
};
const OperationModal: FC<OperationModalProps> = (props) => {
const { styles } = useStyles();
const { done, open, current, onDone, onSubmit, children } = props;
// 图标状态管理
const [iconType, setIconType] = useState<'default' | 'upload'>(
current?.logo ? 'upload' : 'default',
);
const [selectedIcon, setSelectedIcon] = useState<string | null>(
current?.logo || defaultIcons[0],
);
const [iconPreview, setIconPreview] = useState<string | null>(
current?.logo || null,
);
const [fileList, setFileList] = useState<any[]>([]); // 控制上传图像展示
// 图标上传逻辑
const beforeUpload = (file: File) => {
const isValidType = file.type === 'image/png' || file.type === 'image/jpeg';
if (!isValidType) {
message.error('仅支持 PNG 或 JPEG 格式');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 强制设置裁剪尺寸为 40x40
canvas.width = 40;
canvas.height = 40;
ctx.drawImage(img, 0, 0, 40, 40);
// 生成 Base64 图像
const base64 = canvas.toDataURL('image/png');
setSelectedIcon(base64);
setIconPreview(base64);
setFileList([
{
uid: '-1',
name: 'icon.png',
status: 'done',
url: base64,
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
type: 'image/png',
}),
},
]);
};
img.onerror = () => {
message.error('图像加载失败');
};
img.src = e.target?.result as string;
};
reader.onerror = () => {
message.error('读取图像失败');
};
reader.readAsDataURL(file);
return false; // 阻止自动上传
};
// Base64 → Blob 转换工具函数
const dataURLtoBlob = (dataurl: string) => {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
};
if (!open) {
return null;
}
return (
<ModalForm<StoryType>
open={open}
title={done ? null : `故事${current ? '编辑' : '添加'}`}
className={styles.standardListForm}
width={640}
onFinish={async (values) => {
onSubmit({
...values,
logo: selectedIcon || '',
});
}}
initialValues={{
...current,
logo: current?.logo ? 'upload' : 'default',
}}
submitter={{
render: (_, dom) => (done ? null : dom),
}}
trigger={<>{children}</>}
modalProps={{
onCancel: () => onDone(),
destroyOnClose: true,
bodyStyle: done
? {
padding: '72px 0',
}
: {},
}}
>
{!done ? (
<>
<ProFormText
name="title"
label="故事名称"
rules={[
{
required: true,
message: '请输入故事名称',
},
]}
placeholder="请输入"
/>
{/* 图标选择方式 */}
<ProFormText
name="logo"
label="图标选择"
hidden
rules={[{ required: true, message: '请选择图标' }]}
fieldProps={{
value: iconType,
}}
/>
<div style={{ marginBottom: 16 }}>
<span style={{ fontWeight: 'bold' }}></span>
<Radio.Group
value={iconType}
onChange={(e) => {
const type = e.target.value;
setIconType(type);
if (type === 'default') {
setSelectedIcon(defaultIcons[0]);
setIconPreview(defaultIcons[0]);
setFileList([]);
} else {
setSelectedIcon(null);
setIconPreview(null);
}
}}
style={{ display: 'flex', gap: 16, marginTop: 8 }}
>
<Radio value="default"></Radio>
<Radio value="upload"></Radio>
</Radio.Group>
</div>
{/* 默认图标库 */}
{iconType === 'default' && (
<div style={{ marginBottom: 16 }}>
<span style={{ fontWeight: 'bold' }}></span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 8 }}>
{defaultIcons.map((icon, index) => (
<img
key={index}
src={icon}
alt="icon"
style={{
width: 40,
height: 40,
cursor: 'pointer',
border: selectedIcon === icon ? '2px solid #1890ff' : 'none',
borderRadius: 4,
}}
onClick={() => {
setSelectedIcon(icon);
setIconPreview(icon);
}}
/>
))}
</div>
</div>
)}
{/* 图标上传 + 裁剪 */}
{iconType === 'upload' && (
<div style={{ marginBottom: 24 }}>
<span style={{ fontWeight: 'bold' }}>40x40</span>
<ImgCrop
rotationSlider
aspect={1} // 强制 1:1 宽高比
modalTitle="裁剪图像"
quality={0.8}
onModalOk={() => {
// 裁剪完成后自动更新 fileList 和 Base64 数据
const img = new Image();
img.onload = () => {
if (img.width !== 40 || img.height !== 40) {
message.error('裁剪图像尺寸必须为 40x40 像素');
setIconPreview(null);
setFileList([]);
return;
}
const canvas = document.createElement('canvas');
canvas.width = 40;
canvas.height = 40;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, 40, 40);
const base64 = canvas.toDataURL('image/png');
setSelectedIcon(base64);
setIconPreview(base64);
setFileList([
{
uid: '-1',
name: 'icon.png',
status: 'done',
url: base64,
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
type: 'image/png',
}),
},
]);
};
img.src = iconPreview;
}}
>
<Upload
name="icon"
listType="picture-card"
showUploadList={false}
beforeUpload={beforeUpload}
onChange={({ fileList }) => {
setFileList(fileList);
}}
fileList={fileList}
style={{ marginTop: 8 }}
>
{iconPreview ? (
<img
src={iconPreview}
alt="icon"
style={{ width: '100%', height: '100%' }}
/>
) : (
<div style={{ fontSize: 20 }}>+</div>
)}
</Upload>
</ImgCrop>
</div>
)}
{/* 其他表单项 */}
<ProFormDateTimePicker
name="createTime"
label="开始时间"
rules={[
{
required: true,
message: '请选择开始时间',
},
]}
fieldProps={{
style: {
width: '100%',
},
}}
placeholder="请选择"
/>
<ProFormSelect
name="ownerId"
label="故事负责人"
rules={[
{
required: true,
message: '请选择故事负责人',
},
]}
options={[
{
label: '付晓晓',
value: 'xiao',
},
{
label: '周毛毛',
value: 'mao',
},
]}
placeholder="请选择管理员"
/>
<ProFormTextArea
name="description"
label="产品描述"
rules={[
{
message: '请输入至少五个字符的产品描述!',
min: 5,
},
]}
placeholder="请输入至少五个字符"
/>
</>
) : (
<Result
status="success"
title="操作成功"
subTitle={`${current?.instanceId ? '编辑' : '创建'}成功`}
extra={
<Button type="primary" onClick={onDone}>
</Button>
}
className={styles.formResult}
/>
)}
</ModalForm>
);
};
export default OperationModal;

View File

@@ -0,0 +1,97 @@
// file: SubTimeLineItemModal.tsx
import React, { useEffect } from 'react';
import { Form, Modal, Input, DatePicker, Upload, Button } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import moment from 'moment';
interface ModalProps {
visible: boolean;
onCancel: () => void;
onOk: (values: any) => void;
initialValues?: any;
}
const SubTimeLineItemModal: React.FC<ModalProps> = ({ visible, onCancel, onOk, initialValues }) => {
const [form] = Form.useForm();
useEffect(() => {
if (initialValues) {
form.setFieldsValue({
title: initialValues.title,
date: initialValues.date ? moment(initialValues.date) : undefined,
location: initialValues.location,
description: initialValues.description,
cover: initialValues.cover,
});
}
}, [initialValues]);
const handleOk = async () => {
try {
const values = await form.validateFields();
const newItem = {
...values,
id: initialValues?.id || Date.now(),
cover:
form.getFieldValue('cover') ||
(initialValues && initialValues.cover),
date: values.date.format('YYYY-MM-DD'),
};
onOk(newItem);
} catch (error) {
console.error('表单验证失败:', error);
}
};
const uploadProps = {
beforeUpload: () => false,
onChange: ({ fileList }) => {},
listType: 'picture' as const,
maxCount: 1,
};
return (
<Modal
open={visible}
title={initialValues ? '编辑时间点' : '添加时间点'}
onCancel={() => {
form.resetFields();
onCancel();
}}
footer={[
<Button key="back" onClick={onCancel}>
</Button>,
<Button key="submit" type="primary" onClick={handleOk}>
</Button>,
]}
>
<Form form={form} layout="vertical" name="timeLineItemForm">
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
<Input placeholder="请输入标题" />
</Form.Item>
<Form.Item label="时间" name="date" rules={[{ required: true, message: '请选择时间' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="地点" name="location">
<Input placeholder="请输入地点" />
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={4} placeholder="请输入时间点描述" />
</Form.Item>
<Form.Item label="封面图片" name="cover">
<Upload {...uploadProps} maxCount={1}>
<Button icon={<UploadOutlined />}></Button>
</Upload>
</Form.Item>
</Form>
</Modal>
);
};
export default SubTimeLineItemModal;

View File

@@ -0,0 +1,182 @@
import {DeleteOutlined, DownOutlined, EditOutlined, PlusOutlined, UpOutlined} from '@ant-design/icons';
import {useIntl, useRequest} from '@umijs/max';
import { Button, Card, Popconfirm, message } from 'antd';
import React, {useState} from 'react';
import {queryStoryItemImages, removeStoryItem} from '../../service';
import useStyles from './index.style';
import {StoryItem} from "@/pages/story/data";
import TimelineImage from "@/components/TimelineImage";
import TimelineItemDrawer from '../TimelineItemDrawer';
const TimelineItem: React.FC<{
item: StoryItem;
handleOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
refresh: () => void;
}> = ({ item, handleOption, refresh }) => {
const { styles } = useStyles();
const intl = useIntl();
const [expanded, setExpanded] = useState(false);
const [showActions, setShowActions] = useState(false);
const [subItemsExpanded, setSubItemsExpanded] = useState(false);
const [openDetail, setOpenDetail] = useState(false)
const { data: imagesList } = useRequest(
async () => {
return await queryStoryItemImages(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' }));
}
};
const toggleDescription = () => {
setExpanded(!expanded);
};
const toggleSubItems = () => {
setSubItemsExpanded(!subItemsExpanded);
};
const displayedDescription = expanded
? item.description
: item.description?.substring(0, 100) + (item.description && item.description.length > 100 ? '...' : '');
return (
<Card
className={styles.timelineItem}
title={item.title}
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
onClick={() => setOpenDetail(true)}
extra={
<div
className={styles.actions}
>
{showActions && (
<>
<Button
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleOption(item, 'editSubItem');
}}
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' })}
onConfirm={(e) => {
e?.stopPropagation();
handleDelete()
}}
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>
}
// onClick={() => onDetail(item)}
hoverable
>
<div className={styles.content}>
<div className={styles.date}>
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
</div>
<div className={styles.description}>
{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 && (
<>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
{imagesList.map((imageInstanceId, index) => (
<TimelineImage
key={imageInstanceId + index}
title={imageInstanceId}
imageInstanceId={imageInstanceId}
/>
))}
</div>
</>
)}
{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}>
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
</div>
<div className={styles.subItemContent}>
{subItem.description}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
<TimelineItemDrawer
storyItem={item}
open={openDetail}
setOpen={setOpenDetail}
/>
</Card>
);
};
export default TimelineItem;

View File

@@ -0,0 +1,38 @@
.timeline-item {
margin-bottom: 24px;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
.timeline-event-container {
padding: 16px;
background-color: #fff;
border: 1px solid #e8e8e8;
}
.timeline-content-text {
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
.sub-timeline-wrapper {
margin-top: 16px;
background-color: #f9f9f9;
padding: 16px;
border-radius: 8px;
}
.sub-timeline-item {
padding: 8px 0;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
}

View File

@@ -0,0 +1,90 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
timelineItem: {
marginBottom: '20px',
boxShadow: token.boxShadow,
borderRadius: token.borderRadius,
transition: 'all 0.3s',
cursor: 'pointer',
'&:hover': {
boxShadow: token.boxShadowSecondary,
},
},
actions: {
display: 'flex',
gap: '8px',
alignItems: 'center',
height: '24px',
width: '120px',
},
content: {
padding: '10px 0',
},
cover: {
width: '100%',
height: '200px',
overflow: 'hidden',
borderRadius: '4px',
marginBottom: '15px',
img: {
width: '100%',
height: '100%',
objectFit: 'cover',
},
},
date: {
fontSize: '14px',
color: token.colorTextSecondary,
marginBottom: '10px',
fontWeight: 'bold',
},
description: {
fontSize: '16px',
lineHeight: '1.6',
color: token.colorText,
marginBottom: '15px',
'.ant-btn-link': {
padding: '0 4px',
},
},
subItems: {
borderTop: `1px dashed ${token.colorBorder}`,
paddingTop: '15px',
marginTop: '15px',
},
subItemsHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
fontWeight: 'bold',
color: token.colorTextHeading,
marginBottom: '10px',
padding: '5px 0',
},
subItemsList: {
maxHeight: '300px',
overflowY: 'auto',
},
subItem: {
display: 'flex',
marginBottom: '10px',
'&:last-child': {
marginBottom: 0,
},
},
subItemDate: {
fontWeight: 'bold',
minWidth: '100px',
color: token.colorTextSecondary,
},
subItemContent: {
flex: 1,
color: token.colorText,
},
};
});
export default useStyles;

View File

@@ -0,0 +1,139 @@
import TimelineImage from '@/components/TimelineImage';
import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal';
import SubTimeLineItemModal from '@/pages/story/components/SubTimeLineItemModal';
import { StoryItem } from '@/pages/story/data';
import { queryStoryItemImages } from '@/pages/story/service';
import { EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { useRequest } from '@umijs/max';
import { Button, Drawer, Space } from 'antd';
import React, { useEffect } from 'react';
interface Props {
storyItem: StoryItem;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const TimelineItemDrawer = (props: Props) => {
const { storyItem, open, setOpen } = props;
const [editModalVisible, setEditModalVisible] = React.useState(false);
const [openAddSubItemModal, setOpenAddSubItemModal] = React.useState(false);
const [openEditMainItemModal, setOpenEditMainItemModal] = React.useState(false);
const { data: imagesList, run } = useRequest(
async (itemId) => {
return await queryStoryItemImages(itemId);
},
{
manual: true,
},
);
useEffect(() => {
if (open) {
console.log(storyItem);
run(storyItem.instanceId);
}
}, [open]);
const closeDrawer = () => {
setOpen(false);
};
const handleEditMainItem = (updatedItem: any) => {
const mergedEvent = {
...storyItem,
...updatedItem,
};
setOpenEditMainItemModal(false);
};
const handleAddSubItem = () => {
setOpenAddSubItemModal(false);
};
return (
<div>
{/* 主时间点详情抽屉 */}
<Drawer
width={1000}
placement="right"
onClose={() => {
closeDrawer();
}}
open={open}
title={storyItem.title}
footer={
<div style={{ textAlign: 'right' }}>
<Space>
<Button
icon={<PlusCircleOutlined />}
type="primary"
onClick={() => setOpenAddSubItemModal(true)}
>
</Button>
<Button icon={<EditOutlined />} onClick={() => setEditModalVisible(true)}>
</Button>
<Button onClick={closeDrawer}></Button>
</Space>
</div>
}
>
<p>
<strong></strong> {storyItem.description}
</p>
<p>
<strong></strong> {storyItem.storyItemTime}
</p>
<p>
<strong></strong> {storyItem.location}
</p>
{/* 封面图 */}
{storyItem.coverInstanceId && (
<>
<p>
<strong></strong>
</p>
<TimelineImage
title={storyItem.title + 'cover'}
imageInstanceId={storyItem.coverInstanceId}
/>
</>
)}
{/* 时刻图库 */}
{imagesList && imagesList.length > 0 && (
<>
<p>
<strong></strong>
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
{imagesList.map((imageInstanceId, index) => (
<TimelineImage
key={imageInstanceId + index}
title={imageInstanceId}
imageInstanceId={imageInstanceId}
/>
))}
</div>
</>
)}
{/* 添加子时间点模态框 */}
<AddTimeLineItemModal
visible={openAddSubItemModal}
onOk={handleAddSubItem}
onCancel={() => setOpenAddSubItemModal(false)}
lineId={storyItem.storyInstanceId}
storyItemId={storyItem.instanceId}
/>
{/* 编辑主时间点模态框 */}
<SubTimeLineItemModal
visible={openEditMainItemModal}
onOk={handleEditMainItem}
onCancel={() => setOpenEditMainItemModal(false)}
initialValues={storyItem}
/>
</Drawer>
</div>
);
};
export default TimelineItemDrawer;