2025-07-22 22:52:55 +08:00
|
|
|
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
|
2025-08-04 16:56:39 +08:00
|
|
|
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
|
2025-08-05 19:02:14 +08:00
|
|
|
import { addStoryItem } from '@/pages/story/service';
|
2025-08-06 18:41:32 +08:00
|
|
|
import { getImagesList } from '@/services/file/api'; // 引入获取图库图片的API
|
|
|
|
|
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
2025-07-22 22:52:55 +08:00
|
|
|
import { useRequest } from '@umijs/max';
|
2025-08-06 18:41:32 +08:00
|
|
|
import {
|
|
|
|
|
Button,
|
|
|
|
|
Card,
|
|
|
|
|
Cascader,
|
|
|
|
|
Checkbox,
|
|
|
|
|
DatePicker,
|
|
|
|
|
Form,
|
|
|
|
|
Image,
|
|
|
|
|
Input,
|
|
|
|
|
InputRef,
|
|
|
|
|
message,
|
|
|
|
|
Modal,
|
|
|
|
|
Pagination,
|
|
|
|
|
Spin,
|
|
|
|
|
Tabs,
|
|
|
|
|
Upload,
|
|
|
|
|
} from 'antd';
|
2025-07-22 22:52:55 +08:00
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import moment from 'moment';
|
2025-08-06 18:41:32 +08:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2025-07-22 22:52:55 +08:00
|
|
|
|
|
|
|
|
interface ModalProps {
|
|
|
|
|
visible: boolean;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
onOk: () => void;
|
2025-08-05 19:02:14 +08:00
|
|
|
storyId: string | number | undefined;
|
2025-07-22 22:52:55 +08:00
|
|
|
initialValues?: any;
|
2025-08-05 19:02:14 +08:00
|
|
|
storyItemId?: string; // 是否根节点
|
|
|
|
|
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem';
|
2025-07-22 22:52:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|
|
|
|
visible,
|
|
|
|
|
onCancel,
|
|
|
|
|
onOk,
|
2025-08-05 19:02:14 +08:00
|
|
|
storyId,
|
2025-07-22 22:52:55 +08:00
|
|
|
initialValues,
|
2025-08-05 19:02:14 +08:00
|
|
|
storyItemId,
|
|
|
|
|
option,
|
2025-07-22 22:52:55 +08:00
|
|
|
}) => {
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
const [fileList, setFileList] = useState<any[]>([]);
|
|
|
|
|
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
|
2025-08-06 18:41:32 +08:00
|
|
|
const [galleryImages, setGalleryImages] = useState<any[]>([]); // 图库图片列表
|
|
|
|
|
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
|
|
|
|
initialValues?.galleryImageIds || [],
|
|
|
|
|
); // 选中的图库图片
|
|
|
|
|
const [galleryLoading, setGalleryLoading] = useState(false);
|
|
|
|
|
const [activeTab, setActiveTab] = useState('upload'); // 图片选择标签页
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [pageSize] = useState(20);
|
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
|
|
|
const searchInputRef = useRef<InputRef>(null);
|
|
|
|
|
|
2025-07-22 22:52:55 +08:00
|
|
|
useEffect(() => {
|
2025-08-06 18:41:32 +08:00
|
|
|
if (initialValues && option.startsWith('edit')) {
|
2025-07-22 22:52:55 +08:00
|
|
|
form.setFieldsValue({
|
|
|
|
|
title: initialValues.title,
|
2025-08-06 18:41:32 +08:00
|
|
|
date: initialValues.storyItemTime ? moment(initialValues.storyItemTime) : undefined,
|
2025-07-22 22:52:55 +08:00
|
|
|
location: initialValues.location,
|
|
|
|
|
description: initialValues.description,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-05 19:02:14 +08:00
|
|
|
}, [initialValues, option]);
|
2025-08-06 18:41:32 +08:00
|
|
|
|
|
|
|
|
// 获取图库图片
|
|
|
|
|
const fetchGalleryImages = async (page: number = 1, keyword: string = '') => {
|
|
|
|
|
if (visible) {
|
|
|
|
|
setGalleryLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await getImagesList({
|
|
|
|
|
current: page,
|
|
|
|
|
pageSize: pageSize,
|
|
|
|
|
keyword: keyword,
|
|
|
|
|
});
|
|
|
|
|
const images = response.data.list.map((img: any) => ({
|
|
|
|
|
instanceId: img.instanceId,
|
|
|
|
|
imageName: img.imageName,
|
|
|
|
|
url: `/file/image-low-res/${img.instanceId}`,
|
|
|
|
|
}));
|
|
|
|
|
setGalleryImages(images);
|
|
|
|
|
setTotal(response.data.total);
|
|
|
|
|
setCurrentPage(page);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.error('获取图库图片失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setGalleryLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (visible && activeTab === 'gallery') {
|
|
|
|
|
fetchGalleryImages(1, searchKeyword);
|
|
|
|
|
}
|
|
|
|
|
}, [visible, activeTab, searchKeyword]);
|
|
|
|
|
|
2025-07-22 22:52:55 +08:00
|
|
|
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 = {
|
|
|
|
|
storyItemTime: dayjs(values.date).format('YYYY-MM-DDTHH:mm:ss'),
|
2025-08-06 18:41:32 +08:00
|
|
|
title: values.title,
|
|
|
|
|
description: values.description,
|
|
|
|
|
masterItemId: initialValues?.masterItemId,
|
2025-07-22 22:52:55 +08:00
|
|
|
subItems: initialValues?.subItems || [],
|
2025-08-05 19:02:14 +08:00
|
|
|
storyInstanceId: storyId,
|
2025-07-22 22:52:55 +08:00
|
|
|
location,
|
2025-08-06 18:41:32 +08:00
|
|
|
instanceId: initialValues?.instanceId,
|
|
|
|
|
// 添加选中的图库图片ID
|
|
|
|
|
relatedImageInstanceIds: selectedGalleryImages,
|
2025-07-22 22:52:55 +08:00
|
|
|
};
|
|
|
|
|
// 构建 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);
|
|
|
|
|
}
|
2025-08-06 18:41:32 +08:00
|
|
|
|
|
|
|
|
// 添加上传的图片文件
|
2025-07-22 22:52:55 +08:00
|
|
|
if (imageList.length > 0) {
|
|
|
|
|
imageList.forEach((file) => {
|
|
|
|
|
if (file.originFileObj && file.originFileObj instanceof File) {
|
|
|
|
|
formData.append('images', file.originFileObj);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-06 18:41:32 +08:00
|
|
|
|
|
|
|
|
console.log(formData);
|
2025-07-22 22:52:55 +08:00
|
|
|
// 提交
|
|
|
|
|
submitItem(formData);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('表单验证失败:', error);
|
|
|
|
|
message.error('请检查表单内容');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 })),
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-06 18:41:32 +08:00
|
|
|
// 切换图库图片选择状态
|
|
|
|
|
const toggleGalleryImageSelection = (instanceId: string) => {
|
|
|
|
|
setSelectedGalleryImages((prev) => {
|
|
|
|
|
if (prev.includes(instanceId)) {
|
|
|
|
|
return prev.filter((id) => id !== instanceId);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, instanceId];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理分页变化
|
|
|
|
|
const handlePageChange = (page: number) => {
|
|
|
|
|
fetchGalleryImages(page, searchKeyword);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理搜索
|
|
|
|
|
const handleSearch = (value: string) => {
|
|
|
|
|
setSearchKeyword(value);
|
|
|
|
|
fetchGalleryImages(1, value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 渲染图库图片选择器
|
|
|
|
|
const renderGallerySelector = () => (
|
|
|
|
|
<div>
|
|
|
|
|
{/* 搜索框 */}
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
<Input
|
|
|
|
|
ref={searchInputRef}
|
|
|
|
|
placeholder="搜索图片名称"
|
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
onPressEnter={(e) => handleSearch(e.currentTarget.value)}
|
|
|
|
|
style={{ width: 200 }}
|
|
|
|
|
allowClear
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={() => handleSearch(searchInputRef.current?.input?.value || '')}
|
|
|
|
|
style={{ marginLeft: 8 }}
|
|
|
|
|
>
|
|
|
|
|
搜索
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{galleryLoading ? (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
|
|
|
|
|
gap: '10px',
|
|
|
|
|
maxHeight: '300px',
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{galleryImages.map((image) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={image.instanceId}
|
|
|
|
|
size="small"
|
|
|
|
|
hoverable
|
|
|
|
|
onClick={() => toggleGalleryImageSelection(image.instanceId)}
|
|
|
|
|
style={{
|
|
|
|
|
border: selectedGalleryImages.includes(image.instanceId)
|
|
|
|
|
? '2px solid #1890ff'
|
|
|
|
|
: '1px solid #f0f0f0',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ position: 'relative' }}>
|
|
|
|
|
<Image
|
|
|
|
|
src={image.url}
|
|
|
|
|
alt={image.imageName}
|
|
|
|
|
style={{ width: '100%', height: '100px', objectFit: 'cover' }}
|
|
|
|
|
preview={false}
|
|
|
|
|
/>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={selectedGalleryImages.includes(image.instanceId)}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '4px',
|
|
|
|
|
right: '4px',
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
marginTop: '4px',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
title={image.imageName}
|
|
|
|
|
>
|
|
|
|
|
{image.imageName}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{galleryImages.length === 0 && !galleryLoading && (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
|
|
|
{searchKeyword ? '未找到相关图片' : '图库中暂无图片'}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 分页器 */}
|
|
|
|
|
{total > 0 && (
|
|
|
|
|
<div style={{ marginTop: 16, textAlign: 'right' }}>
|
|
|
|
|
<Pagination
|
|
|
|
|
current={currentPage}
|
|
|
|
|
pageSize={pageSize}
|
|
|
|
|
total={total}
|
|
|
|
|
onChange={handlePageChange}
|
|
|
|
|
showSizeChanger={false}
|
|
|
|
|
showQuickJumper
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-22 22:52:55 +08:00
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={visible}
|
|
|
|
|
title={initialValues ? '编辑时间点' : '添加时间点'}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
form.resetFields();
|
2025-08-06 18:41:32 +08:00
|
|
|
setSelectedGalleryImages([]);
|
|
|
|
|
setSearchKeyword('');
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
setGalleryImages([]);
|
2025-07-22 22:52:55 +08:00
|
|
|
onCancel();
|
|
|
|
|
}}
|
2025-08-06 18:41:32 +08:00
|
|
|
width={800}
|
2025-07-22 22:52:55 +08:00
|
|
|
footer={[
|
|
|
|
|
<Button key="back" onClick={onCancel}>
|
|
|
|
|
取消
|
|
|
|
|
</Button>,
|
|
|
|
|
<Button key="submit" type="primary" onClick={handleOk} loading={loading}>
|
|
|
|
|
确定
|
|
|
|
|
</Button>,
|
|
|
|
|
]}
|
|
|
|
|
>
|
|
|
|
|
<Form form={form} layout="vertical">
|
2025-08-06 18:41:32 +08:00
|
|
|
{['editSubItem', 'addSubItem'].includes(option) && (
|
|
|
|
|
<Form.Item label={'主时间点'}>{initialValues.title}</Form.Item>
|
|
|
|
|
)}
|
2025-07-22 22:52:55 +08:00
|
|
|
<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>
|
|
|
|
|
|
2025-08-06 18:41:32 +08:00
|
|
|
{/* 时刻图库 */}
|
|
|
|
|
<Form.Item label="附图" name="images">
|
|
|
|
|
<Tabs
|
|
|
|
|
activeKey={activeTab}
|
|
|
|
|
onChange={(key) => {
|
|
|
|
|
setActiveTab(key);
|
|
|
|
|
if (key === 'gallery' && galleryImages.length === 0) {
|
|
|
|
|
fetchGalleryImages(1, searchKeyword);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
items={[
|
|
|
|
|
{
|
|
|
|
|
key: 'upload',
|
|
|
|
|
label: '上传图片',
|
|
|
|
|
children: (
|
|
|
|
|
<Upload {...uploadImagesProps} maxCount={5}>
|
|
|
|
|
<div>
|
|
|
|
|
<PlusOutlined />
|
|
|
|
|
<div style={{ marginTop: 8 }}>上传图片</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Upload>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'gallery',
|
|
|
|
|
label: `从图库选择${selectedGalleryImages.length > 0 ? ` (${selectedGalleryImages.length})` : ''}`,
|
|
|
|
|
children: renderGallerySelector(),
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
2025-07-22 22:52:55 +08:00
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default AddTimeLineItemModal;
|