实现所有功能页面组件
- 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