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,122 @@
/**
* 格式化日期时间
*/
export function formatDateTime(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
/**
* 格式化日期
*/
export function formatDate(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}
/**
* 格式化时间
*/
export function formatTime(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
/**
* 格式化相对时间
*/
export function formatRelativeTime(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date
const now = new Date()
const diff = now.getTime() - d.getTime()
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (days > 0) return `${days}天前`
if (hours > 0) return `${hours}小时前`
if (minutes > 0) return `${minutes}分钟前`
return '刚刚'
}
/**
* 格式化数字(添加千分位)
*/
export function formatNumber(num: number | undefined | null): string {
if (num == null) return '0'
return num.toLocaleString('zh-CN')
}
/**
* 格式化金额
*/
export function formatCurrency(amount: number | undefined | null, currency: string = 'USD'): string {
if (amount == null) amount = 0
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount)
}
/**
* 格式化百分比
*/
export function formatPercent(value: number, decimals: number = 1): string {
return `${(value * 100).toFixed(decimals)}%`
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
/**
* 截断文本
*/
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return `${text.substring(0, maxLength)}...`
}
/**
* 隐藏邮箱中间部分
*/
export function maskEmail(email: string): string {
const [local, domain] = email.split('@')
if (!domain) return email
if (local.length <= 2) return `${local}***@${domain}`
return `${local.substring(0, 2)}***@${domain}`
}
/**
* 隐藏 Token
*/
export function maskToken(token: string, visibleChars: number = 8): string {
if (token.length <= visibleChars * 2) return '***'
return `${token.substring(0, visibleChars)}...${token.substring(token.length - visibleChars)}`
}

View File

@@ -0,0 +1,5 @@
// Utils barrel export
export * from './storage'
export * from './format'
export * from './json-parser'
export * from './status-check'

View File

@@ -0,0 +1,126 @@
import { describe, it, expect } from 'vitest'
import { parseAccountJson, isValidEmail, isValidToken } from './json-parser'
describe('parseAccountJson', () => {
it('should parse valid JSON array', () => {
const json = JSON.stringify([
{ account: 'test@example.com', password: 'pass123', token: 'token123456' },
])
const result = parseAccountJson(json)
expect(result.success).toBe(true)
expect(result.data).toHaveLength(1)
expect(result.data?.[0].account).toBe('test@example.com')
})
it('should parse multiple accounts', () => {
const json = JSON.stringify([
{ account: 'user1@example.com', password: 'pass1', token: 'token1234567890' },
{ account: 'user2@example.com', password: 'pass2', token: 'token0987654321' },
])
const result = parseAccountJson(json)
expect(result.success).toBe(true)
expect(result.data).toHaveLength(2)
})
it('should reject non-array JSON', () => {
const json = JSON.stringify({ account: 'test@example.com' })
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('数组格式')
})
it('should reject empty array', () => {
const json = JSON.stringify([])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('不能为空')
})
it('should reject invalid JSON', () => {
const result = parseAccountJson('not valid json')
expect(result.success).toBe(false)
expect(result.error).toContain('解析失败')
})
it('should reject missing account field', () => {
const json = JSON.stringify([{ password: 'pass', token: 'token123456' }])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('account')
})
it('should reject missing password field', () => {
const json = JSON.stringify([{ account: 'test@example.com', token: 'token123456' }])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('password')
})
it('should reject missing token field', () => {
const json = JSON.stringify([{ account: 'test@example.com', password: 'pass' }])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('token')
})
it('should reject non-string account', () => {
const json = JSON.stringify([{ account: 123, password: 'pass', token: 'token123456' }])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('account')
})
it('should reject null items in array', () => {
const json = JSON.stringify([null])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('有效的对象')
})
it('should indicate which record has error', () => {
const json = JSON.stringify([
{ account: 'valid@example.com', password: 'pass', token: 'token123456' },
{ account: 'invalid', password: 'pass' }, // missing token
])
const result = parseAccountJson(json)
expect(result.success).toBe(false)
expect(result.error).toContain('第 2 条')
})
})
describe('isValidEmail', () => {
it('should accept valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true)
expect(isValidEmail('user.name@domain.co.uk')).toBe(true)
})
it('should reject invalid email', () => {
expect(isValidEmail('invalid')).toBe(false)
expect(isValidEmail('invalid@')).toBe(false)
expect(isValidEmail('@domain.com')).toBe(false)
expect(isValidEmail('')).toBe(false)
})
})
describe('isValidToken', () => {
it('should accept valid token', () => {
expect(isValidToken('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9')).toBe(true)
expect(isValidToken('1234567890')).toBe(true)
})
it('should reject short token', () => {
expect(isValidToken('short')).toBe(false)
expect(isValidToken('')).toBe(false)
})
})

View File

