- 时长: {task.duration}
+
+ {/* 平台顶部条 */}
+ {platform && (
+
+ {platform.icon}
+ {platform.name}
)}
+
+
+
+
+
{task.title}
+ {task.hasHighRisk && (
+
+
+ 高风险
+
+ )}
+
+
+
+
+ {task.creatorName}
+
+
+
+ {task.agencyName}
+
+
+
+
+
+
+ {task.projectName}
+
+
+ {task.submittedAt}
+
+
+ {'duration' in task && (
+
+ 时长: {task.duration}
+
+ )}
+
)
diff --git a/frontend/app/brand/rules/page.tsx b/frontend/app/brand/rules/page.tsx
index b80c2a2..b0d20ae 100644
--- a/frontend/app/brand/rules/page.tsx
+++ b/frontend/app/brand/rules/page.tsx
@@ -1,28 +1,76 @@
'use client'
import { useState } from 'react'
-import { Plus, Shield, AlertTriangle, Ban, Building2 } from 'lucide-react'
+import { Plus, Shield, Ban, Building2, Search, X, Upload, Trash2, FileText, Check, Download, Eye } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
-import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/ui/Modal'
-import { Select } from '@/components/ui/Select'
-import { ErrorTag, WarningTag, SuccessTag } from '@/components/ui/Tag'
+
+// 平台规则库数据
+const platformRuleLibraries = [
+ {
+ 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',
+ },
+]
// 模拟规则数据
-const mockRules = {
+const initialRules = {
forbiddenWords: [
- { id: '1', word: '最好', category: '极限词', severity: 'high' },
- { id: '2', word: '第一', category: '极限词', severity: 'high' },
- { id: '3', word: '最佳', category: '极限词', severity: 'high' },
- { id: '4', word: '100%有效', category: '虚假宣称', severity: 'high' },
- { id: '5', word: '立即见效', category: '虚假宣称', severity: 'medium' },
- { id: '6', word: '永久', category: '极限词', severity: 'medium' },
+ { id: '1', word: '最好', category: '极限词' },
+ { id: '2', word: '第一', category: '极限词' },
+ { id: '3', word: '最佳', category: '极限词' },
+ { id: '4', word: '100%有效', category: '虚假宣称' },
+ { id: '5', word: '立即见效', category: '虚假宣称' },
+ { id: '6', word: '永久', category: '极限词' },
+ { id: '7', word: '绝对', category: '极限词' },
+ { id: '8', word: '最低价', category: '价格欺诈' },
],
competitors: [
- { id: '1', name: '竞品A', logoUrl: '' },
- { id: '2', name: '竞品B', logoUrl: '' },
- { id: '3', name: '竞品C', logoUrl: '' },
+ { id: '1', name: '竞品A' },
+ { id: '2', name: '竞品B' },
+ { id: '3', name: '竞品C' },
],
whitelist: [
{ id: '1', term: '品牌专属术语1', reason: '品牌授权使用' },
@@ -31,120 +79,380 @@ const mockRules = {
}
const categoryOptions = [
- { value: 'absolute_term', label: '极限词' },
- { value: 'false_claim', label: '虚假宣称' },
- { value: 'platform_rule', label: '平台规则' },
- { value: 'custom', label: '自定义' },
+ { value: '极限词', label: '极限词' },
+ { value: '虚假宣称', label: '虚假宣称' },
+ { value: '价格欺诈', label: '价格欺诈' },
+ { value: '平台规则', label: '平台规则' },
+ { value: '自定义', label: '自定义' },
]
-const severityOptions = [
- { value: 'high', label: '高风险' },
- { value: 'medium', label: '中风险' },
- { value: 'low', label: '低风险' },
-]
-
-function SeverityTag({ severity }: { severity: string }) {
- if (severity === 'high') return
高风险
- if (severity === 'medium') return
中风险
- return
低风险
-}
-
export default function RulesPage() {
- const [activeTab, setActiveTab] = useState<'forbidden' | 'competitors' | 'whitelist'>('forbidden')
- const [showAddModal, setShowAddModal] = useState(false)
- const [newWord, setNewWord] = useState('')
- const [newCategory, setNewCategory] = useState('absolute_term')
- const [newSeverity, setNewSeverity] = useState('high')
+ const [activeTab, setActiveTab] = useState<'platforms' | 'forbidden' | 'competitors' | 'whitelist'>('platforms')
+ const [rules, setRules] = useState(initialRules)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [platforms, setPlatforms] = useState(platformRuleLibraries)
+ // 上传规则库
+ const [showUploadModal, setShowUploadModal] = useState(false)
+ const [uploadPlatform, setUploadPlatform] = useState('')
+ const [uploadFile, setUploadFile] = useState
(null)
+
+ // 重新上传规则库
+ const [showReuploadModal, setShowReuploadModal] = useState(false)
+ const [reuploadPlatform, setReuploadPlatform] = useState(null)
+
+ // 下载确认
+ const [showDownloadModal, setShowDownloadModal] = useState(false)
+ const [downloadPlatform, setDownloadPlatform] = useState(null)
+
+ // 查看规则库详情
+ const [showDetailModal, setShowDetailModal] = useState(false)
+ const [selectedPlatform, setSelectedPlatform] = useState(null)
+
+ // 添加违禁词
+ const [showAddWordModal, setShowAddWordModal] = useState(false)
+ const [newWord, setNewWord] = useState('')
+ const [newCategory, setNewCategory] = useState('极限词')
+ const [batchWords, setBatchWords] = useState('')
+
+ // 添加竞品
+ const [showAddCompetitorModal, setShowAddCompetitorModal] = useState(false)
+ const [newCompetitor, setNewCompetitor] = useState('')
+
+ // 添加白名单
+ const [showAddWhitelistModal, setShowAddWhitelistModal] = useState(false)
+ const [newWhitelistTerm, setNewWhitelistTerm] = useState('')
+ const [newWhitelistReason, setNewWhitelistReason] = useState('')
+
+ // 过滤违禁词
+ const filteredWords = rules.forbiddenWords.filter(w =>
+ searchQuery === '' ||
+ w.word.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ w.category.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+
+ // 查看平台详情
+ const viewPlatformDetail = (platform: typeof platformRuleLibraries[0]) => {
+ setSelectedPlatform(platform)
+ setShowDetailModal(true)
+ }
+
+ // 重新上传
+ const handleReupload = (platform: typeof platformRuleLibraries[0]) => {
+ setReuploadPlatform(platform)
+ setShowReuploadModal(true)
+ }
+
+ // 下载规则
+ const handleDownload = (platform: typeof platformRuleLibraries[0]) => {
+ setDownloadPlatform(platform)
+ setShowDownloadModal(true)
+ }
+
+ // 添加单个违禁词
const handleAddWord = () => {
if (!newWord.trim()) return
- // TODO: 调用 API 添加
- setShowAddModal(false)
+ setRules({
+ ...rules,
+ forbiddenWords: [
+ ...rules.forbiddenWords,
+ { id: Date.now().toString(), word: newWord.trim(), category: newCategory }
+ ]
+ })
setNewWord('')
+ setShowAddWordModal(false)
+ }
+
+ // 批量添加违禁词
+ const handleBatchAdd = () => {
+ const words = batchWords.split('\n').filter(w => w.trim())
+ if (words.length === 0) return
+ const newWords = words.map((word, i) => ({
+ id: `${Date.now()}-${i}`,
+ word: word.trim(),
+ category: newCategory
+ }))
+ setRules({
+ ...rules,
+ forbiddenWords: [...rules.forbiddenWords, ...newWords]
+ })
+ setBatchWords('')
+ setShowAddWordModal(false)
+ }
+
+ // 删除违禁词
+ const handleDeleteWord = (id: string) => {
+ setRules({
+ ...rules,
+ forbiddenWords: rules.forbiddenWords.filter(w => w.id !== id)
+ })
+ }
+
+ // 添加竞品
+ const handleAddCompetitor = () => {
+ if (!newCompetitor.trim()) return
+ setRules({
+ ...rules,
+ competitors: [
+ ...rules.competitors,
+ { id: Date.now().toString(), name: newCompetitor.trim() }
+ ]
+ })
+ setNewCompetitor('')
+ setShowAddCompetitorModal(false)
+ }
+
+ // 删除竞品
+ const handleDeleteCompetitor = (id: string) => {
+ setRules({
+ ...rules,
+ competitors: rules.competitors.filter(c => c.id !== id)
+ })
+ }
+
+ // 添加白名单
+ const handleAddWhitelist = () => {
+ if (!newWhitelistTerm.trim()) return
+ setRules({
+ ...rules,
+ whitelist: [
+ ...rules.whitelist,
+ { id: Date.now().toString(), term: newWhitelistTerm.trim(), reason: newWhitelistReason.trim() }
+ ]
+ })
+ setNewWhitelistTerm('')
+ setNewWhitelistReason('')
+ setShowAddWhitelistModal(false)
+ }
+
+ // 删除白名单
+ const handleDeleteWhitelist = (id: string) => {
+ setRules({
+ ...rules,
+ whitelist: rules.whitelist.filter(w => w.id !== id)
+ })
}
return (
-
-
规则配置
+
+
规则配置
+
配置平台规则库和自定义审核规则,代理商可在此基础上调整风险等级
{/* 标签页 */}
-
+
+
- {/* 违禁词列表 */}
+ {/* 平台规则库 */}
+ {activeTab === 'platforms' && (
+
+
+ 平台规则库
+
+ 管理各平台的审核规则库,启用后将应用于对应平台的视频审核
+
+
+
+
+ {platforms.map((platform) => (
+
+
+
+ {platform.icon}
+
+
+
{platform.name}
+
{platform.version}
+
+
+
+
+ {platform.rules.forbiddenWords} 违禁词
+ {platform.rules.whitelist} 白名单
+
+
+
+
更新于 {platform.updatedAt}
+
+
+
+
+
+
+
+ ))}
+
+ {/* 上传规则库 */}
+
+
+
+
+ )}
+
+ {/* 自定义违禁词列表 */}
{activeTab === 'forbidden' && (
-
- 违禁词列表
-
-
+ 自定义违禁词
+ 在平台规则库基础上,添加品牌专属的违禁词规则
-
-
-
-
-
- | 词汇 |
- 分类 |
- 风险等级 |
- 操作 |
-
-
-
- {mockRules.forbiddenWords.map((word) => (
-
- | {word.word} |
- {word.category} |
- |
-
-
- |
-
- ))}
-
-
+
+ {/* 搜索框和添加按钮 */}
+
+
+
+ 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 && (
+
+ )}
)}
@@ -153,27 +461,38 @@ export default function RulesPage() {
{activeTab === 'competitors' && (
-
- 竞品列表
-
-
+ 竞品列表
+ 系统将在视频中检测以下竞品的 Logo 或品牌名称
-
- 系统将在视频中检测以下竞品的 Logo 或品牌名称
-
- {mockRules.competitors.map((competitor) => (
-
+ {rules.competitors.map((competitor) => (
+
-
-
+
+
-
{competitor.name}
+
{competitor.name}
-
+
))}
+
+ {/* 添加竞品按钮 */}
+
@@ -183,70 +502,452 @@ export default function RulesPage() {
{activeTab === 'whitelist' && (
-
- 白名单
-
-
+ 白名单
+ 白名单中的词汇即使命中违禁词也不会触发告警
-
- 白名单中的词汇即使命中违禁词也不会触发告警
-
-
-
-
-
- | 词汇 |
- 原因 |
- 操作 |
-
-
-
- {mockRules.whitelist.map((item) => (
-
- | {item.term} |
- {item.reason} |
-
-
- |
-
- ))}
-
-
+
+ {rules.whitelist.map((item) => (
+
+
+
{item.term}
+
{item.reason}
+
+
+
+ ))}
+
+ {/* 添加白名单按钮 */}
+
)}
- {/* 添加违禁词弹窗 */}
+ {/* 上传规则库弹窗 */}
setShowAddModal(false)}
- title="添加违禁词"
- size="sm"
+ isOpen={showUploadModal}
+ onClose={() => { setShowUploadModal(false); setUploadPlatform(''); setUploadFile(null); }}
+ title="上传平台规则库"
>
-
setNewWord(e.target.value)}
- />
-
)
}
diff --git a/frontend/components/layout/DesktopLayout.tsx b/frontend/components/layout/DesktopLayout.tsx
index 3057f32..ee513b1 100644
--- a/frontend/components/layout/DesktopLayout.tsx
+++ b/frontend/components/layout/DesktopLayout.tsx
@@ -14,10 +14,12 @@ export function DesktopLayout({
className = '',
}: DesktopLayoutProps) {
return (
-
+
-
- {children}
+
+
+ {children}
+
)
diff --git a/frontend/components/layout/MobileLayout.tsx b/frontend/components/layout/MobileLayout.tsx
index 07a780b..410f014 100644
--- a/frontend/components/layout/MobileLayout.tsx
+++ b/frontend/components/layout/MobileLayout.tsx
@@ -19,10 +19,12 @@ export function MobileLayout({
className = '',
}: MobileLayoutProps) {
return (
-
+
{showStatusBar &&
}
-
- {children}
+
+
+ {children}
+
{showBottomNav && }
diff --git a/frontend/components/layout/ResponsiveLayout.tsx b/frontend/components/layout/ResponsiveLayout.tsx
index 843d6e8..6a27844 100644
--- a/frontend/components/layout/ResponsiveLayout.tsx
+++ b/frontend/components/layout/ResponsiveLayout.tsx
@@ -37,7 +37,7 @@ export function ResponsiveLayout({
const closeSidebar = () => setSidebarOpen(false)
return (
-
+
{/* 移动端:汉堡菜单按钮 */}
{isMobile && !sidebarOpen && (
- {children}
+
+ {children}
+
)
diff --git a/frontend/components/ui/Modal.tsx b/frontend/components/ui/Modal.tsx
index 00b85bd..401aa6a 100644
--- a/frontend/components/ui/Modal.tsx
+++ b/frontend/components/ui/Modal.tsx
@@ -25,7 +25,15 @@ const sizeStyles = {
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
- full: 'max-w-[90vw] max-h-[90vh]',
+ full: 'max-w-[90vw]',
+};
+
+const bodyMaxHeightStyles = {
+ sm: 'max-h-[50vh]',
+ md: 'max-h-[60vh]',
+ lg: 'max-h-[65vh]',
+ xl: 'max-h-[70vh]',
+ full: 'max-h-[75vh]',
};
export const Modal: React.FC
= ({
@@ -105,7 +113,7 @@ export const Modal: React.FC = ({
)}
{/* Body */}
-
+
{children}
diff --git a/frontend/lib/platforms.ts b/frontend/lib/platforms.ts
new file mode 100644
index 0000000..1415e70
--- /dev/null
+++ b/frontend/lib/platforms.ts
@@ -0,0 +1,22 @@
+// 平台配置 - 共享给所有端使用
+export const platformOptions = [
+ { id: 'douyin', name: '抖音', icon: '🎵', bgColor: 'bg-[#25F4EE]/15', textColor: 'text-[#25F4EE]', borderColor: 'border-[#25F4EE]/30' },
+ { id: 'xiaohongshu', name: '小红书', icon: '📕', bgColor: 'bg-[#fe2c55]/15', textColor: 'text-[#fe2c55]', borderColor: 'border-[#fe2c55]/30' },
+ { id: 'bilibili', name: 'B站', icon: '📺', bgColor: 'bg-[#00a1d6]/15', textColor: 'text-[#00a1d6]', borderColor: 'border-[#00a1d6]/30' },
+ { id: 'kuaishou', name: '快手', icon: '⚡', bgColor: 'bg-[#ff4906]/15', textColor: 'text-[#ff4906]', borderColor: 'border-[#ff4906]/30' },
+ { id: 'weibo', name: '微博', icon: '🔴', bgColor: 'bg-[#e6162d]/15', textColor: 'text-[#e6162d]', borderColor: 'border-[#e6162d]/30' },
+ { id: 'wechat', name: '微信视频号', icon: '💬', bgColor: 'bg-[#07c160]/15', textColor: 'text-[#07c160]', borderColor: 'border-[#07c160]/30' },
+]
+
+export type PlatformId = typeof platformOptions[number]['id']
+
+export function getPlatformInfo(platformId: string) {
+ return platformOptions.find(p => p.id === platformId)
+}
+
+// 平台标签组件的样式类
+export function getPlatformTagClasses(platformId: string) {
+ const platform = getPlatformInfo(platformId)
+ if (!platform) return ''
+ return `${platform.bgColor} ${platform.textColor} ${platform.borderColor}`
+}