From 63ae33288d51d7bf78dccb5be05b0a3cfcd51027 Mon Sep 17 00:00:00 2001 From: jiangh277 Date: Mon, 4 Aug 2025 16:56:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9B=BE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/routes.ts | 6 + src/{utils => commonConstant}/chinaRegion.ts | 0 .../commonConstant.ts | 0 src/components/Hooks/useFetchImageUrl.ts | 4 +- src/components/TimelineImage/index.tsx | 41 +- src/pages/gallery/components/GalleryTable.tsx | 121 ++++++ .../gallery/components/GalleryToolbar.tsx | 110 +++++ src/pages/gallery/components/GridView.tsx | 125 ++++++ src/pages/gallery/components/ListView.tsx | 98 +++++ src/pages/gallery/index.css | 74 ++++ src/pages/gallery/index.tsx | 383 ++++++++++++++++++ src/pages/gallery/style.style.ts | 15 + src/pages/gallery/typings.d.ts | 8 + .../components/AddTimeLineItemModal.tsx | 2 +- .../basic-list/components/OperationModal.tsx | 2 +- .../components/TimelineItem/TimelineItem.tsx | 24 +- .../components/TimelineItemDrawer.tsx | 4 - src/pages/list/basic-list/service.ts | 2 +- src/services/file/api.ts | 51 +++ src/services/file/index.ts | 4 + src/services/file/typings.d.ts | 26 ++ src/types/common.d.ts | 12 + src/types/image.d.ts | 7 + src/utils.ts | 2 +- src/utils/timelineUtils.ts | 101 +++++ 25 files changed, 1184 insertions(+), 38 deletions(-) rename src/{utils => commonConstant}/chinaRegion.ts (100%) rename src/{utils => commonConstant}/commonConstant.ts (100%) create mode 100644 src/pages/gallery/components/GalleryTable.tsx create mode 100644 src/pages/gallery/components/GalleryToolbar.tsx create mode 100644 src/pages/gallery/components/GridView.tsx create mode 100644 src/pages/gallery/components/ListView.tsx create mode 100644 src/pages/gallery/index.css create mode 100644 src/pages/gallery/index.tsx create mode 100644 src/pages/gallery/style.style.ts create mode 100644 src/pages/gallery/typings.d.ts create mode 100644 src/services/file/api.ts create mode 100644 src/services/file/index.ts create mode 100644 src/services/file/typings.d.ts create mode 100644 src/types/common.d.ts create mode 100644 src/types/image.d.ts create mode 100644 src/utils/timelineUtils.ts diff --git a/config/routes.ts b/config/routes.ts index b740add..6a237ef 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -78,6 +78,12 @@ export default [ path: '/timeline', component: './list/basic-list', }, + { + name: '图库', + icon: 'smile', + path: '/gallery', + component: './gallery', + }, { path: '/timeline/:id', component: './list/basic-list/detail', diff --git a/src/utils/chinaRegion.ts b/src/commonConstant/chinaRegion.ts similarity index 100% rename from src/utils/chinaRegion.ts rename to src/commonConstant/chinaRegion.ts diff --git a/src/utils/commonConstant.ts b/src/commonConstant/commonConstant.ts similarity index 100% rename from src/utils/commonConstant.ts rename to src/commonConstant/commonConstant.ts diff --git a/src/components/Hooks/useFetchImageUrl.ts b/src/components/Hooks/useFetchImageUrl.ts index 84f412f..39bedb6 100644 --- a/src/components/Hooks/useFetchImageUrl.ts +++ b/src/components/Hooks/useFetchImageUrl.ts @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; const useFetchImageUrl = (imageInstanceId: string) => { const [imageUrl, setImageUrl] = useState("error"); - const { data: response, run } = useRequest( + const { data: response, run, loading } = useRequest( () => { return fetchImage(imageInstanceId); }, @@ -25,6 +25,6 @@ const useFetchImageUrl = (imageInstanceId: string) => { run(); } }, [imageInstanceId]); - return imageUrl; + return {imageUrl, loading}; }; export default useFetchImageUrl; diff --git a/src/components/TimelineImage/index.tsx b/src/components/TimelineImage/index.tsx index ab3cbc3..b24a48b 100644 --- a/src/components/TimelineImage/index.tsx +++ b/src/components/TimelineImage/index.tsx @@ -1,8 +1,12 @@ import useFetchImageUrl from '@/components/Hooks/useFetchImageUrl'; import { Image } from 'antd'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { LoadingOutlined } from "@ant-design/icons"; -import './index.css'; +interface ImageItem { + instanceId: string; + imageName: string; +} interface Props { src?: string; @@ -11,23 +15,50 @@ interface Props { height?: string | number; fallback?: string; imageInstanceId?: string; + imageList?: ImageItem[]; + currentIndex?: number; } const TimelineImage: React.FC = (props) => { - const { src, title, imageInstanceId, fallback, width = 200, height = 200 } = props; - const imageUrl = useFetchImageUrl(imageInstanceId ?? ''); + const { + src, + title, + imageInstanceId, + fallback, + width = 200, + height = 200, + imageList = [], + currentIndex = 0 + } = props; + + const { imageUrl, loading } = useFetchImageUrl(imageInstanceId ?? ''); + const [previewVisible, setPreviewVisible] = useState(false); + + // 构建预览列表 + const previewList = imageList.map(item => ({ + src: item.instanceId, + title: item.imageName + })); + + // 预览配置 + const previewConfig = { + visible: previewVisible, + onVisibleChange: (visible: boolean) => setPreviewVisible(visible), + current: currentIndex, + }; return (
{title}
diff --git a/src/pages/gallery/components/GalleryTable.tsx b/src/pages/gallery/components/GalleryTable.tsx new file mode 100644 index 0000000..c69b25b --- /dev/null +++ b/src/pages/gallery/components/GalleryTable.tsx @@ -0,0 +1,121 @@ +// src/pages/gallery/components/GalleryTable.tsx +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"; + +interface GalleryTableProps { + imageList: ImageItem[]; + loading: boolean; + pagination: { + current: number; + pageSize: number; + total?: number; + }; + selectedRowKeys: string[]; + onChange: (pagination: any) => void; + onSelectedRowsChange: (keys: string[]) => void; + onPreview: (index: number) => void; + onDownload: (instanceId: string, imageName: string) => void; + onDelete: (instanceId: string, imageName: string) => void; +} + +const GalleryTable: FC = ({ + imageList, + loading, + pagination, + selectedRowKeys, + onChange, + onSelectedRowsChange, + onPreview, + onDownload, + onDelete, +}) => { + const columns: ProColumns[] = [ + { + title: '图片', + dataIndex: 'instanceId', + width: 120, + render: (_, record) => ( +
+ ), + }, + { + title: '名称', + dataIndex: 'imageName', + ellipsis: true, + }, + { + title: '大小', + dataIndex: 'size', + renderText: (text) => (text ? formatBytes(text) : '-'), + }, + { + title: '上传时间', + dataIndex: 'uploadTime', + valueType: 'dateTime', + }, + { + title: '操作', + valueType: 'option', + render: (_, record) => [ + { + const index = imageList.findIndex((img) => img.instanceId === record.instanceId); + onPreview(index); + }} + > + 预览 + , + onDownload(record.instanceId, record.imageName)}> + 下载 + , + onDelete(record.instanceId, record.imageName)} + style={{ color: 'red' }} + > + 删除 + , + ], + }, + ]; + + return ( + + columns={columns} + dataSource={imageList} + rowKey="instanceId" + pagination={{ + current: pagination.current, + pageSize: pagination.pageSize, + total: pagination.total, + showSizeChanger: true, + pageSizeOptions: ['10', '20', '50', '100'], + }} + loading={loading} + search={false} + options={false} + onChange={onChange} + rowSelection={{ + selectedRowKeys, + onChange: (keys) => onSelectedRowsChange(keys as string[]), + }} + /> + ); +}; + +export default GalleryTable; diff --git a/src/pages/gallery/components/GalleryToolbar.tsx b/src/pages/gallery/components/GalleryToolbar.tsx new file mode 100644 index 0000000..f4265f8 --- /dev/null +++ b/src/pages/gallery/components/GalleryToolbar.tsx @@ -0,0 +1,110 @@ +import { + BarsOutlined, + BorderOutlined, + DeleteOutlined, + DownloadOutlined, + SmallDashOutlined, + UploadOutlined +} from '@ant-design/icons'; +import type { RadioChangeEvent } from 'antd'; +import { Button, Radio, Space, Upload } from 'antd'; +import { FC } from 'react'; +import type { UploadFile } from 'antd/es/upload/interface'; + +interface GalleryToolbarProps { + viewMode: 'small' | 'large' | 'list' | 'table'; + batchMode: boolean; + selectedCount: number; + onViewModeChange: (e: RadioChangeEvent) => void; + onBatchModeToggle: () => void; + onCancelBatch: () => void; + onBatchDownload: () => void; + onBatchDelete: () => void; + onUpload: (file: UploadFile) => void; + uploading: boolean; +} + +const GalleryToolbar: FC = ({ + viewMode, + batchMode, + selectedCount, + onViewModeChange, + onBatchModeToggle, + onCancelBatch, + onBatchDownload, + onBatchDelete, + onUpload, + uploading + }) => { + const beforeUpload = (file: UploadFile) => { + // 只允许上传图片 + const isImage = file.type?.startsWith('image/'); + if (!isImage) { + console.error(`${file.name} 不是图片文件`); + return false; + } + return true; + }; + + return ( + + {batchMode ? ( + <> + + + + + ) : ( + <> + onUpload(file as UploadFile)} + showUploadList={false} + multiple + > + + + + + )} + + + 小图 + + + 大图 + + + 列表 + + 表格 + + + ); +}; + +export default GalleryToolbar; diff --git a/src/pages/gallery/components/GridView.tsx b/src/pages/gallery/components/GridView.tsx new file mode 100644 index 0000000..5b6a73d --- /dev/null +++ b/src/pages/gallery/components/GridView.tsx @@ -0,0 +1,125 @@ +// src/pages/gallery/components/GridView.tsx +import { ImageItem } from '@/pages/gallery/typings'; +import { DeleteOutlined, DownloadOutlined, EyeOutlined, MoreOutlined } from '@ant-design/icons'; +import { Button, Checkbox, Dropdown, Menu, Spin } from 'antd'; +import React, { FC, useCallback } from 'react'; +import '../index.css'; + +interface GridViewProps { + imageList: ImageItem[]; + viewMode: 'small' | 'large' | 'list' | 'table'; + batchMode: boolean; + selectedRowKeys: string[]; + onPreview: (index: number) => void; + onSelect: (instanceId: string, checked: boolean) => void; + onDownload: (instanceId: string, imageName: string) => void; + onDelete: (instanceId: string, imageName: string) => void; + loadingMore?: boolean; + onScroll: (e: React.UIEvent) => void; +} + +const GridView: FC = ({ + imageList, + viewMode, + batchMode, + selectedRowKeys, + onPreview, + onSelect, + onDownload, + onDelete, + loadingMore, + onScroll, +}) => { + const getImageSize = useCallback(() => { + switch (viewMode) { + case 'small': + return { width: 150, height: 150 }; + case 'large': + return { width: 300, height: 300 }; + default: + return { width: 150, height: 150 }; + } + }, [viewMode]); + + const imageSize = getImageSize(); + + const getImageMenu = useCallback( + (item: ImageItem) => ( + + } + onClick={() => { + const index = imageList.findIndex((img) => img.instanceId === item.instanceId); + onPreview(index); + }} + > + 预览 + + } + onClick={() => onDownload(item.instanceId, item.imageName)} + > + 下载 + + + } + danger + onClick={() => onDelete(item.instanceId, item.imageName)} + > + 删除 + + + ), + [imageList, onPreview, onDownload, onDelete], + ); + + return ( +
+ {imageList.map((item: ImageItem, index: number) => ( +
+ {batchMode && ( + onSelect(item.instanceId, e.target.checked)} + /> + )} +
!batchMode && onPreview(index)} + /> +
+
+ {item.imageName} +
+ +
+
+ ))} + {loadingMore && ( +
+ +
+ )} +
+ ); +}; + +export default GridView; diff --git a/src/pages/gallery/components/ListView.tsx b/src/pages/gallery/components/ListView.tsx new file mode 100644 index 0000000..e05dd79 --- /dev/null +++ b/src/pages/gallery/components/ListView.tsx @@ -0,0 +1,98 @@ +// src/pages/gallery/components/ListView.tsx +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"; + +interface ListViewProps { + imageList: ImageItem[]; + batchMode: boolean; + selectedRowKeys: string[]; + onPreview: (index: number) => void; + onSelect: (instanceId: string, checked: boolean) => void; + onDownload: (instanceId: string, imageName: string) => void; + onDelete: (instanceId: string, imageName: string) => void; + loadingMore?: boolean; + onScroll: (e: React.UIEvent) => void; +} + +const ListView: FC = ({ + imageList, + batchMode, + selectedRowKeys, + onPreview, + onSelect, + onDownload, + onDelete, + loadingMore, + onScroll, +}) => { + const imageSize = { width: '100%', height: 200 }; + + return ( +
+ {imageList.map((item: ImageItem, index: number) => ( + +
+ {batchMode && ( + onSelect(item.instanceId, e.target.checked)} + /> + )} +
!batchMode && onPreview(index)} + /> +
+
+ {item.imageName} +
+
+ {item.size && 大小: {formatBytes(item.size)}} + {item.createTime && ( + 创建时间: {new Date(item.createTime).toLocaleString()} + )} +
+
+
+
+
+ + ))} + {loadingMore && ( +
+ +
+ )} +
+ ); +}; + +export default ListView; diff --git a/src/pages/gallery/index.css b/src/pages/gallery/index.css new file mode 100644 index 0000000..7e2c905 --- /dev/null +++ b/src/pages/gallery/index.css @@ -0,0 +1,74 @@ +.small-grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; +} +.large-grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 24px; +} + +.image-card { + position: relative; + overflow: hidden; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s; +} + +.image-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.image-checkbox { + position: absolute; + top: 8px; + left: 8px; + z-index: 10; +} + +.image-wrapper { + cursor: pointer; +} + +.image-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; +} + +.image-title { + flex: 1; + margin-right: 8px; + overflow: hidden; + font-size: 12px; + white-space: nowrap; + text-overflow: ellipsis; +} + +@media (max-width: 768px) { + .small-grid-view { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 12px; + } + + .large-grid-view { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + } +} + +@media (max-width: 480px) { + .small-grid-view { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 8px; + } + + .large-grid-view { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 12px; + } +} diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx new file mode 100644 index 0000000..3dd5123 --- /dev/null +++ b/src/pages/gallery/index.tsx @@ -0,0 +1,383 @@ +// 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 type { RadioChangeEvent } from 'antd'; +import type { UploadFile } from 'antd/es/upload/interface'; +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"; + +const Gallery: FC = () => { + const [viewMode, setViewMode] = useState<'small' | 'large' | 'list' | 'table'>('small'); + const [page, setPage] = useState(1); + const [previewVisible, setPreviewVisible] = useState(false); + const [previewCurrent, setPreviewCurrent] = useState(0); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [batchMode, setBatchMode] = useState(false); + const [uploading, setUploading] = useState(false); + const pageSize = 5; + 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: tableData, loading: tableLoading, run: fetchTableData } = useRequest( + async (params) => await getImagesList(params), + { + manual: true, + } + ); + + // 当视图模式改变时重置分页 + const handleViewModeChange = (e: RadioChangeEvent) => { + const newViewMode = e.target.value; + setViewMode(newViewMode); + + // 重置分页 + if (newViewMode === 'table') { + setTablePagination(initPagination); + fetchTableData(initPagination); + } else { + setPage(1); + } + + // 清除选择 + setSelectedRowKeys([]); + setBatchMode(false); + }; + + // 处理表格分页变化 + 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 + }); + } + }, [viewMode, tablePagination, fetchTableData]); + + const imageList = useMemo((): ImageItem[] => { + if (viewMode === 'table') { + return tableData?.list || []; + } + return data?.list || []; + }, [data, tableData, viewMode]); + + const handleScroll = useCallback((e: React.UIEvent) => { + // 表格模式不需要滚动加载 + 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); + } + } + }, [loading, loadingMore, data?.total, imageList.length, viewMode]); + + // 处理图片预览 + const handlePreview = useCallback((index: number) => { + setPreviewCurrent(index); + setPreviewVisible(true); + }, []); + + // 关闭预览 + const handlePreviewClose = useCallback(() => { + setPreviewVisible(false); + }, []); + + // 切换图片 + const handlePreviewChange = useCallback((current: number) => { + setPreviewCurrent(current); + }, []); + + // 下载图片 + const handleDownload = useCallback((instanceId: string, imageName?: string) => { + const link = document.createElement('a'); + link.href = `/file/image/${instanceId}`; + link.download = imageName ?? 'default'; + link.click(); + }, []); + + // 上传图片 + 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(); + } + } 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('删除成功'); + + // 表格模式也需要刷新数据 + if (viewMode === 'table') { + fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize }); + } else { + refresh(); + } + + // 如果在批量选择中,也需要移除 + setSelectedRowKeys(prev => prev.filter(key => key !== instanceId)); + } catch (error) { + message.error('删除失败'); + } + } + }); + }, [refresh, viewMode, tablePagination, fetchTableData]); + + // 批量删除 + const handleBatchDelete = useCallback(() => { + if (selectedRowKeys.length === 0) { + message.warning('请先选择要删除的图片'); + return; + } + + Modal.confirm({ + title: '确认批量删除', + content: `确定要删除选中的 ${selectedRowKeys.length} 张图片吗?此操作不可恢复。`, + okText: '删除', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + const promises = selectedRowKeys.map((id) => deleteImage({ instanceId: id })); + await Promise.all(promises); + message.success(`成功删除 ${selectedRowKeys.length} 张图片`); + setSelectedRowKeys([]); + setBatchMode(false); + + // 表格模式也需要刷新数据 + if (viewMode === 'table') { + fetchTableData({ current: tablePagination.current, pageSize: tablePagination.pageSize }); + } else { + refresh(); + } + } catch (error) { + message.error('批量删除失败'); + } + } + }); + }, [selectedRowKeys, refresh, viewMode, tablePagination, fetchTableData]); + + // 批量下载 + const handleBatchDownload = useCallback(() => { + if (selectedRowKeys.length === 0) { + message.warning('请先选择要下载的图片'); + return; + } + + selectedRowKeys.forEach((id, index) => { + // 添加延迟避免浏览器限制 + setTimeout(() => { + const item = imageList.find(img => img.instanceId === id); + if (item) { + handleDownload(id, item.imageName); + } + }, index * 100); + }); + + message.success(`开始下载 ${selectedRowKeys.length} 张图片`); + }, [selectedRowKeys, imageList, handleDownload]); + + // 切换选择 + const handleSelect = useCallback((instanceId: string, checked: boolean) => { + setSelectedRowKeys(prev => { + if (checked) { + return [...prev, instanceId]; + } else { + return prev.filter(key => key !== instanceId); + } + }); + }, []); + + // 全选/取消全选 + const handleSelectAll = useCallback((checked: boolean) => { + if (checked) { + setSelectedRowKeys(imageList.map(item => item.instanceId)); + } else { + setSelectedRowKeys([]); + } + }, [imageList]); + + // 取消批量操作 + const handleCancelBatch = useCallback(() => { + setSelectedRowKeys([]); + setBatchMode(false); + }, []); + + // 根据视图模式获取当前数据和加载状态 + const getCurrentDataAndLoading = () => { + if (viewMode === 'table') { + return { currentData: tableData, currentLoading: tableLoading }; + } else { + return { currentData: data, currentLoading: loading }; + } + }; + + const { currentLoading } = getCurrentDataAndLoading(); + + // 渲染视图 + const renderView = () => { + switch (viewMode) { + case 'table': + return ( + + ); + case 'list': + return ( + + ); + default: + return ( + + ); + } + }; + + return ( +
+ setBatchMode(true)} + onCancelBatch={handleCancelBatch} + onBatchDownload={handleBatchDownload} + onBatchDelete={handleBatchDelete} + onUpload={handleUpload} + uploading={uploading} + /> + } + > + {currentLoading && ((viewMode === 'table' && tablePagination.current === 1) || (viewMode !== 'table' && page === 1)) ? ( +
+ +
+ ) : imageList.length === 0 ? ( + + ) : ( + renderView() + )} + + {/* 预览组件 */} + setPreviewVisible(visible), + onChange: handlePreviewChange, + }} + > + {imageList.map((item: ImageItem) => ( + {item.imageName} + ))} + +
+
+ ); +}; + +export default Gallery; diff --git a/src/pages/gallery/style.style.ts b/src/pages/gallery/style.style.ts new file mode 100644 index 0000000..eb0e5be --- /dev/null +++ b/src/pages/gallery/style.style.ts @@ -0,0 +1,15 @@ +import { createStyles } from 'antd-style'; + +const useStyles = createStyles(() => { + return { + smallGridView: { + '.small-grid-view': { + display: 'grid', + 'grid-template-columns': 'repeat(auto-fill, minmax(150px, 1fr))', + gap: '16px', + }, + }, + }; +}); + +export default useStyles; diff --git a/src/pages/gallery/typings.d.ts b/src/pages/gallery/typings.d.ts new file mode 100644 index 0000000..ec8655f --- /dev/null +++ b/src/pages/gallery/typings.d.ts @@ -0,0 +1,8 @@ +export interface ImageItem { + instanceId: string; + imageName: string; + size?: number; + createTime?: string; + updateTime?: string; + uploadTime?: string; +} diff --git a/src/pages/list/basic-list/components/AddTimeLineItemModal.tsx b/src/pages/list/basic-list/components/AddTimeLineItemModal.tsx index 05da73f..2c2e183 100644 --- a/src/pages/list/basic-list/components/AddTimeLineItemModal.tsx +++ b/src/pages/list/basic-list/components/AddTimeLineItemModal.tsx @@ -1,6 +1,6 @@ // src/pages/list/basic-list/components/AddTimeLineItemModal.tsx import { addStoryItem } from '@/pages/list/basic-list/service'; -import chinaRegion, { code2Location } from '@/utils/chinaRegion'; +import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion'; import { UploadOutlined } from '@ant-design/icons'; import { useRequest } from '@umijs/max'; import { Button, Cascader, DatePicker, Form, Input, message, Modal, Upload } from 'antd'; diff --git a/src/pages/list/basic-list/components/OperationModal.tsx b/src/pages/list/basic-list/components/OperationModal.tsx index 6c8533a..9616c08 100644 --- a/src/pages/list/basic-list/components/OperationModal.tsx +++ b/src/pages/list/basic-list/components/OperationModal.tsx @@ -10,7 +10,7 @@ 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 '@/utils/commonConstant'; +import { defaultIcons } from '@/commonConstant/commonConstant'; type OperationModalProps = { done: boolean; diff --git a/src/pages/list/basic-list/components/TimelineItem/TimelineItem.tsx b/src/pages/list/basic-list/components/TimelineItem/TimelineItem.tsx index 620e281..e857cc9 100644 --- a/src/pages/list/basic-list/components/TimelineItem/TimelineItem.tsx +++ b/src/pages/list/basic-list/components/TimelineItem/TimelineItem.tsx @@ -1,4 +1,4 @@ -import { Badge, Col, Drawer, Row, Timeline } from 'antd'; +import { Badge, Col, Row, Timeline } from 'antd'; import React from 'react'; import TimelineImage from '@/components/TimelineImage'; @@ -15,9 +15,6 @@ const TimelineItem = ({ event: initialEvent, onUpdate }: TimelineItemProps) => { const [openMainDrawer, setOpenMainDrawer] = React.useState(false); const [expanded, setExpanded] = React.useState(false); // 控制子项展开状态 - const [openSubDrawer, setOpenSubDrawer] = React.useState(false); - const [selectedSubItem, setSelectedSubItem] = React.useState(null); - const showMainDrawer = () => { setOpenMainDrawer(true); }; @@ -128,25 +125,6 @@ const TimelineItem = ({ event: initialEvent, onUpdate }: TimelineItemProps) => { setOpen={setOpenMainDrawer} /> - {/* 子时间点详情抽屉 */} - {selectedSubItem && ( - setOpenSubDrawer(false)} - open={openSubDrawer} - title={selectedSubItem.title} - > -

