代理商端前端: - 新增达人管理页面(含任务申诉次数管理) - 新增消息中心(含申诉次数申请审批) - 新增 Brief 管理(列表、详情) - 新增审核中心(脚本审核、视频审核) - 新增数据报表页面 品牌方端前端: - 优化首页仪表盘布局 - 新增项目管理(列表、详情、创建) - 新增代理商管理页面 - 新增审核中心(脚本终审、视频终审) - 新增系统设置页面 文档更新: - 申诉次数改为按任务分配(每任务初始1次) - 更新 PRD、FeatureSummary、User_Role_Interfaces 等文档 - 更新 UI 设计规范和开发计划 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
|
import { Button } from '@/components/ui/Button'
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Download,
|
|
Calendar,
|
|
FileText,
|
|
Video,
|
|
Users,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock
|
|
} from 'lucide-react'
|
|
|
|
// 模拟数据
|
|
const mockStats = {
|
|
totalScripts: 156,
|
|
totalVideos: 128,
|
|
passRate: 85,
|
|
avgReviewTime: 4.2,
|
|
trend: {
|
|
scripts: '+12%',
|
|
videos: '+8%',
|
|
passRate: '+3%',
|
|
reviewTime: '-15%',
|
|
},
|
|
}
|
|
|
|
const mockProjectStats = [
|
|
{ name: 'XX品牌618推广', scripts: 45, videos: 38, passRate: 92 },
|
|
{ name: '新品口红系列', scripts: 32, videos: 28, passRate: 85 },
|
|
{ name: '护肤品秋季活动', scripts: 28, videos: 25, passRate: 78 },
|
|
{ name: 'XX运动品牌', scripts: 51, videos: 37, passRate: 88 },
|
|
]
|
|
|
|
const mockWeeklyData = [
|
|
{ day: '周一', submitted: 25, passed: 22, rejected: 3 },
|
|
{ day: '周二', submitted: 32, passed: 28, rejected: 4 },
|
|
{ day: '周三', submitted: 18, passed: 16, rejected: 2 },
|
|
{ day: '周四', submitted: 41, passed: 35, rejected: 6 },
|
|
{ day: '周五', submitted: 35, passed: 30, rejected: 5 },
|
|
{ day: '周六', submitted: 12, passed: 11, rejected: 1 },
|
|
{ day: '周日', submitted: 8, passed: 7, rejected: 1 },
|
|
]
|
|
|
|
const mockCreatorRanking = [
|
|
{ name: '小美护肤', passRate: 95, total: 28 },
|
|
{ name: '健身教练王', passRate: 92, total: 15 },
|
|
{ name: '美妆Lisa', passRate: 88, total: 22 },
|
|
{ name: '时尚达人', passRate: 82, total: 18 },
|
|
{ name: '美食探店', passRate: 78, total: 25 },
|
|
]
|
|
|
|
function StatCard({ title, value, unit, trend, icon: Icon, color }: {
|
|
title: string
|
|
value: number | string
|
|
unit?: string
|
|
trend?: string
|
|
icon: React.ElementType
|
|
color: string
|
|
}) {
|
|
const isPositive = trend?.startsWith('+') || trend?.startsWith('-') && title.includes('时间')
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-4">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-sm text-text-secondary">{title}</p>
|
|
<div className="flex items-baseline gap-1 mt-1">
|
|
<span className={`text-2xl font-bold ${color}`}>{value}</span>
|
|
{unit && <span className="text-text-secondary">{unit}</span>}
|
|
</div>
|
|
{trend && (
|
|
<div className={`flex items-center gap-1 mt-1 text-xs ${isPositive ? 'text-accent-green' : 'text-accent-coral'}`}>
|
|
{isPositive ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
|
{trend} vs 上周
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className={`w-10 h-10 rounded-lg ${color.replace('text-', 'bg-')}/20 flex items-center justify-center`}>
|
|
<Icon size={20} className={color} />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export default function AgencyReportsPage() {
|
|
const [dateRange, setDateRange] = useState('week')
|
|
|
|
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-3">
|
|
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
|
<button
|
|
type="button"
|
|
onClick={() => setDateRange('week')}
|
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
dateRange === 'week' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary'
|
|
}`}
|
|
>
|
|
本周
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDateRange('month')}
|
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
dateRange === 'month' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary'
|
|
}`}
|
|
>
|
|
本月
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDateRange('quarter')}
|
|
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
dateRange === 'quarter' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary'
|
|
}`}
|
|
>
|
|
本季度
|
|
</button>
|
|
</div>
|
|
<Button variant="secondary">
|
|
<Download size={16} />
|
|
导出报表
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 核心指标 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<StatCard
|
|
title="脚本审核量"
|
|
value={mockStats.totalScripts}
|
|
trend={mockStats.trend.scripts}
|
|
icon={FileText}
|
|
color="text-accent-indigo"
|
|
/>
|
|
<StatCard
|
|
title="视频审核量"
|
|
value={mockStats.totalVideos}
|
|
trend={mockStats.trend.videos}
|
|
icon={Video}
|
|
color="text-purple-400"
|
|
/>
|
|
<StatCard
|
|
title="通过率"
|
|
value={mockStats.passRate}
|
|
unit="%"
|
|
trend={mockStats.trend.passRate}
|
|
icon={CheckCircle}
|
|
color="text-accent-green"
|
|
/>
|
|
<StatCard
|
|
title="平均审核时长"
|
|
value={mockStats.avgReviewTime}
|
|
unit="小时"
|
|
trend={mockStats.trend.reviewTime}
|
|
icon={Clock}
|
|
color="text-orange-400"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* 本周趋势 */}
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 size={18} className="text-blue-500" />
|
|
审核趋势
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{mockWeeklyData.map((day) => (
|
|
<div key={day.day} className="flex items-center gap-4">
|
|
<div className="w-12 text-sm text-text-secondary font-medium">{day.day}</div>
|
|
<div className="flex-1">
|
|
<div className="flex h-6 rounded-full overflow-hidden bg-bg-elevated">
|
|
<div
|
|
className="bg-accent-green transition-all"
|
|
style={{ width: `${(day.passed / day.submitted) * 100}%` }}
|
|
/>
|
|
<div
|
|
className="bg-accent-coral transition-all"
|
|
style={{ width: `${(day.rejected / day.submitted) * 100}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="w-24 text-right text-sm">
|
|
<span className="text-accent-green font-medium">{day.passed}</span>
|
|
<span className="text-text-tertiary"> / </span>
|
|
<span className="text-text-secondary">{day.submitted}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-6 mt-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-accent-green rounded" />
|
|
<span className="text-text-secondary">通过</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-accent-coral rounded" />
|
|
<span className="text-text-secondary">驳回</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 达人排名 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users size={18} className="text-accent-indigo" />
|
|
达人通过率排名
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{mockCreatorRanking.map((creator, index) => (
|
|
<div key={creator.name} className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated">
|
|
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-sm font-bold ${
|
|
index === 0 ? 'bg-yellow-500/20 text-yellow-400' :
|
|
index === 1 ? 'bg-gray-500/20 text-gray-400' :
|
|
index === 2 ? 'bg-orange-500/20 text-orange-400' : 'bg-bg-page text-text-tertiary'
|
|
}`}>
|
|
{index + 1}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-text-primary truncate">{creator.name}</div>
|
|
<div className="text-xs text-text-tertiary">{creator.total} 条审核</div>
|
|
</div>
|
|
<div className={`font-bold ${creator.passRate >= 90 ? 'text-accent-green' : creator.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
|
|
{creator.passRate}%
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 项目统计 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>项目统计</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<table className="w-full">
|
|
<thead>
|
|
<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 text-center">脚本数</th>
|
|
<th className="pb-3 font-medium text-center">视频数</th>
|
|
<th className="pb-3 font-medium text-center">通过率</th>
|
|
<th className="pb-3 font-medium">通过率分布</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{mockProjectStats.map((project) => (
|
|
<tr key={project.name} className="border-b border-border-subtle last:border-0">
|
|
<td className="py-4 font-medium text-text-primary">{project.name}</td>
|
|
<td className="py-4 text-center text-text-secondary">{project.scripts}</td>
|
|
<td className="py-4 text-center text-text-secondary">{project.videos}</td>
|
|
<td className="py-4 text-center">
|
|
<span className={`font-medium ${project.passRate >= 90 ? 'text-accent-green' : project.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
|
|
{project.passRate}%
|
|
</span>
|
|
</td>
|
|
<td className="py-4">
|
|
<div className="w-full h-2 bg-bg-elevated rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${project.passRate >= 90 ? 'bg-accent-green' : project.passRate >= 80 ? 'bg-accent-indigo' : 'bg-orange-400'}`}
|
|
style={{ width: `${project.passRate}%` }}
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|