From 13e2bc96378d89637d0ed8ceb2c748a7472a0d61 Mon Sep 17 00:00:00 2001
From: Leo <98382335+gaoziman@users.noreply.github.com>
Date: Sun, 19 Oct 2025 21:47:52 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9F=BA=E7=A1=80=E9=A1=B9?=
=?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84=E5=92=8C=E8=B7=AF=E7=94=B1=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加 main.tsx 应用入口
- 添加 App.tsx 根组件
- 配置路由系统(React Router v7)
- 添加状态管理(Zustand)
- 定义 TypeScript 类型
---
src/App.tsx | 16 ++++
src/main.tsx | 9 +++
src/routes/index.tsx | 55 +++++++++++++
src/stores/useGalleryStore.ts | 93 +++++++++++++++++++++
src/stores/useSettingsStore.ts | 34 ++++++++
src/stores/useStorageStore.ts | 107 +++++++++++++++++++++++++
src/stores/useUploadStore.ts | 116 +++++++++++++++++++++++++++
src/types/index.ts | 142 +++++++++++++++++++++++++++++++++
8 files changed, 572 insertions(+)
create mode 100644 src/App.tsx
create mode 100644 src/main.tsx
create mode 100644 src/routes/index.tsx
create mode 100644 src/stores/useGalleryStore.ts
create mode 100644 src/stores/useSettingsStore.ts
create mode 100644 src/stores/useStorageStore.ts
create mode 100644 src/stores/useUploadStore.ts
create mode 100644 src/types/index.ts
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[];
+}