新增图库
This commit is contained in:
parent
56a0042011
commit
63ae33288d
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> = (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 (
|
||||
<div className="tl-image-container" style={{ width, height }}>
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={src ?? imageUrl}
|
||||
preview={loading ? false : previewConfig}
|
||||
height={height}
|
||||
width={width}
|
||||
alt={title}
|
||||
fallback={
|
||||
fallback ??
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v////y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEE......'
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v////y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEeg......'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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<GalleryTableProps> = ({
|
||||
imageList,
|
||||
loading,
|
||||
pagination,
|
||||
selectedRowKeys,
|
||||
onChange,
|
||||
onSelectedRowsChange,
|
||||
onPreview,
|
||||
onDownload,
|
||||
onDelete,
|
||||
}) => {
|
||||
const columns: ProColumns<ImageItem>[] = [
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'instanceId',
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<div
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundImage: `url(/file/image/${record.instanceId})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'imageName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
dataIndex: 'size',
|
||||
renderText: (text) => (text ? formatBytes(text) : '-'),
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'uploadTime',
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, record) => [
|
||||
<a
|
||||
key="preview"
|
||||
onClick={() => {
|
||||
const index = imageList.findIndex((img) => img.instanceId === record.instanceId);
|
||||
onPreview(index);
|
||||
}}
|
||||
>
|
||||
预览
|
||||
</a>,
|
||||
<a key="download" onClick={() => onDownload(record.instanceId, record.imageName)}>
|
||||
下载
|
||||
</a>,
|
||||
<a
|
||||
key="delete"
|
||||
onClick={() => onDelete(record.instanceId, record.imageName)}
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
删除
|
||||
</a>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProTable<ImageItem>
|
||||
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;
|
|
@ -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<GalleryToolbarProps> = ({
|
||||
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 (
|
||||
<Space>
|
||||
{batchMode ? (
|
||||
<>
|
||||
<Button onClick={onCancelBatch}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onBatchDownload}
|
||||
disabled={selectedCount === 0}
|
||||
>
|
||||
下载({selectedCount})
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onBatchDelete}
|
||||
disabled={selectedCount === 0}
|
||||
>
|
||||
删除({selectedCount})
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={({ file }) => onUpload(file as UploadFile)}
|
||||
showUploadList={false}
|
||||
multiple
|
||||
>
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
loading={uploading}
|
||||
>
|
||||
上传图片
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button onClick={onBatchModeToggle}>批量操作</Button>
|
||||
</>
|
||||
)}
|
||||
<Radio.Group
|
||||
value={viewMode}
|
||||
onChange={onViewModeChange}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value="small">
|
||||
<SmallDashOutlined /> 小图
|
||||
</Radio.Button>
|
||||
<Radio.Button value="large">
|
||||
<BorderOutlined /> 大图
|
||||
</Radio.Button>
|
||||
<Radio.Button value="list">
|
||||
<BarsOutlined /> 列表
|
||||
</Radio.Button>
|
||||
<Radio.Button value="table">表格</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryToolbar;
|
|
@ -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<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const GridView: FC<GridViewProps> = ({
|
||||
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) => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key="preview"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => {
|
||||
const index = imageList.findIndex((img) => img.instanceId === item.instanceId);
|
||||
onPreview(index);
|
||||
}}
|
||||
>
|
||||
预览
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="download"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => onDownload(item.instanceId, item.imageName)}
|
||||
>
|
||||
下载
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
key="delete"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => onDelete(item.instanceId, item.imageName)}
|
||||
>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
),
|
||||
[imageList, onPreview, onDownload, onDelete],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={viewMode === 'small' ? 'small-grid-view' : 'large-grid-view'}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{imageList.map((item: ImageItem, index: number) => (
|
||||
<div key={item.instanceId} className="image-card">
|
||||
{batchMode && (
|
||||
<Checkbox
|
||||
className="image-checkbox"
|
||||
checked={selectedRowKeys.includes(item.instanceId)}
|
||||
onChange={(e) => onSelect(item.instanceId, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="image-wrapper"
|
||||
style={{
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
backgroundImage: `url(/file/image/${item.instanceId})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
onClick={() => !batchMode && onPreview(index)}
|
||||
/>
|
||||
<div className="image-info">
|
||||
<div className="image-title" title={item.imageName}>
|
||||
{item.imageName}
|
||||
</div>
|
||||
<Dropdown overlay={getImageMenu(item)} trigger={['click']}>
|
||||
<Button type="text" icon={<MoreOutlined />} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loadingMore && (
|
||||
<div style={{ gridColumn: '1 / -1', textAlign: 'center', padding: '20px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridView;
|
|
@ -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<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const ListView: FC<ListViewProps> = ({
|
||||
imageList,
|
||||
batchMode,
|
||||
selectedRowKeys,
|
||||
onPreview,
|
||||
onSelect,
|
||||
onDownload,
|
||||
onDelete,
|
||||
loadingMore,
|
||||
onScroll,
|
||||
}) => {
|
||||
const imageSize = { width: '100%', height: 200 };
|
||||
|
||||
return (
|
||||
<div
|
||||
className="list-view"
|
||||
onScroll={onScroll}
|
||||
style={{ maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }}
|
||||
>
|
||||
{imageList.map((item: ImageItem, index: number) => (
|
||||
<Card key={item.instanceId} className="list-item-card" size="small">
|
||||
<div className="list-item">
|
||||
{batchMode && (
|
||||
<Checkbox
|
||||
checked={selectedRowKeys.includes(item.instanceId)}
|
||||
onChange={(e) => onSelect(item.instanceId, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="image-wrapper"
|
||||
style={{
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
backgroundImage: `url(/file/image/${item.instanceId})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
onClick={() => !batchMode && onPreview(index)}
|
||||
/>
|
||||
<div className="list-item-info">
|
||||
<div className="image-title" title={item.imageName}>
|
||||
{item.imageName}
|
||||
</div>
|
||||
<div className="image-meta">
|
||||
{item.size && <span>大小: {formatBytes(item.size)}</span>}
|
||||
{item.createTime && (
|
||||
<span>创建时间: {new Date(item.createTime).toLocaleString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="list-item-actions">
|
||||
<Button type="text" icon={<EyeOutlined />} onClick={() => onPreview(index)} />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => onDownload(item.instanceId, item.imageName)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => onDelete(item.instanceId, item.imageName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{loadingMore && (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListView;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<string[]>([]);
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}, [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 (
|
||||
<GalleryTable
|
||||
imageList={imageList}
|
||||
loading={tableLoading}
|
||||
pagination={{
|
||||
current: tablePagination.current,
|
||||
pageSize: tablePagination.pageSize,
|
||||
total: tableData?.total,
|
||||
}}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onChange={handleTableChange}
|
||||
onSelectedRowsChange={setSelectedRowKeys}
|
||||
onPreview={handlePreview}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
);
|
||||
case 'list':
|
||||
return (
|
||||
<ListView
|
||||
imageList={imageList}
|
||||
batchMode={batchMode}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onPreview={handlePreview}
|
||||
onSelect={handleSelect}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
loadingMore={loadingMore}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<GridView
|
||||
imageList={imageList}
|
||||
viewMode={viewMode}
|
||||
batchMode={batchMode}
|
||||
selectedRowKeys={selectedRowKeys}
|
||||
onPreview={handlePreview}
|
||||
onSelect={handleSelect}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
loadingMore={loadingMore}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageContainer
|
||||
title="我的照片"
|
||||
extra={
|
||||
<GalleryToolbar
|
||||
viewMode={viewMode}
|
||||
batchMode={batchMode}
|
||||
selectedCount={selectedRowKeys.length}
|
||||
onViewModeChange={handleViewModeChange}
|
||||
onBatchModeToggle={() => setBatchMode(true)}
|
||||
onCancelBatch={handleCancelBatch}
|
||||
onBatchDownload={handleBatchDownload}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onUpload={handleUpload}
|
||||
uploading={uploading}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{currentLoading && ((viewMode === 'table' && tablePagination.current === 1) || (viewMode !== 'table' && page === 1)) ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : imageList.length === 0 ? (
|
||||
<Empty description="暂无图片" />
|
||||
) : (
|
||||
renderView()
|
||||
)}
|
||||
|
||||
{/* 预览组件 */}
|
||||
<Image.PreviewGroup
|
||||
preview={{
|
||||
visible: previewVisible,
|
||||
current: previewCurrent,
|
||||
onVisibleChange: (visible) => setPreviewVisible(visible),
|
||||
onChange: handlePreviewChange,
|
||||
}}
|
||||
>
|
||||
{imageList.map((item: ImageItem) => (
|
||||
<Image
|
||||
key={item.instanceId}
|
||||
src={`/file/image/${item.instanceId}`}
|
||||
style={{ display: 'none' }}
|
||||
alt={item.imageName}
|
||||
/>
|
||||
))}
|
||||
</Image.PreviewGroup>
|
||||
</PageContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Gallery;
|
|
@ -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;
|
|
@ -0,0 +1,8 @@
|
|||
export interface ImageItem {
|
||||
instanceId: string;
|
||||
imageName: string;
|
||||
size?: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
uploadTime?: string;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<TimelineEvent | null>(null);
|
||||
|
||||
const showMainDrawer = () => {
|
||||
setOpenMainDrawer(true);
|
||||
};
|
||||
|
@ -128,25 +125,6 @@ const TimelineItem = ({ event: initialEvent, onUpdate }: TimelineItemProps) => {
|
|||
setOpen={setOpenMainDrawer}
|
||||
/>
|
||||
|
||||
{/* 子时间点详情抽屉 */}
|
||||
{selectedSubItem && (
|
||||
<Drawer
|
||||
width={640}
|
||||
placement="right"
|
||||
onClose={() => setOpenSubDrawer(false)}
|
||||
open={openSubDrawer}
|
||||
title={selectedSubItem.title}
|
||||
>
|
||||
<p style={{ marginBottom: 24 }}>
|
||||
<strong>描述:</strong>
|
||||
{selectedSubItem.description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>时间:</strong>
|
||||
{selectedSubItem.time || '未设置'}
|
||||
</p>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -115,10 +115,6 @@ const TimelineItemDrawer = (props: Props) => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* 编辑模态框入口 */}
|
||||
{/*<Button type="default" onClick={() => setOpenEditMainItemModal(true)} block style={{ marginTop: '16px' }}>
|
||||
编辑时间点信息
|
||||
</Button>*/}
|
||||
{/* 添加子时间点模态框 */}
|
||||
<AddTimeLineItemModal
|
||||
visible={openAddSubItemModal}
|
||||
|
|
|
@ -72,7 +72,7 @@ export async function queryStoryItemImages(itemId: string): Promise<{ data: stri
|
|||
}
|
||||
|
||||
export async function fetchImage(imageInstanceId: string): Promise<any> {
|
||||
return request(`/file/download/cover/${imageInstanceId}`, {
|
||||
return request(`/file/image/${imageInstanceId}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
getResponse: true,
|
||||
|
|
|
@ -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<any> {
|
||||
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<CommonResponse<CommonListResponse<ImageItem>>>('/file/image/list', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteImage(params: { instanceId: string }) {
|
||||
return request<CommonResponse<any>>(`/file/image/${params.instanceId}`, {
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function uploadImage(params: FormData) {
|
||||
return request<CommonResponse<any>>('/file/upload-image', {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import * as api from './api'
|
||||
export default {
|
||||
api
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
declare namespace API {
|
||||
type ImageInfo = {
|
||||
instanceId: string;
|
||||
size?: number;
|
||||
uploadTime?: string;
|
||||
contentType?: string;
|
||||
imageName?: string;
|
||||
}
|
||||
type ListResponse<T> = {
|
||||
list?: T[];
|
||||
total?: number;
|
||||
pageSize?: number;
|
||||
pageNumber?: number;
|
||||
pages?: number;
|
||||
}
|
||||
type Response<T> = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: T;
|
||||
}
|
||||
type ResponseBase = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
}
|
||||
}
|
||||
export default API;
|
|
@ -0,0 +1,12 @@
|
|||
export interface CommonResponse<T> {
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: T;
|
||||
}
|
||||
export type CommonListResponse<T> = {
|
||||
list?: T[];
|
||||
total?: number;
|
||||
pageSize?: number;
|
||||
pageNumber?: number;
|
||||
pages?: number;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface ImageInfo {
|
||||
instanceId: string;
|
||||
size?: number;
|
||||
uploadTime?: string;
|
||||
contentType?: string;
|
||||
imageName?: string;
|
||||
}
|
|
@ -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[] {
|
||||
|
|
|
@ -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<string, number> = {
|
||||
'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<string, number> = {
|
||||
'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;
|
||||
}
|
Loading…
Reference in New Issue