主要更新: - 更新代理商端文档,明确项目由品牌方分配流程 - 新增Brief配置详情页(已配置)设计稿 - 完善工作台紧急待办中品牌新任务功能 - 整理Pencil设计文件中代理商端页面顺序 - 新增后端FastAPI框架及核心API - 新增前端Next.js页面和组件库 - 添加.gitignore排除构建和缓存文件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
256 lines
9.4 KiB
TypeScript
256 lines
9.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Plus, Shield, AlertTriangle, Ban, Building2 } 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 mockRules = {
|
|
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' },
|
|
],
|
|
competitors: [
|
|
{ id: '1', name: '竞品A', logoUrl: '' },
|
|
{ id: '2', name: '竞品B', logoUrl: '' },
|
|
{ id: '3', name: '竞品C', logoUrl: '' },
|
|
],
|
|
whitelist: [
|
|
{ id: '1', term: '品牌专属术语1', reason: '品牌授权使用' },
|
|
{ id: '2', term: '特定产品名', reason: '官方产品名称' },
|
|
],
|
|
}
|
|
|
|
const categoryOptions = [
|
|
{ value: 'absolute_term', label: '极限词' },
|
|
{ value: 'false_claim', label: '虚假宣称' },
|
|
{ value: 'platform_rule', label: '平台规则' },
|
|
{ value: 'custom', label: '自定义' },
|
|
]
|
|
|
|
const severityOptions = [
|
|
{ value: 'high', label: '高风险' },
|
|
{ value: 'medium', label: '中风险' },
|
|
{ value: 'low', label: '低风险' },
|
|
]
|
|
|
|
function SeverityTag({ severity }: { severity: string }) {
|
|
if (severity === 'high') return <ErrorTag>高风险</ErrorTag>
|
|
if (severity === 'medium') return <WarningTag>中风险</WarningTag>
|
|
return <SuccessTag>低风险</SuccessTag>
|
|
}
|
|
|
|
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 handleAddWord = () => {
|
|
if (!newWord.trim()) return
|
|
// TODO: 调用 API 添加
|
|
setShowAddModal(false)
|
|
setNewWord('')
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-gray-900">规则配置</h1>
|
|
</div>
|
|
|
|
{/* 标签页 */}
|
|
<div className="flex gap-2 border-b">
|
|
<button
|
|
type="button"
|
|
className={`px-4 py-2 border-b-2 transition-colors ${
|
|
activeTab === 'forbidden'
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
onClick={() => setActiveTab('forbidden')}
|
|
>
|
|
<Ban size={16} className="inline mr-2" />
|
|
违禁词 ({mockRules.forbiddenWords.length})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`px-4 py-2 border-b-2 transition-colors ${
|
|
activeTab === 'competitors'
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
onClick={() => setActiveTab('competitors')}
|
|
>
|
|
<Building2 size={16} className="inline mr-2" />
|
|
竞品列表 ({mockRules.competitors.length})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`px-4 py-2 border-b-2 transition-colors ${
|
|
activeTab === 'whitelist'
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
onClick={() => setActiveTab('whitelist')}
|
|
>
|
|
<Shield size={16} className="inline mr-2" />
|
|
白名单 ({mockRules.whitelist.length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* 违禁词列表 */}
|
|
{activeTab === 'forbidden' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>违禁词列表</span>
|
|
<Button size="sm" icon={Plus} onClick={() => setShowAddModal(true)}>
|
|
添加
|
|
</Button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b text-left text-sm text-gray-500">
|
|
<th className="pb-3 font-medium">词汇</th>
|
|
<th className="pb-3 font-medium">分类</th>
|
|
<th className="pb-3 font-medium">风险等级</th>
|
|
<th className="pb-3 font-medium">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{mockRules.forbiddenWords.map((word) => (
|
|
<tr key={word.id} className="border-b last:border-0">
|
|
<td className="py-3 font-medium text-gray-900">{word.word}</td>
|
|
<td className="py-3 text-gray-600">{word.category}</td>
|
|
<td className="py-3"><SeverityTag severity={word.severity} /></td>
|
|
<td className="py-3">
|
|
<Button size="sm" variant="ghost">删除</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 竞品列表 */}
|
|
{activeTab === 'competitors' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>竞品列表</span>
|
|
<Button size="sm" icon={Plus}>添加竞品</Button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
系统将在视频中检测以下竞品的 Logo 或品牌名称
|
|
</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{mockRules.competitors.map((competitor) => (
|
|
<div key={competitor.id} className="p-4 border rounded-lg flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<Building2 size={20} className="text-gray-400" />
|
|
</div>
|
|
<span className="font-medium">{competitor.name}</span>
|
|
</div>
|
|
<Button size="sm" variant="ghost">删除</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 白名单 */}
|
|
{activeTab === 'whitelist' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>白名单</span>
|
|
<Button size="sm" icon={Plus}>添加</Button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
白名单中的词汇即使命中违禁词也不会触发告警
|
|
</p>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b text-left text-sm text-gray-500">
|
|
<th className="pb-3 font-medium">词汇</th>
|
|
<th className="pb-3 font-medium">原因</th>
|
|
<th className="pb-3 font-medium">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{mockRules.whitelist.map((item) => (
|
|
<tr key={item.id} className="border-b last:border-0">
|
|
<td className="py-3 font-medium text-gray-900">{item.term}</td>
|
|
<td className="py-3 text-gray-600">{item.reason}</td>
|
|
<td className="py-3">
|
|
<Button size="sm" variant="ghost">删除</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 添加违禁词弹窗 */}
|
|
<Modal
|
|
isOpen={showAddModal}
|
|
onClose={() => setShowAddModal(false)}
|
|
title="添加违禁词"
|
|
size="sm"
|
|
>
|
|
<div className="space-y-4">
|
|
<Input
|
|
label="违禁词"
|
|
placeholder="输入违禁词"
|
|
value={newWord}
|
|
onChange={(e) => setNewWord(e.target.value)}
|
|
/>
|
|
<Select
|
|
label="分类"
|
|
options={categoryOptions}
|
|
value={newCategory}
|
|
onChange={(e) => setNewCategory(e.target.value)}
|
|
/>
|
|
<Select
|
|
label="风险等级"
|
|
options={severityOptions}
|
|
value={newSeverity}
|
|
onChange={(e) => setNewSeverity(e.target.value)}
|
|
/>
|
|
<div className="flex gap-3 justify-end pt-4">
|
|
<Button variant="ghost" onClick={() => setShowAddModal(false)}>取消</Button>
|
|
<Button onClick={handleAddWord}>添加</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|