Files
MediaCrawler/webui/src/components/config/ParsedIdList.tsx
T
程序员阿江(Relakkes) 65ddaa8828 refactor: 将 WebUI 源码纳入仓库,移除打包产物
- 新增 webui/ 前端源码目录(React+Vite+TypeScript+Tailwind)
- 删除 api/webui/ 中旧的打包静态资源
- 更新 .gitignore 忽略 api/webui/ 构建输出和 webui/node_modules
- 更新 README 增加前端构建说明
2026-07-01 23:05:03 +08:00

97 lines
2.7 KiB
TypeScript

import { useMemo } from 'react'
import { Check, AlertTriangle, X } from 'lucide-react'
import { parseMultipleUrls, type ParsedId } from '@/lib/urlParser'
interface ParsedIdListProps {
value: string
platform: string
type: 'detail' | 'creator'
onRemove?: (index: number) => void
disabled?: boolean
}
export function ParsedIdList({ value, platform, type, onRemove, disabled }: ParsedIdListProps) {
const parsed = useMemo(() => {
return parseMultipleUrls(value, platform)
}, [value, platform])
if (parsed.length === 0) return null
const handleRemove = (index: number) => {
if (disabled || !onRemove) return
const items = value
.split(/[,\n]+/)
.map(s => s.trim())
.filter(Boolean)
items.splice(index, 1)
onRemove(index)
}
return (
<div className="space-y-1.5 mt-2">
<div className="text-[10px] text-cyber-text-muted font-mono">
已识别 {parsed.length} 个{type === 'detail' ? '帖子/视频' : '创作者'}:
</div>
<div className="flex flex-wrap gap-1.5">
{parsed.map((item, index) => (
<ParsedIdTag
key={`${item.id}-${index}`}
item={item}
expectedType={type}
onRemove={!disabled ? () => handleRemove(index) : undefined}
/>
))}
</div>
</div>
)
}
interface ParsedIdTagProps {
item: ParsedId
expectedType: 'detail' | 'creator'
onRemove?: () => void
}
function ParsedIdTag({ item, expectedType, onRemove }: ParsedIdTagProps) {
// 检查类型是否匹配
const typeMatch = item.type === 'unknown' ||
(expectedType === 'detail' && item.type === 'video') ||
(expectedType === 'creator' && item.type === 'creator')
// 警告状态:小红书需要xsec_token
const needsWarning = !item.isValid || !typeMatch
return (
<span
className={`
inline-flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-mono
${needsWarning
? 'bg-cyber-neon-orange/10 border border-cyber-neon-orange/30 text-cyber-neon-orange'
: 'bg-cyber-neon-cyan/10 border border-cyber-neon-cyan/30 text-cyber-neon-cyan'
}
`}
title={item.original}
>
{needsWarning ? (
<AlertTriangle className="w-3 h-3 flex-shrink-0" />
) : (
<Check className="w-3 h-3 flex-shrink-0" />
)}
<span className="max-w-[120px] truncate">
{item.id.length > 20 ? item.id.slice(0, 8) + '...' + item.id.slice(-8) : item.id}
</span>
{onRemove && (
<button
type="button"
onClick={onRemove}
className="hover:text-cyber-neon-pink transition-colors ml-0.5"
>
<X className="w-3 h-3" />
</button>
)}
</span>
)
}