Your Name 37ac749071 fix: 修复前端代码质量问题
- 创建 Toast 通知组件,替换所有 alert() 调用
- 修复 useReview hook 内存泄漏(setInterval 清理)
- 移除所有 console.error 和 console.log 语句
- 为复制操作失败添加用户友好的 toast 提示

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:48:22 +08:00

567 lines
22 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 } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useToast } from '@/components/ui/Toast'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import {
ArrowLeft,
FileText,
Shield,
Settings,
Plus,
Trash2,
AlertTriangle,
CheckCircle,
Video,
Bot,
Users,
Save,
Upload,
Download,
ChevronDown,
ChevronUp
} from 'lucide-react'
// 模拟数据
const mockData = {
project: {
id: 'proj-001',
name: 'XX品牌618推广',
},
brief: {
title: 'XX品牌618推广Brief',
description: '本次618大促营销活动需要达人围绕夏日护肤、美妆新品进行内容创作。',
requirements: [
'视频时长60-90秒',
'必须展示产品使用过程',
'需要口播品牌slogan"XX品牌夏日焕新"',
'背景音乐需使用品牌指定曲库',
],
keywords: ['夏日护肤', '清爽', '补水', '防晒', '焕新'],
forbiddenWords: ['最好', '第一', '绝对', '100%'],
referenceLinks: [
{ title: '品牌视觉指南', url: 'https://example.com/brand-guide.pdf' },
{ title: '产品资料包', url: 'https://example.com/product-pack.zip' },
],
deadline: '2026-06-10',
},
rules: {
aiReview: {
enabled: true,
strictness: 'medium', // low, medium, high
checkItems: [
{ id: 'forbidden_words', name: '违禁词检测', enabled: true },
{ id: 'competitor', name: '竞品提及检测', enabled: true },
{ id: 'brand_tone', name: '品牌调性检测', enabled: true },
{ id: 'duration', name: '视频时长检测', enabled: true },
{ id: 'music', name: '背景音乐检测', enabled: false },
],
},
manualReview: {
scriptRequired: true,
videoRequired: true,
agencyCanApprove: true, // 代理商是否有终审权限
brandFinalReview: true, // 品牌方是否需要终审
},
appealRules: {
maxAppeals: 3, // 最大申诉次数
appealDeadline: 48, // 申诉处理时限(小时)
},
},
}
// 严格程度选项
const strictnessOptions = [
{ value: 'low', label: '宽松', description: '仅检测明显违规内容' },
{ value: 'medium', label: '标准', description: '平衡检测,推荐使用' },
{ value: 'high', label: '严格', description: '严格检测,可能有较多误判' },
]
export default function ProjectConfigPage() {
const router = useRouter()
const params = useParams()
const toast = useToast()
const projectId = params.id as string
const [brief, setBrief] = useState(mockData.brief)
const [rules, setRules] = useState(mockData.rules)
const [isSaving, setIsSaving] = useState(false)
const [activeSection, setActiveSection] = useState<string | null>('brief')
// 新增需求
const [newRequirement, setNewRequirement] = useState('')
// 新增关键词
const [newKeyword, setNewKeyword] = useState('')
// 新增违禁词
const [newForbiddenWord, setNewForbiddenWord] = useState('')
const handleSave = async () => {
setIsSaving(true)
await new Promise(resolve => setTimeout(resolve, 1000))
setIsSaving(false)
toast.success('配置已保存')
}
const addRequirement = () => {
if (newRequirement.trim()) {
setBrief({ ...brief, requirements: [...brief.requirements, newRequirement.trim()] })
setNewRequirement('')
}
}
const removeRequirement = (index: number) => {
setBrief({ ...brief, requirements: brief.requirements.filter((_, i) => i !== index) })
}
const addKeyword = () => {
if (newKeyword.trim() && !brief.keywords.includes(newKeyword.trim())) {
setBrief({ ...brief, keywords: [...brief.keywords, newKeyword.trim()] })
setNewKeyword('')
}
}
const removeKeyword = (keyword: string) => {
setBrief({ ...brief, keywords: brief.keywords.filter(k => k !== keyword) })
}
const addForbiddenWord = () => {
if (newForbiddenWord.trim() && !brief.forbiddenWords.includes(newForbiddenWord.trim())) {
setBrief({ ...brief, forbiddenWords: [...brief.forbiddenWords, newForbiddenWord.trim()] })
setNewForbiddenWord('')
}
}
const removeForbiddenWord = (word: string) => {
setBrief({ ...brief, forbiddenWords: brief.forbiddenWords.filter(w => w !== word) })
}
const toggleAiCheckItem = (itemId: string) => {
setRules({
...rules,
aiReview: {
...rules.aiReview,
checkItems: rules.aiReview.checkItems.map(item =>
item.id === itemId ? { ...item, enabled: !item.enabled } : item
),
},
})
}
const SectionHeader = ({ title, icon: Icon, section }: { title: string; icon: React.ElementType; section: string }) => (
<button
type="button"
onClick={() => setActiveSection(activeSection === section ? null : section)}
className="w-full flex items-center justify-between p-4 hover:bg-bg-elevated/50 rounded-xl transition-colors"
>
<span className="flex items-center gap-2 font-semibold text-text-primary">
<Icon size={18} className="text-accent-indigo" />
{title}
</span>
{activeSection === section ? (
<ChevronUp size={18} className="text-text-tertiary" />
) : (
<ChevronDown size={18} className="text-text-tertiary" />
)}
</button>
)
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => router.back()}
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
>
<ArrowLeft size={20} className="text-text-secondary" />
</button>
<div>
<h1 className="text-2xl font-bold text-text-primary">Brief和规则配置</h1>
<p className="text-sm text-text-secondary mt-0.5">
{mockData.project.name}
</p>
</div>
</div>
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
{isSaving ? '保存中...' : (
<>
<Save size={16} />
</>
)}
</Button>
</div>
{/* Brief配置 */}
<Card>
<SectionHeader title="Brief配置" icon={FileText} section="brief" />
{activeSection === 'brief' && (
<CardContent className="space-y-6 pt-0">
{/* 基本信息 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-text-secondary mb-1.5 block">Brief标题</label>
<Input
value={brief.title}
onChange={(e) => setBrief({ ...brief, title: e.target.value })}
/>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<Input
type="date"
value={brief.deadline}
onChange={(e) => setBrief({ ...brief, deadline: e.target.value })}
/>
</div>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<textarea
value={brief.description}
onChange={(e) => setBrief({ ...brief, description: e.target.value })}
className="w-full h-24 p-3 rounded-xl bg-bg-elevated border border-border-subtle text-text-primary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
/>
</div>
{/* 创作要求 */}
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<div className="space-y-2">
{brief.requirements.map((req, index) => (
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-bg-elevated">
<CheckCircle size={16} className="text-accent-green flex-shrink-0" />
<span className="flex-1 text-text-primary">{req}</span>
<button
type="button"
onClick={() => removeRequirement(index)}
className="p-1 rounded hover:bg-bg-page text-text-tertiary hover:text-accent-coral transition-colors"
>
<Trash2 size={14} />
</button>
</div>
))}
<div className="flex gap-2">
<Input
value={newRequirement}
onChange={(e) => setNewRequirement(e.target.value)}
placeholder="添加新的创作要求"
onKeyDown={(e) => e.key === 'Enter' && addRequirement()}
/>
<Button variant="secondary" onClick={addRequirement}>
<Plus size={16} />
</Button>
</div>
</div>
</div>
{/* 关键词 */}
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<div className="flex flex-wrap gap-2 mb-3">
{brief.keywords.map((keyword) => (
<span
key={keyword}
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-accent-indigo/15 text-accent-indigo text-sm"
>
{keyword}
<button
type="button"
onClick={() => removeKeyword(keyword)}
className="hover:text-accent-coral transition-colors"
>
×
</button>
</span>
))}
</div>
<div className="flex gap-2">
<Input
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
placeholder="添加关键词"
onKeyDown={(e) => e.key === 'Enter' && addKeyword()}
/>
<Button variant="secondary" onClick={addKeyword}>
<Plus size={16} />
</Button>
</div>
</div>
{/* 违禁词 */}
<div>
<label className="text-sm text-text-secondary mb-2 block flex items-center gap-2">
<AlertTriangle size={14} className="text-accent-coral" />
</label>
<div className="flex flex-wrap gap-2 mb-3">
{brief.forbiddenWords.map((word) => (
<span
key={word}
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-accent-coral/15 text-accent-coral text-sm"
>
{word}
<button
type="button"
onClick={() => removeForbiddenWord(word)}
className="hover:text-accent-coral/70 transition-colors"
>
×
</button>
</span>
))}
</div>
<div className="flex gap-2">
<Input
value={newForbiddenWord}
onChange={(e) => setNewForbiddenWord(e.target.value)}
placeholder="添加违禁词"
onKeyDown={(e) => e.key === 'Enter' && addForbiddenWord()}
/>
<Button variant="secondary" onClick={addForbiddenWord}>
<Plus size={16} />
</Button>
</div>
</div>
{/* 参考资料 */}
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<div className="space-y-2">
{brief.referenceLinks.map((link, index) => (
<div key={index} className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated">
<FileText size={16} className="text-accent-indigo" />
<span className="flex-1 text-text-primary">{link.title}</span>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-accent-indigo hover:underline text-sm"
>
</a>
</div>
))}
<Button variant="secondary" className="w-full">
<Upload size={16} />
</Button>
</div>
</div>
</CardContent>
)}
</Card>
{/* AI审核规则 */}
<Card>
<SectionHeader title="AI审核规则" icon={Bot} section="ai" />
{activeSection === 'ai' && (
<CardContent className="space-y-6 pt-0">
{/* AI审核开关 */}
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
<div>
<p className="font-medium text-text-primary">AI自动审核</p>
<p className="text-sm text-text-secondary">AI预审</p>
</div>
<button
type="button"
onClick={() => setRules({ ...rules, aiReview: { ...rules.aiReview, enabled: !rules.aiReview.enabled } })}
className={`relative w-12 h-6 rounded-full transition-colors ${
rules.aiReview.enabled ? 'bg-accent-indigo' : 'bg-bg-page'
}`}
>
<span
className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
rules.aiReview.enabled ? 'left-7' : 'left-1'
}`}
/>
</button>
</div>
{rules.aiReview.enabled && (
<>
{/* 严格程度 */}
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<div className="grid grid-cols-3 gap-3">
{strictnessOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setRules({ ...rules, aiReview: { ...rules.aiReview, strictness: option.value } })}
className={`p-4 rounded-xl border-2 text-left transition-all ${
rules.aiReview.strictness === option.value
? 'border-accent-indigo bg-accent-indigo/10'
: 'border-border-subtle hover:border-border-subtle/80'
}`}
>
<p className={`font-medium ${rules.aiReview.strictness === option.value ? 'text-accent-indigo' : 'text-text-primary'}`}>
{option.label}
</p>
<p className="text-xs text-text-tertiary mt-1">{option.description}</p>
</button>
))}
</div>
</div>
{/* 检测项目 */}
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<div className="space-y-2">
{rules.aiReview.checkItems.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated"
>
<span className="text-text-primary">{item.name}</span>
<button
type="button"
onClick={() => toggleAiCheckItem(item.id)}
className={`relative w-10 h-5 rounded-full transition-colors ${
item.enabled ? 'bg-accent-green' : 'bg-bg-page'
}`}
>
<span
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
item.enabled ? 'left-5' : 'left-0.5'
}`}
/>
</button>
</div>
))}
</div>
</div>
</>
)}
</CardContent>
)}
</Card>
{/* 人工审核规则 */}
<Card>
<SectionHeader title="人工审核规则" icon={Users} section="manual" />
{activeSection === 'manual' && (
<CardContent className="space-y-4 pt-0">
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-secondary">/</p>
</div>
<button
type="button"
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, scriptRequired: !rules.manualReview.scriptRequired } })}
className={`relative w-12 h-6 rounded-full transition-colors ${
rules.manualReview.scriptRequired ? 'bg-accent-indigo' : 'bg-bg-page'
}`}
>
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
rules.manualReview.scriptRequired ? 'left-7' : 'left-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-secondary">/</p>
</div>
<button
type="button"
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, videoRequired: !rules.manualReview.videoRequired } })}
className={`relative w-12 h-6 rounded-full transition-colors ${
rules.manualReview.videoRequired ? 'bg-accent-indigo' : 'bg-bg-page'
}`}
>
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
rules.manualReview.videoRequired ? 'left-7' : 'left-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-secondary">/</p>
</div>
<button
type="button"
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, agencyCanApprove: !rules.manualReview.agencyCanApprove } })}
className={`relative w-12 h-6 rounded-full transition-colors ${
rules.manualReview.agencyCanApprove ? 'bg-accent-indigo' : 'bg-bg-page'
}`}
>
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
rules.manualReview.agencyCanApprove ? 'left-7' : 'left-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-secondary"></p>
</div>
<button
type="button"
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, brandFinalReview: !rules.manualReview.brandFinalReview } })}
className={`relative w-12 h-6 rounded-full transition-colors ${
rules.manualReview.brandFinalReview ? 'bg-accent-indigo' : 'bg-bg-page'
}`}
>
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
rules.manualReview.brandFinalReview ? 'left-7' : 'left-1'
}`} />
</button>
</div>
</CardContent>
)}
</Card>
{/* 申诉规则 */}
<Card>
<SectionHeader title="申诉规则" icon={Shield} section="appeal" />
{activeSection === 'appeal' && (
<CardContent className="space-y-4 pt-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<Input
type="number"
min={1}
max={10}
value={rules.appealRules.maxAppeals}
onChange={(e) => setRules({
...rules,
appealRules: { ...rules.appealRules, maxAppeals: parseInt(e.target.value) || 1 }
})}
/>
<p className="text-xs text-text-tertiary mt-1"></p>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<Input
type="number"
min={1}
max={168}
value={rules.appealRules.appealDeadline}
onChange={(e) => setRules({
...rules,
appealRules: { ...rules.appealRules, appealDeadline: parseInt(e.target.value) || 24 }
})}
/>
<p className="text-xs text-text-tertiary mt-1"></p>
</div>
</div>
</CardContent>
)}
</Card>
</div>
)
}