Your Name a8be7bbca9 feat: 前端剩余页面全面对接后端 API(Phase 2 完成)
为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用:
- 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API
- 添加 loading 骨架屏、error toast 提示、submitting 状态管理
- 数据映射:TaskResponse → 页面视图模型,处理类型差异
- 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo
- Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等
- 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:29:43 +08:00

1273 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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<string, { icon: string; color: string; name: string }> = {
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 (
<div className="animate-pulse space-y-4">
<div className="h-6 bg-bg-elevated rounded w-1/4" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="p-4 rounded-xl border border-border-subtle">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-bg-elevated rounded-xl" />
<div className="space-y-2 flex-1">
<div className="h-4 bg-bg-elevated rounded w-1/2" />
<div className="h-3 bg-bg-elevated rounded w-1/3" />
</div>
</div>
<div className="h-3 bg-bg-elevated rounded w-2/3 mb-3" />
<div className="h-3 bg-bg-elevated rounded w-1/2" />
</div>
))}
</div>
</div>
)
}
function WordsSkeleton() {
return (
<div className="animate-pulse space-y-4">
<div className="flex gap-3">
<div className="h-10 bg-bg-elevated rounded-xl flex-1 max-w-md" />
<div className="h-10 bg-bg-elevated rounded-xl w-32" />
</div>
{[1, 2].map((group) => (
<div key={group} className="space-y-2">
<div className="h-4 bg-bg-elevated rounded w-20" />
<div className="flex flex-wrap gap-2">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-8 bg-bg-elevated rounded-lg w-20" />
))}
</div>
</div>
))}
</div>
)
}
function ListSkeleton({ count = 3 }: { count?: number }) {
return (
<div className="animate-pulse space-y-3">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 rounded-xl border border-border-subtle">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-bg-elevated rounded-lg" />
<div className="h-4 bg-bg-elevated rounded w-24" />
</div>
<div className="w-8 h-8 bg-bg-elevated rounded-lg" />
</div>
))}
</div>
)
}
// ===== 主组件 =====
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<ForbiddenWordResponse[]>([])
const [competitors, setCompetitors] = useState<CompetitorResponse[]>([])
const [whitelist, setWhitelist] = useState<WhitelistResponse[]>([])
const [platforms, setPlatforms] = useState<PlatformRuleDisplay[]>([])
// 加载状态
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
// 上传规则库
const [showUploadModal, setShowUploadModal] = useState(false)
const [uploadPlatform, setUploadPlatform] = useState('')
const [uploadFile, setUploadFile] = useState<File | null>(null)
// 重新上传规则库
const [showReuploadModal, setShowReuploadModal] = useState(false)
const [reuploadPlatform, setReuploadPlatform] = useState<PlatformRuleDisplay | null>(null)
// 下载确认
const [showDownloadModal, setShowDownloadModal] = useState(false)
const [downloadPlatform, setDownloadPlatform] = useState<PlatformRuleDisplay | null>(null)
// 查看规则库详情
const [showDetailModal, setShowDetailModal] = useState(false)
const [selectedPlatform, setSelectedPlatform] = useState<PlatformRuleDisplay | null>(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 (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
{/* 标签页 */}
<div className="flex gap-1 p-1 bg-bg-elevated rounded-xl w-fit">
<button
type="button"
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 ${
activeTab === 'platforms'
? 'bg-bg-card text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
onClick={() => setActiveTab('platforms')}
>
<FileText size={16} />
<span className="px-2 py-0.5 rounded-full bg-accent-indigo/15 text-accent-indigo text-xs">
{platforms.length}
</span>
</button>
<button
type="button"
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 ${
activeTab === 'forbidden'
? 'bg-bg-card text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
onClick={() => setActiveTab('forbidden')}
>
<Ban size={16} />
<span className="px-2 py-0.5 rounded-full bg-accent-coral/15 text-accent-coral text-xs">
{forbiddenWords.length}
</span>
</button>
<button
type="button"
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 ${
activeTab === 'competitors'
? 'bg-bg-card text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
onClick={() => setActiveTab('competitors')}
>
<Building2 size={16} />
<span className="px-2 py-0.5 rounded-full bg-accent-amber/15 text-accent-amber text-xs">
{competitors.length}
</span>
</button>
<button
type="button"
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 ${
activeTab === 'whitelist'
? 'bg-bg-card text-text-primary shadow-sm'
: 'text-text-secondary hover:text-text-primary'
}`}
onClick={() => setActiveTab('whitelist')}
>
<Shield size={16} />
<span className="px-2 py-0.5 rounded-full bg-accent-green/15 text-accent-green text-xs">
{whitelist.length}
</span>
</button>
</div>
{/* 平台规则库 */}
{activeTab === 'platforms' && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<p className="text-sm text-text-tertiary mt-1">
</p>
</CardHeader>
<CardContent>
{loading ? (
<CardSkeleton />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{platforms.map((platform) => (
<div
key={platform.id}
className="p-4 rounded-xl border border-border-subtle bg-bg-card hover:border-accent-indigo/50 transition-all"
>
<div className="flex items-center gap-3 mb-3">
<div className={`w-10 h-10 ${platform.color} rounded-xl flex items-center justify-center text-xl`}>
{platform.icon}
</div>
<div>
<h3 className="font-medium text-text-primary">{platform.name}</h3>
<p className="text-xs text-text-tertiary">{platform.version}</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs text-text-tertiary mb-3">
<span>{platform.rules.forbiddenWords} </span>
<span>{platform.rules.whitelist} </span>
</div>
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
<span className="text-xs text-text-tertiary"> {platform.updatedAt}</span>
<div className="flex gap-1">
<button
type="button"
onClick={() => viewPlatformDetail(platform)}
className="p-1.5 rounded-lg text-text-tertiary hover:text-accent-indigo hover:bg-accent-indigo/10 transition-colors"
title="查看详情"
>
<Eye size={16} />
</button>
<button
type="button"
onClick={() => handleReupload(platform)}
className="p-1.5 rounded-lg text-text-tertiary hover:text-accent-indigo hover:bg-accent-indigo/10 transition-colors"
title="重新上传"
>
<Upload size={16} />
</button>
<button
type="button"
onClick={() => handleDownload(platform)}
className="p-1.5 rounded-lg text-text-tertiary hover:text-accent-green hover:bg-accent-green/10 transition-colors"
title="下载规则"
>
<Download size={16} />
</button>
</div>
</div>
</div>
))}
{/* 上传规则库 */}
<button
type="button"
onClick={() => setShowUploadModal(true)}
className="p-4 rounded-xl border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex flex-col items-center justify-center gap-2 text-text-tertiary hover:text-accent-indigo min-h-[160px]"
>
<Upload size={24} />
<span className="font-medium"></span>
<span className="text-xs"> JSON / Excel </span>
</button>
</div>
)}
</CardContent>
</Card>
)}
{/* 自定义违禁词列表 */}
{activeTab === 'forbidden' && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<p className="text-sm text-text-tertiary mt-1"></p>
</CardHeader>
<CardContent className="space-y-4">
{loading ? (
<WordsSkeleton />
) : (
<>
{/* 搜索框和添加按钮 */}
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-md">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="text"
placeholder="搜索违禁词或分类..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
<button
type="button"
onClick={() => setShowAddWordModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border-2 border-dashed border-border-subtle hover:border-accent-coral hover:bg-accent-coral/5 transition-all text-text-tertiary hover:text-accent-coral"
>
<Plus size={18} />
<span className="font-medium"></span>
</button>
</div>
{/* 按分类分组显示 */}
{(() => {
const grouped = filteredWords.reduce((acc, word) => {
if (!acc[word.category]) acc[word.category] = []
acc[word.category].push(word)
return acc
}, {} as Record<string, typeof filteredWords>)
return Object.entries(grouped).map(([category, words]) => (
<div key={category} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-text-secondary">{category}</span>
<span className="text-xs text-text-tertiary">({words.length})</span>
</div>
<div className="flex flex-wrap gap-2">
{words.map((word) => (
<div
key={word.id}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-elevated border border-border-subtle group hover:border-accent-coral/50"
>
<span className="text-text-primary">{word.word}</span>
<button
type="button"
onClick={() => handleDeleteWord(word.id)}
disabled={submitting}
className="text-text-tertiary hover:text-accent-coral transition-colors disabled:opacity-50"
>
{submitting ? <Loader2 size={14} className="animate-spin" /> : <X size={14} />}
</button>
</div>
))}
</div>
</div>
))
})()}
{filteredWords.length === 0 && (
<div className="text-center py-8 text-text-tertiary">
<Ban size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</div>
)}
</>
)}
</CardContent>
</Card>
)}
{/* 竞品列表 */}
{activeTab === 'competitors' && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<p className="text-sm text-text-tertiary mt-1"> Logo </p>
</CardHeader>
<CardContent>
{loading ? (
<ListSkeleton count={3} />
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{competitors.map((competitor) => (
<div key={competitor.id} className="p-4 rounded-xl bg-bg-elevated border border-border-subtle flex items-center justify-between group hover:border-accent-amber/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-accent-amber/15 rounded-lg flex items-center justify-center">
<Building2 size={20} className="text-accent-amber" />
</div>
<span className="font-medium text-text-primary">{competitor.name}</span>
</div>
<button
type="button"
onClick={() => handleDeleteCompetitor(competitor.id)}
disabled={submitting}
className="p-2 rounded-lg text-text-tertiary hover:text-accent-coral hover:bg-accent-coral/10 transition-colors disabled:opacity-50"
>
{submitting ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
</button>
</div>
))}
{/* 添加竞品按钮 */}
<button
type="button"
onClick={() => setShowAddCompetitorModal(true)}
className="p-4 rounded-xl border-2 border-dashed border-border-subtle hover:border-accent-amber hover:bg-accent-amber/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-amber"
>
<Plus size={20} />
<span className="font-medium"></span>
</button>
</div>
)}
</CardContent>
</Card>
)}
{/* 白名单 */}
{activeTab === 'whitelist' && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<p className="text-sm text-text-tertiary mt-1">使</p>
</CardHeader>
<CardContent>
{loading ? (
<ListSkeleton count={2} />
) : (
<div className="space-y-3">
{whitelist.map((item) => (
<div key={item.id} className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated border border-border-subtle hover:border-accent-green/50">
<div>
<p className="font-medium text-text-primary">{item.term}</p>
<p className="text-sm text-text-tertiary mt-0.5">{item.reason}</p>
</div>
<button
type="button"
onClick={() => handleDeleteWhitelist(item.id)}
disabled={submitting}
className="p-2 rounded-lg text-text-tertiary hover:text-accent-coral hover:bg-accent-coral/10 transition-colors disabled:opacity-50"
>
{submitting ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
</button>
</div>
))}
{/* 添加白名单按钮 */}
<button
type="button"
onClick={() => setShowAddWhitelistModal(true)}
className="w-full p-4 rounded-xl border-2 border-dashed border-border-subtle hover:border-accent-green hover:bg-accent-green/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-green"
>
<Plus size={20} />
<span className="font-medium"></span>
</button>
</div>
)}
</CardContent>
</Card>
)}
{/* 上传规则库弹窗 */}
<Modal
isOpen={showUploadModal}
onClose={() => { setShowUploadModal(false); setUploadPlatform(''); setUploadFile(null); }}
title="上传平台规则库"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<select
value={uploadPlatform}
onChange={(e) => setUploadPlatform(e.target.value)}
className="w-full 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"
>
<option value=""></option>
<option value="douyin"></option>
<option value="xiaohongshu"></option>
<option value="bilibili">B站</option>
<option value="kuaishou"></option>
<option value="weibo"></option>
<option value="wechat"></option>
<option value="other"></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<div className="border-2 border-dashed border-border-subtle rounded-xl p-8 text-center hover:border-accent-indigo transition-colors cursor-pointer">
<Upload size={32} className="mx-auto text-text-tertiary mb-3" />
<p className="text-sm text-text-primary mb-1"></p>
<p className="text-xs text-text-tertiary"> JSON / Excel / Word / PDF </p>
</div>
</div>
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20">
<h4 className="text-sm font-medium text-accent-indigo mb-2"></h4>
<ul className="text-xs text-text-secondary space-y-1">
<li>JSON forbiddenWordswhitelist </li>
<li>Excel </li>
<li>Word / PDFAI </li>
</ul>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setShowUploadModal(false); setUploadPlatform(''); }}>
</Button>
<Button disabled={!uploadPlatform}>
</Button>
</div>
</div>
</Modal>
{/* 查看规则库详情弹窗 */}
<Modal
isOpen={showDetailModal}
onClose={() => { setShowDetailModal(false); setSelectedPlatform(null); }}
title={selectedPlatform ? `${selectedPlatform.name}规则库详情` : '规则库详情'}
size="lg"
>
{selectedPlatform && (
<div className="space-y-4">
<div className="flex items-center gap-4 p-4 rounded-xl bg-bg-elevated">
<div className={`w-14 h-14 ${selectedPlatform.color} rounded-xl flex items-center justify-center text-2xl`}>
{selectedPlatform.icon}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-primary">{selectedPlatform.name}</h3>
<div className="flex items-center gap-4 mt-1 text-sm text-text-tertiary">
<span>{selectedPlatform.version}</span>
<span>{selectedPlatform.updatedAt}</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20 text-center">
<p className="text-2xl font-bold text-accent-coral">{selectedPlatform.rules.forbiddenWords}</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<div className="p-4 rounded-xl bg-accent-green/10 border border-accent-green/20 text-center">
<p className="text-2xl font-bold text-accent-green">{selectedPlatform.rules.whitelist}</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20 text-center">
<p className="text-2xl font-bold text-accent-indigo">12</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
</div>
<div>
<h4 className="text-sm font-medium text-text-primary mb-2"></h4>
<div className="space-y-2">
{[
{ name: '极限词', count: 45 },
{ name: '虚假宣传', count: 38 },
{ name: '敏感词汇', count: 32 },
{ name: '价格欺诈', count: 21 },
{ name: '其他', count: 20 },
].map((cat) => (
<div key={cat.name} className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated">
<span className="text-sm text-text-primary">{cat.name}</span>
<span className="text-sm text-text-tertiary">{cat.count} </span>
</div>
))}
</div>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => setShowDetailModal(false)}>
</Button>
<Button variant="secondary" onClick={() => selectedPlatform && handleDownload(selectedPlatform)}>
<Download size={16} />
</Button>
<Button onClick={() => {
if (selectedPlatform) {
setShowDetailModal(false)
handleReupload(selectedPlatform)
}
}}>
<Upload size={16} />
</Button>
</div>
</div>
)}
</Modal>
{/* 重新上传规则库弹窗 */}
<Modal
isOpen={showReuploadModal}
onClose={() => { setShowReuploadModal(false); setReuploadPlatform(null); }}
title={reuploadPlatform ? `重新上传${reuploadPlatform.name}规则库` : '重新上传规则库'}
>
{reuploadPlatform && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 rounded-xl bg-bg-elevated">
<div className={`w-10 h-10 ${reuploadPlatform.color} rounded-lg flex items-center justify-center text-xl`}>
{reuploadPlatform.icon}
</div>
<div>
<p className="font-medium text-text-primary">{reuploadPlatform.name}</p>
<p className="text-xs text-text-tertiary">{reuploadPlatform.version}</p>
</div>
</div>
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/20">
<p className="text-sm text-accent-amber font-medium mb-1"></p>
<p className="text-xs text-text-secondary"></p>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<div className="border-2 border-dashed border-border-subtle rounded-xl p-6 text-center hover:border-accent-indigo transition-colors cursor-pointer">
<Upload size={28} className="mx-auto text-text-tertiary mb-2" />
<p className="text-sm text-text-primary mb-1"></p>
<p className="text-xs text-text-tertiary"> JSON / Excel / Word / PDF </p>
</div>
<p className="text-xs text-text-tertiary mt-2">Word PDF AI </p>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setShowReuploadModal(false); setReuploadPlatform(null); }}>
</Button>
<Button>
</Button>
</div>
</div>
)}
</Modal>
{/* 下载规则库弹窗 */}
<Modal
isOpen={showDownloadModal}
onClose={() => { setShowDownloadModal(false); setDownloadPlatform(null); }}
title={downloadPlatform ? `下载${downloadPlatform.name}规则库` : '下载规则库'}
>
{downloadPlatform && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 rounded-xl bg-bg-elevated">
<div className={`w-10 h-10 ${downloadPlatform.color} rounded-lg flex items-center justify-center text-xl`}>
{downloadPlatform.icon}
</div>
<div>
<p className="font-medium text-text-primary">{downloadPlatform.name}</p>
<p className="text-xs text-text-tertiary">{downloadPlatform.version} · {downloadPlatform.updatedAt}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-3"></label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
className="p-3 rounded-xl border-2 border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all text-left"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-accent-green/15 flex items-center justify-center">
<FileText size={18} className="text-accent-green" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">JSON</p>
<p className="text-xs text-text-tertiary"></p>
</div>
</div>
</button>
<button
type="button"
className="p-3 rounded-xl border-2 border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all text-left"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
<FileText size={18} className="text-accent-indigo" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">Excel</p>
<p className="text-xs text-text-tertiary"></p>
</div>
</div>
</button>
<button
type="button"
className="p-3 rounded-xl border-2 border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all text-left"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-[#2b579a]/15 flex items-center justify-center">
<FileText size={18} className="text-[#2b579a]" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">Word</p>
<p className="text-xs text-text-tertiary"></p>
</div>
</div>
</button>
<button
type="button"
className="p-3 rounded-xl border-2 border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all text-left"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-accent-coral/15 flex items-center justify-center">
<FileText size={18} className="text-accent-coral" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">PDF</p>
<p className="text-xs text-text-tertiary"></p>
</div>
</div>
</button>
</div>
</div>
<div className="p-3 rounded-xl bg-bg-elevated text-sm">
<div className="flex justify-between text-text-secondary mb-1">
<span></span>
<span className="text-text-primary">{downloadPlatform.rules.forbiddenWords} </span>
</div>
<div className="flex justify-between text-text-secondary">
<span></span>
<span className="text-text-primary">{downloadPlatform.rules.whitelist} </span>
</div>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setShowDownloadModal(false); setDownloadPlatform(null); }}>
</Button>
<Button>
<Download size={16} />
</Button>
</div>
</div>
)}
</Modal>
{/* 添加违禁词弹窗 */}
<Modal
isOpen={showAddWordModal}
onClose={() => { setShowAddWordModal(false); setNewWord(''); setBatchWords(''); }}
title="添加违禁词"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<select
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
className="w-full 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"
>
{categoryOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<div className="flex gap-2">
<input
type="text"
value={newWord}
onChange={(e) => 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"
/>
<Button onClick={handleAddWord} disabled={!newWord.trim() || submitting}>
{submitting ? <Loader2 size={16} className="animate-spin" /> : '添加'}
</Button>
</div>
</div>
<div className="relative">
<div className="absolute inset-x-0 top-1/2 border-t border-border-subtle" />
<div className="relative flex justify-center">
<span className="bg-bg-card px-3 text-sm text-text-tertiary"></span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
<Upload size={14} className="inline mr-1" />
</label>
<textarea
value={batchWords}
onChange={(e) => setBatchWords(e.target.value)}
placeholder="最好&#10;第一&#10;最佳&#10;..."
className="w-full h-32 px-4 py-3 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary placeholder-text-tertiary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo font-mono text-sm"
/>
<div className="flex justify-between items-center mt-2">
<span className="text-xs text-text-tertiary">
{batchWords.split('\n').filter(w => w.trim()).length}
</span>
<Button onClick={handleBatchAdd} disabled={!batchWords.trim() || submitting}>
{submitting ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" />
...
</span>
) : '批量添加'}
</Button>
</div>
</div>
</div>
</Modal>
{/* 添加竞品弹窗 */}
<Modal
isOpen={showAddCompetitorModal}
onClose={() => { setShowAddCompetitorModal(false); setNewCompetitor(''); }}
title="添加竞品"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<input
type="text"
value={newCompetitor}
onChange={(e) => setNewCompetitor(e.target.value)}
placeholder="输入竞品品牌名称"
className="w-full 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"
/>
</div>
<p className="text-sm text-text-tertiary">
AI将在视频中自动检测该品牌的Logo或名称出现
</p>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setShowAddCompetitorModal(false); setNewCompetitor(''); }}>
</Button>
<Button onClick={handleAddCompetitor} disabled={!newCompetitor.trim() || submitting}>
{submitting ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" />
...
</span>
) : '添加'}
</Button>
</div>
</div>
</Modal>
{/* 添加白名单弹窗 */}
<Modal
isOpen={showAddWhitelistModal}
onClose={() => { setShowAddWhitelistModal(false); setNewWhitelistTerm(''); setNewWhitelistReason(''); }}
title="添加白名单"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<input
type="text"
value={newWhitelistTerm}
onChange={(e) => setNewWhitelistTerm(e.target.value)}
placeholder="输入需要豁免的词汇"
className="w-full 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<input
type="text"
value={newWhitelistReason}
onChange={(e) => setNewWhitelistReason(e.target.value)}
placeholder="例如:品牌授权使用"
className="w-full 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"
/>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="ghost" onClick={() => { setShowAddWhitelistModal(false); setNewWhitelistTerm(''); setNewWhitelistReason(''); }}>
</Button>
<Button onClick={handleAddWhitelist} disabled={!newWhitelistTerm.trim() || submitting}>
{submitting ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" />
...
</span>
) : '添加'}
</Button>
</div>
</div>
</Modal>
</div>
)
}