Your Name e4959d584f feat: 完善代理商端业务逻辑与前后端框架
主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

345 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 Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
import { ProgressBar } from '@/components/ui/ProgressBar'
import { Button } from '@/components/ui/Button'
import {
TrendingUp,
TrendingDown,
BarChart3,
Target,
AlertTriangle,
Clock,
ChevronRight,
Shield,
Users,
FileVideo
} from 'lucide-react'
// 模拟核心指标
const metrics = {
totalReviews: 1234,
totalTrend: '+12%',
passRate: 78.5,
passRateTrend: '+5.2%',
hardRecall: 96.2,
hardRecallTarget: 95,
sentimentBlocks: 23,
sentimentTrend: '-18%',
avgCycle: 4.2,
avgCycleTarget: 5,
}
// 模拟趋势数据
const weeklyData = [
{ day: '周一', submitted: 45, passed: 40, failed: 5 },
{ day: '周二', submitted: 52, passed: 48, failed: 4 },
{ day: '周三', submitted: 38, passed: 35, failed: 3 },
{ day: '周四', submitted: 61, passed: 54, failed: 7 },
{ day: '周五', submitted: 55, passed: 50, failed: 5 },
{ day: '周六', submitted: 28, passed: 26, failed: 2 },
{ day: '周日', submitted: 22, passed: 20, failed: 2 },
]
// 模拟违规类型分布
const violationTypes = [
{ type: '违禁词', count: 156, percentage: 45, color: 'bg-red-500' },
{ type: '竞品露出', count: 89, percentage: 26, color: 'bg-orange-500' },
{ type: '卖点遗漏', count: 67, percentage: 19, color: 'bg-yellow-500' },
{ type: '舆情风险', count: 34, percentage: 10, color: 'bg-purple-500' },
]
// 模拟代理商排名
const agencyRanking = [
{ name: '星耀传媒', passRate: 92, reviews: 156, trend: 'up' },
{ name: '创意无限', passRate: 88, reviews: 134, trend: 'up' },
{ name: '美妆达人MCN', passRate: 82, reviews: 98, trend: 'down' },
{ name: '时尚风向标', passRate: 78, reviews: 87, trend: 'stable' },
]
// 模拟风险预警
const riskAlerts = [
{
id: 'alert-001',
level: 'high',
title: '代理商A竞品露出集中',
description: '过去24小时内5条视频触发"竞品露出"',
time: '10分钟前',
},
{
id: 'alert-002',
level: 'medium',
title: '达人B连续未通过',
description: '连续3次提交未通过建议沟通',
time: '2小时前',
},
{
id: 'alert-003',
level: 'low',
title: '舆情风险上升',
description: '本周舆情风险拦截数异常上升,建议检查阈值',
time: '5小时前',
},
]
function MetricCard({
title,
value,
unit = '',
trend,
target,
icon: Icon,
color,
}: {
title: string
value: number | string
unit?: string
trend?: string
target?: number
icon: React.ElementType
color: string
}) {
return (
<Card>
<CardContent className="py-4">
<div className="flex items-start justify-between">
<div>
<div className="text-sm text-text-secondary mb-1">{title}</div>
<div className="flex items-baseline gap-1">
<span className={`text-3xl font-bold ${color}`}>{value}</span>
{unit && <span className="text-lg text-text-secondary">{unit}</span>}
</div>
{trend && (
<div className={`text-xs mt-1 flex items-center gap-1 ${
trend.includes('+') || trend.includes('↓') ? 'text-accent-green' : trend.includes('-') ? 'text-accent-coral' : 'text-text-secondary'
}`}>
{trend.includes('+') ? <TrendingUp size={12} /> : trend.includes('-') && !trend.includes('↓') ? <TrendingDown size={12} /> : null}
{trend} vs
</div>
)}
{target && (
<div className="text-xs text-text-tertiary mt-1">
{target}{unit} {Number(value) >= target ? '✅' : '⚠️'}
</div>
)}
</div>
<div className={`w-12 h-12 rounded-lg ${color.replace('text-', 'bg-').replace('600', '').replace('900', '')}/20 flex items-center justify-center`}>
<Icon size={24} className={color} />
</div>
</div>
</CardContent>
</Card>
)
}
function AlertLevelIcon({ level }: { level: string }) {
if (level === 'high') return <AlertTriangle size={16} className="text-red-500" />
if (level === 'medium') return <AlertTriangle size={16} className="text-orange-500" />
return <AlertTriangle size={16} className="text-yellow-500" />
}
export default function BrandDashboard() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-text-primary"></h1>
<div className="text-sm text-text-secondary">{new Date().toLocaleString('zh-CN')}</div>
</div>
{/* 核心指标卡片 */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<MetricCard
title="本月审核总量"
value={metrics.totalReviews}
trend={metrics.totalTrend}
icon={FileVideo}
color="text-text-primary"
/>
<MetricCard
title="初审通过率"
value={metrics.passRate}
unit="%"
trend={metrics.passRateTrend}
icon={Target}
color="text-accent-green"
/>
<MetricCard
title="硬性召回率"
value={metrics.hardRecall}
unit="%"
target={metrics.hardRecallTarget}
icon={Shield}
color="text-accent-indigo"
/>
<MetricCard
title="舆情拦截数"
value={metrics.sentimentBlocks}
trend={metrics.sentimentTrend + ' ↓'}
icon={AlertTriangle}
color="text-purple-400"
/>
<MetricCard
title="平均审核周期"
value={metrics.avgCycle}
unit="小时"
target={metrics.avgCycleTarget}
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">
{weeklyData.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.failed / 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">
<AlertTriangle size={18} className="text-red-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{riskAlerts.map((alert) => (
<div
key={alert.id}
className={`p-3 rounded-lg border cursor-pointer hover:shadow-sm transition-shadow ${
alert.level === 'high'
? 'bg-accent-coral/10 border-accent-coral/30'
: alert.level === 'medium'
? 'bg-orange-500/10 border-orange-500/30'
: 'bg-yellow-500/10 border-yellow-500/30'
}`}
>
<div className="flex items-start gap-2">
<AlertLevelIcon level={alert.level} />
<div className="flex-1 min-w-0">
<div className="font-medium text-text-primary text-sm">{alert.title}</div>
<div className="text-xs text-text-secondary mt-0.5">{alert.description}</div>
<div className="text-xs text-text-tertiary mt-1">{alert.time}</div>
</div>
</div>
</div>
))}
<Button variant="ghost" fullWidth size="sm">
<ChevronRight size={14} />
</Button>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 违规类型分布 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{violationTypes.map((item) => (
<div key={item.type}>
<div className="flex justify-between text-sm mb-2">
<span className="text-text-primary font-medium">{item.type}</span>
<span className="text-text-secondary">{item.count} ({item.percentage}%)</span>
</div>
<div className="h-2 bg-bg-elevated rounded-full overflow-hidden">
<div className={`h-full ${item.color} transition-all`} style={{ width: `${item.percentage}%` }} />
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 代理商排名 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users size={18} className="text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{agencyRanking.map((agency, index) => (
<div key={agency.name} className="flex items-center gap-4 p-3 rounded-lg bg-bg-elevated">
<div className={`w-8 h-8 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">{agency.name}</div>
<div className="text-xs text-text-secondary">{agency.reviews} </div>
</div>
<div className="text-right">
<div className={`font-bold ${agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
{agency.passRate}%
</div>
<div className="flex items-center justify-end gap-1 text-xs">
{agency.trend === 'up' && <TrendingUp size={12} className="text-accent-green" />}
{agency.trend === 'down' && <TrendingDown size={12} className="text-accent-coral" />}
<span className="text-text-tertiary">
{agency.trend === 'up' ? '上升' : agency.trend === 'down' ? '下降' : '持平'}
</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
)
}