feat: Introduce a new design system with dark mode and animations, add a reusable Tabs component, implement an Upload page, and create a backend API for team processes.

This commit is contained in:
2026-02-07 00:10:29 +08:00
parent 070ba6a31e
commit 2fc0dcaba4
4 changed files with 90 additions and 67 deletions

View File

@@ -564,6 +564,16 @@ func processSingleTeam(idx int, req TeamProcessRequest) (result TeamProcessResul
if accountStatus.Status == "error" { if accountStatus.Status == "error" {
logger.Warning(fmt.Sprintf("%s 账户状态检查失败: %s继续尝试", logPrefix, accountStatus.Error), owner.Email, "team") logger.Warning(fmt.Sprintf("%s 账户状态检查失败: %s继续尝试", logPrefix, accountStatus.Error), owner.Email, "team")
} else if accountStatus.PlanType == "free" {
logger.Warning(fmt.Sprintf("%s 母号 plan 为 free非 Team 账户,移除", logPrefix), owner.Email, "team")
if database.Instance != nil {
database.Instance.MarkOwnerAsInvalid(owner.Email)
database.Instance.DeleteTeamOwnerByEmail(owner.Email)
logger.Info(fmt.Sprintf("%s free 母号已删除: %s", logPrefix, owner.Email), owner.Email, "team")
}
result.Errors = append(result.Errors, "账户 plan 为 free非 Team 账户")
result.DurationMs = time.Since(startTime).Milliseconds()
return result
} else { } else {
logger.Info(fmt.Sprintf("%s 账户状态正常: %s", logPrefix, accountStatus.PlanType), owner.Email, "team") logger.Info(fmt.Sprintf("%s 账户状态正常: %s", logPrefix, accountStatus.PlanType), owner.Email, "team")
} }

View File

@@ -16,21 +16,22 @@ interface TabsProps {
export function Tabs({ tabs, activeTab, onChange, className = '' }: TabsProps) { export function Tabs({ tabs, activeTab, onChange, className = '' }: TabsProps) {
return ( return (
<div className={`flex gap-2 border-b border-slate-200 dark:border-slate-800 ${className}`}> <div className={`overflow-x-auto scrollbar-hide -mx-1 px-1 ${className}`}>
<div className="flex gap-1 sm:gap-2 border-b border-slate-200 dark:border-slate-800 min-w-max">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => onChange(tab.id)} onClick={() => onChange(tab.id)}
className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-all duration-200 ${activeTab === tab.id className={`relative flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2.5 sm:py-3 text-xs sm:text-sm font-medium whitespace-nowrap transition-all duration-200 ${activeTab === tab.id
? 'text-blue-600 dark:text-blue-400' ? 'text-blue-600 dark:text-blue-400'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200' : 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
}`} }`}
> >
{tab.icon && <tab.icon className="h-4 w-4" />} {tab.icon && <tab.icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 shrink-0" />}
{tab.label} {tab.label}
{tab.count !== undefined && ( {tab.count !== undefined && (
<span <span
className={`px-2 py-0.5 text-xs rounded-full ${activeTab === tab.id className={`px-1.5 sm:px-2 py-0.5 text-xs rounded-full ${activeTab === tab.id
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
: 'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400' : 'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400'
}`} }`}
@@ -44,5 +45,6 @@ export function Tabs({ tabs, activeTab, onChange, className = '' }: TabsProps) {
</button> </button>
))} ))}
</div> </div>
</div>
) )
} }

View File

@@ -366,6 +366,16 @@ body {
} }
} }
/* Hide scrollbar utility */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Toast notifications */ /* Toast notifications */
.toast { .toast {
position: fixed; position: fixed;

View File

@@ -267,16 +267,16 @@ export default function Upload() {
<div className="h-[calc(100vh-6rem)] flex flex-col gap-6"> <div className="h-[calc(100vh-6rem)] flex flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between shrink-0"> <div className="flex items-center justify-between shrink-0">
<div> <div className="min-w-0">
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2"> <h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<UploadIcon className="h-7 w-7 text-blue-500" /> <UploadIcon className="h-6 w-6 sm:h-7 sm:w-7 text-blue-500 shrink-0" />
</h1> </h1>
<p className="text-sm text-slate-500 dark:text-slate-400"> <p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400 truncate">
Team Owner JSON S2A Team Owner JSON S2A
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 shrink-0">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -288,7 +288,7 @@ export default function Upload() {
}} }}
icon={<RefreshCw className={`h-4 w-4 ${refreshing || polling ? 'animate-spin' : ''}`} />} icon={<RefreshCw className={`h-4 w-4 ${refreshing || polling ? 'animate-spin' : ''}`} />}
> >
<span className="hidden sm:inline"></span>
</Button> </Button>
</div> </div>
</div> </div>
@@ -313,33 +313,33 @@ export default function Upload() {
)} )}
{/* Status Overview - Compact */} {/* Status Overview - Compact */}
<div className="shrink-0 grid grid-cols-2 md:grid-cols-4 gap-3"> <div className="shrink-0 grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-3">
<div className={`p-3 rounded-lg border ${isRunning ? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' : 'bg-slate-50 dark:bg-slate-800/50 border-slate-200 dark:border-slate-700'}`}> <div className={`p-2.5 sm:p-3 rounded-lg border ${isRunning ? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' : 'bg-slate-50 dark:bg-slate-800/50 border-slate-200 dark:border-slate-700'}`}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isRunning ? ( {isRunning ? (
<Loader2 className="h-5 w-5 text-green-500 animate-spin" /> <Loader2 className="h-4 w-4 sm:h-5 sm:w-5 text-green-500 animate-spin shrink-0" />
) : ( ) : (
<div className="h-5 w-5 rounded-full bg-slate-300 dark:bg-slate-600" /> <div className="h-4 w-4 sm:h-5 sm:w-5 rounded-full bg-slate-300 dark:bg-slate-600 shrink-0" />
)} )}
<div> <div className="min-w-0">
<div className="text-xs text-slate-500"></div> <div className="text-xs text-slate-500"></div>
<div className={`font-bold ${isRunning ? 'text-green-600' : 'text-slate-600 dark:text-slate-300'}`}> <div className={`font-bold text-sm sm:text-base ${isRunning ? 'text-green-600' : 'text-slate-600 dark:text-slate-300'}`}>
{isRunning ? '运行中' : '空闲'} {isRunning ? '运行中' : '空闲'}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50"> <div className="p-2.5 sm:p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50">
<div className="text-xs text-blue-600/70 dark:text-blue-400/70"></div> <div className="text-xs text-blue-600/70 dark:text-blue-400/70"></div>
<div className="font-bold text-blue-600 dark:text-blue-400">{stats?.valid || 0}</div> <div className="font-bold text-sm sm:text-base text-blue-600 dark:text-blue-400">{stats?.valid || 0}</div>
</div> </div>
<div className="p-3 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800/50"> <div className="p-2.5 sm:p-3 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800/50">
<div className="text-xs text-orange-600/70 dark:text-orange-400/70"></div> <div className="text-xs text-orange-600/70 dark:text-orange-400/70"></div>
<div className="font-bold text-orange-600 dark:text-orange-400">{totalRegistered}</div> <div className="font-bold text-sm sm:text-base text-orange-600 dark:text-orange-400">{totalRegistered}</div>
</div> </div>
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50"> <div className="p-2.5 sm:p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50">
<div className="text-xs text-green-600/70 dark:text-green-400/70"></div> <div className="text-xs text-green-600/70 dark:text-green-400/70"></div>
<div className="font-bold text-green-600 dark:text-green-400">{totalS2A}</div> <div className="font-bold text-sm sm:text-base text-green-600 dark:text-green-400">{totalS2A}</div>
</div> </div>
</div> </div>
@@ -415,10 +415,10 @@ export default function Upload() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 sm:space-y-4">
{/* 处理数量设置 */} {/* 处理数量设置 */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5 sm:mb-2">
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -430,16 +430,16 @@ export default function Upload() {
onChange={(e) => setProcessCount(Number(e.target.value) || 0)} onChange={(e) => setProcessCount(Number(e.target.value) || 0)}
disabled={isRunning} disabled={isRunning}
placeholder="输入数量" placeholder="输入数量"
className="flex-1" className="flex-1 min-w-0"
/> />
<Button <Button
variant={processCount === 0 ? 'primary' : 'outline'} variant={processCount === 0 ? 'primary' : 'outline'}
size="sm" size="sm"
onClick={() => setProcessCount(0)} onClick={() => setProcessCount(0)}
disabled={isRunning} disabled={isRunning}
className="whitespace-nowrap" className="whitespace-nowrap shrink-0 text-xs sm:text-sm"
> >
({stats?.valid || 0}) ({stats?.valid || 0})
</Button> </Button>
</div> </div>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 mt-1">
@@ -447,7 +447,7 @@ export default function Upload() {
</p> </p>
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3">
<Input <Input
label="每 Team 成员数" label="每 Team 成员数"
type="number" type="number"
@@ -476,40 +476,41 @@ export default function Upload() {
onChange={(e) => setConcurrentS2A(Math.min(4, Math.max(1, Number(e.target.value))))} onChange={(e) => setConcurrentS2A(Math.min(4, Math.max(1, Number(e.target.value))))}
disabled={isRunning} disabled={isRunning}
hint="1-4推荐2" hint="1-4推荐2"
className="col-span-2 sm:col-span-1"
/> />
</div> </div>
{/* 授权方式状态显示 */} {/* 授权方式状态显示 */}
<div className={`p-3 rounded-lg border ${authMethod === 'api' <div className={`p-2.5 sm:p-3 rounded-lg border ${authMethod === 'api'
? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800' ? 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800'
: 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800' : 'bg-gradient-to-r from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 border-emerald-200 dark:border-emerald-800'
}`}> }`}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{authMethod === 'api' ? ( {authMethod === 'api' ? (
<> <>
<div className="p-1.5 rounded bg-blue-500 text-white"> <div className="p-1 sm:p-1.5 rounded bg-blue-500 text-white shrink-0">
<Zap className="h-4 w-4" /> <Zap className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</div> </div>
<div> <div className="min-w-0">
<div className="font-medium text-blue-700 dark:text-blue-300">CodexAuth API </div> <div className="font-medium text-sm text-blue-700 dark:text-blue-300">CodexAuth API </div>
<div className="text-xs text-blue-600/70 dark:text-blue-400/70"> API </div> <div className="text-xs text-blue-600/70 dark:text-blue-400/70 truncate"> API </div>
</div> </div>
</> </>
) : ( ) : (
<> <>
<div className="p-1.5 rounded bg-emerald-500 text-white"> <div className="p-1 sm:p-1.5 rounded bg-emerald-500 text-white shrink-0">
<Monitor className="h-4 w-4" /> <Monitor className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</div> </div>
<div> <div className="min-w-0">
<div className="font-medium text-emerald-700 dark:text-emerald-300"> (chromedp)</div> <div className="font-medium text-sm text-emerald-700 dark:text-emerald-300"></div>
<div className="text-xs text-emerald-600/70 dark:text-emerald-400/70">使</div> <div className="text-xs text-emerald-600/70 dark:text-emerald-400/70 truncate">使 chromedp </div>
</div> </div>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"> <div className="p-2.5 sm:p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch <Switch
checked={useProxy} checked={useProxy}
onChange={setUseProxy} onChange={setUseProxy}
@@ -517,19 +518,19 @@ export default function Upload() {
label="使用代理池" label="使用代理池"
description={ description={
proxyPoolStats && proxyPoolStats.enabled > 0 proxyPoolStats && proxyPoolStats.enabled > 0
? `代理池: ${proxyPoolStats.enabled} 个可用 / ${proxyPoolStats.total} 个总计` ? `可用 ${proxyPoolStats.enabled} / 总计 ${proxyPoolStats.total}`
: '代理池为空,请先在"代理池配置"页面添加代理' : '代理池为空,请先添加代理'
} }
/> />
</div> </div>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700"> <div className="p-2.5 sm:p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch <Switch
checked={includeOwner} checked={includeOwner}
onChange={setIncludeOwner} onChange={setIncludeOwner}
disabled={isRunning} disabled={isRunning}
label="母号也入库" label="母号也入库"
description="开启后,母号Owner账号也会被注册到 S2A" description="母号Owner注册到 S2A"
/> />
</div> </div>