支持缩略图加载,故事项新建调整
This commit is contained in:
parent
141e8d9818
commit
182a58d0db
|
@ -1,12 +1,12 @@
|
|||
import { fetchImage } from '@/pages/story/service';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {fetchImageLowRes} from "@/services/file/api";
|
||||
|
||||
const useFetchImageUrl = (imageInstanceId: string) => {
|
||||
const [imageUrl, setImageUrl] = useState("error");
|
||||
const { data: response, run, loading } = useRequest(
|
||||
() => {
|
||||
return fetchImage(imageInstanceId);
|
||||
return fetchImageLowRes(imageInstanceId);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import useFetchImageUrl from '@/components/Hooks/useFetchImageUrl';
|
||||
import { Image } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface ImageItem {
|
||||
instanceId: string;
|
||||
|
@ -28,18 +27,18 @@ const TimelineImage: React.FC<Props> = (props) => {
|
|||
width = 200,
|
||||
height = 200,
|
||||
imageList = [],
|
||||
currentIndex = 0
|
||||
currentIndex = 0,
|
||||
} = props;
|
||||
|
||||
const { imageUrl, loading } = useFetchImageUrl(imageInstanceId ?? '');
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
|
||||
// 构建预览列表
|
||||
imageList.map(item => ({
|
||||
imageList.map((item) => ({
|
||||
src: item.instanceId,
|
||||
title: item.imageName
|
||||
title: item.imageName,
|
||||
}));
|
||||
// 预览配置
|
||||
// 预览配置
|
||||
const previewConfig = {
|
||||
visible: previewVisible,
|
||||
onVisibleChange: (visible: boolean) => setPreviewVisible(visible),
|
||||
|
@ -50,8 +49,10 @@ const TimelineImage: React.FC<Props> = (props) => {
|
|||
<div className="tl-image-container" style={{ width, height }}>
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={src ?? imageUrl}
|
||||
preview={loading ? false : previewConfig}
|
||||
src={src ?? `/file/image-low-res/${imageInstanceId}`}
|
||||
preview={{
|
||||
src: `/file/image/${imageInstanceId}`,
|
||||
}}
|
||||
height={height}
|
||||
width={width}
|
||||
alt={title}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// src/pages/gallery/components/GalleryTable.tsx
|
||||
import { ImageItem } from '@/pages/gallery/typings';
|
||||
import { formatBytes } from '@/utils/timelineUtils';
|
||||
import type { ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { FC } from 'react';
|
||||
import '../index.css'
|
||||
import {ImageItem} from "@/pages/gallery/typings";
|
||||
import '../index.css';
|
||||
|
||||
interface GalleryTableProps {
|
||||
imageList: ImageItem[];
|
||||
|
@ -43,7 +43,7 @@ const GalleryTable: FC<GalleryTableProps> = ({
|
|||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundImage: `url(/file/image/${record.instanceId})`,
|
||||
backgroundImage: `url(/file/image-low-res/${record.instanceId})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
|
|
|
@ -77,6 +77,16 @@ const GridView: FC<GridViewProps> = ({
|
|||
[imageList, onPreview, onDownload, onDelete],
|
||||
);
|
||||
|
||||
// 根据视图模式确定图像 URL
|
||||
const getImageUrl = (instanceId: string) => {
|
||||
// 小图模式使用低分辨率图像
|
||||
if (viewMode === 'small') {
|
||||
return `/file/image-low-res/${instanceId}`;
|
||||
}
|
||||
// 其他模式使用原图
|
||||
return `/file/image/${instanceId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={viewMode === 'small' ? 'small-grid-view' : 'large-grid-view'}
|
||||
|
@ -96,7 +106,7 @@ const GridView: FC<GridViewProps> = ({
|
|||
style={{
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
backgroundImage: `url(/file/image/${item.instanceId})`,
|
||||
backgroundImage: `url(${getImageUrl(item.instanceId)})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
// src/pages/gallery/components/ListView.tsx
|
||||
import { ImageItem } from '@/pages/gallery/typings';
|
||||
import { formatBytes } from '@/utils/timelineUtils';
|
||||
import { DeleteOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Checkbox, Spin } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import '../index.css'
|
||||
import {ImageItem} from "@/pages/gallery/typings";
|
||||
import '../index.css';
|
||||
|
||||
interface ListViewProps {
|
||||
imageList: ImageItem[];
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
// src/pages/gallery/index.tsx
|
||||
import React, { FC, useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { PageContainer } from "@ant-design/pro-components";
|
||||
import { useRequest } from "@umijs/max";
|
||||
import { getImagesList, deleteImage, uploadImage } from "@/services/file/api";
|
||||
import { Empty, Spin, Image, message, Modal } from 'antd';
|
||||
import { ImageItem } from '@/pages/gallery/typings';
|
||||
import { deleteImage, getImagesList, uploadImage } from '@/services/file/api';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import type { RadioChangeEvent } from 'antd';
|
||||
import { Empty, Image, message, Modal, Spin } from 'antd';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import GalleryTable from './components/GalleryTable';
|
||||
import GalleryToolbar from './components/GalleryToolbar';
|
||||
import GridView from './components/GridView';
|
||||
import ListView from './components/ListView';
|
||||
import GalleryTable from './components/GalleryTable';
|
||||
import './index.css'
|
||||
import { ImageItem } from "@/pages/gallery/typings";
|
||||
import './index.css';
|
||||
|
||||
const Gallery: FC = () => {
|
||||
const [viewMode, setViewMode] = useState<'small' | 'large' | 'list' | 'table'>('small');
|
||||
|
@ -21,28 +21,32 @@ const Gallery: FC = () => {
|
|||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const [batchMode, setBatchMode] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const pageSize = 5;
|
||||
const initPagination = { current: 1, pageSize: 5 }
|
||||
const pageSize = 50;
|
||||
const initPagination = { current: 1, pageSize: 5 };
|
||||
|
||||
// 表格模式使用不同的分页状态
|
||||
const [tablePagination, setTablePagination] = useState(initPagination);
|
||||
|
||||
// 非表格模式的数据请求
|
||||
const { data, loading, loadingMore, refresh } = useRequest(() => {
|
||||
return getImagesList({ current: page, pageSize });
|
||||
}, {
|
||||
loadMore: true,
|
||||
refreshDeps: [page],
|
||||
ready: viewMode !== 'table' // 非表格模式才启用
|
||||
});
|
||||
const { data, loading, loadingMore, refresh } = useRequest(
|
||||
() => {
|
||||
return getImagesList({ current: page, pageSize });
|
||||
},
|
||||
{
|
||||
loadMore: true,
|
||||
refreshDeps: [page],
|
||||
ready: viewMode !== 'table', // 非表格模式才启用
|
||||
},
|
||||
);
|
||||
|
||||
// 表格模式的数据请求
|
||||
const { data: tableData, loading: tableLoading, run: fetchTableData } = useRequest(
|
||||
async (params) => await getImagesList(params),
|
||||
{
|
||||
manual: true,
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: tableData,
|
||||
loading: tableLoading,
|
||||
run: fetchTableData,
|
||||
} = useRequest(async (params) => await getImagesList(params), {
|
||||
manual: true,
|
||||
});
|
||||
|
||||
// 当视图模式改变时重置分页
|
||||
const handleViewModeChange = (e: RadioChangeEvent) => {
|
||||
|
@ -63,18 +67,21 @@ const Gallery: FC = () => {
|
|||
};
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleTableChange = useCallback((pagination: any) => {
|
||||
const { current, pageSize } = pagination;
|
||||
setTablePagination({ current, pageSize });
|
||||
fetchTableData({ current: current, pageSize });
|
||||
}, [fetchTableData]);
|
||||
const handleTableChange = useCallback(
|
||||
(pagination: any) => {
|
||||
const { current, pageSize } = pagination;
|
||||
setTablePagination({ current, pageSize });
|
||||
fetchTableData({ current: current, pageSize });
|
||||
},
|
||||
[fetchTableData],
|
||||
);
|
||||
|
||||
// 当表格分页变化时重新获取数据
|
||||
useEffect(() => {
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize
|
||||
pageSize: tablePagination.pageSize,
|
||||
});
|
||||
}
|
||||
}, [viewMode, tablePagination, fetchTableData]);
|
||||
|
@ -86,17 +93,20 @@ const Gallery: FC = () => {
|
|||
return data?.list || [];
|
||||
}, [data, tableData, viewMode]);
|
||||
|
||||
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
// 表格模式不需要滚动加载
|
||||
if (viewMode === 'table') return;
|
||||
const handleScroll = useCallback(
|
||||
(e: React.UIEvent<HTMLDivElement>) => {
|
||||
// 表格模式不需要滚动加载
|
||||
if (viewMode === 'table') return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 10 && !loading && !loadingMore) {
|
||||
if (data?.total && imageList.length < data.total) {
|
||||
setPage(prev => prev + 1);
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 10 && !loading && !loadingMore) {
|
||||
if (data?.total && imageList.length < data.total) {
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [loading, loadingMore, data?.total, imageList.length, viewMode]);
|
||||
},
|
||||
[loading, loadingMore, data?.total, imageList.length, viewMode],
|
||||
);
|
||||
|
||||
// 处理图片预览
|
||||
const handlePreview = useCallback((index: number) => {
|
||||
|
@ -123,66 +133,75 @@ const Gallery: FC = () => {
|
|||
}, []);
|
||||
|
||||
// 上传图片
|
||||
const handleUpload = useCallback(async (file: UploadFile) => {
|
||||
// 检查文件类型
|
||||
if (!file.type?.startsWith('image/')) {
|
||||
message.error('只能上传图片文件!');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file as any);
|
||||
|
||||
// 调用上传API
|
||||
await uploadImage(formData);
|
||||
message.success(`${file.name} 上传成功`);
|
||||
|
||||
// 上传成功后刷新列表
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize
|
||||
});
|
||||
} else {
|
||||
refresh();
|
||||
const handleUpload = useCallback(
|
||||
async (file: UploadFile) => {
|
||||
// 检查文件类型
|
||||
if (!file.type?.startsWith('image/')) {
|
||||
message.error('只能上传图片文件!');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`${file.name} 上传失败`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, [viewMode, tablePagination, fetchTableData, refresh]);
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file as any);
|
||||
|
||||
// 调用上传API
|
||||
await uploadImage(formData);
|
||||
message.success(`${file.name} 上传成功`);
|
||||
|
||||
// 上传成功后刷新列表
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize,
|
||||
});
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`${file.name} 上传失败`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
},
|
||||
[viewMode, tablePagination, fetchTableData, refresh],
|
||||
);
|
||||
|
||||
// 删除图片确认
|
||||
const handleDelete = useCallback((instanceId: string, imageName: string) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除图片 "${imageName}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteImage({ instanceId });
|
||||
message.success('删除成功');
|
||||
const handleDelete = useCallback(
|
||||
(instanceId: string, imageName: string) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除图片 "${imageName}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteImage({ instanceId });
|
||||
message.success('删除成功');
|
||||
|
||||
// 表格模式也需要刷新数据
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize });
|
||||
} else {
|
||||
refresh();
|
||||
// 表格模式也需要刷新数据
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize,
|
||||
});
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
|
||||
// 如果在批量选择中,也需要移除
|
||||
setSelectedRowKeys((prev) => prev.filter((key) => key !== instanceId));
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
|
||||
// 如果在批量选择中,也需要移除
|
||||
setSelectedRowKeys(prev => prev.filter(key => key !== instanceId));
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [refresh, viewMode, tablePagination, fetchTableData]);
|
||||
},
|
||||
});
|
||||
},
|
||||
[refresh, viewMode, tablePagination, fetchTableData],
|
||||
);
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = useCallback(() => {
|
||||
|
@ -207,14 +226,17 @@ const Gallery: FC = () => {
|
|||
|
||||
// 表格模式也需要刷新数据
|
||||
if (viewMode === 'table') {
|
||||
fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize });
|
||||
fetchTableData({
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize,
|
||||
});
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('批量删除失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [selectedRowKeys, refresh, viewMode, tablePagination, fetchTableData]);
|
||||
|
||||
|
@ -228,7 +250,7 @@ const Gallery: FC = () => {
|
|||
selectedRowKeys.forEach((id, index) => {
|
||||
// 添加延迟避免浏览器限制
|
||||
setTimeout(() => {
|
||||
const item = imageList.find(img => img.instanceId === id);
|
||||
const item = imageList.find((img) => img.instanceId === id);
|
||||
if (item) {
|
||||
handleDownload(id, item.imageName);
|
||||
}
|
||||
|
@ -240,23 +262,26 @@ const Gallery: FC = () => {
|
|||
|
||||
// 切换选择
|
||||
const handleSelect = useCallback((instanceId: string, checked: boolean) => {
|
||||
setSelectedRowKeys(prev => {
|
||||
setSelectedRowKeys((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, instanceId];
|
||||
} else {
|
||||
return prev.filter(key => key !== instanceId);
|
||||
return prev.filter((key) => key !== instanceId);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 全选/取消全选
|
||||
const handleSelectAll = useCallback((checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedRowKeys(imageList.map(item => item.instanceId));
|
||||
} else {
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
}, [imageList]);
|
||||
const handleSelectAll = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedRowKeys(imageList.map((item) => item.instanceId));
|
||||
} else {
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
},
|
||||
[imageList],
|
||||
);
|
||||
|
||||
// 取消批量操作
|
||||
const handleCancelBatch = useCallback(() => {
|
||||
|
@ -347,7 +372,9 @@ const Gallery: FC = () => {
|
|||
/>
|
||||
}
|
||||
>
|
||||
{currentLoading && ((viewMode === 'table' && tablePagination.current === 1) || (viewMode !== 'table' && page === 1)) ? (
|
||||
{currentLoading &&
|
||||
((viewMode === 'table' && tablePagination.current === 1) ||
|
||||
(viewMode !== 'table' && page === 1)) ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
|
@ -357,7 +384,7 @@ const Gallery: FC = () => {
|
|||
renderView()
|
||||
)}
|
||||
|
||||
{/* 预览组件 */}
|
||||
{/* 预览组件 - 根据视图模式决定使用哪种图像 */}
|
||||
<Image.PreviewGroup
|
||||
preview={{
|
||||
visible: previewVisible,
|
||||
|
@ -369,7 +396,11 @@ const Gallery: FC = () => {
|
|||
{imageList.map((item: ImageItem) => (
|
||||
<Image
|
||||
key={item.instanceId}
|
||||
src={`/file/image/${item.instanceId}`}
|
||||
src={
|
||||
viewMode === 'small'
|
||||
? `/file/image/${item.instanceId}`
|
||||
: `/file/image-low-res/${item.instanceId}`
|
||||
}
|
||||
style={{ display: 'none' }}
|
||||
alt={item.imageName}
|
||||
/>
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
// 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 { getImagesList } from '@/services/file/api'; // 引入获取图库图片的API
|
||||
import { PlusOutlined, SearchOutlined } 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 {
|
||||
Button,
|
||||
Card,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputRef,
|
||||
message,
|
||||
Modal,
|
||||
Pagination,
|
||||
Spin,
|
||||
Tabs,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface ModalProps {
|
||||
visible: boolean;
|
||||
|
@ -31,18 +47,61 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<any[]>([]);
|
||||
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues && option === 'edit') {
|
||||
if (initialValues && option.startsWith('edit')) {
|
||||
form.setFieldsValue({
|
||||
title: initialValues.title,
|
||||
storyItemTime: initialValues.date ? moment(initialValues.date) : undefined,
|
||||
date: initialValues.storyItemTime ? moment(initialValues.storyItemTime) : undefined,
|
||||
location: initialValues.location,
|
||||
description: initialValues.description,
|
||||
cover: initialValues.cover ? [{ url: initialValues.cover }] : [],
|
||||
images: initialValues.images?.map((url) => ({ url })) || [],
|
||||
});
|
||||
}
|
||||
}, [initialValues, option]);
|
||||
|
||||
// 获取图库图片
|
||||
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]);
|
||||
|
||||
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
|
@ -63,16 +122,17 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
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,
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
masterItemId: initialValues?.masterItemId,
|
||||
subItems: initialValues?.subItems || [],
|
||||
storyInstanceId: storyId,
|
||||
location,
|
||||
instanceId: initialValues?.instanceId,
|
||||
// 添加选中的图库图片ID
|
||||
relatedImageInstanceIds: selectedGalleryImages,
|
||||
};
|
||||
delete newItem.cover;
|
||||
delete newItem.images;
|
||||
// 构建 FormData
|
||||
const formData = new FormData();
|
||||
|
||||
|
@ -83,7 +143,8 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
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) {
|
||||
|
@ -91,6 +152,8 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(formData);
|
||||
// 提交
|
||||
submitItem(formData);
|
||||
} catch (error) {
|
||||
|
@ -99,26 +162,6 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
|
@ -135,14 +178,149 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
defaultFileList: initialValues?.images?.map((url) => ({ url })),
|
||||
};
|
||||
|
||||
// 切换图库图片选择状态
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title={initialValues ? '编辑时间点' : '添加时间点'}
|
||||
onCancel={() => {
|
||||
form.resetFields();
|
||||
setSelectedGalleryImages([]);
|
||||
setSearchKeyword('');
|
||||
setCurrentPage(1);
|
||||
setGalleryImages([]);
|
||||
onCancel();
|
||||
}}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
取消
|
||||
|
@ -153,7 +331,9 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{['editSubItem', 'addSubItem'].includes( option) && <Form.Item label={'主时间点'}>{storyItemId}</Form.Item>}
|
||||
{['editSubItem', 'addSubItem'].includes(option) && (
|
||||
<Form.Item label={'主时间点'}>{initialValues.title}</Form.Item>
|
||||
)}
|
||||
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入标题" />
|
||||
</Form.Item>
|
||||
|
@ -176,21 +356,36 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
|||
<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 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(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
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 TimelineImage from '@/components/TimelineImage';
|
||||
import { StoryItem } from '@/pages/story/data';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditOutlined,
|
||||
UpOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl, useRequest } from '@umijs/max';
|
||||
import { Button, Card, message, Popconfirm } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { queryStoryItemImages, removeStoryItem } from '../../service';
|
||||
import TimelineItemDrawer from '../TimelineItemDrawer';
|
||||
import useStyles from './index.style';
|
||||
|
||||
const TimelineItem: React.FC<{
|
||||
item: StoryItem;
|
||||
|
@ -18,12 +23,10 @@ const TimelineItem: React.FC<{
|
|||
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 [openDetail, setOpenDetail] = useState(false);
|
||||
const { data: imagesList } = useRequest(async () => {
|
||||
return await queryStoryItemImages(item.instanceId);
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!item.instanceId) return;
|
||||
|
@ -48,7 +51,8 @@ const TimelineItem: React.FC<{
|
|||
|
||||
const displayedDescription = expanded
|
||||
? item.description
|
||||
: item.description?.substring(0, 100) + (item.description && item.description.length > 100 ? '...' : '');
|
||||
: item.description?.substring(0, 100) +
|
||||
(item.description && item.description.length > 100 ? '...' : '');
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -56,11 +60,8 @@ const TimelineItem: React.FC<{
|
|||
title={item.title}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
onClick={() => setOpenDetail(true)}
|
||||
extra={
|
||||
<div
|
||||
className={styles.actions}
|
||||
>
|
||||
<div className={styles.actions}>
|
||||
{showActions && (
|
||||
<>
|
||||
<Button
|
||||
|
@ -68,11 +69,11 @@ const TimelineItem: React.FC<{
|
|||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOption(item, 'editSubItem');
|
||||
handleOption(item, 'edit');
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.edit' })}
|
||||
/>
|
||||
<Button
|
||||
{/*<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
|
@ -80,13 +81,13 @@ const TimelineItem: React.FC<{
|
|||
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()
|
||||
handleDelete();
|
||||
}}
|
||||
okText={intl.formatMessage({ id: 'story.yes' })}
|
||||
cancelText={intl.formatMessage({ id: 'story.no' })}
|
||||
|
@ -107,10 +108,10 @@ const TimelineItem: React.FC<{
|
|||
hoverable
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.date}>
|
||||
<div className={styles.date} onClick={() => setOpenDetail(true)}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<div className={styles.description} onClick={() => setOpenDetail(true)}>
|
||||
{displayedDescription}
|
||||
{item.description && item.description.length > 100 && (
|
||||
<Button
|
||||
|
@ -160,9 +161,7 @@ const TimelineItem: React.FC<{
|
|||
<div className={styles.subItemDate}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
</div>
|
||||
<div className={styles.subItemContent}>
|
||||
{subItem.description}
|
||||
</div>
|
||||
<div className={styles.subItemContent}>{subItem.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -170,11 +169,7 @@ const TimelineItem: React.FC<{
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TimelineItemDrawer
|
||||
storyItem={item}
|
||||
open={openDetail}
|
||||
setOpen={setOpenDetail}
|
||||
/>
|
||||
<TimelineItemDrawer storyItem={item} open={openDetail} setOpen={setOpenDetail} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -29,7 +29,6 @@ const TimelineItemDrawer = (props: Props) => {
|
|||
);
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
console.log(storyItem);
|
||||
run(storyItem.instanceId);
|
||||
}
|
||||
}, [open]);
|
||||
|
@ -52,9 +51,7 @@ const TimelineItemDrawer = (props: Props) => {
|
|||
<Drawer
|
||||
width={1000}
|
||||
placement="right"
|
||||
onClose={() => {
|
||||
closeDrawer();
|
||||
}}
|
||||
onClose={closeDrawer}
|
||||
open={open}
|
||||
title={storyItem.title}
|
||||
footer={
|
||||
|
|
|
@ -91,22 +91,7 @@ const Index = () => {
|
|||
</div>
|
||||
<FloatButton onClick={() => {
|
||||
setCurrentOption('add');
|
||||
setCurrentItem({
|
||||
coverInstanceId: "",
|
||||
createTime: "",
|
||||
description: "",
|
||||
id: 0,
|
||||
images: [],
|
||||
instanceId: "",
|
||||
isRoot: 0,
|
||||
location: "",
|
||||
masterItemId: "",
|
||||
storyInstanceId: "",
|
||||
storyItemTime: "",
|
||||
subItems: [],
|
||||
title: "",
|
||||
updateTime: ""
|
||||
});
|
||||
setCurrentItem();
|
||||
setOpenAddItemModal(true);
|
||||
}} />
|
||||
<AddTimeLineItemModal
|
||||
|
|
|
@ -15,7 +15,13 @@ export async function fetchImage(imageInstanceId: string): Promise<any> {
|
|||
getResponse: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchImageLowRes(imageInstanceId: string): Promise<any> {
|
||||
return request(`/file/image-low-res/${imageInstanceId}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
getResponse: true,
|
||||
});
|
||||
}
|
||||
export async function getImagesList(
|
||||
params: {
|
||||
// query
|
||||
|
|
Loading…
Reference in New Issue