diff --git a/dashboard/src/routes/setup/StepForms.tsx b/dashboard/src/routes/setup/StepForms.tsx index 2643a75b..9c935bf7 100644 --- a/dashboard/src/routes/setup/StepForms.tsx +++ b/dashboard/src/routes/setup/StepForms.tsx @@ -1,6 +1,6 @@ // 设置向导各步骤表单组件 -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' @@ -8,6 +8,13 @@ import { Switch } from '@/components/ui/switch' import { Separator } from '@/components/ui/separator' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { X, ExternalLink, Eye, EyeOff } from 'lucide-react' import type { BotBasicConfig, @@ -18,12 +25,141 @@ import type { } from './types' // ====== 步骤1:Bot基础配置 ====== + +const KNOWN_PLATFORMS: Record = { + qq: 'qq', + telegram: 'telegram', + tg: 'telegram', + discord: 'discord', + kook: 'kook', +} + +const PLATFORM_OPTIONS = [ + { value: 'qq', label: 'QQ' }, + { value: 'telegram', label: 'Telegram' }, + { value: 'discord', label: 'Discord' }, + { value: 'kook', label: 'Kook' }, + { value: 'custom', label: '其他平台' }, +] + +function normalizePlatform(raw: string): string { + const key = raw.trim().toLowerCase() + return KNOWN_PLATFORMS[key] || key +} + +function deriveSelectedPlatform(config: BotBasicConfig): { selected: string; customName: string } { + const platform = config.platform + // Legacy: no platform set but has QQ account + if (!platform && config.qq_account > 0) { + return { selected: 'qq', customName: '' } + } + if (!platform) { + return { selected: '', customName: '' } + } + const known = PLATFORM_OPTIONS.find((opt) => opt.value === platform && opt.value !== 'custom') + if (known) { + return { selected: platform, customName: '' } + } + return { selected: 'custom', customName: platform } +} + +function upsertPlatformAccount(platforms: string[], platformName: string, accountId: string): string[] { + const normalized = normalizePlatform(platformName) + const filtered = platforms.filter((p) => { + const prefix = p.split(':')[0] + return normalizePlatform(prefix) !== normalized + }) + if (accountId.trim()) { + filtered.push(`${normalized}:${accountId.trim()}`) + } + return filtered +} + +function getPrimaryAccount(platforms: string[], platformName: string): string { + const normalized = normalizePlatform(platformName) + const entry = platforms.find((p) => { + const prefix = p.split(':')[0] + return normalizePlatform(prefix) === normalized + }) + return entry ? entry.split(':').slice(1).join(':') : '' +} + interface BotBasicFormProps { config: BotBasicConfig onChange: (config: BotBasicConfig) => void } export function BotBasicForm({ config, onChange }: BotBasicFormProps) { + const derived = deriveSelectedPlatform(config) + const [selectedPlatform, setSelectedPlatform] = useState(derived.selected) + const [customPlatformName, setCustomPlatformName] = useState(derived.customName) + const [primaryAccount, setPrimaryAccount] = useState(() => { + if (derived.selected === 'qq') { + return config.qq_account > 0 ? String(config.qq_account) : '' + } + if (config.platform) { + return getPrimaryAccount(config.platforms, config.platform) + } + return '' + }) + + // Re-derive when config loads from API (e.g. after initial fetch) + useEffect(() => { + const d = deriveSelectedPlatform(config) + setSelectedPlatform(d.selected) + setCustomPlatformName(d.customName) + if (d.selected === 'qq') { + setPrimaryAccount(config.qq_account > 0 ? String(config.qq_account) : '') + } else if (config.platform) { + setPrimaryAccount(getPrimaryAccount(config.platforms, config.platform)) + } + }, [config.platform, config.qq_account, config.platforms]) + + const handlePlatformChange = (value: string) => { + setSelectedPlatform(value) + const realPlatform = value === 'custom' ? customPlatformName : value + setPrimaryAccount('') + onChange({ + ...config, + platform: normalizePlatform(realPlatform), + qq_account: value === 'qq' ? config.qq_account : config.qq_account, // preserve + }) + } + + const handleCustomNameChange = (name: string) => { + setCustomPlatformName(name) + const normalized = normalizePlatform(name) + // Move account to new platform name if we had one + const newPlatforms = primaryAccount + ? upsertPlatformAccount(config.platforms, normalized, primaryAccount) + : config.platforms + onChange({ + ...config, + platform: normalized, + platforms: newPlatforms, + }) + } + + const handleAccountChange = (accountId: string) => { + setPrimaryAccount(accountId) + const realPlatform = selectedPlatform === 'custom' ? customPlatformName : selectedPlatform + const normalized = normalizePlatform(realPlatform) + + if (normalized === 'qq') { + onChange({ + ...config, + qq_account: Number(accountId) || 0, + platform: 'qq', + }) + } else { + onChange({ + ...config, + platform: normalized, + platforms: upsertPlatformAccount(config.platforms, normalized, accountId), + }) + } + } + const handleAddAlias = (alias: string) => { if (alias.trim() && !config.alias_names.includes(alias.trim())) { onChange({ @@ -43,21 +179,67 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) { return (
- - - onChange({ ...config, qq_account: Number(e.target.value) }) - } - /> + +

