import type { ComponentType, ReactNode, KeyboardEvent } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Database, Globe, Image as ImageIcon, KeyRound, MessageSquare, Play, Square, X } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import { Button } from '@/components/ui/button'
import { useCrawlerStore } from '@/store/crawlerStore'
import { usePlatforms, useConfigOptions, useStartCrawler, useStopCrawler } from '@/hooks/useCrawler'
import { ParsedIdList } from './ParsedIdList'
type SectionProps = {
title: string
description: string
icon: ComponentType<{ className?: string }>
children: ReactNode
className?: string
}
function Section({ title, description, icon: Icon, children, className = '' }: SectionProps) {
return (
)
}
type FieldProps = {
label: string
hint?: string
children: ReactNode
}
function Field({ label, hint, children }: FieldProps) {
return (
{hint ? (
{hint}
) : null}
{children}
)
}
type KeywordInputProps = {
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
}
function KeywordInput({ value, onChange, placeholder, disabled }: KeywordInputProps) {
const [inputValue, setInputValue] = useState('')
// 将逗号分隔的字符串转换为数组
const keywords = value ? value.split(',').map((k) => k.trim()).filter(Boolean) : []
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
const trimmed = inputValue.trim()
if (trimmed && !keywords.includes(trimmed)) {
const newKeywords = [...keywords, trimmed]
onChange(newKeywords.join(','))
setInputValue('')
}
}
}
const removeKeyword = (keywordToRemove: string) => {
const newKeywords = keywords.filter((k) => k !== keywordToRemove)
onChange(newKeywords.join(','))
}
return (
setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className="h-9 text-xs"
/>
{keywords.length > 0 && (
{keywords.map((keyword) => (
{keyword}
{!disabled && (
)}
))}
)}
)
}
export function CrawlerConfigPanel() {
const { t } = useTranslation('config')
const config = useCrawlerStore((state) => state.config)
const updateConfig = useCrawlerStore((state) => state.updateConfig)
const status = useCrawlerStore((state) => state.status)
const { data: platforms } = usePlatforms()
const { data: options } = useConfigOptions()
const { mutate: startCrawler, isPending: isStarting } = useStartCrawler()
const { mutate: stopCrawler, isPending: isStopping } = useStopCrawler()
const isDisabled = status === 'running' || status === 'stopping'
const isRunning = status === 'running'
const isBusy = isStarting || isStopping || status === 'stopping'
const handleStart = () => {
startCrawler(config)
}
const handleStop = () => {
stopCrawler()
}
return (
{/* Row 1: Three Config Columns */}
{/* Column 1: Target & Mode Section */}
{/* Column 2: Authentication Section */}
{config.login_type === 'cookie' ? (
) : null}
{config.login_type === 'cookie' && (config.platform === 'xhs' || config.platform === 'dy') ? (
{t('warning.cookieSlider')}
) : null}
{/* Column 3: Output & Runtime Section */}
{
const isChecked = checked === true
updateConfig({
enable_comments: isChecked,
enable_sub_comments: isChecked ? config.enable_sub_comments : false,
})
}}
disabled={isDisabled}
/>
{t('field.commentExtraction')}
updateConfig({ enable_sub_comments: checked === true })}
disabled={isDisabled || !config.enable_comments}
/>
{t('field.subComments')}
updateConfig({ enable_media: checked === true })}
disabled={isDisabled}
/>
{t('field.downloadMedia')}
{t('field.downloadMediaHint')}
updateConfig({ headless: checked === true })}
disabled={isDisabled}
/>
{t('field.headlessMode')}
{t('field.headlessModeHint')}
{/* Row 2: Start/Stop Button - Full Width */}
{isRunning ? (
) : (
)}
)
}