@@ -0,0 +1,96 @@
import type { AccountInput } from '../types'
export interface ParseResult {
success: boolean
data?: AccountInput[]
error?: string
}
/**
* Parse and validate JSON account data
*/
export function parseAccountJson(jsonString: string): ParseResult {
try {
const data = JSON.parse(jsonString)
if (!Array.isArray(data)) {
return {
success: false,
error: 'JSON 文件必须是数组格式',
}
}
if (data.length === 0) {
return {
success: false,
error: 'JSON 数组不能为空',
}
}
// Validate each account
const accounts: AccountInput[] = []
for (let i = 0; i < data.length; i++) {
const item = data[i]
if (typeof item !== 'object' || item === null) {
return {
success: false,
error: `${i + 1} 条记录不是有效的对象`,
}
}
if (!item.account || typeof item.account !== 'string') {
return {
success: false,
error: `${i + 1} 条记录缺少有效的 account 字段`,
}
}
if (!item.password || typeof item.password !== 'string') {
return {
success: false,
error: `${i + 1} 条记录缺少有效的 password 字段`,
}
}
if (!item.token || typeof item.token !== 'string') {
return {
success: false,
error: `${i + 1} 条记录缺少有效的 token 字段`,
}
}
accounts.push({
account: item.account,
password: item.password,
token: item.token,
})
}
return {
success: true,
data: accounts,
}
} catch {
return {
success: false,
error: 'JSON 解析失败,请检查文件格式',
}
}
}
/**
* Validate email format
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
/**
* Validate token format (basic check)
*/
export function isValidToken(token: string): boolean {
// Token should be a non-empty string with reasonable length
return typeof token === 'string' && token.length >= 10
}

View File

@@ -0,0 +1,54 @@
import type { AccountStatus } from '../types'
/**
* Map HTTP status code to account status
*/
export function mapHttpStatusToAccountStatus(httpStatus: number): AccountStatus {
switch (httpStatus) {
case 200:
return 'active'
case 401:
return 'token_expired'
case 403:
return 'banned'
default:
return 'error'
}
}
/**
* Check if account status allows pooling
*/
export function canPoolAccount(status: AccountStatus): boolean {
return status === 'active'
}
/**
* Get status display text
*/
export function getStatusDisplayText(status: AccountStatus): string {
const statusMap: Record<AccountStatus, string> = {
pending: '待检查',
checking: '检查中',
active: '正常',
banned: '封禁',
token_expired: '过期',
error: '错误',
}
return statusMap[status] || '未知'
}
/**
* Get status color class
*/
export function getStatusColorClass(status: AccountStatus): string {
const colorMap: Record<AccountStatus, string> = {
pending: 'text-slate-500',
checking: 'text-blue-500',
active: 'text-green-500',
banned: 'text-red-500',
token_expired: 'text-orange-500',
error: 'text-yellow-500',
}
return colorMap[status] || 'text-slate-500'
}

View File

View File

@@ -0,0 +1,107 @@
import type { AppConfig, AddRecord } from '../types'
import { defaultConfig } from '../types'
const STORAGE_KEYS = {
CONFIG: 'codex-pool-config',
RECORDS: 'codex-pool-records',
} as const
/**
* 保存配置到 localStorage
*/
export function saveConfig(config: AppConfig): void {
try {
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(config))
} catch (error) {
console.error('Failed to save config:', error)
}
}
/**
* 从 localStorage 加载配置
*/
export function loadConfig(): AppConfig {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CONFIG)
if (stored) {
const parsed = JSON.parse(stored)
// Migration: handle old email config format -> new services array format
let emailConfig = { ...defaultConfig.email }
if (parsed.email) {
if (parsed.email.services && Array.isArray(parsed.email.services)) {
// New format - use directly
emailConfig.services = parsed.email.services
} else if (parsed.email.apiBase || parsed.email.domains) {
// Old format - migrate to new services array
const domains = parsed.email.domains || (parsed.email.domain ? [parsed.email.domain] : ['esyteam.edu.kg'])
emailConfig.services = [{
name: 'default',
apiBase: parsed.email.apiBase || 'https://mail.esyteam.edu.kg',
apiToken: parsed.email.apiToken || '',
domain: domains[0] || 'esyteam.edu.kg',
}]
}
}
// 合并默认配置,确保新增字段有默认值
return {
...defaultConfig,
...parsed,
s2a: { ...defaultConfig.s2a, ...parsed.s2a },
pooling: { ...defaultConfig.pooling, ...parsed.pooling },
check: { ...defaultConfig.check, ...parsed.check },
email: emailConfig,
}
}
} catch (error) {
console.error('Failed to load config:', error)
}
return defaultConfig
}
/**
* 保存记录到 localStorage
*/
export function saveRecords(records: AddRecord[]): void {
try {
localStorage.setItem(STORAGE_KEYS.RECORDS, JSON.stringify(records))
} catch (error) {
console.error('Failed to save records:', error)
}
}
/**
* 从 localStorage 加载记录
*/
export function loadRecords(): AddRecord[] {
try {
const stored = localStorage.getItem(STORAGE_KEYS.RECORDS)
if (stored) {
return JSON.parse(stored)
}
} catch (error) {
console.error('Failed to load records:', error)
}
return []
}
/**
* 生成唯一 ID
*/
export function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}
/**
* 清除所有存储数据
*/
export function clearStorage(): void {
try {
localStorage.removeItem(STORAGE_KEYS.CONFIG)
localStorage.removeItem(STORAGE_KEYS.RECORDS)
} catch (error) {
console.error('Failed to clear storage:', error)
}
}