'use client' import { useState, useEffect, useCallback } from 'react' import { Plus, Shield, Ban, Building2, Search, X, Upload, Trash2, FileText, Download, Eye, Loader2 } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { Modal } from '@/components/ui/Modal' import { useToast } from '@/components/ui/Toast' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { ForbiddenWordResponse, CompetitorResponse, WhitelistResponse, PlatformRuleResponse, } from '@/types/rules' // ===== 平台规则库 mock 数据 (USE_MOCK 模式) ===== interface PlatformRuleDisplay { id: string name: string icon: string color: string rules: { forbiddenWords: number; competitors: number; whitelist: number } version: string updatedAt: string } const platformRuleLibraries: PlatformRuleDisplay[] = [ { id: 'douyin', name: '抖音', icon: '🎵', color: 'bg-[#25F4EE]', rules: { forbiddenWords: 156, competitors: 0, whitelist: 12 }, version: 'v2024.02', updatedAt: '2024-02-01', }, { id: 'xiaohongshu', name: '小红书', icon: '📕', color: 'bg-[#fe2c55]', rules: { forbiddenWords: 142, competitors: 0, whitelist: 8 }, version: 'v2024.01', updatedAt: '2024-01-20', }, { id: 'bilibili', name: 'B站', icon: '📺', color: 'bg-[#00a1d6]', rules: { forbiddenWords: 98, competitors: 0, whitelist: 15 }, version: 'v2024.02', updatedAt: '2024-02-03', }, { id: 'kuaishou', name: '快手', icon: '⚡', color: 'bg-[#ff4906]', rules: { forbiddenWords: 134, competitors: 0, whitelist: 10 }, version: 'v2024.01', updatedAt: '2024-01-15', }, { id: 'weibo', name: '微博', icon: '🔴', color: 'bg-[#e6162d]', rules: { forbiddenWords: 89, competitors: 0, whitelist: 6 }, version: 'v2023.12', updatedAt: '2023-12-20', }, ] // ===== Mock 数据 (USE_MOCK 模式) ===== const mockForbiddenWords: ForbiddenWordResponse[] = [ { id: '1', word: '最好', category: '极限词', severity: 'high' }, { id: '2', word: '第一', category: '极限词', severity: 'high' }, { id: '3', word: '最佳', category: '极限词', severity: 'high' }, { id: '4', word: '100%有效', category: '虚假宣称', severity: 'critical' }, { id: '5', word: '立即见效', category: '虚假宣称', severity: 'critical' }, { id: '6', word: '永久', category: '极限词', severity: 'medium' }, { id: '7', word: '绝对', category: '极限词', severity: 'medium' }, { id: '8', word: '最低价', category: '价格欺诈', severity: 'high' }, ] const mockCompetitors: CompetitorResponse[] = [ { id: '1', name: '竞品A', brand_id: '', keywords: ['竞品A', '品牌A'] }, { id: '2', name: '竞品B', brand_id: '', keywords: ['竞品B', '品牌B'] }, { id: '3', name: '竞品C', brand_id: '', keywords: ['竞品C', '品牌C'] }, ] const mockWhitelist: WhitelistResponse[] = [ { id: '1', term: '品牌专属术语1', reason: '品牌授权使用', brand_id: '' }, { id: '2', term: '特定产品名', reason: '官方产品名称', brand_id: '' }, ] // ===== 平台图标映射 (用于 API 模式下的平台展示) ===== const platformDisplayMap: Record = { douyin: { icon: '🎵', color: 'bg-[#25F4EE]', name: '抖音' }, xiaohongshu: { icon: '📕', color: 'bg-[#fe2c55]', name: '小红书' }, bilibili: { icon: '📺', color: 'bg-[#00a1d6]', name: 'B站' }, kuaishou: { icon: '⚡', color: 'bg-[#ff4906]', name: '快手' }, weibo: { icon: '🔴', color: 'bg-[#e6162d]', name: '微博' }, wechat: { icon: '📱', color: 'bg-[#07c160]', name: '微信视频号' }, } const categoryOptions = [ { value: '极限词', label: '极限词' }, { value: '虚假宣称', label: '虚假宣称' }, { value: '价格欺诈', label: '价格欺诈' }, { value: '平台规则', label: '平台规则' }, { value: '自定义', label: '自定义' }, ] // ===== 将 PlatformRuleResponse 转换为 PlatformRuleDisplay ===== function toPlatformDisplay(rule: PlatformRuleResponse): PlatformRuleDisplay { const display = platformDisplayMap[rule.platform] || { icon: '📋', color: 'bg-gray-400', name: rule.platform, } return { id: rule.platform, name: display.name, icon: display.icon, color: display.color, rules: { forbiddenWords: Array.isArray(rule.rules) ? rule.rules.length : 0, competitors: 0, whitelist: 0, }, version: rule.version, updatedAt: rule.updated_at, } } // ===== Loading Skeleton 组件 ===== function CardSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
) } function WordsSkeleton() { return (
{[1, 2].map((group) => (
{[1, 2, 3, 4].map((i) => (
))}
))}
) } function ListSkeleton({ count = 3 }: { count?: number }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
) } // ===== 主组件 ===== export default function RulesPage() { const toast = useToast() // Tab 选择 const [activeTab, setActiveTab] = useState<'platforms' | 'forbidden' | 'competitors' | 'whitelist'>('platforms') const [searchQuery, setSearchQuery] = useState('') // 数据状态 const [forbiddenWords, setForbiddenWords] = useState([]) const [competitors, setCompetitors] = useState([]) const [whitelist, setWhitelist] = useState([]) const [platforms, setPlatforms] = useState([]) // 加载状态 const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) // 上传规则库 const [showUploadModal, setShowUploadModal] = useState(false) const [uploadPlatform, setUploadPlatform] = useState('') const [uploadFile, setUploadFile] = useState(null) // 重新上传规则库 const [showReuploadModal, setShowReuploadModal] = useState(false) const [reuploadPlatform, setReuploadPlatform] = useState(null) // 下载确认 const [showDownloadModal, setShowDownloadModal] = useState(false) const [downloadPlatform, setDownloadPlatform] = useState(null) // 查看规则库详情 const [showDetailModal, setShowDetailModal] = useState(false) const [selectedPlatform, setSelectedPlatform] = useState(null) // 添加违禁词 const [showAddWordModal, setShowAddWordModal] = useState(false) const [newWord, setNewWord] = useState('') const [newCategory, setNewCategory] = useState('极限词') const [batchWords, setBatchWords] = useState('') // 添加竞品 const [showAddCompetitorModal, setShowAddCompetitorModal] = useState(false) const [newCompetitor, setNewCompetitor] = useState('') // 添加白名单 const [showAddWhitelistModal, setShowAddWhitelistModal] = useState(false) const [newWhitelistTerm, setNewWhitelistTerm] = useState('') const [newWhitelistReason, setNewWhitelistReason] = useState('') // ===== 数据加载 ===== const loadForbiddenWords = useCallback(async () => { if (USE_MOCK) { setForbiddenWords(mockForbiddenWords) return } try { const res = await api.listForbiddenWords() setForbiddenWords(res.items) } catch (err) { toast.error('加载违禁词失败:' + (err instanceof Error ? err.message : '未知错误')) } }, [toast]) const loadCompetitors = useCallback(async () => { if (USE_MOCK) { setCompetitors(mockCompetitors) return } try { const res = await api.listCompetitors() setCompetitors(res.items) } catch (err) { toast.error('加载竞品列表失败:' + (err instanceof Error ? err.message : '未知错误')) } }, [toast]) const loadWhitelist = useCallback(async () => { if (USE_MOCK) { setWhitelist(mockWhitelist) return } try { const res = await api.listWhitelist() setWhitelist(res.items) } catch (err) { toast.error('加载白名单失败:' + (err instanceof Error ? err.message : '未知错误')) } }, [toast]) const loadPlatformRules = useCallback(async () => { if (USE_MOCK) { setPlatforms(platformRuleLibraries) return } try { const res = await api.listPlatformRules() setPlatforms(res.items.map(toPlatformDisplay)) } catch (err) { toast.error('加载平台规则失败:' + (err instanceof Error ? err.message : '未知错误')) } }, [toast]) const loadAllData = useCallback(async () => { setLoading(true) await Promise.all([ loadForbiddenWords(), loadCompetitors(), loadWhitelist(), loadPlatformRules(), ]) setLoading(false) }, [loadForbiddenWords, loadCompetitors, loadWhitelist, loadPlatformRules]) useEffect(() => { loadAllData() }, [loadAllData]) // ===== 过滤违禁词 ===== const filteredWords = forbiddenWords.filter(w => searchQuery === '' || w.word.toLowerCase().includes(searchQuery.toLowerCase()) || w.category.toLowerCase().includes(searchQuery.toLowerCase()) ) // ===== 平台操作 ===== const viewPlatformDetail = (platform: PlatformRuleDisplay) => { setSelectedPlatform(platform) setShowDetailModal(true) } const handleReupload = (platform: PlatformRuleDisplay) => { setReuploadPlatform(platform) setShowReuploadModal(true) } const handleDownload = (platform: PlatformRuleDisplay) => { setDownloadPlatform(platform) setShowDownloadModal(true) } // ===== 违禁词操作 ===== const handleAddWord = async () => { if (!newWord.trim()) return setSubmitting(true) try { if (USE_MOCK) { const newItem: ForbiddenWordResponse = { id: Date.now().toString(), word: newWord.trim(), category: newCategory, severity: 'medium', } setForbiddenWords(prev => [...prev, newItem]) } else { await api.addForbiddenWord({ word: newWord.trim(), category: newCategory, severity: 'medium' }) await loadForbiddenWords() } toast.success('违禁词添加成功') setNewWord('') setShowAddWordModal(false) } catch (err) { toast.error('添加违禁词失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const handleBatchAdd = async () => { const words = batchWords.split('\n').filter(w => w.trim()) if (words.length === 0) return setSubmitting(true) try { if (USE_MOCK) { const newWords: ForbiddenWordResponse[] = words.map((word, i) => ({ id: `${Date.now()}-${i}`, word: word.trim(), category: newCategory, severity: 'medium', })) setForbiddenWords(prev => [...prev, ...newWords]) } else { for (const word of words) { await api.addForbiddenWord({ word: word.trim(), category: newCategory, severity: 'medium' }) } await loadForbiddenWords() } toast.success(`成功添加 ${words.length} 个违禁词`) setBatchWords('') setShowAddWordModal(false) } catch (err) { toast.error('批量添加违禁词失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const handleDeleteWord = async (id: string) => { setSubmitting(true) try { if (USE_MOCK) { setForbiddenWords(prev => prev.filter(w => w.id !== id)) } else { await api.deleteForbiddenWord(id) await loadForbiddenWords() } toast.success('违禁词已删除') } catch (err) { toast.error('删除违禁词失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } // ===== 竞品操作 ===== const handleAddCompetitor = async () => { if (!newCompetitor.trim()) return setSubmitting(true) try { if (USE_MOCK) { const newItem: CompetitorResponse = { id: Date.now().toString(), name: newCompetitor.trim(), brand_id: '', keywords: [newCompetitor.trim()], } setCompetitors(prev => [...prev, newItem]) } else { await api.addCompetitor({ name: newCompetitor.trim(), brand_id: '', keywords: [newCompetitor.trim()] }) await loadCompetitors() } toast.success('竞品添加成功') setNewCompetitor('') setShowAddCompetitorModal(false) } catch (err) { toast.error('添加竞品失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const handleDeleteCompetitor = async (id: string) => { setSubmitting(true) try { if (USE_MOCK) { setCompetitors(prev => prev.filter(c => c.id !== id)) } else { await api.deleteCompetitor(id) await loadCompetitors() } toast.success('竞品已删除') } catch (err) { toast.error('删除竞品失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } // ===== 白名单操作 ===== const handleAddWhitelist = async () => { if (!newWhitelistTerm.trim()) return setSubmitting(true) try { if (USE_MOCK) { const newItem: WhitelistResponse = { id: Date.now().toString(), term: newWhitelistTerm.trim(), reason: newWhitelistReason.trim(), brand_id: '', } setWhitelist(prev => [...prev, newItem]) } else { await api.addToWhitelist({ term: newWhitelistTerm.trim(), reason: newWhitelistReason.trim(), brand_id: '' }) await loadWhitelist() } toast.success('白名单添加成功') setNewWhitelistTerm('') setNewWhitelistReason('') setShowAddWhitelistModal(false) } catch (err) { toast.error('添加白名单失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const handleDeleteWhitelist = async (id: string) => { setSubmitting(true) try { if (USE_MOCK) { setWhitelist(prev => prev.filter(w => w.id !== id)) } else { // 白名单目前没有 delete API,本地移除 setWhitelist(prev => prev.filter(w => w.id !== id)) } toast.success('白名单已删除') } catch (err) { toast.error('删除白名单失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } return (

