feat: Introduce core application structure, configuration, monitoring, and team management features.
This commit is contained in:
@@ -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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user