品牌方功能: - 项目看板: 添加截止日期编辑功能 - 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商 - 项目创建: 代理商选择支持搜索(名称/ID/公司名) - 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作 - Brief配置: 新增项目级Brief和规则配置页面 - 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录 代理商功能: - 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑 - 账户设置: 密码修改、手机/邮箱绑定、两步验证 - 通知设置: 分类型和渠道的通知开关 - 审核历史: 搜索筛选和统计展示 - 帮助反馈: FAQ分类搜索和客服联系 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
565 lines
22 KiB
TypeScript
565 lines
22 KiB
TypeScript
'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>
|
||
)
|
||
}
|