feat(agency): 优化审核台列表页并新增申诉处理功能
审核台列表页优化: - 每个任务项显示文件名、文件大小 - 添加下载按钮支持文件下载 - 点击"审核"按钮才跳转到详情页 - 按风险等级显示不同颜色的状态标签和按钮 新增申诉处理功能: - 申诉列表页:展示所有达人申诉,支持搜索和状态筛选 - 申诉详情页:查看申诉内容、附件、原审核问题 - 处理决策面板:通过/驳回申诉并填写处理意见 - 侧边栏添加"申诉处理"菜单入口 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
576c89c8c4
commit
66748d4f19
348
frontend/app/agency/appeals/[id]/page.tsx
Normal file
348
frontend/app/agency/appeals/[id]/page.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
'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 {
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
User,
|
||||
FileText,
|
||||
Video,
|
||||
Download,
|
||||
File,
|
||||
Send,
|
||||
Image as ImageIcon
|
||||
} from 'lucide-react'
|
||||
|
||||
// 申诉状态类型
|
||||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||||
|
||||
// 模拟申诉详情数据
|
||||
const mockAppealDetail = {
|
||||
id: 'appeal-001',
|
||||
taskId: 'task-001',
|
||||
taskTitle: '夏日护肤推广脚本',
|
||||
creatorId: 'creator-001',
|
||||
creatorName: '小美护肤',
|
||||
creatorAvatar: '小',
|
||||
type: 'ai' as const,
|
||||
contentType: 'script' as const,
|
||||
reason: 'AI误判',
|
||||
content: '脚本中提到的"某品牌"是泛指,并非特指竞品,AI系统可能误解了语境。我在脚本中使用的是泛化表述,并没有提及任何具体的竞品名称。请代理商重新审核此处,谢谢!',
|
||||
status: 'pending' as AppealStatus,
|
||||
createdAt: '2026-02-06 10:30',
|
||||
// 附件
|
||||
attachments: [
|
||||
{ id: 'att-001', name: '品牌授权证明.pdf', size: '1.2 MB', type: 'pdf' },
|
||||
{ id: 'att-002', name: '脚本原文截图.png', size: '345 KB', type: 'image' },
|
||||
],
|
||||
// 原审核问题
|
||||
originalIssue: {
|
||||
type: 'ai',
|
||||
title: '疑似竞品提及',
|
||||
description: '脚本第3段提到"某品牌",可能涉及竞品露出风险。',
|
||||
location: '脚本第3段,第2行',
|
||||
},
|
||||
// 相关任务信息
|
||||
taskInfo: {
|
||||
projectName: 'XX品牌618推广',
|
||||
scriptFileName: '夏日护肤推广_脚本v2.docx',
|
||||
scriptFileSize: '245 KB',
|
||||
},
|
||||
}
|
||||
|
||||
// 状态配置
|
||||
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
|
||||
pending: { label: '待处理', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15', icon: Clock },
|
||||
processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageSquare },
|
||||
approved: { label: '已通过', color: 'text-accent-green', bgColor: 'bg-accent-green/15', icon: CheckCircle },
|
||||
rejected: { label: '已驳回', color: 'text-accent-coral', bgColor: 'bg-accent-coral/15', icon: XCircle },
|
||||
}
|
||||
|
||||
export default function AgencyAppealDetailPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const [appeal] = useState(mockAppealDetail)
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const status = statusConfig[appeal.status]
|
||||
const StatusIcon = status.icon
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!replyContent.trim()) {
|
||||
alert('请填写处理意见')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('申诉已通过')
|
||||
router.push('/agency/appeals')
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!replyContent.trim()) {
|
||||
alert('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
// 模拟提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
alert('申诉已驳回')
|
||||
router.push('/agency/appeals')
|
||||
}
|
||||
|
||||
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">申诉处理</h1>
|
||||
<p className="text-sm text-text-secondary mt-0.5">申诉编号: {appeal.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 px-4 py-2 rounded-xl ${status.bgColor}`}>
|
||||
<StatusIcon size={18} className={status.color} />
|
||||
<span className={`font-medium ${status.color}`}>{status.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:申诉详情 */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* 申诉人信息 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User size={18} className="text-accent-indigo" />
|
||||
申诉人信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-accent-indigo to-purple-500 flex items-center justify-center text-white font-bold text-lg">
|
||||
{appeal.creatorAvatar}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{appeal.creatorName}</p>
|
||||
<p className="text-sm text-text-secondary">达人ID: {appeal.creatorId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border-subtle grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-text-tertiary">任务名称</span>
|
||||
<p className="text-text-primary mt-1">{appeal.taskTitle}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-tertiary">所属项目</span>
|
||||
<p className="text-text-primary mt-1">{appeal.taskInfo.projectName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-tertiary">内容类型</span>
|
||||
<p className="text-text-primary mt-1 flex items-center gap-1">
|
||||
{appeal.contentType === 'script' ? <FileText size={14} /> : <Video size={14} />}
|
||||
{appeal.contentType === 'script' ? '脚本' : '视频'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-tertiary">提交时间</span>
|
||||
<p className="text-text-primary mt-1">{appeal.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 原审核问题 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle size={18} className="text-accent-coral" />
|
||||
原审核问题
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-0.5 text-xs rounded bg-accent-coral/20 text-accent-coral">
|
||||
{appeal.originalIssue.type === 'ai' ? 'AI检测' : '人工审核'}
|
||||
</span>
|
||||
<span className="font-medium text-text-primary">{appeal.originalIssue.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">{appeal.originalIssue.description}</p>
|
||||
<p className="text-xs text-text-tertiary mt-2">位置: {appeal.originalIssue.location}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 申诉内容 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare size={18} className="text-accent-indigo" />
|
||||
申诉内容
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="text-sm text-text-tertiary">申诉原因</span>
|
||||
<p className="text-text-primary mt-1 font-medium">{appeal.reason}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-text-tertiary">详细说明</span>
|
||||
<p className="text-text-primary mt-1 leading-relaxed">{appeal.content}</p>
|
||||
</div>
|
||||
|
||||
{/* 附件 */}
|
||||
{appeal.attachments.length > 0 && (
|
||||
<div>
|
||||
<span className="text-sm text-text-tertiary">附件材料</span>
|
||||
<div className="mt-2 space-y-2">
|
||||
{appeal.attachments.map((att) => (
|
||||
<div
|
||||
key={att.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
{att.type === 'image' ? (
|
||||
<ImageIcon size={20} className="text-accent-indigo" />
|
||||
) : (
|
||||
<File size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{att.name}</p>
|
||||
<p className="text-xs text-text-tertiary">{att.size}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-lg hover:bg-bg-page transition-colors"
|
||||
title="下载"
|
||||
>
|
||||
<Download size={16} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:处理面板 */}
|
||||
<div className="space-y-6">
|
||||
{/* 相关文件 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-accent-indigo" />
|
||||
相关文件
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<File size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">
|
||||
{appeal.taskInfo.scriptFileName}
|
||||
</p>
|
||||
<p className="text-xs text-text-tertiary">{appeal.taskInfo.scriptFileSize}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-lg hover:bg-bg-page transition-colors"
|
||||
title="下载"
|
||||
>
|
||||
<Download size={16} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 处理决策 */}
|
||||
{appeal.status === 'pending' || appeal.status === 'processing' ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>处理决策</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">处理意见</label>
|
||||
<textarea
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder="请输入处理意见或驳回原因..."
|
||||
className="w-full h-32 p-3 rounded-xl bg-bg-elevated border border-border-subtle text-text-primary placeholder-text-tertiary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1 bg-accent-green hover:bg-accent-green/80"
|
||||
onClick={handleApprove}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
通过申诉
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1 border-accent-coral text-accent-coral hover:bg-accent-coral/10"
|
||||
onClick={handleReject}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<XCircle size={16} />
|
||||
驳回申诉
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary text-center">
|
||||
通过申诉将撤销原审核问题,驳回将维持原审核结果
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>处理结果</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`p-4 rounded-xl ${status.bgColor}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<StatusIcon size={18} className={status.color} />
|
||||
<span className={`font-medium ${status.color}`}>
|
||||
{appeal.status === 'approved' ? '申诉已通过' : '申诉已驳回'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{appeal.status === 'approved'
|
||||
? '经核实,达人申诉理由成立,已撤销原审核问题。'
|
||||
: '经核实,原审核问题有效,申诉不成立。'}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
280
frontend/app/agency/appeals/page.tsx
Normal file
280
frontend/app/agency/appeals/page.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
MessageSquare,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
Search,
|
||||
Filter,
|
||||
ChevronRight,
|
||||
User,
|
||||
FileText,
|
||||
Video
|
||||
} from 'lucide-react'
|
||||
|
||||
// 申诉状态类型
|
||||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||||
|
||||
// 申诉类型
|
||||
type AppealType = 'ai' | 'agency'
|
||||
|
||||
// 申诉数据类型
|
||||
interface Appeal {
|
||||
id: string
|
||||
taskId: string
|
||||
taskTitle: string
|
||||
creatorId: string
|
||||
creatorName: string
|
||||
type: AppealType
|
||||
contentType: 'script' | 'video'
|
||||
reason: string
|
||||
content: string
|
||||
status: AppealStatus
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// 模拟申诉数据
|
||||
const mockAppeals: Appeal[] = [
|
||||
{
|
||||
id: 'appeal-001',
|
||||
taskId: 'task-001',
|
||||
taskTitle: '夏日护肤推广脚本',
|
||||
creatorId: 'creator-001',
|
||||
creatorName: '小美护肤',
|
||||
type: 'ai',
|
||||
contentType: 'script',
|
||||
reason: 'AI误判',
|
||||
content: '脚本中提到的"某品牌"是泛指,并非特指竞品,请重新审核。',
|
||||
status: 'pending',
|
||||
createdAt: '2026-02-06 10:30',
|
||||
},
|
||||
{
|
||||
id: 'appeal-002',
|
||||
taskId: 'task-002',
|
||||
taskTitle: '新品口红试色',
|
||||
creatorId: 'creator-002',
|
||||
creatorName: '美妆Lisa',
|
||||
type: 'agency',
|
||||
contentType: 'video',
|
||||
reason: '审核标准不清晰',
|
||||
content: '视频中的背景音乐已获得授权,附上授权证明。代理商反馈的"版权问题"不成立。',
|
||||
status: 'processing',
|
||||
createdAt: '2026-02-05 14:20',
|
||||
},
|
||||
{
|
||||
id: 'appeal-003',
|
||||
taskId: 'task-003',
|
||||
taskTitle: '健身器材推荐',
|
||||
creatorId: 'creator-003',
|
||||
creatorName: '健身教练王',
|
||||
type: 'ai',
|
||||
contentType: 'script',
|
||||
reason: '违禁词误判',
|
||||
content: 'AI标记的"最好"是在描述个人体验,并非绝对化用语,符合广告法要求。',
|
||||
status: 'approved',
|
||||
createdAt: '2026-02-04 09:15',
|
||||
updatedAt: '2026-02-04 16:30',
|
||||
},
|
||||
{
|
||||
id: 'appeal-004',
|
||||
taskId: 'task-004',
|
||||
taskTitle: '美妆新品测评',
|
||||
creatorId: 'creator-004',
|
||||
creatorName: '达人小红',
|
||||
type: 'agency',
|
||||
contentType: 'video',
|
||||
reason: '品牌调性理解差异',
|
||||
content: '代理商认为风格不符,但Brief中未明确禁止该风格,请提供更具体的标准。',
|
||||
status: 'rejected',
|
||||
createdAt: '2026-02-03 11:00',
|
||||
updatedAt: '2026-02-03 18:45',
|
||||
},
|
||||
]
|
||||
|
||||
// 状态配置
|
||||
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
|
||||
pending: { label: '待处理', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15', icon: Clock },
|
||||
processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageSquare },
|
||||
approved: { label: '已通过', color: 'text-accent-green', bgColor: 'bg-accent-green/15', icon: CheckCircle },
|
||||
rejected: { label: '已驳回', color: 'text-accent-coral', bgColor: 'bg-accent-coral/15', icon: XCircle },
|
||||
}
|
||||
|
||||
// 类型配置
|
||||
const typeConfig: Record<AppealType, { label: string; color: string }> = {
|
||||
ai: { label: 'AI审核申诉', color: 'text-accent-indigo' },
|
||||
agency: { label: '代理商审核申诉', color: 'text-purple-400' },
|
||||
}
|
||||
|
||||
function AppealCard({ appeal }: { appeal: Appeal }) {
|
||||
const status = statusConfig[appeal.status]
|
||||
const type = typeConfig[appeal.type]
|
||||
const StatusIcon = status.icon
|
||||
|
||||
return (
|
||||
<Link href={`/agency/appeals/${appeal.id}`}>
|
||||
<div className="p-4 rounded-xl bg-bg-elevated hover:bg-bg-elevated/80 transition-colors cursor-pointer">
|
||||
{/* 顶部:状态和类型 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-lg ${status.bgColor} flex items-center justify-center`}>
|
||||
<StatusIcon size={16} className={status.color} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-text-primary">{appeal.taskTitle}</span>
|
||||
<div className="flex items-center gap-2 text-xs text-text-tertiary">
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={10} />
|
||||
{appeal.creatorName}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{appeal.contentType === 'script' ? <FileText size={10} /> : <Video size={10} />}
|
||||
{appeal.contentType === 'script' ? '脚本' : '视频'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${status.bgColor} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
<ChevronRight size={16} className="text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 申诉信息 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-text-tertiary">申诉类型:</span>
|
||||
<span className={type.color}>{type.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-text-tertiary">申诉原因:</span>
|
||||
<span className="text-text-primary">{appeal.reason}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary line-clamp-2">{appeal.content}</p>
|
||||
</div>
|
||||
|
||||
{/* 底部时间 */}
|
||||
<div className="flex items-center justify-between text-xs text-text-tertiary pt-3 border-t border-border-subtle">
|
||||
<span>提交时间: {appeal.createdAt}</span>
|
||||
{appeal.updatedAt && <span>处理时间: {appeal.updatedAt}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AgencyAppealsPage() {
|
||||
const [filter, setFilter] = useState<AppealStatus | 'all'>('all')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// 统计
|
||||
const pendingCount = mockAppeals.filter(a => a.status === 'pending').length
|
||||
const processingCount = mockAppeals.filter(a => a.status === 'processing').length
|
||||
|
||||
// 筛选
|
||||
const filteredAppeals = mockAppeals.filter(appeal => {
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
appeal.taskTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
appeal.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
appeal.reason.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesFilter = filter === 'all' || appeal.status === filter
|
||||
return matchesSearch && matchesFilter
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">申诉处理</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">处理达人提交的申诉请求</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="px-3 py-1.5 bg-accent-amber/20 text-accent-amber rounded-lg font-medium">
|
||||
{pendingCount} 待处理
|
||||
</span>
|
||||
<span className="px-3 py-1.5 bg-accent-indigo/20 text-accent-indigo rounded-lg font-medium">
|
||||
{processingCount} 处理中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索任务名称或达人..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
||||
{[
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'processing', label: '处理中' },
|
||||
{ value: 'approved', label: '已通过' },
|
||||
{ value: 'rejected', label: '已驳回' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => setFilter(tab.value as AppealStatus | 'all')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
filter === tab.value ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 申诉列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare size={18} className="text-accent-indigo" />
|
||||
申诉列表
|
||||
<span className="ml-auto text-sm font-normal text-text-secondary">
|
||||
共 {filteredAppeals.length} 条
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{filteredAppeals.length > 0 ? (
|
||||
filteredAppeals.map((appeal) => (
|
||||
<AppealCard key={appeal.id} appeal={appeal} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 text-text-tertiary">
|
||||
<MessageSquare size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>{searchQuery || filter !== 'all' ? '没有找到匹配的申诉' : '暂无申诉记录'}</p>
|
||||
{(searchQuery || filter !== 'all') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSearchQuery(''); setFilter('all'); }}
|
||||
className="mt-3 text-sm text-accent-indigo hover:underline"
|
||||
>
|
||||
清除筛选条件
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -13,7 +13,10 @@ import {
|
||||
Clock,
|
||||
User,
|
||||
AlertTriangle,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Download,
|
||||
Eye,
|
||||
File
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟脚本待审列表
|
||||
@ -21,30 +24,51 @@ const mockScriptTasks = [
|
||||
{
|
||||
id: 'script-001',
|
||||
title: '夏日护肤推广脚本',
|
||||
fileName: '夏日护肤推广_脚本v2.docx',
|
||||
fileSize: '245 KB',
|
||||
creatorName: '小美护肤',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 88,
|
||||
riskLevel: 'low' as const,
|
||||
submittedAt: '2026-02-06 14:30',
|
||||
hasHighRisk: false,
|
||||
},
|
||||
{
|
||||
id: 'script-002',
|
||||
title: '新品口红试色脚本',
|
||||
fileName: '口红试色_脚本v1.docx',
|
||||
fileSize: '312 KB',
|
||||
creatorName: '美妆Lisa',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 72,
|
||||
riskLevel: 'medium' as const,
|
||||
submittedAt: '2026-02-06 12:15',
|
||||
hasHighRisk: true,
|
||||
},
|
||||
{
|
||||
id: 'script-003',
|
||||
title: '健身器材推荐脚本',
|
||||
fileName: '健身器材_推荐脚本.pdf',
|
||||
fileSize: '189 KB',
|
||||
creatorName: '健身教练王',
|
||||
projectName: 'XX运动品牌',
|
||||
aiScore: 95,
|
||||
riskLevel: 'low' as const,
|
||||
submittedAt: '2026-02-06 10:00',
|
||||
hasHighRisk: false,
|
||||
},
|
||||
{
|
||||
id: 'script-004',
|
||||
title: '618大促预热脚本',
|
||||
fileName: '618预热_脚本final.docx',
|
||||
fileSize: '278 KB',
|
||||
creatorName: '达人D',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 62,
|
||||
riskLevel: 'high' as const,
|
||||
submittedAt: '2026-02-06 09:00',
|
||||
hasHighRisk: true,
|
||||
},
|
||||
]
|
||||
|
||||
// 模拟视频待审列表
|
||||
@ -52,9 +76,12 @@ const mockVideoTasks = [
|
||||
{
|
||||
id: 'video-001',
|
||||
title: '夏日护肤推广',
|
||||
fileName: '夏日护肤_成片v2.mp4',
|
||||
fileSize: '128 MB',
|
||||
creatorName: '小美护肤',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 85,
|
||||
riskLevel: 'low' as const,
|
||||
duration: '02:15',
|
||||
submittedAt: '2026-02-06 15:00',
|
||||
hasHighRisk: false,
|
||||
@ -62,61 +89,175 @@ const mockVideoTasks = [
|
||||
{
|
||||
id: 'video-002',
|
||||
title: '新品口红试色',
|
||||
fileName: '口红试色_终版.mp4',
|
||||
fileSize: '256 MB',
|
||||
creatorName: '美妆Lisa',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 68,
|
||||
riskLevel: 'medium' as const,
|
||||
duration: '03:42',
|
||||
submittedAt: '2026-02-06 13:45',
|
||||
hasHighRisk: true,
|
||||
},
|
||||
{
|
||||
id: 'video-003',
|
||||
title: '美妆新品体验',
|
||||
fileName: '美妆体验_v3.mp4',
|
||||
fileSize: '198 MB',
|
||||
creatorName: '达人C',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 58,
|
||||
riskLevel: 'high' as const,
|
||||
duration: '04:20',
|
||||
submittedAt: '2026-02-06 11:30',
|
||||
hasHighRisk: true,
|
||||
},
|
||||
{
|
||||
id: 'video-004',
|
||||
title: '618大促预热',
|
||||
fileName: '618预热_final.mp4',
|
||||
fileSize: '167 MB',
|
||||
creatorName: '达人D',
|
||||
projectName: 'XX品牌618推广',
|
||||
aiScore: 91,
|
||||
riskLevel: 'low' as const,
|
||||
duration: '01:45',
|
||||
submittedAt: '2026-02-06 10:15',
|
||||
hasHighRisk: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 风险等级配置
|
||||
const riskLevelConfig = {
|
||||
low: { label: 'AI通过', color: 'bg-accent-green', textColor: 'text-accent-green' },
|
||||
medium: { label: '风险:中', color: 'bg-accent-amber', textColor: 'text-accent-amber' },
|
||||
high: { label: '风险:高', color: 'bg-accent-coral', textColor: 'text-accent-coral' },
|
||||
}
|
||||
|
||||
function ScoreTag({ score }: { score: number }) {
|
||||
if (score >= 85) return <SuccessTag>{score}分</SuccessTag>
|
||||
if (score >= 70) return <WarningTag>{score}分</WarningTag>
|
||||
return <ErrorTag>{score}分</ErrorTag>
|
||||
}
|
||||
|
||||
function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof mockVideoTasks[0]; type: 'script' | 'video' }) {
|
||||
const href = type === 'script' ? `/agency/review/script/${task.id}` : `/agency/review/video/${task.id}`
|
||||
type ScriptTask = typeof mockScriptTasks[0]
|
||||
type VideoTask = typeof mockVideoTasks[0]
|
||||
|
||||
function ScriptTaskCard({ task }: { task: ScriptTask }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
// 模拟下载
|
||||
console.log('下载脚本:', task.fileName)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className="p-4 rounded-lg border border-border-subtle hover:border-accent-indigo/50 hover:bg-accent-indigo/5 transition-all cursor-pointer">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-text-primary truncate">{task.title}</h4>
|
||||
{task.hasHighRisk && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-coral/20 text-accent-coral rounded">
|
||||
<AlertTriangle size={12} />
|
||||
高风险
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={12} />
|
||||
{task.creatorName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ScoreTag score={task.aiScore} />
|
||||
<div className="p-4 rounded-xl bg-bg-elevated">
|
||||
{/* 顶部:达人名 · 任务名 + 状态标签 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${riskConfig.color}`} />
|
||||
<span className="font-medium text-text-primary">{task.creatorName} · {task.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-text-tertiary">
|
||||
<span>{task.projectName}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
</div>
|
||||
{'duration' in task && (
|
||||
<div className="mt-2 text-xs text-text-tertiary">
|
||||
时长: {task.duration}
|
||||
</div>
|
||||
)}
|
||||
<span className={`text-xs ${riskConfig.textColor}`}>{riskConfig.label}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<File size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/script/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
审核
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoTaskCard({ task }: { task: VideoTask }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
// 模拟下载
|
||||
console.log('下载视频:', task.fileName)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 rounded-xl bg-bg-elevated">
|
||||
{/* 顶部:达人名 · 任务名 + 状态标签 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${riskConfig.color}`} />
|
||||
<span className="font-medium text-text-primary">{task.creatorName} · {task.title}</span>
|
||||
</div>
|
||||
<span className={`text-xs ${riskConfig.textColor}`}>{riskConfig.label}</span>
|
||||
</div>
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/15 flex items-center justify-center">
|
||||
<Video size={20} className="text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize} · {task.duration}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/video/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
审核
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -205,15 +346,15 @@ export default function AgencyReviewListPage() {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-accent-indigo" />
|
||||
脚本审核
|
||||
<span className="ml-auto text-sm font-normal text-text-secondary">
|
||||
{filteredScripts.length} 条待审
|
||||
<span className="ml-auto text-sm font-normal text-accent-indigo">
|
||||
{filteredScripts.length} 条待审核
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{filteredScripts.length > 0 ? (
|
||||
filteredScripts.map((task) => (
|
||||
<TaskCard key={task.id} task={task} type="script" />
|
||||
<ScriptTaskCard key={task.id} task={task} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
@ -232,15 +373,15 @@ export default function AgencyReviewListPage() {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Video size={18} className="text-purple-400" />
|
||||
视频审核
|
||||
<span className="ml-auto text-sm font-normal text-text-secondary">
|
||||
{filteredVideos.length} 条待审
|
||||
<span className="ml-auto text-sm font-normal text-accent-indigo">
|
||||
{filteredVideos.length} 条待审核
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{filteredVideos.length > 0 ? (
|
||||
filteredVideos.map((task) => (
|
||||
<TaskCard key={task.id} task={task} type="video" />
|
||||
<VideoTaskCard key={task.id} task={task} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
|
||||
@ -16,7 +16,8 @@ import {
|
||||
FolderKanban,
|
||||
PlusCircle,
|
||||
ClipboardCheck,
|
||||
Bot
|
||||
Bot,
|
||||
MessageSquare
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@ -37,6 +38,7 @@ const creatorNavItems: NavItem[] = [
|
||||
const agencyNavItems: NavItem[] = [
|
||||
{ icon: LayoutDashboard, label: '工作台', href: '/agency' },
|
||||
{ icon: Scan, label: '审核台', href: '/agency/review' },
|
||||
{ icon: MessageSquare, label: '申诉处理', href: '/agency/appeals' },
|
||||
{ icon: FileText, label: 'Brief 配置', href: '/agency/briefs' },
|
||||
{ icon: Users, label: '达人管理', href: '/agency/creators' },
|
||||
{ icon: BarChart3, label: '数据报表', href: '/agency/reports' },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user