feat: Implement batch team owner processing with file upload, configuration, and real-time status monitoring.

This commit is contained in:
2026-01-30 14:30:42 +08:00
parent 402daf79ad
commit d7f4724473
6 changed files with 289 additions and 93 deletions

View File

@@ -0,0 +1,72 @@
import { useId } from 'react'
interface SwitchProps {
checked: boolean
onChange: (checked: boolean) => void
disabled?: boolean
label?: string
description?: string
className?: string
}
export default function Switch({
checked,
onChange,
disabled = false,
label,
description,
className = '',
}: SwitchProps) {
const id = useId()
return (
<div className={`flex items-center justify-between ${className}`}>
{(label || description) && (
<div className="flex-1 mr-3">
{label && (
<label
htmlFor={id}
className="text-sm font-medium text-slate-700 dark:text-slate-300 cursor-pointer"
>
{label}
</label>
)}
{description && (
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
{description}
</p>
)}
</div>
)}
<button
id={id}
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={`
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full
border-2 border-transparent transition-colors duration-200 ease-in-out
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
${checked
? 'bg-blue-500'
: 'bg-slate-200 dark:bg-slate-700'
}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
<span
aria-hidden="true"
className={`
pointer-events-none inline-block h-5 w-5 transform rounded-full
bg-white shadow-lg ring-0 transition duration-200 ease-in-out
${checked ? 'translate-x-5' : 'translate-x-0'}
`}
/>
</button>
</div>
)
}
export type { SwitchProps }

View File

@@ -31,3 +31,6 @@ export { ToastProvider, useToast } from './Toast'
export { Tabs } from './Tabs'
export type { TabItem } from './Tabs'
export { default as Switch } from './Switch'
export type { SwitchProps } from './Switch'

View File

@@ -14,7 +14,7 @@ import {
Clock,
Save,
} from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common'
import type { DashboardStats } from '../types'
interface PoolStatus {
@@ -148,11 +148,19 @@ export default function Monitor() {
polling_interval: pollingInterval,
}),
})
if (!res.ok) {
console.error('保存设置失败:', res.status)
const data = await res.json()
if (!res.ok || data.code !== 0) {
console.error('保存设置失败:', data.message || res.status)
alert('保存设置失败: ' + (data.message || '未知错误'))
setLoading(false)
return
}
console.log('保存设置成功:', data)
} catch (e) {
console.error('保存设置失败:', e)
alert('保存设置失败: ' + (e instanceof Error ? e.message : '网络错误'))
setLoading(false)
return
}
// 更新本地状态
setPoolStatus(prev => prev ? {
@@ -438,96 +446,98 @@ export default function Monitor() {
</Card>
</div>
{/* 配置面板 */}
{/* 配置面板 - 使用 flex 布局让两卡片等高,底部按钮对齐 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 目标设置 */}
<Card className="glass-card">
<Card className="glass-card flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-5 w-5 text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input
label="目标账号数"
type="number"
min={1}
max={1000}
value={targetInput}
onChange={(e) => setTargetInput(Number(e.target.value))}
hint="期望保持的活跃账号数量"
/>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="autoAdd"
checked={autoAdd}
onChange={(e) => setAutoAdd(e.target.checked)}
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
<CardContent className="flex flex-col flex-1">
<div className="space-y-4 flex-1">
<Input
label="目标账号数"
type="number"
min={1}
max={1000}
value={targetInput}
onChange={(e) => setTargetInput(Number(e.target.value))}
hint="期望保持的活跃账号数量"
/>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch
checked={autoAdd}
onChange={setAutoAdd}
label="启用自动补号"
description="开启后,当号池不足时自动补充账号"
/>
</div>
<Input
label="最小间隔 (秒)"
type="number"
min={60}
max={3600}
value={minInterval}
onChange={(e) => setMinInterval(Number(e.target.value))}
hint="两次自动补号的最小间隔"
disabled={!autoAdd}
/>
<label htmlFor="autoAdd" className="text-sm text-slate-700 dark:text-slate-300">
</label>
</div>
<Input
label="最小间隔 (秒)"
type="number"
min={60}
max={3600}
value={minInterval}
onChange={(e) => setMinInterval(Number(e.target.value))}
hint="两次自动补号的最小间隔"
disabled={!autoAdd}
/>
<Button onClick={handleSetTarget} loading={loading} className="w-full">
</Button>
<div className="mt-4">
<Button onClick={handleSetTarget} loading={loading} className="w-full">
</Button>
</div>
</CardContent>
</Card>
{/* 轮询控制 */}
<Card className="glass-card">
<Card className="glass-card flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="w-full">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
()
</label>
<input
type="number"
min={10}
max={600}
value={pollingInterval}
onChange={(e) => setPollingInterval(Number(e.target.value) || 60)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2"
/>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
(10-600)
</p>
</div>
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100"></p>
<p className="text-sm text-slate-500">
{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}
</p>
<CardContent className="flex flex-col flex-1">
<div className="space-y-4 flex-1">
<div className="w-full">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
()
</label>
<input
type="number"
min={10}
max={600}
value={pollingInterval}
onChange={(e) => setPollingInterval(Number(e.target.value) || 60)}
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
bg-white dark:bg-slate-800
text-slate-900 dark:text-slate-100
border-slate-300 dark:border-slate-600
focus:border-blue-500 focus:ring-blue-500
focus:outline-none focus:ring-2"
/>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
(10-600)
</p>
</div>
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100"></p>
<p className="text-sm text-slate-500">
{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}
</p>
</div>
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
</div>
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
</div>
</div>
<div className="flex gap-3">
<div className="flex gap-3 mt-4">
<Button
onClick={handleSavePollingSettings}
loading={loading}

View File

@@ -16,7 +16,7 @@ import {
import { FileDropzone } from '../components/upload'
import LogStream from '../components/upload/LogStream'
import OwnerList from '../components/upload/OwnerList'
import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input } from '../components/common'
import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input, Switch } from '../components/common'
import { useConfig } from '../hooks/useConfig'
interface OwnerStats {
@@ -71,6 +71,7 @@ export default function Upload() {
const [concurrentTeams, setConcurrentTeams] = useState(2)
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
const [proxy, setProxy] = useState('')
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
@@ -175,6 +176,7 @@ export default function Upload() {
browser_type: browserType,
headless: true, // 始终使用无头模式
proxy,
include_owner: includeOwner, // 母号也入库
}),
})
@@ -190,7 +192,7 @@ export default function Upload() {
alert('启动失败')
}
setLoading(false)
}, [stats, membersPerTeam, concurrentTeams, browserType, proxy, fetchStatus])
}, [stats, membersPerTeam, concurrentTeams, browserType, proxy, includeOwner, fetchStatus])
// 停止处理
const handleStop = useCallback(async () => {
@@ -427,6 +429,16 @@ export default function Upload() {
disabled={isRunning}
/>
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
<Switch
checked={includeOwner}
onChange={setIncludeOwner}
disabled={isRunning}
label="母号也入库"
description="开启后母号Owner账号也会被注册到 S2A"
/>
</div>
<div className="flex gap-2 pt-2">
{isRunning ? (
<Button