feat: Introduce core application structure, configuration, monitoring, and team management features.

This commit is contained in:
2026-02-03 06:45:54 +08:00
parent 637753ddaa
commit b20399a00a
18 changed files with 961 additions and 631 deletions

View File

@@ -211,13 +211,13 @@ export default function LiveLogViewer({
>
<span className="text-slate-500 flex-shrink-0 font-medium">{log.timestamp}</span>
<span className={`flex-shrink-0 uppercase font-bold w-20 text-center rounded-[4px] px-1 ${levelColors[log.level] || 'text-slate-400'}`}>
{log.level}
{log.level === 'success' ? '✓' : log.level}
</span>
<span className="text-slate-400 flex-shrink-0 opacity-80">[{log.module}]</span>
{log.email && (
<span className="text-purple-400 flex-shrink-0 truncate max-w-[180px] font-medium">{log.email}</span>
)}
<span className="text-slate-200 flex-1 break-all">{log.message}</span>
<span className={`flex-1 break-all ${log.level === 'success' ? 'text-green-400 font-bold' : 'text-slate-200'}`}>{log.message}</span>
</div>
))
)}

View File

@@ -1,5 +1,5 @@
import { type SelectHTMLAttributes, forwardRef } from 'react'
import { ChevronDown } from 'lucide-react'
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
import { ChevronDown, Check } from 'lucide-react'
export interface SelectOption {
value: string | number
@@ -7,62 +7,125 @@ export interface SelectOption {
disabled?: boolean
}
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
export interface SelectProps {
label?: string
error?: string
hint?: string
options: SelectOption[]
placeholder?: string
value?: string | number
onChange?: (e: { target: { value: string | number } }) => void
className?: string
disabled?: boolean
id?: string
}
const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ className = '', label, error, hint, options, placeholder, id, ...props }, ref) => {
const Select = forwardRef<any, SelectProps>(
({ className = '', label, error, hint, options, placeholder, value, onChange, disabled, id }, ref) => {
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const selectId = id || label?.toLowerCase().replace(/\s+/g, '-')
// Expose some functionality if needed
useImperativeHandle(ref, () => ({
focus: () => containerRef.current?.focus(),
}))
// Handle click outside to close
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedOption = options.find((opt) => opt.value === value)
const handleSelect = (option: SelectOption) => {
if (option.disabled || disabled) return
if (onChange) {
onChange({ target: { value: option.value } })
}
setIsOpen(false)
}
return (
<div className="w-full">
<div className={`w-full ${className}`} ref={containerRef}>
{label && (
<label
htmlFor={selectId}
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5"
>
{label}
</label>
)}
<div className="relative">
<select
ref={ref}
<button
type="button"
id={selectId}
className={`w-full px-3 py-2 text-sm rounded-lg border transition-colors appearance-none
bg-white dark:bg-slate-800
disabled={disabled}
onClick={() => !disabled && setIsOpen(!isOpen)}
className={`
relative w-full flex items-center justify-between
px-3 py-2.5 text-sm rounded-xl border transition-all duration-200
bg-white dark:bg-slate-800/50
text-slate-900 dark:text-slate-100
${
error
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: 'border-slate-300 dark:border-slate-600 focus:border-blue-500 focus:ring-blue-500'
${isOpen
? 'border-blue-500 ring-2 ring-blue-500/20 shadow-sm'
: error
? 'border-red-500'
: 'border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600'
}
focus:outline-none focus:ring-2 focus:ring-offset-0
disabled:opacity-50 disabled:cursor-not-allowed
pr-10
${className}`}
{...props}
${disabled ? 'opacity-50 cursor-not-allowed bg-slate-50 dark:bg-slate-900/50' : 'cursor-pointer'}
focus:outline-none
`}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none" />
<span className={`block truncate ${!selectedOption ? 'text-slate-400' : ''}`}>
{selectedOption ? selectedOption.label : placeholder || '请选择...'}
</span>
<span className="flex items-center pointer-events-none transition-transform duration-200" style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0)' }}>
<ChevronDown className={`h-4 w-4 ${isOpen ? 'text-blue-500' : 'text-slate-400'}`} />
</span>
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-2 py-1 overflow-auto text-base bg-white dark:bg-slate-800 rounded-xl shadow-xl max-h-60 ring-1 ring-black/5 dark:ring-white/10 focus:outline-none sm:text-sm animate-in fade-in zoom-in-95 duration-100">
{options.length === 0 ? (
<div className="px-4 py-2 text-slate-500 dark:text-slate-400 italic">
</div>
) : (
options.map((option) => (
<div
key={option.value}
onClick={() => handleSelect(option)}
className={`
relative cursor-pointer select-none py-2.5 pl-10 pr-4 transition-colors
${option.value === value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'
}
${option.disabled ? 'opacity-50 cursor-not-allowed grayscale' : ''}
`}
>
<span className="block truncate">{option.label}</span>
{option.value === value && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600 dark:text-blue-400">
<Check className="h-4 w-4" />
</span>
)}
</div>
))
)}
</div>
)}
</div>
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
{error && <p className="mt-1.5 text-xs font-medium text-red-500 ml-1">{error}</p>}
{hint && !error && (
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{hint}</p>
<p className="mt-1.5 text-xs text-slate-500 dark:text-slate-400 ml-1">{hint}</p>
)}
</div>
)

View File

@@ -16,6 +16,7 @@ import {
Trash2,
UserPlus,
Globe,
BarChart3,
} from 'lucide-react'
interface SidebarProps {
@@ -36,6 +37,7 @@ const navItems: NavItem[] = [
{ to: '/records', icon: History, label: '加号记录' },
{ to: '/accounts', icon: Users, label: '号池账号' },
{ to: '/monitor', icon: Activity, label: '号池监控' },
{ to: '/s2a-stats', icon: BarChart3, label: 'S2A 统计' },
{ to: '/cleaner', icon: Trash2, label: '定期清理' },
{ to: '/team-reg', icon: UserPlus, label: 'Team 注册' },
{
@@ -46,7 +48,7 @@ const navItems: NavItem[] = [
{ to: '/config', icon: Cog, label: '配置概览' },
{ to: '/config/s2a', icon: Server, label: 'S2A 配置' },
{ to: '/config/email', icon: Mail, label: '邮箱配置' },
{ to: '/config/codex-proxy', icon: Globe, label: 'CodexAuth代理池' },
{ to: '/config/codex-proxy', icon: Globe, label: '代理配置' },
]
},
]
@@ -133,7 +135,7 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white/80 dark:bg-slate-900/90 backdrop-blur-xl border-r border-slate-200 dark:border-slate-800 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:z-auto ${isOpen ? 'translate-x-0' : '-translate-x-full'
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white/80 dark:bg-slate-900/90 backdrop-blur-xl border-r border-slate-200 dark:border-slate-800 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:sticky lg:top-0 lg:h-screen lg:z-auto ${isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
{/* Mobile close button */}