- 描述: - {selectedSubItem.description} -

-

- 时间: - {selectedSubItem.time || '未设置'} -

-
- )}
); diff --git a/src/pages/list/basic-list/components/TimelineItemDrawer.tsx b/src/pages/list/basic-list/components/TimelineItemDrawer.tsx index cda50a4..e309151 100644 --- a/src/pages/list/basic-list/components/TimelineItemDrawer.tsx +++ b/src/pages/list/basic-list/components/TimelineItemDrawer.tsx @@ -115,10 +115,6 @@ const TimelineItemDrawer = (props: Props) => { )} - {/* 编辑模态框入口 */} - {/**/} {/* 添加子时间点模态框 */} { - return request(`/file/download/cover/${imageInstanceId}`, { + return request(`/file/image/${imageInstanceId}`, { method: 'GET', responseType: 'blob', getResponse: true, diff --git a/src/services/file/api.ts b/src/services/file/api.ts new file mode 100644 index 0000000..d4021ab --- /dev/null +++ b/src/services/file/api.ts @@ -0,0 +1,51 @@ +import {request} from "@@/exports"; +import {CommonListResponse, CommonResponse} from "@/types/common"; +import {ImageItem} from "@/pages/gallery/typings"; +// 查询storyItem图片列表 +export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> { + return request(`/story/item/images/${itemId}`, { + method: 'GET', + }); +} +// 获取图片 +export async function fetchImage(imageInstanceId: string): Promise { + return request(`/file/download/cover/${imageInstanceId}`, { + method: 'GET', + responseType: 'blob', + getResponse: true, + }); +} + +export async function getImagesList( + params: { + // query + /** 当前的页码 */ + current?: number; + /** 页面的容量 */ + pageSize?: number; + }, + options?: { [key: string]: any }, +) { + return request>>('/file/image/list', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +export async function deleteImage(params: { instanceId: string }) { + return request>(`/file/image/${params.instanceId}`, { + method: 'DELETE', + params: { + ...params, + }, + }); +} +export async function uploadImage(params: FormData) { + return request>('/file/upload-image', { + method: 'POST', + data: params, + }); +} diff --git a/src/services/file/index.ts b/src/services/file/index.ts new file mode 100644 index 0000000..0e2cd6d --- /dev/null +++ b/src/services/file/index.ts @@ -0,0 +1,4 @@ +import * as api from './api' +export default { + api +} diff --git a/src/services/file/typings.d.ts b/src/services/file/typings.d.ts new file mode 100644 index 0000000..54b977e --- /dev/null +++ b/src/services/file/typings.d.ts @@ -0,0 +1,26 @@ +declare namespace API { + type ImageInfo = { + instanceId: string; + size?: number; + uploadTime?: string; + contentType?: string; + imageName?: string; + } + type ListResponse = { + list?: T[]; + total?: number; + pageSize?: number; + pageNumber?: number; + pages?: number; + } + type Response = { + code?: number; + message?: string; + data?: T; + } + type ResponseBase = { + code?: number; + message?: string; + } +} +export default API; diff --git a/src/types/common.d.ts b/src/types/common.d.ts new file mode 100644 index 0000000..56bd7db --- /dev/null +++ b/src/types/common.d.ts @@ -0,0 +1,12 @@ +export interface CommonResponse { + code?: number; + message?: string; + data?: T; +} +export type CommonListResponse = { + list?: T[]; + total?: number; + pageSize?: number; + pageNumber?: number; + pages?: number; +} diff --git a/src/types/image.d.ts b/src/types/image.d.ts new file mode 100644 index 0000000..0b9c484 --- /dev/null +++ b/src/types/image.d.ts @@ -0,0 +1,7 @@ +export interface ImageInfo { + instanceId: string; + size?: number; + uploadTime?: string; + contentType?: string; + imageName?: string; +} diff --git a/src/utils.ts b/src/utils.ts index c8a7cd0..43c5386 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import {StoryItem, TimelineEvent} from "@/pages/list/basic-list/data"; -import {CommonConstant} from "@/utils/commonConstant"; +import {CommonConstant} from "@/commonConstant/commonConstant"; export function getTimelineItems(id: string): TimelineEvent[] { diff --git a/src/utils/timelineUtils.ts b/src/utils/timelineUtils.ts new file mode 100644 index 0000000..68b0e1d --- /dev/null +++ b/src/utils/timelineUtils.ts @@ -0,0 +1,101 @@ +/** + * 字节转换函数 + * 将字节大小转换为适当的单位(B, KB, MB, GB, TB等) + * + * @param bytes - 字节数 + * @param decimals - 小数位数,默认为2 + * @returns 格式化后的文件大小字符串 + */ +export function formatBytes(bytes: number, decimals: number = 2): string { + if (bytes === 0) return '0 B'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +/** + * 字节转换函数(另一种实现方式) + * 提供更精确的转换控制 + * + * @param bytes - 字节数 + * @param options - 转换选项 + * @returns 格式化后的文件大小字符串 + */ +export function convertBytes( + bytes: number, + options?: { + decimals?: number; + binary?: boolean; // 是否使用二进制单位(1024)而非十进制(1000) + } +): string { + const { decimals = 2, binary = true } = options || {}; + + if (bytes === 0) return '0 B'; + + const base = binary ? 1024 : 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = binary + ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(base)); + const value = bytes / Math.pow(base, i); + + return parseFloat(value.toFixed(dm)) + ' ' + sizes[i]; +} + +/** + * 解析文件大小字符串为字节数 + * + * @param sizeStr - 文件大小字符串,如 "1.5 MB", "2GB" 等 + * @returns 字节数,如果解析失败返回 NaN + */ +export function parseBytes(sizeStr: string): number { + if (!sizeStr) return NaN; + + const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB|EB|ZB|YB|KIB|MIB|GIB|TIB|PIB|EIB|ZIB|YIB)$/i); + if (!match) return NaN; + + const value = parseFloat(match[1]); + const unit = match[2].toUpperCase(); + + const binaryUnits: Record = { + 'B': 1, + 'KIB': Math.pow(1024, 1), + 'MIB': Math.pow(1024, 2), + 'GIB': Math.pow(1024, 3), + 'TIB': Math.pow(1024, 4), + 'PIB': Math.pow(1024, 5), + 'EIB': Math.pow(1024, 6), + 'ZIB': Math.pow(1024, 7), + 'YIB': Math.pow(1024, 8) + }; + + const decimalUnits: Record = { + 'B': 1, + 'KB': Math.pow(1000, 1), + 'MB': Math.pow(1000, 2), + 'GB': Math.pow(1000, 3), + 'TB': Math.pow(1000, 4), + 'PB': Math.pow(1000, 5), + 'EB': Math.pow(1000, 6), + 'ZB': Math.pow(1000, 7), + 'YB': Math.pow(1000, 8) + }; + + // 优先使用二进制单位 + if (binaryUnits[unit]) { + return value * binaryUnits[unit]; + } + + if (decimalUnits[unit]) { + return value * decimalUnits[unit]; + } + + return NaN; +}