feat: 完善品牌方和代理商前端功能

品牌方功能:
- 项目看板: 添加截止日期编辑功能
- 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商
- 项目创建: 代理商选择支持搜索(名称/ID/公司名)
- 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作
- Brief配置: 新增项目级Brief和规则配置页面
- 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录

代理商功能:
- 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑
- 账户设置: 密码修改、手机/邮箱绑定、两步验证
- 通知设置: 分类型和渠道的通知开关
- 审核历史: 搜索筛选和统计展示
- 帮助反馈: FAQ分类搜索和客服联系

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-06 17:40:11 +08:00
co-authored by Claude Opus 4.5
parent ae74c515c7
commit 964797d2e9
14 changed files with 4349 additions and 154 deletions
@@ -0,0 +1,564 @@
'use client'
import { useState } from 'react'
import { useRouter, useParams } from 'next/navigation'
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 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)
alert('配置已保存')
}
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>
)
}