添加基础项目结构和路由配置

- 添加 main.tsx 应用入口
- 添加 App.tsx 根组件
- 配置路由系统(React Router v7)
- 添加状态管理(Zustand)
- 定义 TypeScript 类型
This commit is contained in:
Leo 2025-10-19 21:47:52 +08:00
parent 7d32dee944
commit 13e2bc9637
8 changed files with 572 additions and 0 deletions

16
src/App.tsx Normal file
View File

@ -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 (
<ConfigProvider theme={theme} locale={zhCN}>
<RouterProvider router={router} />
</ConfigProvider>
);
}
export default App;

9
src/main.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

55
src/routes/index.tsx Normal file
View File

@ -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: <MainLayout />,
children: [
{
index: true,
element: <Navigate to="/dashboard" replace />,
},
{
path: 'dashboard',
element: <Dashboard />,
},
{
path: 'upload',
element: <Upload />,
},
{
path: 'gallery',
element: <Gallery />,
},
{
path: 'links',
element: <Links />,
},
{
path: 'tools',
element: <Tools />,
},
{
path: 'storage',
element: <Storage />,
},
{
path: 'analytics',
element: <Analytics />,
},
{
path: 'settings',
element: <Settings />,
},
],
},
]);

View File

@ -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<GalleryFilters>) => 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<GalleryState>((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 }),
}));

View File

@ -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<UserSettings>) => void;
resetSettings: () => void;
}
const defaultSettings: UserSettings = {
theme: 'light',
language: 'zh-CN',
autoCompress: true,
uploadQuality: 80,
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
...defaultSettings,
updateSettings: (settings) =>
set((state) => ({
...state,
...settings,
})),
resetSettings: () => set(defaultSettings),
}),
{
name: 'picstack-settings',
}
)
);

View File

@ -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<StorageSource, 'id' | 'createdAt' | 'status'>) => void;
removeSource: (id: string) => void;
updateSource: (id: string, updates: Partial<StorageSource>) => void;
setActiveSource: (id: string) => void;
updateStats: (stats: StorageStats) => void;
testConnection: (id: string) => Promise<boolean>;
}
export const useStorageStore = create<StorageState>()(
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,
}),
}
)
);

View File

@ -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<void>;
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<UploadState>((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),
}));
},
}));

142
src/types/index.ts Normal file
View File

@ -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[];
}