feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
148
frontend/src/context/ConfigContext.tsx
Normal file
148
frontend/src/context/ConfigContext.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import type { AppConfig } from '../types'
|
||||
import { defaultConfig } from '../types'
|
||||
import { loadConfig, saveConfig } from '../utils/storage'
|
||||
import { S2AClient } from '../api/s2a'
|
||||
|
||||
interface ConfigContextValue {
|
||||
config: AppConfig
|
||||
updateConfig: (updates: Partial<AppConfig>) => void
|
||||
updateS2AConfig: (updates: Partial<AppConfig['s2a']>) => void
|
||||
updatePoolingConfig: (updates: Partial<AppConfig['pooling']>) => void
|
||||
updateCheckConfig: (updates: Partial<AppConfig['check']>) => void
|
||||
updateEmailConfig: (updates: Partial<AppConfig['email']>) => void
|
||||
isConnected: boolean
|
||||
testConnection: () => Promise<boolean>
|
||||
s2aClient: S2AClient | null
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextValue | null>(null)
|
||||
|
||||
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [config, setConfig] = useState<AppConfig>(defaultConfig)
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [s2aClient, setS2aClient] = useState<S2AClient | null>(null)
|
||||
|
||||
// Load config from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedConfig = loadConfig()
|
||||
setConfig(savedConfig)
|
||||
|
||||
// Create S2A client if config is available
|
||||
if (savedConfig.s2a.apiBase && savedConfig.s2a.adminKey) {
|
||||
const client = new S2AClient({
|
||||
baseUrl: savedConfig.s2a.apiBase,
|
||||
apiKey: savedConfig.s2a.adminKey,
|
||||
})
|
||||
setS2aClient(client)
|
||||
|
||||
// Test connection on load
|
||||
client.testConnection().then(setIsConnected)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update S2A client when config changes
|
||||
useEffect(() => {
|
||||
if (config.s2a.apiBase && config.s2a.adminKey) {
|
||||
const client = new S2AClient({
|
||||
baseUrl: config.s2a.apiBase,
|
||||
apiKey: config.s2a.adminKey,
|
||||
})
|
||||
setS2aClient(client)
|
||||
} else {
|
||||
setS2aClient(null)
|
||||
setIsConnected(false)
|
||||
}
|
||||
}, [config.s2a.apiBase, config.s2a.adminKey])
|
||||
|
||||
const updateConfig = useCallback((updates: Partial<AppConfig>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = { ...prev, ...updates }
|
||||
saveConfig(newConfig)
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updateS2AConfig = useCallback((updates: Partial<AppConfig['s2a']>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
s2a: { ...prev.s2a, ...updates },
|
||||
}
|
||||
saveConfig(newConfig)
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updatePoolingConfig = useCallback((updates: Partial<AppConfig['pooling']>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
pooling: { ...prev.pooling, ...updates },
|
||||
}
|
||||
saveConfig(newConfig)
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updateCheckConfig = useCallback((updates: Partial<AppConfig['check']>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
check: { ...prev.check, ...updates },
|
||||
}
|
||||
saveConfig(newConfig)
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updateEmailConfig = useCallback((updates: Partial<AppConfig['email']>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
email: { ...prev.email, ...updates },
|
||||
}
|
||||
saveConfig(newConfig)
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const testConnection = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
// 使用后端代理 API 来测试 S2A 连接(避免 CORS 问题)
|
||||
const res = await fetch('http://localhost:8088/api/s2a/test')
|
||||
const connected = res.ok
|
||||
setIsConnected(connected)
|
||||
return connected
|
||||
} catch {
|
||||
setIsConnected(false)
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
config,
|
||||
updateConfig,
|
||||
updateS2AConfig,
|
||||
updatePoolingConfig,
|
||||
updateCheckConfig,
|
||||
updateEmailConfig,
|
||||
isConnected,
|
||||
testConnection,
|
||||
s2aClient,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useConfigContext(): ConfigContextValue {
|
||||
const context = useContext(ConfigContext)
|
||||
if (!context) {
|
||||
throw new Error('useConfigContext must be used within a ConfigProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
110
frontend/src/context/RecordsContext.tsx
Normal file
110
frontend/src/context/RecordsContext.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import type { AddRecord } from '../types'
|
||||
import { loadRecords, saveRecords, generateId } from '../utils/storage'
|
||||
|
||||
interface RecordsContextValue {
|
||||
records: AddRecord[]
|
||||
addRecord: (record: Omit<AddRecord, 'id' | 'timestamp'>) => void
|
||||
deleteRecord: (id: string) => void
|
||||
clearRecords: () => void
|
||||
getRecordsByDateRange: (startDate: Date, endDate: Date) => AddRecord[]
|
||||
getStats: () => {
|
||||
totalRecords: number
|
||||
totalAdded: number
|
||||
totalSuccess: number
|
||||
totalFailed: number
|
||||
todayAdded: number
|
||||
weekAdded: number
|
||||
}
|
||||
}
|
||||
|
||||
const RecordsContext = createContext<RecordsContextValue | null>(null)
|
||||
|
||||
export function RecordsProvider({ children }: { children: ReactNode }) {
|
||||
const [records, setRecords] = useState<AddRecord[]>([])
|
||||
|
||||
// Load records from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedRecords = loadRecords()
|
||||
setRecords(savedRecords)
|
||||
}, [])
|
||||
|
||||
const addRecord = useCallback((record: Omit<AddRecord, 'id' | 'timestamp'>) => {
|
||||
const newRecord: AddRecord = {
|
||||
...record,
|
||||
id: generateId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
setRecords((prev) => {
|
||||
const updated = [newRecord, ...prev]
|
||||
saveRecords(updated)
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
const deleteRecord = useCallback((id: string) => {
|
||||
setRecords((prev) => {
|
||||
const updated = prev.filter((r) => r.id !== id)
|
||||
saveRecords(updated)
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
const clearRecords = useCallback(() => {
|
||||
setRecords([])
|
||||
saveRecords([])
|
||||
}, [])
|
||||
|
||||
const getRecordsByDateRange = useCallback(
|
||||
(startDate: Date, endDate: Date): AddRecord[] => {
|
||||
return records.filter((record) => {
|
||||
const recordDate = new Date(record.timestamp)
|
||||
return recordDate >= startDate && recordDate <= endDate
|
||||
})
|
||||
},
|
||||
[records]
|
||||
)
|
||||
|
||||
const getStats = useCallback(() => {
|
||||
const now = new Date()
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const weekStart = new Date(todayStart)
|
||||
weekStart.setDate(weekStart.getDate() - 7)
|
||||
|
||||
const todayRecords = records.filter((r) => new Date(r.timestamp) >= todayStart)
|
||||
const weekRecords = records.filter((r) => new Date(r.timestamp) >= weekStart)
|
||||
|
||||
return {
|
||||
totalRecords: records.length,
|
||||
totalAdded: records.reduce((sum, r) => sum + r.total, 0),
|
||||
totalSuccess: records.reduce((sum, r) => sum + r.success, 0),
|
||||
totalFailed: records.reduce((sum, r) => sum + r.failed, 0),
|
||||
todayAdded: todayRecords.reduce((sum, r) => sum + r.success, 0),
|
||||
weekAdded: weekRecords.reduce((sum, r) => sum + r.success, 0),
|
||||
}
|
||||
}, [records])
|
||||
|
||||
return (
|
||||
<RecordsContext.Provider
|
||||
value={{
|
||||
records,
|
||||
addRecord,
|
||||
deleteRecord,
|
||||
clearRecords,
|
||||
getRecordsByDateRange,
|
||||
getStats,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecordsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useRecordsContext(): RecordsContextValue {
|
||||
const context = useContext(RecordsContext)
|
||||
if (!context) {
|
||||
throw new Error('useRecordsContext must be used within a RecordsProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
2
frontend/src/context/index.ts
Normal file
2
frontend/src/context/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ConfigProvider, useConfigContext } from './ConfigContext'
|
||||
export { RecordsProvider, useRecordsContext } from './RecordsContext'
|
||||
Reference in New Issue
Block a user