'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Plus, Shield, Ban, Building2, Search, X, Upload, Trash2, FileText, Eye, Loader2, CheckCircle, Clock, AlertTriangle, Edit3 } 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' // upload via api.proxyUpload directly import type { ForbiddenWordResponse, CompetitorResponse, WhitelistResponse, BrandPlatformRuleResponse, ParsedRulesData, } from '@/types/rules' // ===== 平台图标映射 ===== 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: '微信视频号' }, } function getPlatformDisplay(platform: string) { return platformDisplayMap[platform] || { icon: '📋', color: 'bg-gray-400', name: platform } } type IconComponent = typeof CheckCircle const statusConfig: Record = { active: { label: '生效中', color: 'text-accent-green', bg: 'bg-accent-green/15', icon: CheckCircle }, draft: { label: '待确认', color: 'text-accent-amber', bg: 'bg-accent-amber/15', icon: Clock }, inactive: { label: '已停用', color: 'text-text-tertiary', bg: 'bg-bg-elevated', icon: AlertTriangle }, } // ===== Mock 数据 ===== const mockPlatformRules: BrandPlatformRuleResponse[] = [ { id: 'pr-mock001', platform: 'douyin', brand_id: 'BR000001', document_url: 'https://example.com/rules.pdf', document_name: '抖音广告规则2024.pdf', parsed_rules: { forbidden_words: ['最好', '第一', '最佳', '绝对', '100%'], restricted_words: [{ word: '效果显著', condition: '需要提供数据支撑', suggestion: '建议改为"改善效果"' }], duration: { min_seconds: 7, max_seconds: 60 }, content_requirements: ['必须展示产品正面', '口播品牌名至少1次'], other_rules: [{ rule: '水印要求', description: '视频不得包含第三方水印' }], }, status: 'active', created_at: '2024-02-01T10:00:00Z', updated_at: '2024-02-01T10:30:00Z', }, { id: 'pr-mock002', platform: 'xiaohongshu', brand_id: 'BR000001', document_url: 'https://example.com/xhs-rules.docx', document_name: '小红书投放规范.docx', parsed_rules: { forbidden_words: ['最好', '绝对', '100%'], restricted_words: [], duration: null, content_requirements: ['需要真实使用体验分享'], other_rules: [], }, status: 'active', created_at: '2024-01-20T08:00:00Z', updated_at: '2024-01-20T09:00:00Z', }, { id: 'pr-mock003', platform: 'bilibili', brand_id: 'BR000001', document_url: 'https://example.com/bili-rules.xlsx', document_name: 'B站违禁词清单.xlsx', parsed_rules: { forbidden_words: ['最好', '第一'], restricted_words: [], duration: { min_seconds: 15 }, content_requirements: [], other_rules: [{ rule: '弹幕互动', description: '鼓励弹幕互动但不得刷屏' }], }, status: 'draft', created_at: '2024-02-03T14:00:00Z', updated_at: '2024-02-03T14:00:00Z', }, ] 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: '' }, ] const categoryOptions = [ { value: '极限词', label: '极限词' }, { value: '功效词', label: '功效词' }, { value: '虚假宣称', label: '虚假宣称' }, { value: '价格欺诈', label: '价格欺诈' }, { value: '平台规则', label: '平台规则' }, { value: '自定义', label: '自定义' }, ] // ===== 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() const fileInputRef = useRef(null) const [isOssUploading, setIsOssUploading] = useState(false) const [ossProgress, setOssProgress] = useState(0) // 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 [platformRules, setPlatformRules] = useState([]) // 加载状态 const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [parsing, setParsing] = useState(false) // 上传规则文档 const [showUploadModal, setShowUploadModal] = useState(false) const [uploadPlatform, setUploadPlatform] = useState('') const [uploadFile, setUploadFile] = useState(null) // 查看/编辑解析结果 const [showDetailModal, setShowDetailModal] = useState(false) const [selectedRule, setSelectedRule] = useState(null) const [editingRules, setEditingRules] = useState(null) const [editingForbiddenInput, setEditingForbiddenInput] = useState('') // 添加违禁词 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) { setPlatformRules(mockPlatformRules); return } try { const res = await api.listBrandPlatformRules() setPlatformRules(res.items) } 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 activeRulesCount = platformRules.filter(r => r.status === 'active').length const handleUploadAndParse = async () => { if (!uploadPlatform || !uploadFile) return setParsing(true) try { let documentUrl: string let documentName = uploadFile.name if (USE_MOCK) { // Mock: 模拟上传和解析 await new Promise(r => setTimeout(r, 2000)) const newRule: BrandPlatformRuleResponse = { id: `pr-mock${Date.now()}`, platform: uploadPlatform, brand_id: 'BR000001', document_url: 'https://mock.example.com/' + uploadFile.name, document_name: uploadFile.name, parsed_rules: { forbidden_words: ['最好', '第一', '100%'], restricted_words: [{ word: '效果好', condition: '需提供证据', suggestion: '改为"改善效果"' }], duration: { min_seconds: 7 }, content_requirements: ['必须展示产品'], other_rules: [], }, status: 'draft', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } setPlatformRules(prev => [newRule, ...prev]) setShowUploadModal(false) setUploadPlatform('') setUploadFile(null) toast.success('文档解析完成,请确认解析结果') // 打开详情编辑 setSelectedRule(newRule) setEditingRules(newRule.parsed_rules) setShowDetailModal(true) setParsing(false) return } // 真实模式: 上传到 TOS (通过后端代理) setIsOssUploading(true) setOssProgress(0) const uploadResult = await api.proxyUpload(uploadFile, 'rules', (pct) => { setOssProgress(Math.min(95, Math.round(pct * 0.95))) }) setOssProgress(100) setIsOssUploading(false) documentUrl = uploadResult.url // 调用 AI 解析 const parsed = await api.parsePlatformRule({ document_url: documentUrl, document_name: documentName, platform: uploadPlatform, brand_id: '', // 后端从 token 获取 }) await loadPlatformRules() setShowUploadModal(false) setUploadPlatform('') setUploadFile(null) toast.success('文档解析完成,请确认解析结果') // 打开详情编辑 const newRule: BrandPlatformRuleResponse = { id: parsed.id, platform: parsed.platform, brand_id: parsed.brand_id, document_url: parsed.document_url, document_name: parsed.document_name, parsed_rules: parsed.parsed_rules, status: parsed.status, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } setSelectedRule(newRule) setEditingRules(parsed.parsed_rules) setShowDetailModal(true) } catch (err) { toast.error('文档解析失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setParsing(false) setIsOssUploading(false) } } const handleConfirmRule = async () => { if (!selectedRule || !editingRules) return setSubmitting(true) try { if (USE_MOCK) { setPlatformRules(prev => prev.map(r => r.id === selectedRule.id ? { ...r, parsed_rules: editingRules, status: 'active', updated_at: new Date().toISOString() } : r.platform === selectedRule.platform && r.status === 'active' ? { ...r, status: 'inactive' } : r )) } else { await api.confirmPlatformRule(selectedRule.id, { parsed_rules: editingRules }) await loadPlatformRules() } toast.success('规则已确认生效') setShowDetailModal(false) setSelectedRule(null) setEditingRules(null) } catch (err) { toast.error('确认失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const handleDeleteRule = async (ruleId: string) => { setSubmitting(true) try { if (USE_MOCK) { setPlatformRules(prev => prev.filter(r => r.id !== ruleId)) } else { await api.deletePlatformRule(ruleId) await loadPlatformRules() } toast.success('规则已删除') } catch (err) { toast.error('删除失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } const viewRuleDetail = (rule: BrandPlatformRuleResponse) => { setSelectedRule(rule) setEditingRules({ ...rule.parsed_rules }) setShowDetailModal(true) } // ===== 编辑解析结果辅助 ===== const addForbiddenWord = () => { if (!editingForbiddenInput.trim() || !editingRules) return setEditingRules({ ...editingRules, forbidden_words: [...editingRules.forbidden_words, editingForbiddenInput.trim()], }) setEditingForbiddenInput('') } const removeForbiddenWord = (index: number) => { if (!editingRules) return setEditingRules({ ...editingRules, forbidden_words: editingRules.forbidden_words.filter((_, i) => i !== index), }) } const removeContentReq = (index: number) => { if (!editingRules) return setEditingRules({ ...editingRules, content_requirements: editingRules.content_requirements.filter((_, i) => i !== index), }) } // ===== 违禁词操作 ===== const handleAddWord = async () => { if (!newWord.trim()) return setSubmitting(true) try { if (USE_MOCK) { setForbiddenWords(prev => [...prev, { id: Date.now().toString(), word: newWord.trim(), category: newCategory, severity: 'medium' }]) } 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) { setCompetitors(prev => [...prev, { id: Date.now().toString(), name: newCompetitor.trim(), brand_id: '', keywords: [newCompetitor.trim()] }]) } 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) { setWhitelist(prev => [...prev, { id: Date.now().toString(), term: newWhitelistTerm.trim(), reason: newWhitelistReason.trim(), brand_id: '' }]) } 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 { await api.deleteWhitelistItem(id) await loadWhitelist() } toast.success('白名单已删除') } catch (err) { toast.error('删除白名单失败:' + (err instanceof Error ? err.message : '未知错误')) } finally { setSubmitting(false) } } return (