- 机器人登录使用的QQ账号 + 选择机器人运行的平台

+ {selectedPlatform === 'custom' && ( +
+ + handleCustomNameChange(e.target.value)} + /> +
+ )} + + {selectedPlatform === 'qq' && ( +
+ + handleAccountChange(e.target.value)} + /> +

+ 机器人登录使用的QQ账号 +

+
+ )} + + {selectedPlatform && selectedPlatform !== 'qq' && (selectedPlatform !== 'custom' || customPlatformName) && ( +
+ + handleAccountChange(e.target.value)} + /> +

+ 机器人在该平台上的账号标识 +

+
+ )} +
{ const botConfig = (data.config.bot || {}) as Partial return { + platform: botConfig.platform || (botConfig.qq_account ? 'qq' : ''), qq_account: botConfig.qq_account || 0, + platforms: botConfig.platforms || [], nickname: botConfig.nickname || '', alias_names: botConfig.alias_names || [], } diff --git a/dashboard/src/routes/setup.tsx b/dashboard/src/routes/setup/index.tsx similarity index 87% rename from dashboard/src/routes/setup.tsx rename to dashboard/src/routes/setup/index.tsx index c7a8f31f..ef2b0eb6 100644 --- a/dashboard/src/routes/setup.tsx +++ b/dashboard/src/routes/setup/index.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { Sparkles, ArrowRight, @@ -10,6 +11,7 @@ import { Smile, Settings, Key, + Globe, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' @@ -26,6 +28,12 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' import { APP_NAME } from '@/lib/version' import { useToast } from '@/hooks/use-toast' @@ -36,14 +44,14 @@ import type { EmojiConfig, OtherBasicConfig, SiliconFlowConfig, -} from './setup/types' +} from './types' import { BotBasicForm, PersonalityForm, EmojiForm, OtherBasicForm, SiliconFlowForm, -} from './setup/StepForms' +} from './StepForms' import { loadBotBasicConfig, loadPersonalityConfig, @@ -56,10 +64,18 @@ import { saveOtherBasicConfig, saveSiliconFlowConfig, completeSetup, -} from './setup/api' +} from './api' import { RestartProvider, useRestart } from '@/lib/restart-context' import { RestartOverlay } from '@/components/restart-overlay' +const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const +const LANGUAGE_NAMES: Record = { + zh: '中文', + en: 'English', + ja: '日本語', + ko: '한국어', +} + // 主导出组件:包装 RestartProvider export function SetupPage() { return ( @@ -74,6 +90,8 @@ function SetupPageContent() { const navigate = useNavigate() const { toast } = useToast() const { triggerRestart } = useRestart() + const { i18n: i18nInstance } = useTranslation() + const currentLang = i18nInstance.language || 'zh' const [currentStep, setCurrentStep] = useState(0) const [isCompleting, setIsCompleting] = useState(false) const [isSaving, setIsSaving] = useState(false) @@ -81,7 +99,9 @@ function SetupPageContent() { // 步骤1:Bot基础信息 const [botBasic, setBotBasic] = useState({ + platform: '', qq_account: 0, + platforms: [], nickname: '', alias_names: [], }) @@ -232,7 +252,31 @@ function SetupPageContent() { } } + // Step 1 验证 + function validateBotBasic(config: BotBasicConfig): string | null { + if (!config.platform) return '请选择平台' + if (!config.nickname.trim()) return '请输入昵称' + if (config.platform === 'qq') { + if (!config.qq_account || config.qq_account <= 0) return '请输入QQ账号' + } else { + const hasAccount = config.platforms.some( + (p) => p.startsWith(config.platform + ':') && p.split(':')[1]?.trim() + ) + if (!hasAccount) return '请输入账号ID' + } + return null + } + const handleNext = async () => { + // Step 1 验证 + if (currentStep === 0) { + const error = validateBotBasic(botBasic) + if (error) { + toast({ title: '验证失败', description: error, variant: 'destructive' }) + return + } + } + // 保存当前步骤 const saved = await saveCurrentStep() if (!saved) return @@ -319,6 +363,37 @@ function SetupPageContent() { {/* 重启遮罩层 */} + {/* 语言切换 */} +
+ + + + + + {LANGUAGE_CODES.map((code) => ( + i18nInstance.changeLanguage(code)} + className={cn( + 'cursor-pointer', + currentLang.split('-')[0] === code && 'font-semibold text-primary' + )} + > + {currentLang.split('-')[0] === code && ( + + )} + {LANGUAGE_NAMES[code]} + + ))} + + +
+ {/* 背景装饰 */}
diff --git a/dashboard/src/routes/setup/types.ts b/dashboard/src/routes/setup/types.ts index 1bc81fb1..35a295eb 100644 --- a/dashboard/src/routes/setup/types.ts +++ b/dashboard/src/routes/setup/types.ts @@ -9,7 +9,9 @@ export interface SetupStep { // 步骤1:Bot基础信息 export interface BotBasicConfig { - qq_account: number + platform: string // Primary platform name (normalized, lowercase) + qq_account: number // QQ account (preserved always for webui compat) + platforms: string[] // Other platform accounts "platform:account" nickname: string alias_names: string[] }