规则配置

配置平台规则库和自定义审核规则,代理商可在此基础上调整风险等级

{/* 标签页 */}
{/* 平台规则库 */} {activeTab === 'platforms' && ( 平台规则库

管理各平台的审核规则库,启用后将应用于对应平台的视频审核

{loading ? ( ) : (
{platforms.map((platform) => (
{platform.icon}

{platform.name}

{platform.version}

{platform.rules.forbiddenWords} 违禁词 {platform.rules.whitelist} 白名单
更新于 {platform.updatedAt}
))} {/* 上传规则库 */}
)}
)} {/* 自定义违禁词列表 */} {activeTab === 'forbidden' && ( 自定义违禁词

在平台规则库基础上,添加品牌专属的违禁词规则

{loading ? ( ) : ( <> {/* 搜索框和添加按钮 */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
{/* 按分类分组显示 */} {(() => { const grouped = filteredWords.reduce((acc, word) => { if (!acc[word.category]) acc[word.category] = [] acc[word.category].push(word) return acc }, {} as Record) return Object.entries(grouped).map(([category, words]) => (
{category} ({words.length})
{words.map((word) => (
{word.word}
))}
)) })()} {filteredWords.length === 0 && (

暂无自定义违禁词

)} )}
)} {/* 竞品列表 */} {activeTab === 'competitors' && ( 竞品列表

系统将在视频中检测以下竞品的 Logo 或品牌名称

{loading ? ( ) : (
{competitors.map((competitor) => (
{competitor.name}
))} {/* 添加竞品按钮 */}
)}
)} {/* 白名单 */} {activeTab === 'whitelist' && ( 白名单

白名单中的词汇即使命中违禁词也不会触发告警

{loading ? ( ) : (
{whitelist.map((item) => (

{item.term}

{item.reason}

))} {/* 添加白名单按钮 */}
)}
)} {/* 上传规则库弹窗 */} { setShowUploadModal(false); setUploadPlatform(''); setUploadFile(null); }} title="上传平台规则库" >

