feat: 完善设置向导,添加平台选择和账号管理功能
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// 设置向导各步骤表单组件
|
// 设置向导各步骤表单组件
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
@@ -8,6 +8,13 @@ import { Switch } from '@/components/ui/switch'
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { X, ExternalLink, Eye, EyeOff } from 'lucide-react'
|
||||||
import type {
|
import type {
|
||||||
BotBasicConfig,
|
BotBasicConfig,
|
||||||
@@ -18,12 +25,141 @@ import type {
|
|||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
// ====== 步骤1:Bot基础配置 ======
|
// ====== 步骤1:Bot基础配置 ======
|
||||||
|
|
||||||
|
const KNOWN_PLATFORMS: Record<string, string> = {
|
||||||
|
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 {
|
interface BotBasicFormProps {
|
||||||
config: BotBasicConfig
|
config: BotBasicConfig
|
||||||
onChange: (config: BotBasicConfig) => void
|
onChange: (config: BotBasicConfig) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
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) => {
|
const handleAddAlias = (alias: string) => {
|
||||||
if (alias.trim() && !config.alias_names.includes(alias.trim())) {
|
if (alias.trim() && !config.alias_names.includes(alias.trim())) {
|
||||||
onChange({
|
onChange({
|
||||||
@@ -42,21 +178,67 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="platform">平台 *</Label>
|
||||||
|
<Select value={selectedPlatform} onValueChange={handlePlatformChange}>
|
||||||
|
<SelectTrigger id="platform">
|
||||||
|
<SelectValue placeholder="请选择平台" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PLATFORM_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
选择机器人运行的平台
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedPlatform === 'custom' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="custom_platform_name">平台名称 *</Label>
|
||||||
|
<Input
|
||||||
|
id="custom_platform_name"
|
||||||
|
placeholder="请输入平台名称,如 matrix"
|
||||||
|
value={customPlatformName}
|
||||||
|
onChange={(e) => handleCustomNameChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedPlatform === 'qq' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label htmlFor="qq_account">QQ账号 *</Label>
|
<Label htmlFor="qq_account">QQ账号 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="qq_account"
|
id="qq_account"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="请输入机器人的QQ账号"
|
placeholder="请输入机器人的QQ账号"
|
||||||
value={config.qq_account || ''}
|
value={primaryAccount}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleAccountChange(e.target.value)}
|
||||||
onChange({ ...config, qq_account: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
机器人登录使用的QQ账号
|
机器人登录使用的QQ账号
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedPlatform && selectedPlatform !== 'qq' && (selectedPlatform !== 'custom' || customPlatformName) && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="primary_account">账号ID *</Label>
|
||||||
|
<Input
|
||||||
|
id="primary_account"
|
||||||
|
placeholder="请输入机器人的账号ID"
|
||||||
|
value={primaryAccount}
|
||||||
|
onChange={(e) => handleAccountChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
机器人在该平台上的账号标识
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label htmlFor="nickname">昵称 *</Label>
|
<Label htmlFor="nickname">昵称 *</Label>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export async function loadBotBasicConfig(): Promise<BotBasicConfig> {
|
|||||||
const botConfig = (data.config.bot || {}) as Partial<BotBasicConfig>
|
const botConfig = (data.config.bot || {}) as Partial<BotBasicConfig>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
platform: botConfig.platform || (botConfig.qq_account ? 'qq' : ''),
|
||||||
qq_account: botConfig.qq_account || 0,
|
qq_account: botConfig.qq_account || 0,
|
||||||
|
platforms: botConfig.platforms || [],
|
||||||
nickname: botConfig.nickname || '',
|
nickname: botConfig.nickname || '',
|
||||||
alias_names: botConfig.alias_names || [],
|
alias_names: botConfig.alias_names || [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
Smile,
|
Smile,
|
||||||
Settings,
|
Settings,
|
||||||
Key,
|
Key,
|
||||||
|
Globe,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
@@ -26,6 +28,12 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { APP_NAME } from '@/lib/version'
|
import { APP_NAME } from '@/lib/version'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
@@ -36,14 +44,14 @@ import type {
|
|||||||
EmojiConfig,
|
EmojiConfig,
|
||||||
OtherBasicConfig,
|
OtherBasicConfig,
|
||||||
SiliconFlowConfig,
|
SiliconFlowConfig,
|
||||||
} from './setup/types'
|
} from './types'
|
||||||
import {
|
import {
|
||||||
BotBasicForm,
|
BotBasicForm,
|
||||||
PersonalityForm,
|
PersonalityForm,
|
||||||
EmojiForm,
|
EmojiForm,
|
||||||
OtherBasicForm,
|
OtherBasicForm,
|
||||||
SiliconFlowForm,
|
SiliconFlowForm,
|
||||||
} from './setup/StepForms'
|
} from './StepForms'
|
||||||
import {
|
import {
|
||||||
loadBotBasicConfig,
|
loadBotBasicConfig,
|
||||||
loadPersonalityConfig,
|
loadPersonalityConfig,
|
||||||
@@ -56,10 +64,18 @@ import {
|
|||||||
saveOtherBasicConfig,
|
saveOtherBasicConfig,
|
||||||
saveSiliconFlowConfig,
|
saveSiliconFlowConfig,
|
||||||
completeSetup,
|
completeSetup,
|
||||||
} from './setup/api'
|
} from './api'
|
||||||
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||||
import { RestartOverlay } from '@/components/restart-overlay'
|
import { RestartOverlay } from '@/components/restart-overlay'
|
||||||
|
|
||||||
|
const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const
|
||||||
|
const LANGUAGE_NAMES: Record<typeof LANGUAGE_CODES[number], string> = {
|
||||||
|
zh: '中文',
|
||||||
|
en: 'English',
|
||||||
|
ja: '日本語',
|
||||||
|
ko: '한국어',
|
||||||
|
}
|
||||||
|
|
||||||
// 主导出组件:包装 RestartProvider
|
// 主导出组件:包装 RestartProvider
|
||||||
export function SetupPage() {
|
export function SetupPage() {
|
||||||
return (
|
return (
|
||||||
@@ -74,6 +90,8 @@ function SetupPageContent() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const { triggerRestart } = useRestart()
|
const { triggerRestart } = useRestart()
|
||||||
|
const { i18n: i18nInstance } = useTranslation()
|
||||||
|
const currentLang = i18nInstance.language || 'zh'
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
const [isCompleting, setIsCompleting] = useState(false)
|
const [isCompleting, setIsCompleting] = useState(false)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
@@ -81,7 +99,9 @@ function SetupPageContent() {
|
|||||||
|
|
||||||
// 步骤1:Bot基础信息
|
// 步骤1:Bot基础信息
|
||||||
const [botBasic, setBotBasic] = useState<BotBasicConfig>({
|
const [botBasic, setBotBasic] = useState<BotBasicConfig>({
|
||||||
|
platform: '',
|
||||||
qq_account: 0,
|
qq_account: 0,
|
||||||
|
platforms: [],
|
||||||
nickname: '',
|
nickname: '',
|
||||||
alias_names: [],
|
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 () => {
|
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()
|
const saved = await saveCurrentStep()
|
||||||
if (!saved) return
|
if (!saved) return
|
||||||
@@ -319,6 +363,37 @@ function SetupPageContent() {
|
|||||||
{/* 重启遮罩层 */}
|
{/* 重启遮罩层 */}
|
||||||
<RestartOverlay />
|
<RestartOverlay />
|
||||||
|
|
||||||
|
{/* 语言切换 */}
|
||||||
|
<div className="absolute right-4 top-4 z-20">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="gap-2">
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline text-xs">
|
||||||
|
{LANGUAGE_NAMES[currentLang.split('-')[0] as typeof LANGUAGE_CODES[number]] ?? currentLang}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{LANGUAGE_CODES.map((code) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={code}
|
||||||
|
onClick={() => i18nInstance.changeLanguage(code)}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer',
|
||||||
|
currentLang.split('-')[0] === code && 'font-semibold text-primary'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{currentLang.split('-')[0] === code && (
|
||||||
|
<span className="mr-2">✓</span>
|
||||||
|
)}
|
||||||
|
{LANGUAGE_NAMES[code]}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="absolute left-1/4 top-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-primary/5 blur-3xl" />
|
<div className="absolute left-1/4 top-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-primary/5 blur-3xl" />
|
||||||
@@ -9,7 +9,9 @@ export interface SetupStep {
|
|||||||
|
|
||||||
// 步骤1:Bot基础信息
|
// 步骤1:Bot基础信息
|
||||||
export interface BotBasicConfig {
|
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
|
nickname: string
|
||||||
alias_names: string[]
|
alias_names: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user