diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..ed86b6f --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,16 @@ +import { RouterProvider } from 'react-router-dom'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { router } from './routes'; +import { theme } from './theme'; +import './global.css'; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100644 index 0000000..9b26965 --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,55 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom'; +import { MainLayout } from '../layouts/MainLayout'; +import { Dashboard } from '../pages/Dashboard'; +import { Upload } from '../pages/Upload'; +import { Gallery } from '../pages/Gallery'; +import { Links } from '../pages/Links'; +import { Tools } from '../pages/Tools'; +import { Storage } from '../pages/Storage'; +import { Analytics } from '../pages/Analytics'; +import { Settings } from '../pages/Settings'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'dashboard', + element: , + }, + { + path: 'upload', + element: , + }, + { + path: 'gallery', + element: , + }, + { + path: 'links', + element: , + }, + { + path: 'tools', + element: , + }, + { + path: 'storage', + element: , + }, + { + path: 'analytics', + element: , + }, + { + path: 'settings', + element: , + }, + ], + }, +]); diff --git a/src/stores/useGalleryStore.ts b/src/stores/useGalleryStore.ts new file mode 100644 index 0000000..8babc93 --- /dev/null +++ b/src/stores/useGalleryStore.ts @@ -0,0 +1,93 @@ +import { create } from 'zustand'; +import type { ImageItem, Album, GalleryFilters } from '../types'; + +interface GalleryState { + images: ImageItem[]; + albums: Album[]; + selectedImages: string[]; + viewMode: 'grid' | 'list'; + filters: GalleryFilters; + loading: boolean; + + setImages: (images: ImageItem[]) => void; + setAlbums: (albums: Album[]) => void; + addImage: (image: ImageItem) => void; + deleteImages: (ids: string[]) => void; + toggleImageSelection: (id: string) => void; + clearSelection: () => void; + selectAll: () => void; + setViewMode: (mode: 'grid' | 'list') => void; + updateFilters: (filters: Partial) => void; + resetFilters: () => void; + moveToAlbum: (imageIds: string[], albumId: string) => void; + toggleFavorite: (imageId: string) => void; + setLoading: (loading: boolean) => void; +} + +const defaultFilters: GalleryFilters = { + sortBy: 'date', + sortOrder: 'desc', +}; + +export const useGalleryStore = create((set) => ({ + images: [], + albums: [], + selectedImages: [], + viewMode: 'grid', + filters: defaultFilters, + loading: false, + + setImages: (images) => set({ images }), + + setAlbums: (albums) => set({ albums }), + + addImage: (image) => + set((state) => ({ + images: [image, ...state.images], + })), + + deleteImages: (ids) => + set((state) => ({ + images: state.images.filter((img) => !ids.includes(img.id)), + selectedImages: state.selectedImages.filter((id) => !ids.includes(id)), + })), + + toggleImageSelection: (id) => + set((state) => ({ + selectedImages: state.selectedImages.includes(id) + ? state.selectedImages.filter((imgId) => imgId !== id) + : [...state.selectedImages, id], + })), + + clearSelection: () => set({ selectedImages: [] }), + + selectAll: () => + set((state) => ({ + selectedImages: state.images.map((img) => img.id), + })), + + setViewMode: (mode) => set({ viewMode: mode }), + + updateFilters: (filters) => + set((state) => ({ + filters: { ...state.filters, ...filters }, + })), + + resetFilters: () => set({ filters: defaultFilters }), + + moveToAlbum: (imageIds, albumId) => + set((state) => ({ + images: state.images.map((img) => + imageIds.includes(img.id) ? { ...img, albumId } : img + ), + })), + + toggleFavorite: (imageId) => + set((state) => ({ + images: state.images.map((img) => + img.id === imageId ? { ...img, isFavorite: !img.isFavorite } : img + ), + })), + + setLoading: (loading) => set({ loading }), +})); diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts new file mode 100644 index 0000000..64a7761 --- /dev/null +++ b/src/stores/useSettingsStore.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { UserSettings } from '../types'; + +interface SettingsState extends UserSettings { + updateSettings: (settings: Partial) => void; + resetSettings: () => void; +} + +const defaultSettings: UserSettings = { + theme: 'light', + language: 'zh-CN', + autoCompress: true, + uploadQuality: 80, +}; + +export const useSettingsStore = create()( + persist( + (set) => ({ + ...defaultSettings, + + updateSettings: (settings) => + set((state) => ({ + ...state, + ...settings, + })), + + resetSettings: () => set(defaultSettings), + }), + { + name: 'picstack-settings', + } + ) +); diff --git a/src/stores/useStorageStore.ts b/src/stores/useStorageStore.ts new file mode 100644 index 0000000..2ace625 --- /dev/null +++ b/src/stores/useStorageStore.ts @@ -0,0 +1,107 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { StorageSource, StorageStats } from '../types'; +import { nanoid } from 'nanoid'; + +interface StorageState { + sources: StorageSource[]; + activeSourceId: string | null; + storageStats: StorageStats | null; + + addSource: (source: Omit) => void; + removeSource: (id: string) => void; + updateSource: (id: string, updates: Partial) => void; + setActiveSource: (id: string) => void; + updateStats: (stats: StorageStats) => void; + testConnection: (id: string) => Promise; +} + +export const useStorageStore = create()( + persist( + (set, get) => ({ + sources: [ + { + id: 'local-default', + name: '本地存储', + type: 'local', + config: { + basePath: '/uploads', + }, + isActive: true, + status: 'connected', + createdAt: new Date(), + }, + ], + activeSourceId: 'local-default', + storageStats: { + totalSpace: 1024 * 1024 * 1024 * 100, // 100GB + usedSpace: 1024 * 1024 * 1024 * 5, // 5GB + fileCount: 0, + traffic: { + upload: 0, + download: 0, + }, + cost: 0, + }, + + addSource: (source) => { + const newSource: StorageSource = { + ...source, + id: nanoid(), + status: 'disconnected', + createdAt: new Date(), + }; + + set((state) => ({ + sources: [...state.sources, newSource], + })); + }, + + removeSource: (id) => + set((state) => ({ + sources: state.sources.filter((s) => s.id !== id), + activeSourceId: state.activeSourceId === id ? null : state.activeSourceId, + })), + + updateSource: (id, updates) => + set((state) => ({ + sources: state.sources.map((s) => (s.id === id ? { ...s, ...updates } : s)), + })), + + setActiveSource: (id) => + set((state) => ({ + sources: state.sources.map((s) => ({ + ...s, + isActive: s.id === id, + })), + activeSourceId: id, + })), + + updateStats: (stats) => set({ storageStats: stats }), + + testConnection: async (id) => { + const source = get().sources.find((s) => s.id === id); + if (!source) return false; + + // 这里应该实现实际的连接测试逻辑 + // 暂时返回模拟结果 + await new Promise((resolve) => setTimeout(resolve, 1000)); + + set((state) => ({ + sources: state.sources.map((s) => + s.id === id ? { ...s, status: 'connected' as const } : s + ), + })); + + return true; + }, + }), + { + name: 'picstack-storage', + partialize: (state) => ({ + sources: state.sources, + activeSourceId: state.activeSourceId, + }), + } + ) +); diff --git a/src/stores/useUploadStore.ts b/src/stores/useUploadStore.ts new file mode 100644 index 0000000..6710db2 --- /dev/null +++ b/src/stores/useUploadStore.ts @@ -0,0 +1,116 @@ +import { create } from 'zustand'; +import type { UploadTask } from '../types'; +import { nanoid } from 'nanoid'; + +interface UploadState { + uploadQueue: UploadTask[]; + currentUploading: string[]; + completedFiles: UploadTask[]; + + addToQueue: (files: File[]) => void; + startUpload: (taskId: string) => Promise; + updateProgress: (taskId: string, progress: number) => void; + completeUpload: (taskId: string, uploadedUrl: string, thumbnail: string) => void; + failUpload: (taskId: string, error: string) => void; + pauseUpload: (taskId: string) => void; + cancelUpload: (taskId: string) => void; + clearCompleted: () => void; + removeFromQueue: (taskId: string) => void; +} + +export const useUploadStore = create((set) => ({ + uploadQueue: [], + currentUploading: [], + completedFiles: [], + + addToQueue: (files: File[]) => { + const newTasks: UploadTask[] = files.map((file) => ({ + id: nanoid(), + file, + status: 'pending', + progress: 0, + })); + + set((state) => ({ + uploadQueue: [...state.uploadQueue, ...newTasks], + })); + }, + + startUpload: async (taskId: string) => { + set((state) => ({ + currentUploading: [...state.currentUploading, taskId], + uploadQueue: state.uploadQueue.map((task) => + task.id === taskId ? { ...task, status: 'uploading' as const } : task + ), + })); + + // 这里应该调用实际的上传服务 + // 暂时用模拟数据 + await new Promise((resolve) => setTimeout(resolve, 2000)); + }, + + updateProgress: (taskId: string, progress: number) => { + set((state) => ({ + uploadQueue: state.uploadQueue.map((task) => + task.id === taskId ? { ...task, progress } : task + ), + })); + }, + + completeUpload: (taskId: string, uploadedUrl: string, thumbnail: string) => { + set((state) => { + const completedTask = state.uploadQueue.find((task) => task.id === taskId); + if (!completedTask) return state; + + return { + uploadQueue: state.uploadQueue.filter((task) => task.id !== taskId), + currentUploading: state.currentUploading.filter((id) => id !== taskId), + completedFiles: [ + ...state.completedFiles, + { + ...completedTask, + status: 'success' as const, + progress: 100, + uploadedUrl, + thumbnail, + }, + ], + }; + }); + }, + + failUpload: (taskId: string, error: string) => { + set((state) => ({ + uploadQueue: state.uploadQueue.map((task) => + task.id === taskId ? { ...task, status: 'error' as const, error } : task + ), + currentUploading: state.currentUploading.filter((id) => id !== taskId), + })); + }, + + pauseUpload: (taskId: string) => { + set((state) => ({ + uploadQueue: state.uploadQueue.map((task) => + task.id === taskId ? { ...task, status: 'paused' as const } : task + ), + currentUploading: state.currentUploading.filter((id) => id !== taskId), + })); + }, + + cancelUpload: (taskId: string) => { + set((state) => ({ + uploadQueue: state.uploadQueue.filter((task) => task.id !== taskId), + currentUploading: state.currentUploading.filter((id) => id !== taskId), + })); + }, + + clearCompleted: () => { + set({ completedFiles: [] }); + }, + + removeFromQueue: (taskId: string) => { + set((state) => ({ + uploadQueue: state.uploadQueue.filter((task) => task.id !== taskId), + })); + }, +})); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..37af85d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,142 @@ +// 图片相关类型 +export interface ImageItem { + id: string; + url: string; + thumbnail: string; + filename: string; + size: number; + width: number; + height: number; + format: string; + uploadedAt: Date; + albumId?: string; + tags: string[]; + isFavorite: boolean; + storageSource: string; +} + +// 相册类型 +export interface Album { + id: string; + name: string; + description?: string; + coverImage?: string; + imageCount: number; + createdAt: Date; + updatedAt: Date; +} + +// 上传任务类型 +export interface UploadTask { + id: string; + file: File; + status: 'pending' | 'uploading' | 'success' | 'error' | 'paused'; + progress: number; + error?: string; + uploadedUrl?: string; + thumbnail?: string; +} + +// 存储源配置类型 +export interface StorageSource { + id: string; + name: string; + type: 'local' | 'minio' | 'aliyun-oss' | 'tencent-cos'; + config: StorageConfig; + isActive: boolean; + status: 'connected' | 'disconnected' | 'error'; + createdAt: Date; +} + +export interface StorageConfig { + endpoint?: string; + accessKeyId?: string; + accessKeySecret?: string; + bucket?: string; + region?: string; + basePath?: string; +} + +// 存储统计类型 +export interface StorageStats { + totalSpace: number; + usedSpace: number; + fileCount: number; + traffic: { + upload: number; + download: number; + }; + cost: number; +} + +// 链接生成配置 +export interface LinkConfig { + format: 'markdown' | 'html' | 'url' | 'custom'; + customTemplate?: string; + domain?: string; +} + +// 图片处理配置 +export interface ImageProcessConfig { + compress?: { + quality: number; + maxWidth?: number; + maxHeight?: number; + }; + watermark?: { + type: 'text' | 'image'; + content: string; + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'; + opacity: number; + fontSize?: number; + color?: string; + }; + resize?: { + width: number; + height: number; + keepAspectRatio: boolean; + }; + convert?: { + format: 'jpg' | 'png' | 'webp' | 'avif'; + }; +} + +// 用户设置类型 +export interface UserSettings { + theme: 'light' | 'dark'; + language: 'zh-CN' | 'en-US'; + autoCompress: boolean; + defaultWatermark?: ImageProcessConfig['watermark']; + defaultAlbum?: string; + uploadQuality: number; +} + +// 筛选器类型 +export interface GalleryFilters { + search?: string; + albumId?: string; + tags?: string[]; + dateRange?: [Date, Date]; + format?: string[]; + sortBy: 'date' | 'name' | 'size'; + sortOrder: 'asc' | 'desc'; +} + +// 分析数据类型 +export interface AnalyticsData { + uploadTrend: { + date: string; + count: number; + size: number; + }[]; + trafficTrend: { + date: string; + upload: number; + download: number; + }[]; + storageDistribution: { + type: string; + value: number; + }[]; + popularImages: ImageItem[]; +}