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:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

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

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

View File

@@ -0,0 +1,2 @@
export { ConfigProvider, useConfigContext } from './ConfigContext'
export { RecordsProvider, useRecordsContext } from './RecordsContext'