Your Name 4753626e5a feat: 完成代理商/品牌方前端及文档更新
代理商端前端:
- 新增达人管理页面(含任务申诉次数管理)
- 新增消息中心(含申诉次数申请审批)
- 新增 Brief 管理(列表、详情)
- 新增审核中心(脚本审核、视频审核)
- 新增数据报表页面

品牌方端前端:
- 优化首页仪表盘布局
- 新增项目管理(列表、详情、创建)
- 新增代理商管理页面
- 新增审核中心(脚本终审、视频终审)
- 新增系统设置页面

文档更新:
- 申诉次数改为按任务分配(每任务初始1次)
- 更新 PRD、FeatureSummary、User_Role_Interfaces 等文档
- 更新 UI 设计规范和开发计划

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:39:23 +08:00

317 lines
12 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
import {
ArrowLeft,
FileText,
Upload,
CheckCircle,
Plus,
X,
Save,
Sparkles,
Target,
Ban,
AlertTriangle
} from 'lucide-react'
// 模拟 Brief 详情
const mockBrief = {
id: 'brief-001',
projectName: 'XX品牌618推广',
brandName: 'XX护肤品牌',
status: 'configured',
fileName: 'XX品牌618推广Brief.pdf',
uploadedAt: '2026-02-01',
configuredAt: '2026-02-02',
sellingPoints: [
{ id: 'sp1', content: 'SPF50+ PA++++', required: true },
{ id: 'sp2', content: '轻薄质地,不油腻', required: true },
{ id: 'sp3', content: '延展性好,易推开', required: false },
{ id: 'sp4', content: '适合敏感肌', required: false },
{ id: 'sp5', content: '夏日必备防晒', required: true },
],
blacklistWords: [
{ id: 'bw1', word: '最好', reason: '绝对化用语' },
{ id: 'bw2', word: '第一', reason: '绝对化用语' },
{ id: 'bw3', word: '神器', reason: '夸大宣传' },
{ id: 'bw4', word: '完美', reason: '绝对化用语' },
],
aiParsedContent: {
productName: 'XX品牌防晒霜',
targetAudience: '18-35岁女性',
contentRequirements: '需展示产品质地、使用效果',
restrictions: '不可提及竞品,不可使用绝对化用语',
},
}
export default function BriefConfigPage() {
const router = useRouter()
const params = useParams()
const [brief, setBrief] = useState(mockBrief)
const [newSellingPoint, setNewSellingPoint] = useState('')
const [newBlacklistWord, setNewBlacklistWord] = useState('')
const [isAIParsing, setIsAIParsing] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const handleAIParse = async () => {
setIsAIParsing(true)
// 模拟 AI 解析
await new Promise(resolve => setTimeout(resolve, 2000))
setIsAIParsing(false)
alert('AI 解析完成!')
}
const addSellingPoint = () => {
if (!newSellingPoint.trim()) return
setBrief(prev => ({
...prev,
sellingPoints: [...prev.sellingPoints, { id: `sp${Date.now()}`, content: newSellingPoint, required: false }]
}))
setNewSellingPoint('')
}
const removeSellingPoint = (id: string) => {
setBrief(prev => ({
...prev,
sellingPoints: prev.sellingPoints.filter(sp => sp.id !== id)
}))
}
const toggleRequired = (id: string) => {
setBrief(prev => ({
...prev,
sellingPoints: prev.sellingPoints.map(sp =>
sp.id === id ? { ...sp, required: !sp.required } : sp
)
}))
}
const addBlacklistWord = () => {
if (!newBlacklistWord.trim()) return
setBrief(prev => ({
...prev,
blacklistWords: [...prev.blacklistWords, { id: `bw${Date.now()}`, word: newBlacklistWord, reason: '自定义' }]
}))
setNewBlacklistWord('')
}
const removeBlacklistWord = (id: string) => {
setBrief(prev => ({
...prev,
blacklistWords: prev.blacklistWords.filter(bw => bw.id !== id)
}))
}
const handleSave = async () => {
setIsSaving(true)
await new Promise(resolve => setTimeout(resolve, 1000))
setIsSaving(false)
alert('配置已保存!')
}
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => router.back()} className="p-2 hover:bg-bg-elevated rounded-full">
<ArrowLeft size={20} className="text-text-primary" />
</button>
<div className="flex-1">
<h1 className="text-xl font-bold text-text-primary">{brief.projectName}</h1>
<p className="text-sm text-text-secondary">{brief.brandName}</p>
</div>
<Button onClick={handleSave} disabled={isSaving}>
<Save size={16} />
{isSaving ? '保存中...' : '保存配置'}
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧Brief 文件和 AI 解析 */}
<div className="lg:col-span-2 space-y-6">
{/* Brief 文件 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText size={18} className="text-accent-indigo" />
Brief
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 bg-bg-elevated rounded-lg">
<div className="flex items-center gap-3">
<FileText size={32} className="text-accent-indigo" />
<div>
<p className="font-medium text-text-primary">{brief.fileName}</p>
<p className="text-sm text-text-secondary"> {brief.uploadedAt}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" size="sm">
</Button>
<Button variant="secondary" size="sm">
<Upload size={14} />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* AI 解析结果 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Sparkles size={18} className="text-purple-400" />
AI
</span>
<Button variant="secondary" size="sm" onClick={handleAIParse} disabled={isAIParsing}>
<Sparkles size={14} />
{isAIParsing ? '解析中...' : '重新解析'}
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-bg-elevated rounded-lg">
<p className="text-xs text-text-tertiary mb-1"></p>
<p className="text-text-primary">{brief.aiParsedContent.productName}</p>
</div>
<div className="p-3 bg-bg-elevated rounded-lg">
<p className="text-xs text-text-tertiary mb-1"></p>
<p className="text-text-primary">{brief.aiParsedContent.targetAudience}</p>
</div>
<div className="p-3 bg-bg-elevated rounded-lg col-span-2">
<p className="text-xs text-text-tertiary mb-1"></p>
<p className="text-text-primary">{brief.aiParsedContent.contentRequirements}</p>
</div>
<div className="p-3 bg-bg-elevated rounded-lg col-span-2">
<p className="text-xs text-text-tertiary mb-1"></p>
<p className="text-text-primary">{brief.aiParsedContent.restrictions}</p>
</div>
</div>
</CardContent>
</Card>
{/* 卖点配置 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target size={18} className="text-accent-green" />
<span className="text-sm font-normal text-text-secondary ml-2">
{brief.sellingPoints.length}
</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{brief.sellingPoints.map((sp) => (
<div key={sp.id} className="flex items-center gap-3 p-3 bg-bg-elevated rounded-lg">
<button
type="button"
onClick={() => toggleRequired(sp.id)}
className={`px-2 py-1 text-xs rounded ${
sp.required ? 'bg-accent-coral/20 text-accent-coral' : 'bg-bg-page text-text-tertiary'
}`}
>
{sp.required ? '必选' : '可选'}
</button>
<span className="flex-1 text-text-primary">{sp.content}</span>
<button
type="button"
onClick={() => removeSellingPoint(sp.id)}
className="p-1 hover:bg-bg-page rounded"
>
<X size={16} className="text-text-tertiary" />
</button>
</div>
))}
<div className="flex gap-2">
<input
type="text"
value={newSellingPoint}
onChange={(e) => setNewSellingPoint(e.target.value)}
placeholder="添加新卖点..."
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
onKeyDown={(e) => e.key === 'Enter' && addSellingPoint()}
/>
<Button variant="secondary" onClick={addSellingPoint}>
<Plus size={16} />
</Button>
</div>
</CardContent>
</Card>
</div>
{/* 右侧:违禁词配置 */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Ban size={18} className="text-accent-coral" />
<span className="text-sm font-normal text-text-secondary ml-2">
{brief.blacklistWords.length}
</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{brief.blacklistWords.map((bw) => (
<div key={bw.id} className="flex items-center justify-between p-3 bg-accent-coral/10 rounded-lg border border-accent-coral/30">
<div>
<span className="font-medium text-accent-coral">{bw.word}</span>
<span className="text-xs text-text-tertiary ml-2">{bw.reason}</span>
</div>
<button
type="button"
onClick={() => removeBlacklistWord(bw.id)}
className="p-1 hover:bg-accent-coral/20 rounded"
>
<X size={14} className="text-text-tertiary" />
</button>
</div>
))}
<div className="flex gap-2 mt-3">
<input
type="text"
value={newBlacklistWord}
onChange={(e) => setNewBlacklistWord(e.target.value)}
placeholder="添加违禁词..."
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
onKeyDown={(e) => e.key === 'Enter' && addBlacklistWord()}
/>
<Button variant="secondary" size="sm" onClick={addBlacklistWord}>
<Plus size={14} />
</Button>
</div>
</CardContent>
</Card>
{/* 配置提示 */}
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
<div className="flex items-start gap-3">
<AlertTriangle size={20} className="text-accent-indigo flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-accent-indigo font-medium"></p>
<ul className="text-xs text-accent-indigo/80 mt-1 space-y-1">
<li> </li>
<li> AI </li>
<li> </li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
)
}