实现所有功能页面组件
- Dashboard 仪表盘:数据统计卡片、存储使用情况、快速操作 - Upload 上传页面:拖拽上传、批量上传、上传进度 - Gallery 图片库:瀑布流布局、筛选排序、批量操作 - Links 链接管理:链接列表、搜索筛选 - Tools 图片工具:压缩、裁剪、格式转换 - Storage 存储配置:OSS配置管理 - Analytics 统计分析:图表展示、数据分析 - Settings 设置页面:个人设置、系统配置
This commit is contained in:
parent
0416f58b3d
commit
6e9449d0f3
63
src/pages/Analytics/index.tsx
Normal file
63
src/pages/Analytics/index.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Row, Col, Statistic } from 'antd';
|
||||||
|
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||||
|
import { FUNCTIONAL_COLORS, PRIMARY_COLORS } from '../../theme/colors';
|
||||||
|
|
||||||
|
export const Analytics: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>统计分析</h1>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xs={24} md={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="本月上传"
|
||||||
|
value={145}
|
||||||
|
suffix="张"
|
||||||
|
prefix={<ArrowUpOutlined />}
|
||||||
|
valueStyle={{ color: FUNCTIONAL_COLORS.SUCCESS }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="本月下载"
|
||||||
|
value={523}
|
||||||
|
suffix="次"
|
||||||
|
prefix={<ArrowDownOutlined />}
|
||||||
|
valueStyle={{ color: PRIMARY_COLORS.PRIMARY }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="本月流量"
|
||||||
|
value={15.6}
|
||||||
|
suffix="GB"
|
||||||
|
precision={1}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="预估成本"
|
||||||
|
value={25.8}
|
||||||
|
prefix="¥"
|
||||||
|
precision={2}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card title="上传趋势" style={{ marginTop: 16 }}>
|
||||||
|
<p style={{ color: '#8C8C8C', textAlign: 'center', padding: 60 }}>
|
||||||
|
图表功能正在开发中...
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
335
src/pages/Dashboard/index.tsx
Normal file
335
src/pages/Dashboard/index.tsx
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Row, Col, Progress, Space } from 'antd';
|
||||||
|
import {
|
||||||
|
CloudUploadOutlined,
|
||||||
|
FileImageOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
ArrowUpOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useStorageStore } from '../../stores/useStorageStore';
|
||||||
|
import { PRIMARY_COLORS, FUNCTIONAL_COLORS, ACCENT_COLORS } from '../../theme/colors';
|
||||||
|
|
||||||
|
export const Dashboard: React.FC = () => {
|
||||||
|
const { storageStats } = useStorageStore();
|
||||||
|
|
||||||
|
const usedPercentage = storageStats
|
||||||
|
? Math.round((storageStats.usedSpace / storageStats.totalSpace) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计卡片组件 - 全新极简设计
|
||||||
|
const StatCard: React.FC<{
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
bgGradient: string;
|
||||||
|
suffix?: string;
|
||||||
|
}> = ({ title, value, icon, bgGradient, suffix }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: bgGradient,
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: '28px',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateY(-8px)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 20px 40px rgba(0, 0, 0, 0.12)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 图标 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 16,
|
||||||
|
background: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 26, color: '#FFFFFF' }}>{icon}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'rgba(255, 255, 255, 0.85)',
|
||||||
|
marginBottom: 8,
|
||||||
|
fontWeight: 500,
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数值 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
lineHeight: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
{suffix && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
marginLeft: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>仪表盘</h1>
|
||||||
|
|
||||||
|
{/* 统计卡片 - 全新无边框渐变设计 */}
|
||||||
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<StatCard
|
||||||
|
title="总文件数"
|
||||||
|
value={storageStats?.fileCount || 0}
|
||||||
|
icon={<FileImageOutlined />}
|
||||||
|
bgGradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<StatCard
|
||||||
|
title="已用空间"
|
||||||
|
value={formatBytes(storageStats?.usedSpace || 0)}
|
||||||
|
icon={<DatabaseOutlined />}
|
||||||
|
bgGradient="linear-gradient(135deg, #10B981 0%, #059669 100%)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<StatCard
|
||||||
|
title="上传流量"
|
||||||
|
value="0 B"
|
||||||
|
icon={<CloudUploadOutlined />}
|
||||||
|
bgGradient="linear-gradient(135deg, #8B5CF6 0%, #6D28D9 100%)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<StatCard
|
||||||
|
title="月增长"
|
||||||
|
value={12.5}
|
||||||
|
suffix="%"
|
||||||
|
icon={<ArrowUpOutlined />}
|
||||||
|
bgGradient="linear-gradient(135deg, #EC4899 0%, #DB2777 100%)"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 存储使用情况 */}
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<span style={{ fontSize: 16, fontWeight: 600 }}>
|
||||||
|
存储空间使用情况
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
className="hover-card"
|
||||||
|
bordered={false}
|
||||||
|
style={{
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#6B7280', fontSize: 14 }}>
|
||||||
|
已使用 <strong style={{ color: '#1F2937' }}>{formatBytes(storageStats?.usedSpace || 0)}</strong>
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#6B7280', fontSize: 14 }}>
|
||||||
|
总计 <strong style={{ color: '#1F2937' }}>{formatBytes(storageStats?.totalSpace || 0)}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={usedPercentage}
|
||||||
|
strokeColor={{
|
||||||
|
'0%': PRIMARY_COLORS.PRIMARY,
|
||||||
|
'100%': ACCENT_COLORS.PINK,
|
||||||
|
}}
|
||||||
|
strokeWidth={12}
|
||||||
|
trailColor="#F3F4F6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: 'linear-gradient(135deg, #F5F3FF 0%, #FAF5FF 100%)',
|
||||||
|
borderRadius: 10,
|
||||||
|
color: PRIMARY_COLORS.PRIMARY,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
剩余空间:{formatBytes((storageStats?.totalSpace || 0) - (storageStats?.usedSpace || 0))}
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Card
|
||||||
|
title="快速操作"
|
||||||
|
className="hover-card"
|
||||||
|
bordered={false}
|
||||||
|
style={{
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||||
|
<a
|
||||||
|
href="/upload"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #F5F3FF 0%, #FAF5FF 100%)',
|
||||||
|
borderRadius: 12,
|
||||||
|
color: PRIMARY_COLORS.PRIMARY,
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
|
e.currentTarget.style.boxShadow = `0 4px 12px ${PRIMARY_COLORS.PRIMARY}20`;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: PRIMARY_COLORS.PRIMARY,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloudUploadOutlined style={{ fontSize: 20, color: '#FFF' }} />
|
||||||
|
</div>
|
||||||
|
上传新图片
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/gallery"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #F5F3FF 0%, #FAF5FF 100%)',
|
||||||
|
borderRadius: 12,
|
||||||
|
color: PRIMARY_COLORS.PRIMARY,
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
|
e.currentTarget.style.boxShadow = `0 4px 12px ${PRIMARY_COLORS.PRIMARY}20`;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: '#8B5CF6',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileImageOutlined style={{ fontSize: 20, color: '#FFF' }} />
|
||||||
|
</div>
|
||||||
|
浏览图片库
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/storage"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #F5F3FF 0%, #FAF5FF 100%)',
|
||||||
|
borderRadius: 12,
|
||||||
|
color: PRIMARY_COLORS.PRIMARY,
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(4px)';
|
||||||
|
e.currentTarget.style.boxShadow = `0 4px 12px ${PRIMARY_COLORS.PRIMARY}20`;
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: FUNCTIONAL_COLORS.SUCCESS,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseOutlined style={{ fontSize: 20, color: '#FFF' }} />
|
||||||
|
</div>
|
||||||
|
配置存储源
|
||||||
|
</a>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
754
src/pages/Gallery/index.tsx
Normal file
754
src/pages/Gallery/index.tsx
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Card, Empty, Space, Input, Select, Button, Checkbox, Tag, Tooltip } from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
HeartOutlined,
|
||||||
|
HeartFilled,
|
||||||
|
DownloadOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import Masonry from 'react-masonry-css';
|
||||||
|
import { useGalleryStore } from '../../stores/useGalleryStore';
|
||||||
|
import type { ImageItem } from '../../types';
|
||||||
|
|
||||||
|
// 模拟图片数据 - 使用不同尺寸的图片实现瀑布流效果
|
||||||
|
const mockImages: ImageItem[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
url: 'https://images.pexels.com/photos/1287145/pexels-photo-1287145.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1287145/pexels-photo-1287145.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'landscape-sunset.jpg',
|
||||||
|
size: 2548000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080, // 16:9 横图
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-15'),
|
||||||
|
tags: ['风景', '日落'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
url: 'https://images.pexels.com/photos/1366630/pexels-photo-1366630.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1366630/pexels-photo-1366630.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'mountain-view.jpg',
|
||||||
|
size: 3120000,
|
||||||
|
width: 1080,
|
||||||
|
height: 1620, // 2:3 竖图
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-14'),
|
||||||
|
tags: ['山', '自然'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
url: 'https://images.pexels.com/photos/1660995/pexels-photo-1660995.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1660995/pexels-photo-1660995.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'ocean-waves.jpg',
|
||||||
|
size: 1890000,
|
||||||
|
width: 1200,
|
||||||
|
height: 1200, // 1:1 正方形
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-13'),
|
||||||
|
tags: ['海洋', '波浪'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
url: 'https://images.pexels.com/photos/1179229/pexels-photo-1179229.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1179229/pexels-photo-1179229.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'forest-path.jpg',
|
||||||
|
size: 2234000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-12'),
|
||||||
|
tags: ['森林', '小径'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
url: 'https://images.pexels.com/photos/1519088/pexels-photo-1519088.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1519088/pexels-photo-1519088.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'city-skyline.jpg',
|
||||||
|
size: 2890000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-11'),
|
||||||
|
tags: ['城市', '天际线'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
url: 'https://images.pexels.com/photos/2387418/pexels-photo-2387418.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/2387418/pexels-photo-2387418.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'desert-dunes.jpg',
|
||||||
|
size: 2156000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-10'),
|
||||||
|
tags: ['沙漠', '沙丘'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
url: 'https://images.pexels.com/photos/1563356/pexels-photo-1563356.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1563356/pexels-photo-1563356.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'aurora-night.jpg',
|
||||||
|
size: 2980000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-09'),
|
||||||
|
tags: ['极光', '夜景'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
url: 'https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'beach-sunset.jpg',
|
||||||
|
size: 2145000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-08'),
|
||||||
|
tags: ['海滩', '日落'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
url: 'https://images.pexels.com/photos/1643457/pexels-photo-1643457.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1643457/pexels-photo-1643457.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'waterfall.jpg',
|
||||||
|
size: 3250000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-07'),
|
||||||
|
tags: ['瀑布', '自然'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
url: 'https://images.pexels.com/photos/1574844/pexels-photo-1574844.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1574844/pexels-photo-1574844.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'starry-sky.jpg',
|
||||||
|
size: 2678000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-06'),
|
||||||
|
tags: ['星空', '夜晚'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
url: 'https://images.pexels.com/photos/1450353/pexels-photo-1450353.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1450353/pexels-photo-1450353.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'autumn-forest.jpg',
|
||||||
|
size: 2234000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-05'),
|
||||||
|
tags: ['秋天', '森林'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
url: 'https://images.pexels.com/photos/2559941/pexels-photo-2559941.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/2559941/pexels-photo-2559941.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'mountain-lake.jpg',
|
||||||
|
size: 2890000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-04'),
|
||||||
|
tags: ['湖泊', '山'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '13',
|
||||||
|
url: 'https://images.pexels.com/photos/1671324/pexels-photo-1671324.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1671324/pexels-photo-1671324.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'tropical-beach.jpg',
|
||||||
|
size: 2456000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-03'),
|
||||||
|
tags: ['热带', '海滩'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
url: 'https://images.pexels.com/photos/1252869/pexels-photo-1252869.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1252869/pexels-photo-1252869.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'misty-mountain.jpg',
|
||||||
|
size: 2123000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-02'),
|
||||||
|
tags: ['雾', '山'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '15',
|
||||||
|
url: 'https://images.pexels.com/photos/1770809/pexels-photo-1770809.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1770809/pexels-photo-1770809.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'cherry-blossom.jpg',
|
||||||
|
size: 2345000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2025-01-01'),
|
||||||
|
tags: ['樱花', '春天'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16',
|
||||||
|
url: 'https://images.pexels.com/photos/1166209/pexels-photo-1166209.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1166209/pexels-photo-1166209.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'canyon-view.jpg',
|
||||||
|
size: 2987000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-31'),
|
||||||
|
tags: ['峡谷', '风景'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '17',
|
||||||
|
url: 'https://images.pexels.com/photos/1619317/pexels-photo-1619317.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1619317/pexels-photo-1619317.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'snowy-peak.jpg',
|
||||||
|
size: 2678000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-30'),
|
||||||
|
tags: ['雪山', '冬天'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
url: 'https://images.pexels.com/photos/1624438/pexels-photo-1624438.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1624438/pexels-photo-1624438.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'lavender-field.jpg',
|
||||||
|
size: 2234000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-29'),
|
||||||
|
tags: ['薰衣草', '花田'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '19',
|
||||||
|
url: 'https://images.pexels.com/photos/1454360/pexels-photo-1454360.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1454360/pexels-photo-1454360.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'river-valley.jpg',
|
||||||
|
size: 2456000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-28'),
|
||||||
|
tags: ['河流', '山谷'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '20',
|
||||||
|
url: 'https://images.pexels.com/photos/1434819/pexels-photo-1434819.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1434819/pexels-photo-1434819.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'northern-lights.jpg',
|
||||||
|
size: 2890000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-27'),
|
||||||
|
tags: ['极光', '北欧'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '21',
|
||||||
|
url: 'https://images.pexels.com/photos/1591373/pexels-photo-1591373.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1591373/pexels-photo-1591373.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'sunrise-clouds.jpg',
|
||||||
|
size: 2123000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-26'),
|
||||||
|
tags: ['日出', '云'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '22',
|
||||||
|
url: 'https://images.pexels.com/photos/1477430/pexels-photo-1477430.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1477430/pexels-photo-1477430.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'tropical-island.jpg',
|
||||||
|
size: 2678000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-25'),
|
||||||
|
tags: ['海岛', '热带'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '23',
|
||||||
|
url: 'https://images.pexels.com/photos/1323550/pexels-photo-1323550.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1323550/pexels-photo-1323550.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'wheat-field.jpg',
|
||||||
|
size: 2345000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-24'),
|
||||||
|
tags: ['麦田', '农田'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '24',
|
||||||
|
url: 'https://images.pexels.com/photos/1761279/pexels-photo-1761279.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1761279/pexels-photo-1761279.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'rocky-coast.jpg',
|
||||||
|
size: 2456000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-23'),
|
||||||
|
tags: ['海岸', '岩石'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '25',
|
||||||
|
url: 'https://images.pexels.com/photos/1655166/pexels-photo-1655166.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1655166/pexels-photo-1655166.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'bamboo-forest.jpg',
|
||||||
|
size: 2234000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-22'),
|
||||||
|
tags: ['竹林', '森林'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '26',
|
||||||
|
url: 'https://images.pexels.com/photos/1659438/pexels-photo-1659438.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1659438/pexels-photo-1659438.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'tulip-garden.jpg',
|
||||||
|
size: 2567000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-21'),
|
||||||
|
tags: ['郁金香', '花园'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '27',
|
||||||
|
url: 'https://images.pexels.com/photos/1366919/pexels-photo-1366919.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1366919/pexels-photo-1366919.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'glacier-lake.jpg',
|
||||||
|
size: 2890000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-20'),
|
||||||
|
tags: ['冰川', '湖泊'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '28',
|
||||||
|
url: 'https://images.pexels.com/photos/1529360/pexels-photo-1529360.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1529360/pexels-photo-1529360.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'sunset-pier.jpg',
|
||||||
|
size: 2123000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-19'),
|
||||||
|
tags: ['码头', '日落'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '29',
|
||||||
|
url: 'https://images.pexels.com/photos/1624496/pexels-photo-1624496.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1624496/pexels-photo-1624496.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'rice-terrace.jpg',
|
||||||
|
size: 2456000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-18'),
|
||||||
|
tags: ['梯田', '农田'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '30',
|
||||||
|
url: 'https://images.pexels.com/photos/1430676/pexels-photo-1430676.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1430676/pexels-photo-1430676.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'alpine-meadow.jpg',
|
||||||
|
size: 2345000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-17'),
|
||||||
|
tags: ['高山', '草甸'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '31',
|
||||||
|
url: 'https://images.pexels.com/photos/1557652/pexels-photo-1557652.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1557652/pexels-photo-1557652.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'palm-beach.jpg',
|
||||||
|
size: 2234000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-16'),
|
||||||
|
tags: ['棕榈树', '海滩'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '32',
|
||||||
|
url: 'https://images.pexels.com/photos/1271619/pexels-photo-1271619.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1271619/pexels-photo-1271619.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'volcano-peak.jpg',
|
||||||
|
size: 2678000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-15'),
|
||||||
|
tags: ['火山', '山峰'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '33',
|
||||||
|
url: 'https://images.pexels.com/photos/1576937/pexels-photo-1576937.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1576937/pexels-photo-1576937.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'summer-field.jpg',
|
||||||
|
size: 2456000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-14'),
|
||||||
|
tags: ['夏天', '田野'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '34',
|
||||||
|
url: 'https://images.pexels.com/photos/1179229/pexels-photo-1179229.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1179229/pexels-photo-1179229.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'pine-forest.jpg',
|
||||||
|
size: 2345000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1280,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-13'),
|
||||||
|
tags: ['松林', '自然'],
|
||||||
|
isFavorite: false,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '35',
|
||||||
|
url: 'https://images.pexels.com/photos/1525041/pexels-photo-1525041.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1525041/pexels-photo-1525041.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'rainbow-sky.jpg',
|
||||||
|
size: 2567000,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-12'),
|
||||||
|
tags: ['彩虹', '天空'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '36',
|
||||||
|
url: 'https://images.pexels.com/photos/1761283/pexels-photo-1761283.jpeg',
|
||||||
|
thumbnail: 'https://images.pexels.com/photos/1761283/pexels-photo-1761283.jpeg?auto=compress&cs=tinysrgb&w=400',
|
||||||
|
filename: 'coral-reef.jpg',
|
||||||
|
size: 2890000,
|
||||||
|
width: 2048,
|
||||||
|
height: 1365,
|
||||||
|
format: 'jpg',
|
||||||
|
uploadedAt: new Date('2024-12-11'),
|
||||||
|
tags: ['珊瑚礁', '海洋'],
|
||||||
|
isFavorite: true,
|
||||||
|
storageSource: 'MinIO',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Gallery: React.FC = () => {
|
||||||
|
const { images, setImages, selectedImages, toggleImageSelection, toggleFavorite } =
|
||||||
|
useGalleryStore();
|
||||||
|
|
||||||
|
// 初始化模拟数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (images.length === 0) {
|
||||||
|
setImages(mockImages);
|
||||||
|
}
|
||||||
|
}, [images.length, setImages]);
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 瀑布流断点配置
|
||||||
|
const breakpointColumnsObj = {
|
||||||
|
default: 4,
|
||||||
|
1400: 3,
|
||||||
|
1000: 2,
|
||||||
|
700: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染瀑布流视图
|
||||||
|
const renderGridView = () => (
|
||||||
|
<Masonry
|
||||||
|
breakpointCols={breakpointColumnsObj}
|
||||||
|
className="masonry-grid"
|
||||||
|
columnClassName="masonry-grid_column"
|
||||||
|
>
|
||||||
|
{images.map((image) => (
|
||||||
|
<Card
|
||||||
|
key={image.id}
|
||||||
|
hoverable
|
||||||
|
className="image-card"
|
||||||
|
cover={
|
||||||
|
<div style={{ position: 'relative', overflow: 'hidden', background: '#f5f5f5' }}>
|
||||||
|
<img
|
||||||
|
src={image.thumbnail}
|
||||||
|
alt={image.filename}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
display: 'block',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="image-overlay"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'opacity 0.3s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space size="large">
|
||||||
|
<Tooltip title="查看">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
style={{ color: '#fff' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="下载">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
style={{ color: '#fff' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
style={{ color: '#fff' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedImages.includes(image.id)}
|
||||||
|
onChange={() => toggleImageSelection(image.id)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={image.isFavorite ? <HeartFilled /> : <HeartOutlined />}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 1,
|
||||||
|
color: image.isFavorite ? '#EC4899' : '#fff',
|
||||||
|
textShadow: '0 1px 3px rgba(0,0,0,0.5)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFavorite(image.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 12 }}
|
||||||
|
style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
title={image.filename}
|
||||||
|
>
|
||||||
|
{image.filename}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#6B7280' }}>
|
||||||
|
{formatFileSize(image.size)} • {image.width}x{image.height}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||||
|
{image.tags.map((tag) => (
|
||||||
|
<Tag key={tag} style={{ margin: 0, fontSize: 11 }}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Masonry>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
{/* 标题行 */}
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<h1 style={{ margin: 0, fontSize: 28, fontWeight: 700 }}>
|
||||||
|
图片库 <span style={{ fontSize: 16, fontWeight: 400, color: '#9CA3AF' }}>({images.length} 张)</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 工具栏 - 搜索和筛选 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 28,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 精致的搜索框 */}
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
placeholder="搜索图片名称、标签..."
|
||||||
|
prefix={<SearchOutlined style={{ color: '#9CA3AF', fontSize: 16 }} />}
|
||||||
|
style={{
|
||||||
|
width: 360,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: '#FFFFFF',
|
||||||
|
border: '1px solid #E5E7EB',
|
||||||
|
fontSize: 14,
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#7C3AED';
|
||||||
|
e.target.style.boxShadow = '0 0 0 3px rgba(124, 58, 237, 0.08), 0 1px 3px rgba(0, 0, 0, 0.08)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = '#E5E7EB';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.05)';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 占位空间 - 让右侧控件靠右显示 */}
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
|
{/* 排序选择器 */}
|
||||||
|
<Select
|
||||||
|
defaultValue="date"
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
width: 140,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value="date">📅 按日期</Select.Option>
|
||||||
|
<Select.Option value="name">📝 按名称</Select.Option>
|
||||||
|
<Select.Option value="size">📦 按大小</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{images.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<Empty description="暂无图片,请先上传图片" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
renderGridView()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
71
src/pages/Links/index.tsx
Normal file
71
src/pages/Links/index.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Select, Input, Button, Space, message } from 'antd';
|
||||||
|
import { CopyOutlined, QrcodeOutlined } from '@ant-design/icons';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
export const Links: React.FC = () => {
|
||||||
|
const [format, setFormat] = React.useState<string>('markdown');
|
||||||
|
const [sampleUrl] = React.useState('https://example.com/image.png');
|
||||||
|
|
||||||
|
const getLinkFormat = () => {
|
||||||
|
switch (format) {
|
||||||
|
case 'markdown':
|
||||||
|
return ``;
|
||||||
|
case 'html':
|
||||||
|
return `<img src="${sampleUrl}" alt="Image" />`;
|
||||||
|
case 'url':
|
||||||
|
return sampleUrl;
|
||||||
|
default:
|
||||||
|
return sampleUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
copy(getLinkFormat());
|
||||||
|
message.success('链接已复制到剪贴板');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>链接管理</h1>
|
||||||
|
|
||||||
|
<Card title="生成链接">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>选择格式</div>
|
||||||
|
<Select
|
||||||
|
value={format}
|
||||||
|
onChange={setFormat}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
options={[
|
||||||
|
{ label: 'Markdown', value: 'markdown' },
|
||||||
|
{ label: 'HTML', value: 'html' },
|
||||||
|
{ label: '直链 URL', value: 'url' },
|
||||||
|
{ label: '自定义格式', value: 'custom' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>生成的链接</div>
|
||||||
|
<TextArea
|
||||||
|
value={getLinkFormat()}
|
||||||
|
rows={4}
|
||||||
|
readOnly
|
||||||
|
style={{ fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" icon={<CopyOutlined />} onClick={handleCopy}>
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
<Button icon={<QrcodeOutlined />}>生成二维码</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
67
src/pages/Settings/index.tsx
Normal file
67
src/pages/Settings/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Form, Switch, Select, Slider, Button, Space, message } from 'antd';
|
||||||
|
import { useSettingsStore } from '../../stores/useSettingsStore';
|
||||||
|
|
||||||
|
export const Settings: React.FC = () => {
|
||||||
|
const { autoCompress, uploadQuality, language, updateSettings, resetSettings } = useSettingsStore();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const handleSave = (values: any) => {
|
||||||
|
updateSettings(values);
|
||||||
|
message.success('设置已保存');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>设置</h1>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
autoCompress,
|
||||||
|
uploadQuality,
|
||||||
|
language,
|
||||||
|
}}
|
||||||
|
onFinish={handleSave}
|
||||||
|
>
|
||||||
|
<Form.Item label="语言设置" name="language">
|
||||||
|
<Select style={{ width: 200 }}>
|
||||||
|
<Select.Option value="zh-CN">简体中文</Select.Option>
|
||||||
|
<Select.Option value="en-US">English</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="自动压缩图片" name="autoCompress" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="上传质量" name="uploadQuality">
|
||||||
|
<Slider
|
||||||
|
min={50}
|
||||||
|
max={100}
|
||||||
|
marks={{
|
||||||
|
50: '50%',
|
||||||
|
75: '75%',
|
||||||
|
100: '100%',
|
||||||
|
}}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存设置
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => resetSettings()}>
|
||||||
|
恢复默认
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
74
src/pages/Storage/index.tsx
Normal file
74
src/pages/Storage/index.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Row, Col, Button, Badge, Space, Tag } from 'antd';
|
||||||
|
import { PlusOutlined, DatabaseOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { useStorageStore } from '../../stores/useStorageStore';
|
||||||
|
import { FUNCTIONAL_COLORS, TAG_COLORS } from '../../theme/colors';
|
||||||
|
|
||||||
|
export const Storage: React.FC = () => {
|
||||||
|
const { sources } = useStorageStore();
|
||||||
|
|
||||||
|
const getStorageTypeName = (type: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
local: '本地存储',
|
||||||
|
minio: 'MinIO',
|
||||||
|
'aliyun-oss': '阿里云 OSS',
|
||||||
|
'tencent-cos': '腾讯云 COS',
|
||||||
|
};
|
||||||
|
return map[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>存储配置</h1>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />}>
|
||||||
|
添加存储源
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{sources.map((source) => (
|
||||||
|
<Col xs={24} md={12} lg={8} key={source.id}>
|
||||||
|
<Card
|
||||||
|
className="hover-card"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<DatabaseOutlined />
|
||||||
|
{source.name}
|
||||||
|
{source.isActive && <Tag color={TAG_COLORS.PRIMARY}>当前使用</Tag>}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
source.status === 'connected' ? (
|
||||||
|
<CheckCircleOutlined style={{ color: FUNCTIONAL_COLORS.SUCCESS }} />
|
||||||
|
) : (
|
||||||
|
<CloseCircleOutlined style={{ color: FUNCTIONAL_COLORS.ERROR }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<span style={{ color: '#8C8C8C' }}>类型:</span>
|
||||||
|
<span>{getStorageTypeName(source.type)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ color: '#8C8C8C' }}>状态:</span>
|
||||||
|
<Badge
|
||||||
|
status={source.status === 'connected' ? 'success' : 'error'}
|
||||||
|
text={source.status === 'connected' ? '已连接' : '未连接'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Button size="small" style={{ marginRight: 8 }}>
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
|
<Button size="small">配置</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
src/pages/Tools/index.tsx
Normal file
73
src/pages/Tools/index.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Tabs } from 'antd';
|
||||||
|
import { CompressOutlined, PictureOutlined, FormatPainterOutlined, ScissorOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const Tools: React.FC = () => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'compress',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<CompressOutlined />
|
||||||
|
图片压缩
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<Card>
|
||||||
|
<p>图片压缩工具正在开发中...</p>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'watermark',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<FormatPainterOutlined />
|
||||||
|
添加水印
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<Card>
|
||||||
|
<p>水印工具正在开发中...</p>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'convert',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<PictureOutlined />
|
||||||
|
格式转换
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<Card>
|
||||||
|
<p>格式转换工具正在开发中...</p>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resize',
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<ScissorOutlined />
|
||||||
|
尺寸调整
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<Card>
|
||||||
|
<p>尺寸调整工具正在开发中...</p>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>图片处理工具</h1>
|
||||||
|
<Card>
|
||||||
|
<Tabs items={items} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
120
src/pages/Upload/index.tsx
Normal file
120
src/pages/Upload/index.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Card, message, Progress, List, Button, Space } from 'antd';
|
||||||
|
import { InboxOutlined, DeleteOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { useUploadStore } from '../../stores/useUploadStore';
|
||||||
|
import { FUNCTIONAL_COLORS, PRIMARY_COLORS } from '../../theme/colors';
|
||||||
|
|
||||||
|
export const Upload: React.FC = () => {
|
||||||
|
const { uploadQueue, addToQueue, removeFromQueue, clearCompleted } = useUploadStore();
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(acceptedFiles: File[]) => {
|
||||||
|
addToQueue(acceptedFiles);
|
||||||
|
message.success(`已添加 ${acceptedFiles.length} 个文件到上传队列`);
|
||||||
|
},
|
||||||
|
[addToQueue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'],
|
||||||
|
},
|
||||||
|
noClick: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (size < 1024) return size + ' B';
|
||||||
|
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
|
||||||
|
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container fade-in">
|
||||||
|
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 600 }}>上传图片</h1>
|
||||||
|
|
||||||
|
<Card style={{ marginBottom: 24 }}>
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={`upload-dropzone ${isDragActive ? 'drag-active' : ''}`}
|
||||||
|
style={{
|
||||||
|
padding: '60px 20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div style={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 16,
|
||||||
|
background: 'linear-gradient(135deg, #F5F3FF 0%, #FAF5FF 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto 16px'
|
||||||
|
}}>
|
||||||
|
<InboxOutlined style={{ fontSize: 40, color: PRIMARY_COLORS.PRIMARY }} />
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 16, marginBottom: 8 }}>
|
||||||
|
{isDragActive ? '松开鼠标即可上传' : '拖拽文件到此处,或点击选择文件'}
|
||||||
|
</p>
|
||||||
|
<p style={{ color: '#8C8C8C', fontSize: 14 }}>
|
||||||
|
支持格式:PNG、JPG、JPEG、GIF、WebP、SVG
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{uploadQueue.length > 0 && (
|
||||||
|
<Card
|
||||||
|
title="上传队列"
|
||||||
|
extra={
|
||||||
|
<Button size="small" onClick={clearCompleted}>
|
||||||
|
清空已完成
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
dataSource={uploadQueue}
|
||||||
|
renderItem={(task) => (
|
||||||
|
<List.Item
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
{task.status === 'success' && (
|
||||||
|
<CheckCircleOutlined style={{ color: FUNCTIONAL_COLORS.SUCCESS, fontSize: 20 }} />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => removeFromQueue(task.id)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={task.file.name}
|
||||||
|
description={
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<span style={{ color: '#8C8C8C' }}>
|
||||||
|
{formatFileSize(task.file.size)}
|
||||||
|
{task.status === 'uploading' && ' - 上传中...'}
|
||||||
|
{task.status === 'success' && ' - 上传成功'}
|
||||||
|
{task.status === 'error' && ' - 上传失败'}
|
||||||
|
</span>
|
||||||
|
{task.status === 'uploading' && (
|
||||||
|
<Progress percent={task.progress} size="small" />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user