点击或拖拽上传文件

支持 JSON / Excel / Word / PDF 格式

文件格式说明

  • JSON 格式:包含 forbiddenWords、whitelist 字段的对象
  • Excel 格式:第一列为词汇,第二列为分类(可选)
  • Word / PDF:AI 将自动识别并提取规则内容
{/* 查看规则库详情弹窗 */} { setShowDetailModal(false); setSelectedPlatform(null); }} title={selectedPlatform ? `${selectedPlatform.name}规则库详情` : '规则库详情'} size="lg" > {selectedPlatform && (
{selectedPlatform.icon}

{selectedPlatform.name}

版本:{selectedPlatform.version} 更新时间:{selectedPlatform.updatedAt}

{selectedPlatform.rules.forbiddenWords}

违禁词

{selectedPlatform.rules.whitelist}

白名单

12

规则分类

规则分类概览

{[ { name: '极限词', count: 45 }, { name: '虚假宣传', count: 38 }, { name: '敏感词汇', count: 32 }, { name: '价格欺诈', count: 21 }, { name: '其他', count: 20 }, ].map((cat) => (
{cat.name} {cat.count} 条
))}
)}
{/* 重新上传规则库弹窗 */} { setShowReuploadModal(false); setReuploadPlatform(null); }} title={reuploadPlatform ? `重新上传${reuploadPlatform.name}规则库` : '重新上传规则库'} > {reuploadPlatform && (
{reuploadPlatform.icon}

{reuploadPlatform.name}

当前版本:{reuploadPlatform.version}

注意

重新上传将覆盖当前规则库,此操作不可撤销

点击或拖拽上传文件

支持 JSON / Excel / Word / PDF 格式

Word 和 PDF 文件将由 AI 自动提取规则内容

)}
{/* 下载规则库弹窗 */} { setShowDownloadModal(false); setDownloadPlatform(null); }} title={downloadPlatform ? `下载${downloadPlatform.name}规则库` : '下载规则库'} > {downloadPlatform && (
{downloadPlatform.icon}

{downloadPlatform.name}

版本:{downloadPlatform.version} · 更新于 {downloadPlatform.updatedAt}

违禁词数量 {downloadPlatform.rules.forbiddenWords} 条
白名单数量 {downloadPlatform.rules.whitelist} 条
)}
{/* 添加违禁词弹窗 */} { setShowAddWordModal(false); setNewWord(''); setBatchWords(''); }} title="添加违禁词" >
setNewWord(e.target.value)} placeholder="输入违禁词" className="flex-1 px-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />