实现所有功能页面组件

- Dashboard 仪表盘:数据统计卡片、存储使用情况、快速操作
- Upload 上传页面:拖拽上传、批量上传、上传进度
- Gallery 图片库:瀑布流布局、筛选排序、批量操作
- Links 链接管理:链接列表、搜索筛选
- Tools 图片工具:压缩、裁剪、格式转换
- Storage 存储配置:OSS配置管理
- Analytics 统计分析:图表展示、数据分析
- Settings 设置页面:个人设置、系统配置
This commit is contained in:
Leo 2025-10-19 21:49:27 +08:00
parent 0416f58b3d
commit 6e9449d0f3
8 changed files with 1557 additions and 0 deletions

View 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>
);
};

View 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
View 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
View 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 `![Image](${sampleUrl})`;
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>
);
};

View 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>
);
};

View 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
View 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
View 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 }}>
PNGJPGJPEGGIFWebPSVG
</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>
);
};