feat: 完善品牌方和代理商前端功能
品牌方功能: - 项目看板: 添加截止日期编辑功能 - 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商 - 项目创建: 代理商选择支持搜索(名称/ID/公司名) - 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作 - Brief配置: 新增项目级Brief和规则配置页面 - 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录 代理商功能: - 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑 - 账户设置: 密码修改、手机/邮箱绑定、两步验证 - 通知设置: 分类型和渠道的通知开关 - 审核历史: 搜索筛选和统计展示 - 帮助反馈: FAQ分类搜索和客服联系 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
ae74c515c7
commit
964797d2e9
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -5,18 +5,27 @@ import { useRouter, useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { SuccessTag, PendingTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
Users,
|
||||
FileText,
|
||||
Video,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Settings,
|
||||
Search,
|
||||
Building2,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
Check,
|
||||
Pencil
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟项目详情数据
|
||||
@@ -27,7 +36,6 @@ const mockProject = {
|
||||
deadline: '2026-06-18',
|
||||
createdAt: '2026-02-01',
|
||||
description: '618大促活动营销内容审核项目',
|
||||
briefUrl: '/briefs/xx-brand-618.pdf',
|
||||
stats: {
|
||||
scriptTotal: 20,
|
||||
scriptPassed: 15,
|
||||
@@ -39,18 +47,27 @@ const mockProject = {
|
||||
videoRejected: 3,
|
||||
},
|
||||
agencies: [
|
||||
{ id: 'agency-001', name: '星耀传媒', creatorCount: 8, passRate: 92 },
|
||||
{ id: 'agency-002', name: '创意无限', creatorCount: 5, passRate: 88 },
|
||||
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 2, passRate: 75 },
|
||||
{ id: 'AG789012', name: '星耀传媒', creatorCount: 8, passRate: 92 },
|
||||
{ id: 'AG456789', name: '创意无限', creatorCount: 5, passRate: 88 },
|
||||
],
|
||||
recentTasks: [
|
||||
{ id: 'task-001', type: 'video', creatorName: '小美护肤', status: 'pending', submittedAt: '2026-02-06 14:30' },
|
||||
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', status: 'approved', submittedAt: '2026-02-06 12:15' },
|
||||
{ id: 'task-003', type: 'video', creatorName: '健身王', status: 'rejected', submittedAt: '2026-02-06 10:00' },
|
||||
{ id: 'task-004', type: 'script', creatorName: '时尚达人', status: 'pending', submittedAt: '2026-02-05 16:45' },
|
||||
{ id: 'task-001', type: 'video', creatorName: '小美护肤', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'pending', submittedAt: '2026-02-06 14:30' },
|
||||
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'approved', submittedAt: '2026-02-06 12:15' },
|
||||
{ id: 'task-003', type: 'video', creatorName: '健身王', agencyId: 'AG456789', agencyName: '创意无限', status: 'rejected', submittedAt: '2026-02-06 10:00' },
|
||||
{ id: 'task-004', type: 'script', creatorName: '时尚达人', agencyId: 'AG456789', agencyName: '创意无限', status: 'pending', submittedAt: '2026-02-05 16:45' },
|
||||
],
|
||||
}
|
||||
|
||||
// 模拟品牌方已添加的代理商(来自代理商管理)
|
||||
const mockManagedAgencies = [
|
||||
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司' },
|
||||
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司' },
|
||||
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司' },
|
||||
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司' },
|
||||
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司' },
|
||||
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司' },
|
||||
]
|
||||
|
||||
function StatCard({ title, value, icon: Icon, color }: { title: string; value: number | string; icon: React.ElementType; color: string }) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -81,11 +98,85 @@ function TaskStatusTag({ status }: { status: string }) {
|
||||
export default function ProjectDetailPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const project = mockProject
|
||||
const projectId = params.id as string
|
||||
const [project, setProject] = useState(mockProject)
|
||||
|
||||
// 添加代理商相关状态
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
|
||||
|
||||
// 代理商操作菜单
|
||||
const [activeAgencyMenu, setActiveAgencyMenu] = useState<string | null>(null)
|
||||
|
||||
// 删除确认
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [agencyToDelete, setAgencyToDelete] = useState<typeof project.agencies[0] | null>(null)
|
||||
|
||||
// 编辑截止日期
|
||||
const [showDeadlineModal, setShowDeadlineModal] = useState(false)
|
||||
const [newDeadline, setNewDeadline] = useState(project.deadline)
|
||||
|
||||
// 保存截止日期
|
||||
const handleSaveDeadline = () => {
|
||||
if (!newDeadline) return
|
||||
setProject({ ...project, deadline: newDeadline })
|
||||
setShowDeadlineModal(false)
|
||||
}
|
||||
|
||||
const scriptPassRate = Math.round((project.stats.scriptPassed / project.stats.scriptTotal) * 100)
|
||||
const videoPassRate = Math.round((project.stats.videoPassed / project.stats.videoTotal) * 100)
|
||||
|
||||
// 过滤可添加的代理商(排除已在项目中的)
|
||||
const availableAgencies = mockManagedAgencies.filter(
|
||||
agency => !project.agencies.some(a => a.id === agency.id)
|
||||
)
|
||||
|
||||
// 搜索过滤
|
||||
const filteredAgencies = availableAgencies.filter(agency =>
|
||||
searchQuery === '' ||
|
||||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// 切换选择
|
||||
const toggleSelectAgency = (agencyId: string) => {
|
||||
setSelectedAgencies(prev =>
|
||||
prev.includes(agencyId)
|
||||
? prev.filter(id => id !== agencyId)
|
||||
: [...prev, agencyId]
|
||||
)
|
||||
}
|
||||
|
||||
// 确认添加
|
||||
const handleAddAgencies = () => {
|
||||
const newAgencies = mockManagedAgencies
|
||||
.filter(a => selectedAgencies.includes(a.id))
|
||||
.map(a => ({ id: a.id, name: a.name, creatorCount: 0, passRate: 0 }))
|
||||
|
||||
setProject({
|
||||
...project,
|
||||
agencies: [...project.agencies, ...newAgencies]
|
||||
})
|
||||
|
||||
setShowAddModal(false)
|
||||
setSelectedAgencies([])
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
// 移除代理商
|
||||
const handleRemoveAgency = async () => {
|
||||
if (!agencyToDelete) return
|
||||
|
||||
setProject({
|
||||
...project,
|
||||
agencies: project.agencies.filter(a => a.id !== agencyToDelete.id)
|
||||
})
|
||||
setShowDeleteModal(false)
|
||||
setAgencyToDelete(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部导航 */}
|
||||
@@ -105,17 +196,44 @@ export default function ProjectDetailPage() {
|
||||
<span className="flex items-center gap-2">
|
||||
<Calendar size={16} />
|
||||
截止日期: {project.deadline}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewDeadline(project.deadline)
|
||||
setShowDeadlineModal(true)
|
||||
}}
|
||||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||||
title="修改截止日期"
|
||||
>
|
||||
<Pencil size={14} className="text-text-tertiary hover:text-accent-indigo" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock size={16} />
|
||||
创建时间: {project.createdAt}
|
||||
</span>
|
||||
<Link href={project.briefUrl} className="flex items-center gap-2 text-accent-indigo hover:underline">
|
||||
<FileText size={16} />
|
||||
查看 Brief
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Brief和规则配置 - 大按钮 */}
|
||||
<Link href={`/brand/projects/${projectId}/config`}>
|
||||
<Card className="hover:border-accent-indigo transition-colors cursor-pointer">
|
||||
<CardContent className="py-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Settings size={24} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Brief和规则配置</p>
|
||||
<p className="text-sm text-text-secondary">配置项目Brief、审核规则、AI检测项等</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-text-tertiary" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<StatCard title="脚本通过率" value={`${scriptPassRate}%`} icon={FileText} color="text-accent-green" />
|
||||
@@ -197,20 +315,58 @@ export default function ProjectDetailPage() {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users size={16} />
|
||||
参与代理商
|
||||
<span className="text-sm font-normal text-text-tertiary">({project.agencies.length})</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-2">
|
||||
{project.agencies.map((agency) => (
|
||||
<div key={agency.id} className="p-3 rounded-lg bg-bg-elevated">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||||
<span className={`text-sm font-medium ${agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
|
||||
{agency.passRate}%
|
||||
</span>
|
||||
<div key={agency.id} className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Building2 size={18} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary text-sm">{agency.name}</p>
|
||||
<p className="text-xs text-text-tertiary">{agency.creatorCount} 位达人 · 通过率 {agency.passRate}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveAgencyMenu(activeAgencyMenu === agency.id ? null : agency.id)}
|
||||
className="p-1.5 rounded hover:bg-bg-page transition-colors"
|
||||
>
|
||||
<MoreHorizontal size={16} className="text-text-tertiary" />
|
||||
</button>
|
||||
{activeAgencyMenu === agency.id && (
|
||||
<div className="absolute right-0 top-8 z-10 w-32 py-1 bg-bg-card rounded-lg shadow-lg border border-border-subtle">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAgencyToDelete(agency)
|
||||
setShowDeleteModal(true)
|
||||
setActiveAgencyMenu(null)
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-accent-coral hover:bg-bg-elevated flex items-center gap-2"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">{agency.creatorCount} 位达人参与</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 添加代理商按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="w-full p-3 rounded-lg border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-indigo"
|
||||
>
|
||||
<Plus size={18} />
|
||||
<span className="text-sm font-medium">添加代理商</span>
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -234,6 +390,7 @@ export default function ProjectDetailPage() {
|
||||
<tr className="border-b border-border-subtle text-left text-sm text-text-secondary">
|
||||
<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>
|
||||
<th className="pb-3 font-medium">提交时间</th>
|
||||
<th className="pb-3 font-medium">操作</th>
|
||||
@@ -249,6 +406,12 @@ export default function ProjectDetailPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 text-text-primary">{task.creatorName}</td>
|
||||
<td className="py-4">
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-bg-elevated text-sm">
|
||||
<Building2 size={14} className="text-accent-indigo" />
|
||||
<span className="text-text-secondary">{task.agencyName}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4"><TaskStatusTag status={task.status} /></td>
|
||||
<td className="py-4 text-sm text-text-tertiary">{task.submittedAt}</td>
|
||||
<td className="py-4">
|
||||
@@ -265,6 +428,176 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 添加代理商弹窗 */}
|
||||
<Modal
|
||||
isOpen={showAddModal}
|
||||
onClose={() => {
|
||||
setShowAddModal(false)
|
||||
setSearchQuery('')
|
||||
setSelectedAgencies([])
|
||||
}}
|
||||
title="添加代理商"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative">
|
||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索代理商名称或ID..."
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 代理商列表 */}
|
||||
<div className="max-h-80 overflow-y-auto space-y-2">
|
||||
{filteredAgencies.length > 0 ? (
|
||||
filteredAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleSelectAgency(agency.id)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-xl border-2 transition-all text-left ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/5'
|
||||
: 'border-transparent bg-bg-elevated hover:bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
|
||||
}`}>
|
||||
{isSelected ? (
|
||||
<Check size={20} className="text-white" />
|
||||
) : (
|
||||
<Building2 size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-text-primary">{agency.name}</p>
|
||||
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary truncate">{agency.companyName}</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
{availableAgencies.length === 0 ? (
|
||||
<>
|
||||
<Users size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>所有代理商都已添加到此项目</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>未找到匹配的代理商</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 已选择提示 */}
|
||||
{selectedAgencies.length > 0 && (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
|
||||
<span className="text-sm text-text-secondary">
|
||||
已选择 <span className="text-accent-indigo font-medium">{selectedAgencies.length}</span> 个代理商
|
||||
</span>
|
||||
<Button variant="primary" onClick={handleAddAgencies}>
|
||||
<Plus size={16} />
|
||||
确认添加
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部提示 */}
|
||||
<p className="text-xs text-text-tertiary pt-2">
|
||||
仅显示已在"代理商管理"中添加的代理商,如需添加新代理商请先前往代理商管理
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}
|
||||
title="移除代理商"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary">
|
||||
确定要将 <span className="text-text-primary font-medium">{agencyToDelete?.name}</span> 从此项目中移除吗?
|
||||
</p>
|
||||
<p className="text-sm text-accent-coral">
|
||||
移除后,该代理商下的达人将无法继续参与此项目的任务。
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="secondary" className="flex-1" onClick={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1 bg-accent-coral hover:bg-accent-coral/80"
|
||||
onClick={handleRemoveAgency}
|
||||
>
|
||||
确认移除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑截止日期弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDeadlineModal}
|
||||
onClose={() => setShowDeadlineModal(false)}
|
||||
title="修改截止日期"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-lg bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">项目名称</p>
|
||||
<p className="font-medium text-text-primary">{project.name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
新截止日期
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="date"
|
||||
value={newDeadline}
|
||||
onChange={(e) => setNewDeadline(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => setShowDeadlineModal(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
onClick={handleSaveDeadline}
|
||||
disabled={!newDeadline}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,15 +12,19 @@ import {
|
||||
FileText,
|
||||
CheckCircle,
|
||||
X,
|
||||
Users
|
||||
Users,
|
||||
Search,
|
||||
Building2
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟代理商列表
|
||||
// 模拟品牌方已添加的代理商(来自代理商管理)
|
||||
const mockAgencies = [
|
||||
{ id: 'agency-001', name: '星耀传媒', creatorCount: 50, passRate: 92 },
|
||||
{ id: 'agency-002', name: '创意无限', creatorCount: 35, passRate: 88 },
|
||||
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 28, passRate: 82 },
|
||||
{ id: 'agency-004', name: '时尚风向标', creatorCount: 42, passRate: 85 },
|
||||
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司', creatorCount: 50, passRate: 92 },
|
||||
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司', creatorCount: 35, passRate: 88 },
|
||||
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司', creatorCount: 28, passRate: 82 },
|
||||
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司', creatorCount: 42, passRate: 85 },
|
||||
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司', creatorCount: 30, passRate: 90 },
|
||||
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司', creatorCount: 25, passRate: 87 },
|
||||
]
|
||||
|
||||
export default function CreateProjectPage() {
|
||||
@@ -30,6 +34,15 @@ export default function CreateProjectPage() {
|
||||
const [briefFile, setBriefFile] = useState<File | null>(null)
|
||||
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [agencySearch, setAgencySearch] = useState('')
|
||||
|
||||
// 搜索过滤代理商
|
||||
const filteredAgencies = mockAgencies.filter(agency =>
|
||||
agencySearch === '' ||
|
||||
agency.name.toLowerCase().includes(agencySearch.toLowerCase()) ||
|
||||
agency.id.toLowerCase().includes(agencySearch.toLowerCase()) ||
|
||||
agency.companyName.toLowerCase().includes(agencySearch.toLowerCase())
|
||||
)
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -145,41 +158,76 @@ export default function CreateProjectPage() {
|
||||
已选择 {selectedAgencies.length} 个
|
||||
</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mockAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleAgency(agency.id)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-text-primary">{agency.name}</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={14} />
|
||||
{agency.creatorCount} 达人
|
||||
</span>
|
||||
<span className={agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}>
|
||||
通过率 {agency.passRate}%
|
||||
</span>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="relative mb-4">
|
||||
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
value={agencySearch}
|
||||
onChange={(e) => setAgencySearch(e.target.value)}
|
||||
placeholder="搜索代理商名称、ID或公司名..."
|
||||
className="w-full pl-11 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 代理商列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-80 overflow-y-auto">
|
||||
{filteredAgencies.length > 0 ? (
|
||||
filteredAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleAgency(agency.id)}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
|
||||
}`}>
|
||||
{isSelected ? (
|
||||
<CheckCircle size={20} className="text-white" />
|
||||
) : (
|
||||
<Building2 size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||||
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary truncate mt-0.5">{agency.companyName}</p>
|
||||
<div className="flex items-center gap-4 mt-1.5 text-xs text-text-tertiary">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
{agency.creatorCount} 达人
|
||||
</span>
|
||||
<span className={agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}>
|
||||
通过率 {agency.passRate}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckCircle size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-2 text-center py-8 text-text-tertiary">
|
||||
<Search size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>未找到匹配的代理商</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-text-tertiary mt-3">
|
||||
仅显示已在"代理商管理"中添加的代理商
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
||||
Reference in New Issue
Block a user