规则配置

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

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

上传各平台的规则文档(PDF / Word / Excel),AI 自动解析提取合规规则,确认后应用于审核

{loading ? ( ) : (
{/* 已有规则列表 */} {platformRules.length > 0 && (
{platformRules.map((rule) => { const display = getPlatformDisplay(rule.platform) const status = statusConfig[rule.status] || statusConfig.draft const StatusIcon = status.icon return (
{display.icon}

{display.name}

{rule.document_name}

{status.label}
{rule.parsed_rules?.forbidden_words?.length || 0} 违禁词 {rule.parsed_rules?.content_requirements?.length || 0} 内容要求 {rule.parsed_rules?.duration && ( 时长 {rule.parsed_rules.duration.min_seconds || '?'}s+ )}
{new Date(rule.updated_at).toLocaleDateString('zh-CN')}
) })} {/* 上传新规则按钮 */}
)} {/* 空状态 */} {platformRules.length === 0 && (

暂无平台规则

上传平台规则文档,AI 将自动提取违禁词、内容要求等合规规则

)}
)}
)} {/* ==================== 自定义违禁词 ==================== */} {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}

))}
)}
)} {/* ==================== 上传规则文档弹窗 ==================== */} { if (!parsing) { setShowUploadModal(false); setUploadPlatform(''); setUploadFile(null) } }} title="上传平台规则文档" >
{ const file = e.target.files?.[0] if (file) setUploadFile(file) }} />
!parsing && fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); e.stopPropagation() }} onDrop={(e) => { e.preventDefault() e.stopPropagation() const file = e.dataTransfer.files?.[0] if (file) setUploadFile(file) }} className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors cursor-pointer ${ uploadFile ? 'border-accent-green bg-accent-green/5' : 'border-border-subtle hover:border-accent-indigo' } ${parsing ? 'opacity-50 pointer-events-none' : ''}`} > {uploadFile ? (

{uploadFile.name}

{(uploadFile.size / 1024).toFixed(1)} KB

) : ( <>

点击或拖拽上传文件

支持 PDF / Word / Excel / TXT 格式

)}

AI 智能解析

  • AI 将自动从文档中提取违禁词、内容要求、时长规则等
  • 解析完成后可编辑调整,确认后即生效
  • 同一平台的新规则生效后,旧规则自动停用
{parsing && (

{isOssUploading ? '正在上传文档...' : 'AI 正在解析规则...'}

{isOssUploading ? `上传进度 ${ossProgress}%` : '这可能需要几秒钟'}

)}
{/* ==================== 规则详情/编辑弹窗 ==================== */} { setShowDetailModal(false); setSelectedRule(null); setEditingRules(null); setEditingForbiddenInput('') }} title={selectedRule ? `${getPlatformDisplay(selectedRule.platform).name} 平台规则` : '规则详情'} size="lg" > {selectedRule && editingRules && (
{/* 头部信息 */}
{getPlatformDisplay(selectedRule.platform).icon}

{getPlatformDisplay(selectedRule.platform).name}

{selectedRule.document_name}

{statusConfig[selectedRule.status]?.label}
{/* 违禁词 */}

违禁词 ({editingRules.forbidden_words.length})

{editingRules.forbidden_words.map((word, i) => ( {word} ))}
setEditingForbiddenInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addForbiddenWord() } }} placeholder="添加违禁词..." className="flex-1 px-3 py-1.5 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
{/* 限制词 */} {editingRules.restricted_words.length > 0 && (

限制词

{editingRules.restricted_words.map((rw, i) => (

{rw.word}

条件:{rw.condition}

建议:{rw.suggestion}

))}
)} {/* 时长要求 */} {editingRules.duration && (

时长要求

{editingRules.duration.min_seconds && 最短 {editingRules.duration.min_seconds} 秒} {editingRules.duration.min_seconds && editingRules.duration.max_seconds && / } {editingRules.duration.max_seconds && 最长 {editingRules.duration.max_seconds} 秒}
)} {/* 内容要求 */} {editingRules.content_requirements.length > 0 && (

内容要求

{editingRules.content_requirements.map((req, i) => (
{req}
))}
)} {/* 其他规则 */} {editingRules.other_rules.length > 0 && (

其他规则

{editingRules.other_rules.map((or, i) => (

{or.rule}

{or.description}

))}
)} {/* 操作按钮 */}
{selectedRule.status === 'active' && ( )}
)}
{/* ==================== 添加违禁词弹窗 ==================== */} { 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" />