Your Name 0bfedb95c8 feat: 为所有终端添加平台显示功能
- 新增 frontend/lib/platforms.ts 共享平台配置模块
- 支持6个平台: 抖音、小红书、B站、快手、微博、微信视频号
- 品牌方终端: 项目看板、项目详情、终审台列表添加平台显示
- 代理商终端: 工作台概览、审核台、Brief配置、达人管理、
  数据报表、消息中心、申诉处理添加平台显示
- 达人端: 任务列表添加平台显示
- 统一使用彩色头部条样式展示平台信息

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 18:53:51 +08:00

431 lines
17 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 { Plus, FileText, Upload, Trash2, Edit, Check, Search, X, Eye } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/ui/Modal'
import { SuccessTag, PendingTag } from '@/components/ui/Tag'
// 平台选项
const platformOptions = [
{ id: 'douyin', name: '抖音', icon: '🎵', color: 'bg-[#1a1a1a]' },
{ id: 'xiaohongshu', name: '小红书', icon: '📕', color: 'bg-[#fe2c55]' },
{ id: 'bilibili', name: 'B站', icon: '📺', color: 'bg-[#00a1d6]' },
{ id: 'kuaishou', name: '快手', icon: '⚡', color: 'bg-[#ff4906]' },
{ id: 'weibo', name: '微博', icon: '🔴', color: 'bg-[#e6162d]' },
{ id: 'wechat', name: '微信视频号', icon: '💬', color: 'bg-[#07c160]' },
]
// 模拟 Brief 列表
const mockBriefs = [
{
id: 'brief-001',
name: '2024 夏日护肤活动',
description: '夏日护肤系列产品推广规范',
status: 'active',
platforms: ['douyin', 'xiaohongshu'],
rulesCount: 12,
creatorsCount: 45,
createdAt: '2024-01-15',
updatedAt: '2024-02-01',
},
{
id: 'brief-002',
name: '新品口红上市',
description: '春季新品口红营销 Brief',
status: 'active',
platforms: ['xiaohongshu', 'bilibili'],
rulesCount: 8,
creatorsCount: 32,
createdAt: '2024-02-01',
updatedAt: '2024-02-03',
},
{
id: 'brief-003',
name: '年货节活动',
description: '春节年货促销活动规范',
status: 'archived',
platforms: ['douyin', 'kuaishou'],
rulesCount: 15,
creatorsCount: 78,
createdAt: '2024-01-01',
updatedAt: '2024-01-20',
},
]
export default function BriefsPage() {
const [briefs, setBriefs] = useState(mockBriefs)
const [showCreateModal, setShowCreateModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// 新建 Brief 表单
const [newBriefName, setNewBriefName] = useState('')
const [newBriefDesc, setNewBriefDesc] = useState('')
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([])
// 查看详情
const [showDetailModal, setShowDetailModal] = useState(false)
const [selectedBrief, setSelectedBrief] = useState<typeof mockBriefs[0] | null>(null)
const filteredBriefs = briefs.filter((brief) =>
brief.name.toLowerCase().includes(searchQuery.toLowerCase())
)
// 切换平台选择
const togglePlatform = (platformId: string) => {
setSelectedPlatforms(prev =>
prev.includes(platformId)
? prev.filter(id => id !== platformId)
: [...prev, platformId]
)
}
// 获取平台信息
const getPlatformInfo = (platformId: string) => {
return platformOptions.find(p => p.id === platformId)
}
// 创建 Brief
const handleCreateBrief = () => {
if (!newBriefName.trim() || selectedPlatforms.length === 0) return
const newBrief = {
id: `brief-${Date.now()}`,
name: newBriefName,
description: newBriefDesc,
status: 'active' as const,
platforms: selectedPlatforms,
rulesCount: 0,
creatorsCount: 0,
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
}
setBriefs([newBrief, ...briefs])
setShowCreateModal(false)
setNewBriefName('')
setNewBriefDesc('')
setSelectedPlatforms([])
}
// 查看 Brief 详情
const viewBriefDetail = (brief: typeof mockBriefs[0]) => {
setSelectedBrief(brief)
setShowDetailModal(true)
}
// 删除 Brief
const handleDeleteBrief = (id: string) => {
setBriefs(briefs.filter(b => b.id !== id))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary">Brief </h1>
<p className="text-sm text-text-secondary mt-1"> Brief</p>
</div>
<Button onClick={() => setShowCreateModal(true)}>
<Plus size={16} />
Brief
</Button>
</div>
{/* 搜索 */}
<div className="relative max-w-md">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="text"
placeholder="搜索 Brief..."
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>
{/* Brief 列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredBriefs.map((brief) => (
<Card key={brief.id} className="hover:shadow-md transition-shadow border border-border-subtle">
<CardContent className="p-5">
<div className="flex items-start justify-between mb-3">
<div className="p-2 bg-accent-indigo/15 rounded-lg">
<FileText size={24} className="text-accent-indigo" />
</div>
{brief.status === 'active' ? (
<SuccessTag>使</SuccessTag>
) : (
<PendingTag></PendingTag>
)}
</div>
<h3 className="font-semibold text-text-primary mb-1">{brief.name}</h3>
<p className="text-sm text-text-tertiary mb-3">{brief.description}</p>
{/* 平台标签 */}
<div className="flex flex-wrap gap-1.5 mb-3">
{brief.platforms.map(platformId => {
const platform = getPlatformInfo(platformId)
return platform ? (
<span
key={platformId}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-bg-elevated text-xs text-text-secondary"
>
<span>{platform.icon}</span>
{platform.name}
</span>
) : null
})}
</div>
<div className="flex gap-4 text-sm text-text-tertiary mb-4">
<span>{brief.rulesCount} </span>
<span>{brief.creatorsCount} </span>
</div>
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
<span className="text-xs text-text-tertiary">
{brief.updatedAt}
</span>
<div className="flex gap-1">
<button
type="button"
onClick={() => viewBriefDetail(brief)}
className="p-1.5 hover:bg-bg-elevated rounded-lg transition-colors"
title="查看详情"
>
<Eye size={16} className="text-text-tertiary hover:text-accent-indigo" />
</button>
<button
type="button"
className="p-1.5 hover:bg-bg-elevated rounded-lg transition-colors"
title="编辑"
>
<Edit size={16} className="text-text-tertiary hover:text-accent-indigo" />
</button>
<button
type="button"
onClick={() => handleDeleteBrief(brief.id)}
className="p-1.5 hover:bg-accent-coral/10 rounded-lg transition-colors"
title="删除"
>
<Trash2 size={16} className="text-text-tertiary hover:text-accent-coral" />
</button>
</div>
</div>
</CardContent>
</Card>
))}
{/* 新建卡片 */}
<button
type="button"
onClick={() => setShowCreateModal(true)}
className="p-5 rounded-xl border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex flex-col items-center justify-center min-h-[240px]"
>
<div className="p-3 bg-bg-elevated rounded-full mb-3">
<Plus size={24} className="text-text-tertiary" />
</div>
<span className="text-text-tertiary font-medium"> Brief</span>
</button>
</div>
{/* 新建 Brief 弹窗 */}
<Modal
isOpen={showCreateModal}
onClose={() => {
setShowCreateModal(false)
setNewBriefName('')
setNewBriefDesc('')
setSelectedPlatforms([])
}}
title="新建 Brief"
size="lg"
>
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-text-primary mb-2">Brief </label>
<input
type="text"
value={newBriefName}
onChange={(e) => setNewBriefName(e.target.value)}
placeholder="输入 Brief 名称"
className="w-full px-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>
<label className="block text-sm font-medium text-text-primary mb-2"></label>
<textarea
value={newBriefDesc}
onChange={(e) => setNewBriefDesc(e.target.value)}
className="w-full h-20 px-4 py-3 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
placeholder="输入 Brief 描述..."
/>
</div>
{/* 选择平台规则库 */}
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
<span className="text-accent-coral">*</span>
</label>
<p className="text-xs text-text-tertiary mb-3"></p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{platformOptions.map((platform) => (
<button
key={platform.id}
type="button"
onClick={() => togglePlatform(platform.id)}
className={`p-3 rounded-xl border-2 transition-all flex items-center gap-3 ${
selectedPlatforms.includes(platform.id)
? 'border-accent-indigo bg-accent-indigo/10'
: 'border-border-subtle hover:border-accent-indigo/50'
}`}
>
<div className={`w-10 h-10 ${platform.color} rounded-lg flex items-center justify-center text-lg`}>
{platform.icon}
</div>
<div className="flex-1 text-left">
<p className="font-medium text-text-primary">{platform.name}</p>
</div>
{selectedPlatforms.includes(platform.id) && (
<div className="w-5 h-5 rounded-full bg-accent-indigo flex items-center justify-center">
<Check size={12} className="text-white" />
</div>
)}
</button>
))}
</div>
</div>
{/* 上传 PDF */}
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
Brief
</label>
<div className="border-2 border-dashed border-border-subtle rounded-xl p-6 text-center hover:border-accent-indigo transition-colors cursor-pointer">
<Upload size={32} className="mx-auto text-text-tertiary mb-2" />
<p className="text-sm text-text-primary"> PDF </p>
<p className="text-xs text-text-tertiary mt-1">AI </p>
</div>
</div>
<div className="flex gap-3 justify-end pt-4 border-t border-border-subtle">
<Button
variant="ghost"
onClick={() => {
setShowCreateModal(false)
setNewBriefName('')
setNewBriefDesc('')
setSelectedPlatforms([])
}}
>
</Button>
<Button
onClick={handleCreateBrief}
disabled={!newBriefName.trim() || selectedPlatforms.length === 0}
>
Brief
</Button>
</div>
</div>
</Modal>
{/* Brief 详情弹窗 */}
<Modal
isOpen={showDetailModal}
onClose={() => {
setShowDetailModal(false)
setSelectedBrief(null)
}}
title={selectedBrief?.name || 'Brief 详情'}
size="lg"
>
{selectedBrief && (
<div className="space-y-5">
<div className="flex items-center gap-4 p-4 rounded-xl bg-bg-elevated">
<div className="p-3 bg-accent-indigo/15 rounded-xl">
<FileText size={28} className="text-accent-indigo" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-text-primary">{selectedBrief.name}</h3>
<p className="text-sm text-text-tertiary mt-0.5">{selectedBrief.description}</p>
</div>
{selectedBrief.status === 'active' ? (
<SuccessTag>使</SuccessTag>
) : (
<PendingTag></PendingTag>
)}
</div>
{/* 应用的平台规则库 */}
<div>
<h4 className="text-sm font-medium text-text-primary mb-3"></h4>
<div className="grid grid-cols-2 gap-3">
{selectedBrief.platforms.map(platformId => {
const platform = getPlatformInfo(platformId)
return platform ? (
<div
key={platformId}
className="p-3 rounded-xl bg-bg-elevated border border-border-subtle flex items-center gap-3"
>
<div className={`w-10 h-10 ${platform.color} rounded-lg flex items-center justify-center text-lg`}>
{platform.icon}
</div>
<div>
<p className="font-medium text-text-primary">{platform.name}</p>
<p className="text-xs text-text-tertiary"></p>
</div>
</div>
) : null
})}
</div>
</div>
{/* 统计数据 */}
<div className="grid grid-cols-3 gap-4">
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20 text-center">
<p className="text-2xl font-bold text-accent-indigo">{selectedBrief.rulesCount}</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<div className="p-4 rounded-xl bg-accent-green/10 border border-accent-green/20 text-center">
<p className="text-2xl font-bold text-accent-green">{selectedBrief.creatorsCount}</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/20 text-center">
<p className="text-2xl font-bold text-accent-amber">{selectedBrief.platforms.length}</p>
<p className="text-sm text-text-secondary mt-1"></p>
</div>
</div>
{/* 时间信息 */}
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated text-sm">
<div>
<span className="text-text-tertiary"></span>
<span className="text-text-primary">{selectedBrief.createdAt}</span>
</div>
<div>
<span className="text-text-tertiary"></span>
<span className="text-text-primary">{selectedBrief.updatedAt}</span>
</div>
</div>
<div className="flex gap-3 justify-end pt-4 border-t border-border-subtle">
<Button variant="ghost" onClick={() => setShowDetailModal(false)}>
</Button>
<Button>
Brief
</Button>
</div>
</div>
)}
</Modal>
</div>
)
}