为品牌方端(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>
1273 lines
51 KiB
TypeScript
1273 lines
51 KiB
TypeScript
'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 格式:包含 forbiddenWords、whitelist 字段的对象</li>
|
||
<li>Excel 格式:第一列为词汇,第二列为分类(可选)</li>
|
||
<li>Word / PDF:AI 将自动识别并提取规则内容</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="最好 第一 最佳 ..."
|
||
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>
|
||
)
|
||
}
|