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:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user