feat: 完善品牌方和代理商前端功能
品牌方功能: - 项目看板: 添加截止日期编辑功能 - 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商 - 项目创建: 代理商选择支持搜索(名称/ID/公司名) - 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作 - Brief配置: 新增项目级Brief和规则配置页面 - 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录 代理商功能: - 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑 - 账户设置: 密码修改、手机/邮箱绑定、两步验证 - 通知设置: 分类型和渠道的通知开关 - 审核历史: 搜索筛选和统计展示 - 帮助反馈: FAQ分类搜索和客服联系 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ae74c515c7
commit
964797d2e9
292
frontend/app/agency/help/page.tsx
Normal file
292
frontend/app/agency/help/page.tsx
Normal file
@ -0,0 +1,292 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
ArrowLeft,
|
||||
MessageCircleQuestion,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Search,
|
||||
FileText,
|
||||
Video,
|
||||
Users,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
Headphones,
|
||||
Mail,
|
||||
Phone
|
||||
} from 'lucide-react'
|
||||
|
||||
// FAQ类型
|
||||
interface FAQ {
|
||||
id: string
|
||||
category: string
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
// FAQ数据
|
||||
const faqData: FAQ[] = [
|
||||
{
|
||||
id: 'faq-1',
|
||||
category: '审核相关',
|
||||
question: '如何查看待审核的任务?',
|
||||
answer: '进入"审核台"页面,您可以看到所有待审核的脚本和视频任务。系统会按照紧急程度和提交时间排序,优先显示即将超时的任务。',
|
||||
},
|
||||
{
|
||||
id: 'faq-2',
|
||||
category: '审核相关',
|
||||
question: 'AI审核标记的问题一定要处理吗?',
|
||||
answer: 'AI审核结果仅供参考。作为代理商,您可以根据实际情况判断AI标记的问题是否需要驳回。如果您认为AI误判,可以直接通过该内容。',
|
||||
},
|
||||
{
|
||||
id: 'faq-3',
|
||||
category: '达人管理',
|
||||
question: '如何邀请新达人加入?',
|
||||
answer: '进入"达人管理"页面,点击"邀请达人"按钮,输入达人的ID(CR开头的6位数字),系统会发送邀请通知给达人。达人确认后即可加入您的团队。',
|
||||
},
|
||||
{
|
||||
id: 'faq-4',
|
||||
category: '达人管理',
|
||||
question: '如何给达人分配项目?',
|
||||
answer: '在达人管理列表中,点击达人卡片右侧的操作菜单,选择"分配项目",然后选择要分配的项目即可。',
|
||||
},
|
||||
{
|
||||
id: 'faq-5',
|
||||
category: '申诉处理',
|
||||
question: '达人申诉后多久需要处理?',
|
||||
answer: '建议在24小时内处理达人的申诉请求。超时未处理的申诉会自动升级提醒。您可以在"申诉处理"页面查看所有待处理的申诉。',
|
||||
},
|
||||
{
|
||||
id: 'faq-6',
|
||||
category: '申诉处理',
|
||||
question: '申诉通过后会发生什么?',
|
||||
answer: '申诉通过后,原审核问题会被撤销,任务状态会更新为"已通过",达人可以继续进行下一步操作。同时系统会通知达人申诉结果。',
|
||||
},
|
||||
{
|
||||
id: 'faq-7',
|
||||
category: '数据报表',
|
||||
question: '如何导出审核数据?',
|
||||
answer: '进入"数据报表"页面,选择需要的时间范围,然后点击"导出报表"按钮。支持导出Excel、CSV和PDF格式。',
|
||||
},
|
||||
{
|
||||
id: 'faq-8',
|
||||
category: '账号相关',
|
||||
question: '如何修改代理商信息?',
|
||||
answer: '进入"个人中心",点击"公司信息"可以修改公司名称、联系方式等信息。注意:公司全称和营业执照信息修改需要重新审核。',
|
||||
},
|
||||
]
|
||||
|
||||
// 分类图标配置
|
||||
const categoryIcons: Record<string, { icon: React.ElementType; color: string }> = {
|
||||
'审核相关': { icon: FileText, color: 'text-accent-indigo' },
|
||||
'达人管理': { icon: Users, color: 'text-accent-green' },
|
||||
'申诉处理': { icon: MessageSquare, color: 'text-accent-amber' },
|
||||
'数据报表': { icon: BarChart3, color: 'text-accent-blue' },
|
||||
'账号相关': { icon: Users, color: 'text-purple-400' },
|
||||
}
|
||||
|
||||
// FAQ Item组件
|
||||
function FAQItem({ faq }: { faq: FAQ }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const categoryConfig = categoryIcons[faq.category] || { icon: MessageCircleQuestion, color: 'text-text-secondary' }
|
||||
const Icon = categoryConfig.icon
|
||||
|
||||
return (
|
||||
<div className="border-b border-border-subtle last:border-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-center justify-between py-4 text-left hover:bg-bg-elevated/30 transition-colors px-2 -mx-2 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg bg-opacity-15 flex items-center justify-center`}
|
||||
style={{ backgroundColor: `${categoryConfig.color.replace('text-', '')}15` }}
|
||||
>
|
||||
<Icon size={16} className={categoryConfig.color} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-text-tertiary">{faq.category}</span>
|
||||
<p className="font-medium text-text-primary">{faq.question}</p>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<ChevronUp size={20} className="text-text-tertiary flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown size={20} className="text-text-tertiary flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="pb-4 pl-13 pr-4">
|
||||
<p className="text-text-secondary leading-relaxed pl-11">{faq.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AgencyHelpPage() {
|
||||
const router = useRouter()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('全部')
|
||||
|
||||
// 获取所有分类
|
||||
const categories = ['全部', ...Array.from(new Set(faqData.map(f => f.category)))]
|
||||
|
||||
// 筛选FAQ
|
||||
const filteredFAQ = faqData.filter(faq => {
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesCategory = selectedCategory === '全部' || faq.category === selectedCategory
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部导航 */}
|
||||
<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">常见问题解答和联系客服</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:FAQ */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* 搜索 */}
|
||||
<div className="relative">
|
||||
<Search size={18} className="absolute left-4 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-11 pr-4 py-3 border border-border-subtle rounded-xl bg-bg-card text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分类筛选 */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
selectedCategory === cat
|
||||
? 'bg-accent-indigo text-white'
|
||||
: 'bg-bg-elevated text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* FAQ列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageCircleQuestion size={18} className="text-accent-indigo" />
|
||||
常见问题
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredFAQ.length > 0 ? (
|
||||
filteredFAQ.map((faq) => (
|
||||
<FAQItem key={faq.id} faq={faq} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
<MessageCircleQuestion size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>没有找到相关问题</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:联系客服 */}
|
||||
<div className="space-y-6">
|
||||
{/* 在线客服 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Headphones size={18} className="text-accent-green" />
|
||||
联系客服
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">
|
||||
如果您的问题未在FAQ中找到答案,可以通过以下方式联系我们
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-bg-elevated">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Headphones size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-text-primary">在线客服</p>
|
||||
<p className="text-sm text-text-tertiary">工作日 9:00-18:00</p>
|
||||
</div>
|
||||
<Button variant="primary" size="sm">
|
||||
立即咨询
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-bg-elevated">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-green/15 flex items-center justify-center">
|
||||
<Phone size={20} className="text-accent-green" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-text-primary">客服热线</p>
|
||||
<p className="text-sm text-accent-indigo font-mono">400-888-8888</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-bg-elevated">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-blue/15 flex items-center justify-center">
|
||||
<Mail size={20} className="text-accent-blue" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-text-primary">邮件支持</p>
|
||||
<p className="text-sm text-accent-indigo">support@miaosi.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 意见反馈 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>意见反馈</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<textarea
|
||||
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"
|
||||
/>
|
||||
<Button variant="primary" className="w-full">
|
||||
提交反馈
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
674
frontend/app/agency/profile/company/page.tsx
Normal file
674
frontend/app/agency/profile/company/page.tsx
Normal file
@ -0,0 +1,674 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building2,
|
||||
Phone,
|
||||
Mail,
|
||||
Search,
|
||||
Loader2,
|
||||
X,
|
||||
Check,
|
||||
CreditCard,
|
||||
ShieldCheck,
|
||||
AlertCircle,
|
||||
ChevronRight
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟企业查询结果
|
||||
interface CompanySearchResult {
|
||||
companyName: string
|
||||
shortName: string
|
||||
businessLicense: string
|
||||
legalPerson: string
|
||||
registeredCapital: string
|
||||
establishDate: string
|
||||
businessScope: string
|
||||
address: string
|
||||
status: string
|
||||
}
|
||||
|
||||
// 模拟企业数据库
|
||||
const mockCompanyDatabase: CompanySearchResult[] = [
|
||||
{
|
||||
companyName: '上海星辰文化传媒有限公司',
|
||||
shortName: '星辰传媒',
|
||||
businessLicense: '91310000MA1FL8XXXX',
|
||||
legalPerson: '张三',
|
||||
registeredCapital: '500万人民币',
|
||||
establishDate: '2020-03-15',
|
||||
businessScope: '文化传媒、广告设计、互联网信息服务',
|
||||
address: '上海市浦东新区张江高科技园区xxx路xxx号',
|
||||
status: '在营',
|
||||
},
|
||||
{
|
||||
companyName: '北京蓝海数字科技有限公司',
|
||||
shortName: '蓝海科技',
|
||||
businessLicense: '91110000MA2BK9YYYY',
|
||||
legalPerson: '李四',
|
||||
registeredCapital: '1000万人民币',
|
||||
establishDate: '2018-06-20',
|
||||
businessScope: '软件开发、信息技术服务、数据处理',
|
||||
address: '北京市海淀区中关村科技园xxx号',
|
||||
status: '在营',
|
||||
},
|
||||
{
|
||||
companyName: '杭州云创网络技术有限公司',
|
||||
shortName: '云创网络',
|
||||
businessLicense: '91330000MA3CL7ZZZZ',
|
||||
legalPerson: '王五',
|
||||
registeredCapital: '300万人民币',
|
||||
establishDate: '2021-01-10',
|
||||
businessScope: '网络技术开发、电子商务、广告服务',
|
||||
address: '浙江省杭州市西湖区xxx路xxx号',
|
||||
status: '在营',
|
||||
},
|
||||
{
|
||||
companyName: '深圳创意无限广告有限公司',
|
||||
shortName: '创意无限',
|
||||
businessLicense: '91440000MA4DM8AAAA',
|
||||
legalPerson: '赵六',
|
||||
registeredCapital: '200万人民币',
|
||||
establishDate: '2019-09-05',
|
||||
businessScope: '广告设计、品牌策划、市场营销',
|
||||
address: '广东省深圳市南山区xxx路xxx号',
|
||||
status: '在营',
|
||||
},
|
||||
{
|
||||
companyName: '成都天府传媒集团有限公司',
|
||||
shortName: '天府传媒',
|
||||
businessLicense: '91510000MA5EN9BBBB',
|
||||
legalPerson: '钱七',
|
||||
registeredCapital: '2000万人民币',
|
||||
establishDate: '2015-12-01',
|
||||
businessScope: '广播电视节目制作、影视策划、新媒体运营',
|
||||
address: '四川省成都市高新区xxx路xxx号',
|
||||
status: '在营',
|
||||
},
|
||||
]
|
||||
|
||||
// 认证状态
|
||||
type VerifyStatus = 'unverified' | 'pending' | 'verified'
|
||||
|
||||
// 认证方式
|
||||
type VerifyMethod = 'bank' | 'legalPerson'
|
||||
|
||||
// 当前公司数据
|
||||
const mockCurrentCompany = {
|
||||
companyName: '上海星辰文化传媒有限公司',
|
||||
shortName: '星辰传媒',
|
||||
businessLicense: '91310000MA1FL8XXXX',
|
||||
legalPerson: '张三',
|
||||
registeredCapital: '500万人民币',
|
||||
establishDate: '2020-03-15',
|
||||
businessScope: '文化传媒、广告设计、互联网信息服务',
|
||||
address: '上海市浦东新区张江高科技园区xxx路xxx号',
|
||||
contactPhone: '021-12345678',
|
||||
contactEmail: 'contact@starmedia.com',
|
||||
verifyStatus: 'verified' as VerifyStatus,
|
||||
// 对公账户信息(验证通过后显示)
|
||||
bankInfo: {
|
||||
bankName: '中国工商银行上海浦东支行',
|
||||
accountNumber: '6222****1234',
|
||||
},
|
||||
}
|
||||
|
||||
export default function AgencyCompanyPage() {
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [formData, setFormData] = useState(mockCurrentCompany)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 企业查询相关状态
|
||||
const [showSearch, setShowSearch] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<CompanySearchResult[]>([])
|
||||
|
||||
// 认证相关状态
|
||||
const [showVerifyModal, setShowVerifyModal] = useState(false)
|
||||
const [verifyMethod, setVerifyMethod] = useState<VerifyMethod | null>(null)
|
||||
const [verifyStep, setVerifyStep] = useState(1)
|
||||
const [verifyCode, setVerifyCode] = useState('')
|
||||
const [isVerifying, setIsVerifying] = useState(false)
|
||||
|
||||
// 搜索企业
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) return
|
||||
|
||||
setIsSearching(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
const results = mockCompanyDatabase.filter(company =>
|
||||
company.companyName.includes(searchQuery) ||
|
||||
company.shortName.includes(searchQuery) ||
|
||||
company.businessLicense.includes(searchQuery)
|
||||
)
|
||||
|
||||
setSearchResults(results)
|
||||
setIsSearching(false)
|
||||
}
|
||||
|
||||
// 选择企业
|
||||
const handleSelectCompany = (company: CompanySearchResult) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
companyName: company.companyName,
|
||||
shortName: company.shortName,
|
||||
businessLicense: company.businessLicense,
|
||||
legalPerson: company.legalPerson,
|
||||
registeredCapital: company.registeredCapital,
|
||||
establishDate: company.establishDate,
|
||||
businessScope: company.businessScope,
|
||||
address: company.address,
|
||||
verifyStatus: 'unverified', // 选择新公司后需要重新验证
|
||||
})
|
||||
setShowSearch(false)
|
||||
setSearchQuery('')
|
||||
setSearchResults([])
|
||||
}
|
||||
|
||||
// 开始验证
|
||||
const handleStartVerify = (method: VerifyMethod) => {
|
||||
setVerifyMethod(method)
|
||||
setVerifyStep(2)
|
||||
}
|
||||
|
||||
// 提交验证
|
||||
const handleSubmitVerify = async () => {
|
||||
if (!verifyCode.trim()) {
|
||||
alert('请输入验证信息')
|
||||
return
|
||||
}
|
||||
|
||||
setIsVerifying(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// 模拟验证成功
|
||||
setFormData({ ...formData, verifyStatus: 'verified' })
|
||||
setIsVerifying(false)
|
||||
setShowVerifyModal(false)
|
||||
setVerifyMethod(null)
|
||||
setVerifyStep(1)
|
||||
setVerifyCode('')
|
||||
alert('企业认证成功!')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
setIsEditing(false)
|
||||
alert('公司信息已保存')
|
||||
}
|
||||
|
||||
// 认证状态显示
|
||||
const renderVerifyStatus = () => {
|
||||
switch (formData.verifyStatus) {
|
||||
case 'verified':
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-green/10">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-green/20 flex items-center justify-center">
|
||||
<ShieldCheck size={20} className="text-accent-green" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-accent-green">已认证</p>
|
||||
<p className="text-sm text-text-secondary">企业身份已通过验证</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'pending':
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-amber/10">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-amber/20 flex items-center justify-center">
|
||||
<Loader2 size={20} className="text-accent-amber animate-spin" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-accent-amber">认证中</p>
|
||||
<p className="text-sm text-text-secondary">正在验证企业身份...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-coral/10">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-coral/20 flex items-center justify-center">
|
||||
<AlertCircle size={20} className="text-accent-coral" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-accent-coral">未认证</p>
|
||||
<p className="text-sm text-text-secondary">请完成企业认证以使用全部功能</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" className="w-full" onClick={() => setShowVerifyModal(true)}>
|
||||
<ShieldCheck size={16} />
|
||||
立即认证
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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">管理代理商公司资质信息</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isEditing ? (
|
||||
<Button variant="primary" onClick={() => setIsEditing(true)}>
|
||||
编辑信息
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-3">
|
||||
<Button variant="secondary" onClick={() => { setIsEditing(false); setShowSearch(false) }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 企业查询面板 */}
|
||||
{showSearch && (
|
||||
<Card className="border-2 border-accent-indigo">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Search size={18} className="text-accent-indigo" />
|
||||
查询企业信息
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowSearch(false); setSearchResults([]) }}
|
||||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||||
>
|
||||
<X size={18} className="text-text-tertiary" />
|
||||
</button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">
|
||||
输入公司名称或统一社会信用代码,系统将自动查询工商信息
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="请输入公司名称或统一社会信用代码"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="primary" onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
|
||||
{isSearching ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
查询中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search size={16} />
|
||||
查询
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
<p className="text-sm text-text-tertiary">找到 {searchResults.length} 家企业,点击选择:</p>
|
||||
{searchResults.map((company) => (
|
||||
<button
|
||||
key={company.businessLicense}
|
||||
type="button"
|
||||
onClick={() => handleSelectCompany(company)}
|
||||
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{company.companyName}</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
法人: {company.legalPerson} · {company.registeredCapital}
|
||||
</p>
|
||||
<p className="text-xs text-text-tertiary mt-1 font-mono">
|
||||
{company.businessLicense}
|
||||
</p>
|
||||
</div>
|
||||
<span className="px-2 py-1 text-xs rounded bg-accent-green/15 text-accent-green">
|
||||
{company.status}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchResults.length === 0 && searchQuery && !isSearching && (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
<Building2 size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>未找到匹配的企业</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 认证弹窗 */}
|
||||
{showVerifyModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<Card className="w-full max-w-lg mx-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<ShieldCheck size={18} className="text-accent-indigo" />
|
||||
企业认证
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowVerifyModal(false); setVerifyMethod(null); setVerifyStep(1); setVerifyCode('') }}
|
||||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||||
>
|
||||
<X size={18} className="text-text-tertiary" />
|
||||
</button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{verifyStep === 1 ? (
|
||||
<>
|
||||
<p className="text-sm text-text-secondary">
|
||||
请选择一种方式验证您是 <span className="text-text-primary font-medium">{formData.companyName}</span> 的员工
|
||||
</p>
|
||||
|
||||
{/* 对公打款验证 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartVerify('bank')}
|
||||
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
|
||||
<CreditCard size={24} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">对公账户打款验证</p>
|
||||
<p className="text-sm text-text-tertiary mt-0.5">向企业对公账户打款,输入收到的金额</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-text-tertiary" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 法人手机验证 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartVerify('legalPerson')}
|
||||
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-green/15 flex items-center justify-center">
|
||||
<Phone size={24} className="text-accent-green" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">法人手机号验证</p>
|
||||
<p className="text-sm text-text-tertiary mt-0.5">向工商登记的法人手机发送验证码</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-text-tertiary" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-text-tertiary text-center pt-2">
|
||||
认证信息仅用于验证企业身份,我们会严格保护您的隐私
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{verifyMethod === 'bank' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20">
|
||||
<p className="text-sm text-accent-indigo font-medium mb-2">对公账户打款验证</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
我们已向 <span className="text-text-primary font-medium">{formData.companyName}</span> 的对公账户打入一笔随机金额(0.01-0.99元)
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">请查询企业对公账户,输入收到的金额(精确到分):</p>
|
||||
<Input
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value)}
|
||||
placeholder="例如: 0.23"
|
||||
className="text-center text-lg font-mono"
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary">打款通常在1-3个工作日内到账</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-accent-green/10 border border-accent-green/20">
|
||||
<p className="text-sm text-accent-green font-medium mb-2">法人手机号验证</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
验证码已发送至法人 <span className="text-text-primary font-medium">{formData.legalPerson}</span> 的工商登记手机号
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">请输入法人收到的6位验证码:</p>
|
||||
<Input
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value)}
|
||||
placeholder="请输入验证码"
|
||||
maxLength={6}
|
||||
className="text-center text-lg font-mono tracking-widest"
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary">验证码5分钟内有效,请联系法人获取</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button variant="secondary" className="flex-1" onClick={() => { setVerifyStep(1); setVerifyCode('') }}>
|
||||
返回
|
||||
</Button>
|
||||
<Button variant="primary" className="flex-1" onClick={handleSubmitVerify} disabled={isVerifying || !verifyCode.trim()}>
|
||||
{isVerifying ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
验证中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check size={16} />
|
||||
确认验证
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Building2 size={18} className="text-accent-indigo" />
|
||||
工商信息
|
||||
</span>
|
||||
{isEditing && !showSearch && (
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowSearch(true)}>
|
||||
<Search size={14} />
|
||||
查询企业
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">公司全称</label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.companyName}
|
||||
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-text-primary font-medium">{formData.companyName}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">公司简称</label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.shortName}
|
||||
onChange={(e) => setFormData({ ...formData, shortName: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-text-primary font-medium">{formData.shortName}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">统一社会信用代码</label>
|
||||
<p className="text-text-primary font-mono">{formData.businessLicense}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">法定代表人</label>
|
||||
<p className="text-text-primary">{formData.legalPerson}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">注册资本</label>
|
||||
<p className="text-text-primary">{formData.registeredCapital}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">成立日期</label>
|
||||
<p className="text-text-primary">{formData.establishDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">经营范围</label>
|
||||
<p className="text-text-primary">{formData.businessScope}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">注册地址</label>
|
||||
<p className="text-text-primary">{formData.address}</p>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="p-3 rounded-lg bg-accent-indigo/10 border border-accent-indigo/20">
|
||||
<p className="text-sm text-accent-indigo">
|
||||
💡 提示:点击右上角"查询企业"按钮,输入公司名称可自动填充工商信息
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 联系信息 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Phone size={18} className="text-accent-green" />
|
||||
联系信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block flex items-center gap-1">
|
||||
<Phone size={14} />
|
||||
联系电话
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.contactPhone}
|
||||
onChange={(e) => setFormData({ ...formData, contactPhone: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-text-primary">{formData.contactPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block flex items-center gap-1">
|
||||
<Mail size={14} />
|
||||
联系邮箱
|
||||
</label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.contactEmail}
|
||||
onChange={(e) => setFormData({ ...formData, contactEmail: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-text-primary">{formData.contactEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:认证状态 */}
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>企业认证</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{renderVerifyStatus()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 已认证显示验证信息 */}
|
||||
{formData.verifyStatus === 'verified' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CreditCard size={18} className="text-accent-indigo" />
|
||||
对公账户
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm text-text-tertiary">开户银行</label>
|
||||
<p className="text-text-primary">{formData.bankInfo?.bankName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-tertiary">账号</label>
|
||||
<p className="text-text-primary font-mono">{formData.bankInfo?.accountNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
194
frontend/app/agency/profile/edit/page.tsx
Normal file
194
frontend/app/agency/profile/edit/page.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
CircleUser,
|
||||
Camera,
|
||||
Copy,
|
||||
Check
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUserData = {
|
||||
avatar: '星',
|
||||
name: '张经理',
|
||||
agencyId: 'AG789012',
|
||||
phone: '138****8888',
|
||||
email: 'zhang@starmedia.com',
|
||||
position: '运营总监',
|
||||
department: '内容审核部',
|
||||
}
|
||||
|
||||
export default function AgencyProfileEditPage() {
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState(mockUserData)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(formData.agencyId)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('个人信息已保存')
|
||||
router.back()
|
||||
}
|
||||
|
||||
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">编辑您的个人资料</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:头像 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>头像</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-32 h-32 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366F1 0%, #4F46E5 100%)',
|
||||
}}
|
||||
>
|
||||
<span className="text-5xl font-bold text-white">{formData.avatar}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute bottom-0 right-0 w-10 h-10 rounded-full bg-accent-indigo flex items-center justify-center text-white shadow-lg hover:bg-accent-indigo/80 transition-colors"
|
||||
>
|
||||
<Camera size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-tertiary mt-4">点击相机图标更换头像</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">支持 JPG、PNG,最大 2MB</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 右侧:基本信息 */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CircleUser size={18} className="text-accent-indigo" />
|
||||
基本信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
{/* 代理商ID(只读) */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">代理商ID</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 px-4 py-2.5 rounded-xl bg-bg-elevated border border-border-subtle">
|
||||
<span className="font-mono font-medium text-accent-indigo">{formData.agencyId}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyId}
|
||||
className="px-4 py-2.5 rounded-xl bg-bg-elevated border border-border-subtle hover:bg-bg-page transition-colors flex items-center gap-2"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={16} className="text-accent-green" />
|
||||
<span className="text-accent-green text-sm">已复制</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={16} className="text-text-secondary" />
|
||||
<span className="text-text-secondary text-sm">复制</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary mt-1">代理商ID不可修改,用于邀请达人时使用</p>
|
||||
</div>
|
||||
|
||||
{/* 姓名 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">姓名</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 职位和部门 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">职位</label>
|
||||
<Input
|
||||
value={formData.position}
|
||||
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
|
||||
placeholder="请输入职位"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">部门</label>
|
||||
<Input
|
||||
value={formData.department}
|
||||
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
|
||||
placeholder="请输入部门"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 联系方式 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">手机号</label>
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">邮箱</label>
|
||||
<Input
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
292
frontend/app/agency/profile/page.tsx
Normal file
292
frontend/app/agency/profile/page.tsx
Normal file
@ -0,0 +1,292 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
CircleUser,
|
||||
Settings,
|
||||
BellRing,
|
||||
History,
|
||||
MessageCircleQuestion,
|
||||
ChevronRight,
|
||||
LogOut,
|
||||
Copy,
|
||||
Check,
|
||||
Building2,
|
||||
Users,
|
||||
FileCheck
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// 代理商数据
|
||||
const mockAgency = {
|
||||
name: '星辰传媒',
|
||||
initial: '星',
|
||||
agencyId: 'AG789012', // 代理商ID
|
||||
companyName: '上海星辰文化传媒有限公司',
|
||||
role: '官方认证代理商',
|
||||
stats: {
|
||||
creators: 156, // 管理达人数
|
||||
reviewed: 1280, // 累计审核数
|
||||
passRate: 88, // 通过率
|
||||
monthlyTasks: 45, // 本月任务
|
||||
},
|
||||
}
|
||||
|
||||
// 菜单项数据
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'company',
|
||||
icon: Building2,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo',
|
||||
title: '公司信息',
|
||||
subtitle: '公司名称、营业执照、联系方式',
|
||||
},
|
||||
{
|
||||
id: 'personal',
|
||||
icon: CircleUser,
|
||||
iconColor: 'text-accent-blue',
|
||||
bgColor: 'bg-accent-blue',
|
||||
title: '个人信息',
|
||||
subtitle: '头像、昵称、负责人信息',
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
icon: Settings,
|
||||
iconColor: 'text-accent-green',
|
||||
bgColor: 'bg-accent-green',
|
||||
title: '账户设置',
|
||||
subtitle: '修改密码、账号安全',
|
||||
},
|
||||
{
|
||||
id: 'notification',
|
||||
icon: BellRing,
|
||||
iconColor: 'text-accent-amber',
|
||||
bgColor: 'bg-accent-amber',
|
||||
title: '消息设置',
|
||||
subtitle: '通知开关、提醒偏好',
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
icon: History,
|
||||
iconColor: 'text-accent-coral',
|
||||
bgColor: 'bg-accent-coral',
|
||||
title: '审核历史',
|
||||
subtitle: '查看历史审核记录',
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
icon: MessageCircleQuestion,
|
||||
iconColor: 'text-text-secondary',
|
||||
bgColor: 'bg-bg-elevated',
|
||||
title: '帮助与反馈',
|
||||
subtitle: '常见问题、联系客服',
|
||||
},
|
||||
]
|
||||
|
||||
// 代理商卡片组件
|
||||
function AgencyCard() {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// 复制代理商ID
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(mockAgency.agencyId)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex flex-col gap-5">
|
||||
{/* 头像和信息 */}
|
||||
<div className="flex items-center gap-5">
|
||||
{/* 头像 */}
|
||||
<div
|
||||
className="w-20 h-20 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366F1 0%, #4F46E5 100%)',
|
||||
}}
|
||||
>
|
||||
<span className="text-[32px] font-bold text-white">{mockAgency.initial}</span>
|
||||
</div>
|
||||
{/* 代理商信息 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xl font-semibold text-text-primary">{mockAgency.name}</span>
|
||||
<span className="text-sm text-text-secondary">{mockAgency.role}</span>
|
||||
{/* 代理商ID */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-text-tertiary">代理商ID:</span>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-bg-elevated">
|
||||
<span className="text-xs font-mono font-medium text-accent-indigo">{mockAgency.agencyId}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyId}
|
||||
className="p-0.5 hover:bg-bg-card rounded transition-colors"
|
||||
title={copied ? '已复制' : '复制代理商ID'}
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={12} className="text-accent-green" />
|
||||
) : (
|
||||
<Copy size={12} className="text-text-tertiary hover:text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{copied && (
|
||||
<span className="text-xs text-accent-green animate-fade-in">已复制</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 公司名称 */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-bg-elevated">
|
||||
<Building2 size={16} className="text-text-tertiary" />
|
||||
<span className="text-sm text-text-secondary">{mockAgency.companyName}</span>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="grid grid-cols-4 gap-4 pt-4 border-t border-border-subtle">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Users size={14} className="text-accent-indigo" />
|
||||
<span className="text-xl font-bold text-text-primary">{mockAgency.stats.creators}</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-secondary">管理达人</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<FileCheck size={14} className="text-accent-blue" />
|
||||
<span className="text-xl font-bold text-text-primary">{mockAgency.stats.reviewed}</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-secondary">累计审核</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-xl font-bold text-accent-green">{mockAgency.stats.passRate}%</span>
|
||||
<span className="text-xs text-text-secondary">通过率</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-xl font-bold text-accent-amber">{mockAgency.stats.monthlyTasks}</span>
|
||||
<span className="text-xs text-text-secondary">本月任务</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 菜单项组件
|
||||
function MenuItem({ item, onClick }: { item: typeof menuItems[0]; onClick: () => void }) {
|
||||
const Icon = item.icon
|
||||
const isPlainBg = item.bgColor === 'bg-bg-elevated'
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex items-center justify-between py-4 w-full text-left hover:bg-bg-elevated/30 transition-colors rounded-lg px-2 -mx-2"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 图标背景 */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-[10px] flex items-center justify-center',
|
||||
isPlainBg ? item.bgColor : `${item.bgColor}/15`
|
||||
)}
|
||||
>
|
||||
<Icon size={20} className={item.iconColor} />
|
||||
</div>
|
||||
{/* 文字 */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-[15px] font-medium text-text-primary">{item.title}</span>
|
||||
<span className="text-[13px] text-text-tertiary">{item.subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-text-tertiary" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// 菜单卡片组件
|
||||
function MenuCard({ onMenuClick }: { onMenuClick: (id: string) => void }) {
|
||||
return (
|
||||
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex flex-col">
|
||||
{menuItems.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<MenuItem item={item} onClick={() => onMenuClick(item.id)} />
|
||||
{index < menuItems.length - 1 && (
|
||||
<div className="h-px bg-border-subtle" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 退出卡片组件
|
||||
function LogoutCard({ onLogout }: { onLogout: () => void }) {
|
||||
return (
|
||||
<div className="bg-bg-card rounded-2xl p-6 card-shadow">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
className="w-full flex items-center justify-center gap-2 py-4 rounded-xl border-[1.5px] border-accent-coral text-accent-coral font-medium hover:bg-accent-coral/10 transition-colors"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AgencyProfilePage() {
|
||||
const router = useRouter()
|
||||
|
||||
// 菜单项点击处理
|
||||
const handleMenuClick = (menuId: string) => {
|
||||
const routes: Record<string, string> = {
|
||||
company: '/agency/profile/company',
|
||||
personal: '/agency/profile/edit',
|
||||
account: '/agency/settings/account',
|
||||
notification: '/agency/settings/notification',
|
||||
history: '/agency/review/history',
|
||||
help: '/agency/help',
|
||||
}
|
||||
const route = routes[menuId]
|
||||
if (route) {
|
||||
router.push(route)
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
// TODO: 实际退出逻辑
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部栏 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-text-primary">个人中心</h1>
|
||||
<p className="text-sm text-text-secondary">管理代理商账户信息和偏好设置</p>
|
||||
</div>
|
||||
|
||||
{/* 内容区 - 响应式布局 */}
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* 代理商卡片 */}
|
||||
<div className="lg:w-[400px] lg:flex-shrink-0">
|
||||
<AgencyCard />
|
||||
</div>
|
||||
|
||||
{/* 菜单和退出 */}
|
||||
<div className="flex-1 flex flex-col gap-5">
|
||||
<MenuCard onMenuClick={handleMenuClick} />
|
||||
<LogoutCard onLogout={handleLogout} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
291
frontend/app/agency/review/history/page.tsx
Normal file
291
frontend/app/agency/review/history/page.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
ArrowLeft,
|
||||
History,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Search,
|
||||
Filter,
|
||||
FileText,
|
||||
Video,
|
||||
User,
|
||||
Calendar,
|
||||
Download
|
||||
} from 'lucide-react'
|
||||
|
||||
// 审核历史记录类型
|
||||
interface ReviewHistoryItem {
|
||||
id: string
|
||||
taskId: string
|
||||
taskTitle: string
|
||||
creatorName: string
|
||||
contentType: 'script' | 'video'
|
||||
result: 'approved' | 'rejected'
|
||||
reason?: string
|
||||
reviewedAt: string
|
||||
projectName: string
|
||||
}
|
||||
|
||||
// 模拟审核历史数据
|
||||
const mockHistoryData: ReviewHistoryItem[] = [
|
||||
{
|
||||
id: 'h-001',
|
||||
taskId: 'task-101',
|
||||
taskTitle: '夏日护肤推广脚本',
|
||||
creatorName: '小美护肤',
|
||||
contentType: 'script',
|
||||
result: 'approved',
|
||||
reviewedAt: '2026-02-06 14:30',
|
||||
projectName: 'XX品牌618推广',
|
||||
},
|
||||
{
|
||||
id: 'h-002',
|
||||
taskId: 'task-102',
|
||||
taskTitle: '新品口红试色视频',
|
||||
creatorName: '美妆Lisa',
|
||||
contentType: 'video',
|
||||
result: 'rejected',
|
||||
reason: '背景音乐版权问题',
|
||||
reviewedAt: '2026-02-06 11:20',
|
||||
projectName: 'YY口红新品发布',
|
||||
},
|
||||
{
|
||||
id: 'h-003',
|
||||
taskId: 'task-103',
|
||||
taskTitle: '健身器材推荐脚本',
|
||||
creatorName: '健身教练王',
|
||||
contentType: 'script',
|
||||
result: 'approved',
|
||||
reviewedAt: '2026-02-05 16:45',
|
||||
projectName: 'ZZ运动品牌推广',
|
||||
},
|
||||
{
|
||||
id: 'h-004',
|
||||
taskId: 'task-104',
|
||||
taskTitle: '美妆新品测评视频',
|
||||
creatorName: '达人小红',
|
||||
contentType: 'video',
|
||||
result: 'rejected',
|
||||
reason: '品牌调性不符',
|
||||
reviewedAt: '2026-02-05 10:15',
|
||||
projectName: 'XX品牌618推广',
|
||||
},
|
||||
{
|
||||
id: 'h-005',
|
||||
taskId: 'task-105',
|
||||
taskTitle: '数码产品开箱脚本',
|
||||
creatorName: '科技小哥',
|
||||
contentType: 'script',
|
||||
result: 'approved',
|
||||
reviewedAt: '2026-02-04 15:30',
|
||||
projectName: 'AA数码新品上市',
|
||||
},
|
||||
]
|
||||
|
||||
export default function AgencyReviewHistoryPage() {
|
||||
const router = useRouter()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterResult, setFilterResult] = useState<'all' | 'approved' | 'rejected'>('all')
|
||||
const [filterType, setFilterType] = useState<'all' | 'script' | 'video'>('all')
|
||||
|
||||
// 筛选数据
|
||||
const filteredHistory = mockHistoryData.filter(item => {
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
item.taskTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.projectName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesResult = filterResult === 'all' || item.result === filterResult
|
||||
const matchesType = filterType === 'all' || item.contentType === filterType
|
||||
return matchesSearch && matchesResult && matchesType
|
||||
})
|
||||
|
||||
// 统计
|
||||
const approvedCount = mockHistoryData.filter(i => i.result === 'approved').length
|
||||
const rejectedCount = mockHistoryData.filter(i => i.result === 'rejected').length
|
||||
|
||||
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">查看您的历史审核记录</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary">
|
||||
<Download size={16} />
|
||||
导出记录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-xl bg-bg-card card-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<History size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-text-primary">{mockHistoryData.length}</p>
|
||||
<p className="text-sm text-text-secondary">总审核数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-bg-card card-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-green/15 flex items-center justify-center">
|
||||
<CheckCircle size={20} className="text-accent-green" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-accent-green">{approvedCount}</p>
|
||||
<p className="text-sm text-text-secondary">已通过</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-bg-card card-shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-coral/15 flex items-center justify-center">
|
||||
<XCircle size={20} className="text-accent-coral" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-accent-coral">{rejectedCount}</p>
|
||||
<p className="text-sm text-text-secondary">已驳回</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="relative flex-1 min-w-[240px] 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-2">
|
||||
<span className="text-sm text-text-tertiary">结果:</span>
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
||||
{[
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'approved', label: '通过' },
|
||||
{ value: 'rejected', label: '驳回' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => setFilterResult(tab.value as typeof filterResult)}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
filterResult === tab.value ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-text-tertiary">类型:</span>
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
||||
{[
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'script', label: '脚本' },
|
||||
{ value: 'video', label: '视频' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => setFilterType(tab.value as typeof filterType)}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
filterType === tab.value ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 历史列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History size={18} className="text-accent-indigo" />
|
||||
审核记录
|
||||
<span className="ml-auto text-sm font-normal text-text-secondary">
|
||||
共 {filteredHistory.length} 条
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{filteredHistory.length > 0 ? (
|
||||
filteredHistory.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 rounded-xl bg-bg-elevated hover:bg-bg-elevated/80 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
item.result === 'approved'
|
||||
? 'bg-accent-green/15 text-accent-green'
|
||||
: 'bg-accent-coral/15 text-accent-coral'
|
||||
}`}>
|
||||
{item.result === 'approved' ? '已通过' : '已驳回'}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-text-tertiary">
|
||||
{item.contentType === 'script' ? <FileText size={12} /> : <Video size={12} />}
|
||||
{item.contentType === 'script' ? '脚本' : '视频'}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-medium text-text-primary mb-1">{item.taskTitle}</h3>
|
||||
<div className="flex items-center gap-4 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={14} />
|
||||
{item.creatorName}
|
||||
</span>
|
||||
<span>{item.projectName}</span>
|
||||
</div>
|
||||
{item.reason && (
|
||||
<p className="mt-2 text-sm text-accent-coral">驳回原因: {item.reason}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-1 text-sm text-text-tertiary">
|
||||
<Calendar size={14} />
|
||||
{item.reviewedAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 text-text-tertiary">
|
||||
<History size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>没有找到匹配的审核记录</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
240
frontend/app/agency/settings/account/page.tsx
Normal file
240
frontend/app/agency/settings/account/page.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Lock,
|
||||
Shield,
|
||||
Smartphone,
|
||||
Mail,
|
||||
Key,
|
||||
Eye,
|
||||
EyeOff,
|
||||
CheckCircle,
|
||||
AlertTriangle
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function AgencyAccountSettingsPage() {
|
||||
const router = useRouter()
|
||||
const [showOldPassword, setShowOldPassword] = useState(false)
|
||||
const [showNewPassword, setShowNewPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 模拟账号安全状态
|
||||
const securityStatus = {
|
||||
phone: { bound: true, value: '138****8888' },
|
||||
email: { bound: true, value: 'zhang@starmedia.com' },
|
||||
twoFactor: { enabled: false },
|
||||
}
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
||||
alert('请填写完整密码信息')
|
||||
return
|
||||
}
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
alert('两次输入的新密码不一致')
|
||||
return
|
||||
}
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
alert('新密码长度不能少于8位')
|
||||
return
|
||||
}
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('密码修改成功')
|
||||
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部导航 */}
|
||||
<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">管理密码和账号安全</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 修改密码 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Lock size={18} className="text-accent-indigo" />
|
||||
修改密码
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">当前密码</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showOldPassword ? 'text' : 'password'}
|
||||
value={passwordForm.oldPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOldPassword(!showOldPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
{showOldPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">新密码</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
value={passwordForm.newPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
|
||||
placeholder="请输入新密码(至少8位)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
{showNewPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">确认新密码</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={handleChangePassword}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? '保存中...' : '确认修改'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 账号安全 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield size={18} className="text-accent-green" />
|
||||
账号安全
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 手机绑定 */}
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Smartphone size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">手机绑定</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{securityStatus.phone.bound ? securityStatus.phone.value : '未绑定'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{securityStatus.phone.bound ? (
|
||||
<CheckCircle size={18} className="text-accent-green" />
|
||||
) : (
|
||||
<AlertTriangle size={18} className="text-accent-amber" />
|
||||
)}
|
||||
<Button variant="secondary" size="sm">
|
||||
{securityStatus.phone.bound ? '更换' : '绑定'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邮箱绑定 */}
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-blue/15 flex items-center justify-center">
|
||||
<Mail size={20} className="text-accent-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">邮箱绑定</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{securityStatus.email.bound ? securityStatus.email.value : '未绑定'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{securityStatus.email.bound ? (
|
||||
<CheckCircle size={18} className="text-accent-green" />
|
||||
) : (
|
||||
<AlertTriangle size={18} className="text-accent-amber" />
|
||||
)}
|
||||
<Button variant="secondary" size="sm">
|
||||
{securityStatus.email.bound ? '更换' : '绑定'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 两步验证 */}
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-amber/15 flex items-center justify-center">
|
||||
<Key size={20} className="text-accent-amber" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">两步验证</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{securityStatus.twoFactor.enabled ? '已开启' : '未开启,建议开启以增强安全'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{securityStatus.twoFactor.enabled ? (
|
||||
<CheckCircle size={18} className="text-accent-green" />
|
||||
) : (
|
||||
<AlertTriangle size={18} className="text-accent-amber" />
|
||||
)}
|
||||
<Button variant="secondary" size="sm">
|
||||
{securityStatus.twoFactor.enabled ? '关闭' : '开启'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
244
frontend/app/agency/settings/notification/page.tsx
Normal file
244
frontend/app/agency/settings/notification/page.tsx
Normal file
@ -0,0 +1,244 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import {
|
||||
ArrowLeft,
|
||||
BellRing,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Smartphone,
|
||||
FileText,
|
||||
Users,
|
||||
AlertTriangle
|
||||
} from 'lucide-react'
|
||||
|
||||
// 通知设置类型
|
||||
interface NotificationSetting {
|
||||
id: string
|
||||
icon: React.ElementType
|
||||
iconColor: string
|
||||
title: string
|
||||
description: string
|
||||
email: boolean
|
||||
push: boolean
|
||||
sms: boolean
|
||||
}
|
||||
|
||||
// 通知设置数据
|
||||
const initialSettings: NotificationSetting[] = [
|
||||
{
|
||||
id: 'review',
|
||||
icon: FileText,
|
||||
iconColor: 'text-accent-indigo',
|
||||
title: '审核任务通知',
|
||||
description: '有新任务待审核时通知',
|
||||
email: true,
|
||||
push: true,
|
||||
sms: false,
|
||||
},
|
||||
{
|
||||
id: 'appeal',
|
||||
icon: MessageSquare,
|
||||
iconColor: 'text-accent-amber',
|
||||
title: '申诉通知',
|
||||
description: '达人提交申诉时通知',
|
||||
email: true,
|
||||
push: true,
|
||||
sms: true,
|
||||
},
|
||||
{
|
||||
id: 'creator',
|
||||
icon: Users,
|
||||
iconColor: 'text-accent-green',
|
||||
title: '达人动态',
|
||||
description: '达人提交内容、完成任务时通知',
|
||||
email: false,
|
||||
push: true,
|
||||
sms: false,
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-accent-coral',
|
||||
title: '紧急通知',
|
||||
description: '任务即将超时、品牌方催促等',
|
||||
email: true,
|
||||
push: true,
|
||||
sms: true,
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
icon: BellRing,
|
||||
iconColor: 'text-accent-blue',
|
||||
title: '系统通知',
|
||||
description: '系统更新、维护公告等',
|
||||
email: true,
|
||||
push: false,
|
||||
sms: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 开关组件
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${
|
||||
checked ? 'bg-accent-indigo' : 'bg-bg-elevated'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
checked ? 'left-6' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AgencyNotificationSettingsPage() {
|
||||
const router = useRouter()
|
||||
const [settings, setSettings] = useState(initialSettings)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const updateSetting = (id: string, field: 'email' | 'push' | 'sms', value: boolean) => {
|
||||
setSettings(prev =>
|
||||
prev.map(s =>
|
||||
s.id === id ? { ...s, [field]: value } : s
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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 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">管理您的通知偏好</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? '保存中...' : '保存设置'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 通知渠道说明 */}
|
||||
<div className="flex gap-6 p-4 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<BellRing size={16} className="text-accent-indigo" />
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary">App推送</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-blue/15 flex items-center justify-center">
|
||||
<Mail size={16} className="text-accent-blue" />
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary">邮件通知</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-green/15 flex items-center justify-center">
|
||||
<Smartphone size={16} className="text-accent-green" />
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary">短信通知</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 通知设置列表 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BellRing size={18} className="text-accent-indigo" />
|
||||
通知类型设置
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
{/* 表头 */}
|
||||
<div className="flex items-center py-3 px-4 text-sm text-text-tertiary border-b border-border-subtle">
|
||||
<div className="flex-1">通知类型</div>
|
||||
<div className="w-20 text-center">App推送</div>
|
||||
<div className="w-20 text-center">邮件</div>
|
||||
<div className="w-20 text-center">短信</div>
|
||||
</div>
|
||||
|
||||
{/* 设置项 */}
|
||||
{settings.map((setting) => {
|
||||
const Icon = setting.icon
|
||||
return (
|
||||
<div
|
||||
key={setting.id}
|
||||
className="flex items-center py-4 px-4 hover:bg-bg-elevated/50 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-opacity-15 flex items-center justify-center`}
|
||||
style={{ backgroundColor: `${setting.iconColor.replace('text-', '')}15` }}
|
||||
>
|
||||
<Icon size={20} className={setting.iconColor} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{setting.title}</p>
|
||||
<p className="text-sm text-text-tertiary">{setting.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-20 flex justify-center">
|
||||
<Toggle
|
||||
checked={setting.push}
|
||||
onChange={(v) => updateSetting(setting.id, 'push', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 flex justify-center">
|
||||
<Toggle
|
||||
checked={setting.email}
|
||||
onChange={(v) => updateSetting(setting.id, 'email', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 flex justify-center">
|
||||
<Toggle
|
||||
checked={setting.sms}
|
||||
onChange={(v) => updateSetting(setting.id, 'sms', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 免打扰设置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>免打扰设置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">夜间免打扰</p>
|
||||
<p className="text-sm text-text-tertiary">22:00 - 08:00 期间不发送推送通知</p>
|
||||
</div>
|
||||
<Toggle checked={true} onChange={() => {}} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -11,18 +11,49 @@ import {
|
||||
Users,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Mail,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
MoreVertical
|
||||
MoreVertical,
|
||||
Building2,
|
||||
AlertCircle,
|
||||
UserPlus,
|
||||
MessageSquareText,
|
||||
Trash2,
|
||||
FolderPlus
|
||||
} from 'lucide-react'
|
||||
|
||||
// 代理商类型
|
||||
interface Agency {
|
||||
id: string
|
||||
agencyId: string // 代理商ID(AG开头)
|
||||
name: string
|
||||
companyName: string
|
||||
email: string
|
||||
status: 'active' | 'pending' | 'paused'
|
||||
creatorCount: number
|
||||
projectCount: number
|
||||
passRate: number
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
joinedAt: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 模拟项目列表(用于分配代理商)
|
||||
const mockProjects = [
|
||||
{ id: 'proj-001', name: 'XX品牌618推广' },
|
||||
{ id: 'proj-002', name: '口红系列推广' },
|
||||
{ id: 'proj-003', name: 'XX运动品牌' },
|
||||
{ id: 'proj-004', name: '护肤品秋季活动' },
|
||||
]
|
||||
|
||||
// 模拟代理商列表
|
||||
const mockAgencies = [
|
||||
const initialAgencies: Agency[] = [
|
||||
{
|
||||
id: 'agency-001',
|
||||
id: 'a-001',
|
||||
agencyId: 'AG789012',
|
||||
name: '星耀传媒',
|
||||
companyName: '上海星耀文化传媒有限公司',
|
||||
email: 'contact@xingyao.com',
|
||||
status: 'active',
|
||||
creatorCount: 50,
|
||||
@ -32,8 +63,10 @@ const mockAgencies = [
|
||||
joinedAt: '2025-06-15',
|
||||
},
|
||||
{
|
||||
id: 'agency-002',
|
||||
id: 'a-002',
|
||||
agencyId: 'AG456789',
|
||||
name: '创意无限',
|
||||
companyName: '深圳创意无限广告有限公司',
|
||||
email: 'hello@chuangyi.com',
|
||||
status: 'active',
|
||||
creatorCount: 35,
|
||||
@ -43,8 +76,10 @@ const mockAgencies = [
|
||||
joinedAt: '2025-08-20',
|
||||
},
|
||||
{
|
||||
id: 'agency-003',
|
||||
id: 'a-003',
|
||||
agencyId: 'AG123456',
|
||||
name: '美妆达人MCN',
|
||||
companyName: '杭州美妆达人网络科技有限公司',
|
||||
email: 'biz@meizhuang.com',
|
||||
status: 'active',
|
||||
creatorCount: 28,
|
||||
@ -54,9 +89,11 @@ const mockAgencies = [
|
||||
joinedAt: '2025-10-10',
|
||||
},
|
||||
{
|
||||
id: 'agency-004',
|
||||
name: '时尚风向标',
|
||||
email: 'info@shishang.com',
|
||||
id: 'a-004',
|
||||
agencyId: 'AG111111',
|
||||
name: '蓝海科技',
|
||||
companyName: '北京蓝海数字科技有限公司',
|
||||
email: 'info@lanhai.com',
|
||||
status: 'pending',
|
||||
creatorCount: 0,
|
||||
projectCount: 0,
|
||||
@ -74,35 +111,130 @@ function StatusTag({ status }: { status: string }) {
|
||||
|
||||
export default function AgenciesManagePage() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||
const [inviteEmail, setInviteEmail] = useState('')
|
||||
const [inviteLink, setInviteLink] = useState('')
|
||||
const [agencies, setAgencies] = useState<Agency[]>(initialAgencies)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
const filteredAgencies = mockAgencies.filter(agency =>
|
||||
// 邀请代理商弹窗
|
||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||
const [inviteAgencyId, setInviteAgencyId] = useState('')
|
||||
const [inviteResult, setInviteResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
// 操作菜单状态
|
||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null)
|
||||
|
||||
// 备注弹窗状态
|
||||
const [remarkModal, setRemarkModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||||
const [remarkText, setRemarkText] = useState('')
|
||||
|
||||
// 删除确认弹窗状态
|
||||
const [deleteModal, setDeleteModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||||
|
||||
// 分配项目弹窗状态
|
||||
const [assignModal, setAssignModal] = useState<{ open: boolean; agency: Agency | null }>({ open: false, agency: null })
|
||||
const [selectedProjects, setSelectedProjects] = useState<string[]>([])
|
||||
|
||||
const filteredAgencies = agencies.filter(agency =>
|
||||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.email.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
agency.agencyId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// 复制代理商ID
|
||||
const handleCopyAgencyId = async (agencyId: string) => {
|
||||
await navigator.clipboard.writeText(agencyId)
|
||||
setCopiedId(agencyId)
|
||||
setTimeout(() => setCopiedId(null), 2000)
|
||||
}
|
||||
|
||||
// 邀请代理商
|
||||
const handleInvite = () => {
|
||||
if (!inviteEmail.trim()) {
|
||||
alert('请输入代理商邮箱')
|
||||
if (!inviteAgencyId.trim()) {
|
||||
setInviteResult({ success: false, message: '请输入代理商ID' })
|
||||
return
|
||||
}
|
||||
// 模拟生成邀请链接
|
||||
const link = `https://miaosi.app/invite/agency/${Date.now()}`
|
||||
setInviteLink(link)
|
||||
|
||||
// 检查代理商ID格式
|
||||
const idPattern = /^AG\d{6}$/
|
||||
if (!idPattern.test(inviteAgencyId.toUpperCase())) {
|
||||
setInviteResult({ success: false, message: '代理商ID格式错误,应为AG+6位数字' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已邀请
|
||||
if (agencies.some(a => a.agencyId === inviteAgencyId.toUpperCase())) {
|
||||
setInviteResult({ success: false, message: '该代理商已在您的列表中' })
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟发送邀请成功
|
||||
setInviteResult({ success: true, message: `已向代理商 ${inviteAgencyId.toUpperCase()} 发送邀请` })
|
||||
}
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(inviteLink)
|
||||
alert('链接已复制')
|
||||
}
|
||||
|
||||
const handleSendInvite = () => {
|
||||
alert(`邀请已发送至 ${inviteEmail}`)
|
||||
const handleCloseInviteModal = () => {
|
||||
setShowInviteModal(false)
|
||||
setInviteEmail('')
|
||||
setInviteLink('')
|
||||
setInviteAgencyId('')
|
||||
setInviteResult(null)
|
||||
}
|
||||
|
||||
// 打开备注弹窗
|
||||
const handleOpenRemark = (agency: Agency) => {
|
||||
setRemarkText(agency.remark || '')
|
||||
setRemarkModal({ open: true, agency })
|
||||
setOpenMenuId(null)
|
||||
}
|
||||
|
||||
// 保存备注
|
||||
const handleSaveRemark = () => {
|
||||
if (remarkModal.agency) {
|
||||
setAgencies(prev => prev.map(a =>
|
||||
a.id === remarkModal.agency!.id ? { ...a, remark: remarkText } : a
|
||||
))
|
||||
}
|
||||
setRemarkModal({ open: false, agency: null })
|
||||
setRemarkText('')
|
||||
}
|
||||
|
||||
// 打开删除确认
|
||||
const handleOpenDelete = (agency: Agency) => {
|
||||
setDeleteModal({ open: true, agency })
|
||||
setOpenMenuId(null)
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const handleConfirmDelete = () => {
|
||||
if (deleteModal.agency) {
|
||||
setAgencies(prev => prev.filter(a => a.id !== deleteModal.agency!.id))
|
||||
}
|
||||
setDeleteModal({ open: false, agency: null })
|
||||
}
|
||||
|
||||
// 打开分配项目弹窗
|
||||
const handleOpenAssign = (agency: Agency) => {
|
||||
setSelectedProjects([])
|
||||
setAssignModal({ open: true, agency })
|
||||
setOpenMenuId(null)
|
||||
}
|
||||
|
||||
// 切换项目选择
|
||||
const toggleProjectSelection = (projectId: string) => {
|
||||
setSelectedProjects(prev =>
|
||||
prev.includes(projectId)
|
||||
? prev.filter(id => id !== projectId)
|
||||
: [...prev, projectId]
|
||||
)
|
||||
}
|
||||
|
||||
// 确认分配项目
|
||||
const handleConfirmAssign = () => {
|
||||
if (assignModal.agency && selectedProjects.length > 0) {
|
||||
const projectNames = mockProjects
|
||||
.filter(p => selectedProjects.includes(p.id))
|
||||
.map(p => p.name)
|
||||
.join('、')
|
||||
alert(`已将代理商「${assignModal.agency.name}」分配到项目「${projectNames}」`)
|
||||
}
|
||||
setAssignModal({ open: false, agency: null })
|
||||
setSelectedProjects([])
|
||||
}
|
||||
|
||||
return (
|
||||
@ -126,7 +258,7 @@ export default function AgenciesManagePage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-text-secondary">总代理商</p>
|
||||
<p className="text-2xl font-bold text-text-primary">{mockAgencies.length}</p>
|
||||
<p className="text-2xl font-bold text-text-primary">{agencies.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/20 flex items-center justify-center">
|
||||
<Users size={20} className="text-accent-indigo" />
|
||||
@ -139,7 +271,7 @@ export default function AgenciesManagePage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-text-secondary">已激活</p>
|
||||
<p className="text-2xl font-bold text-accent-green">{mockAgencies.filter(a => a.status === 'active').length}</p>
|
||||
<p className="text-2xl font-bold text-accent-green">{agencies.filter(a => a.status === 'active').length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-green/20 flex items-center justify-center">
|
||||
<CheckCircle size={20} className="text-accent-green" />
|
||||
@ -152,7 +284,7 @@ export default function AgenciesManagePage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-text-secondary">待接受</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{mockAgencies.filter(a => a.status === 'pending').length}</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{agencies.filter(a => a.status === 'pending').length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
|
||||
<Clock size={20} className="text-yellow-400" />
|
||||
@ -166,7 +298,9 @@ export default function AgenciesManagePage() {
|
||||
<div>
|
||||
<p className="text-sm text-text-secondary">平均通过率</p>
|
||||
<p className="text-2xl font-bold text-text-primary">
|
||||
{Math.round(mockAgencies.filter(a => a.status === 'active').reduce((sum, a) => sum + a.passRate, 0) / mockAgencies.filter(a => a.status === 'active').length)}%
|
||||
{agencies.filter(a => a.status === 'active').length > 0
|
||||
? Math.round(agencies.filter(a => a.status === 'active').reduce((sum, a) => sum + a.passRate, 0) / agencies.filter(a => a.status === 'active').length)
|
||||
: 0}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||
@ -182,10 +316,10 @@ export default function AgenciesManagePage() {
|
||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索代理商名称或邮箱..."
|
||||
placeholder="搜索代理商名称、ID或公司名..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
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>
|
||||
|
||||
@ -196,6 +330,7 @@ export default function AgenciesManagePage() {
|
||||
<thead>
|
||||
<tr className="border-b border-border-subtle text-left text-sm text-text-secondary bg-bg-elevated">
|
||||
<th className="px-6 py-4 font-medium">代理商</th>
|
||||
<th className="px-6 py-4 font-medium">代理商ID</th>
|
||||
<th className="px-6 py-4 font-medium">状态</th>
|
||||
<th className="px-6 py-4 font-medium">达人数</th>
|
||||
<th className="px-6 py-4 font-medium">项目数</th>
|
||||
@ -208,9 +343,43 @@ export default function AgenciesManagePage() {
|
||||
{filteredAgencies.map((agency) => (
|
||||
<tr key={agency.id} className="border-b border-border-subtle last:border-0 hover:bg-bg-elevated/50">
|
||||
<td className="px-6 py-4">
|
||||
<div>
|
||||
<div className="font-medium text-text-primary">{agency.name}</div>
|
||||
<div className="text-sm text-text-tertiary">{agency.email}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Building2 size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||||
{agency.remark && (
|
||||
<span className="px-2 py-0.5 text-xs rounded bg-accent-amber/15 text-accent-amber" title={agency.remark}>
|
||||
有备注
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-text-tertiary">{agency.companyName}</div>
|
||||
{agency.remark && (
|
||||
<p className="text-xs text-text-tertiary mt-0.5 line-clamp-1">{agency.remark}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="px-2 py-1 rounded bg-bg-elevated text-sm font-mono text-accent-indigo">
|
||||
{agency.agencyId}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyAgencyId(agency.agencyId)}
|
||||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||||
title="复制代理商ID"
|
||||
>
|
||||
{copiedId === agency.agencyId ? (
|
||||
<CheckCircle size={14} className="text-accent-green" />
|
||||
) : (
|
||||
<Copy size={14} className="text-text-tertiary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
@ -233,66 +402,243 @@ export default function AgenciesManagePage() {
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-text-tertiary">{agency.joinedAt}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setOpenMenuId(openMenuId === agency.id ? null : agency.id)}
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
{/* 下拉菜单 */}
|
||||
{openMenuId === agency.id && (
|
||||
<div className="absolute right-0 top-full mt-1 w-40 bg-bg-card rounded-xl shadow-lg border border-border-subtle z-10 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenRemark(agency)}
|
||||
className="w-full px-4 py-2.5 text-left text-sm text-text-primary hover:bg-bg-elevated flex items-center gap-2"
|
||||
>
|
||||
<MessageSquareText size={14} className="text-text-secondary" />
|
||||
{agency.remark ? '编辑备注' : '添加备注'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenAssign(agency)}
|
||||
className="w-full px-4 py-2.5 text-left text-sm text-text-primary hover:bg-bg-elevated flex items-center gap-2"
|
||||
>
|
||||
<FolderPlus size={14} className="text-text-secondary" />
|
||||
分配到项目
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenDelete(agency)}
|
||||
className="w-full px-4 py-2.5 text-left text-sm text-accent-coral hover:bg-accent-coral/10 flex items-center gap-2"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
移除代理商
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredAgencies.length === 0 && (
|
||||
<div className="text-center py-12 text-text-tertiary">
|
||||
<Building2 size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>没有找到匹配的代理商</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 邀请代理商弹窗 */}
|
||||
<Modal isOpen={showInviteModal} onClose={() => { setShowInviteModal(false); setInviteEmail(''); setInviteLink(''); }} title="邀请代理商">
|
||||
<Modal isOpen={showInviteModal} onClose={handleCloseInviteModal} title="邀请代理商">
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary text-sm">输入代理商邮箱,系统将发送邀请链接。</p>
|
||||
<p className="text-text-secondary text-sm">
|
||||
输入代理商ID邀请合作。代理商ID可在代理商的个人中心查看。
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">代理商邮箱</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">代理商ID</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder="agency@example.com"
|
||||
className="flex-1 px-4 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
type="text"
|
||||
value={inviteAgencyId}
|
||||
onChange={(e) => {
|
||||
setInviteAgencyId(e.target.value.toUpperCase())
|
||||
setInviteResult(null)
|
||||
}}
|
||||
placeholder="例如: AG789012"
|
||||
className="flex-1 px-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
<Button variant="secondary" onClick={handleInvite}>
|
||||
生成链接
|
||||
查找
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary mt-2">代理商ID格式:AG + 6位数字</p>
|
||||
</div>
|
||||
|
||||
{inviteLink && (
|
||||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-text-primary">邀请链接</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyLink}
|
||||
className="flex items-center gap-1 text-sm text-accent-indigo hover:underline"
|
||||
>
|
||||
<Copy size={14} />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary break-all">{inviteLink}</p>
|
||||
{inviteResult && (
|
||||
<div className={`p-4 rounded-xl flex items-start gap-3 ${
|
||||
inviteResult.success ? 'bg-accent-green/10 border border-accent-green/20' : 'bg-accent-coral/10 border border-accent-coral/20'
|
||||
}`}>
|
||||
{inviteResult.success ? (
|
||||
<CheckCircle size={18} className="text-accent-green flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<AlertCircle size={18} className="text-accent-coral flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<span className={`text-sm ${inviteResult.success ? 'text-accent-green' : 'text-accent-coral'}`}>
|
||||
{inviteResult.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 justify-end pt-4">
|
||||
<Button variant="ghost" onClick={() => { setShowInviteModal(false); setInviteEmail(''); setInviteLink(''); }}>
|
||||
<Button variant="ghost" onClick={handleCloseInviteModal}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSendInvite} disabled={!inviteEmail.trim()}>
|
||||
<Mail size={16} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (inviteResult?.success) {
|
||||
handleCloseInviteModal()
|
||||
}
|
||||
}}
|
||||
disabled={!inviteResult?.success}
|
||||
>
|
||||
<UserPlus size={16} />
|
||||
发送邀请
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 备注弹窗 */}
|
||||
<Modal
|
||||
isOpen={remarkModal.open}
|
||||
onClose={() => { setRemarkModal({ open: false, agency: null }); setRemarkText(''); }}
|
||||
title={`${remarkModal.agency?.remark ? '编辑' : '添加'}备注 - ${remarkModal.agency?.name}`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">备注内容</label>
|
||||
<textarea
|
||||
value={remarkText}
|
||||
onChange={(e) => setRemarkText(e.target.value)}
|
||||
placeholder="输入备注信息,如代理商特点、合作注意事项等..."
|
||||
className="w-full h-32 px-4 py-3 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary placeholder-text-tertiary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="ghost" onClick={() => { setRemarkModal({ open: false, agency: null }); setRemarkText(''); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSaveRemark}>
|
||||
<CheckCircle size={16} />
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<Modal
|
||||
isOpen={deleteModal.open}
|
||||
onClose={() => setDeleteModal({ open: false, agency: null })}
|
||||
title="确认移除代理商"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-accent-coral flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-text-primary font-medium">确定要移除代理商「{deleteModal.agency?.name}」吗?</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
移除后该代理商将无法继续参与您的项目,该代理商下的达人也将受到影响。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="ghost" onClick={() => setDeleteModal({ open: false, agency: null })}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border-accent-coral text-accent-coral hover:bg-accent-coral/10"
|
||||
onClick={handleConfirmDelete}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
确认移除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 分配项目弹窗 */}
|
||||
<Modal
|
||||
isOpen={assignModal.open}
|
||||
onClose={() => { setAssignModal({ open: false, agency: null }); setSelectedProjects([]); }}
|
||||
title={`分配代理商到项目 - ${assignModal.agency?.name}`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary text-sm">
|
||||
选择要将代理商分配到的项目,可多选。
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">选择项目</label>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{mockProjects.map((project) => {
|
||||
const isSelected = selectedProjects.includes(project.id)
|
||||
return (
|
||||
<button
|
||||
key={project.id}
|
||||
type="button"
|
||||
onClick={() => toggleProjectSelection(project.id)}
|
||||
className={`w-full flex items-center gap-3 p-4 rounded-xl border-2 text-left transition-colors ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
|
||||
isSelected ? 'border-accent-indigo bg-accent-indigo' : 'border-border-subtle'
|
||||
}`}>
|
||||
{isSelected && <CheckCircle size={12} className="text-white" />}
|
||||
</div>
|
||||
<span className="text-text-primary">{project.name}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{selectedProjects.length > 0 && (
|
||||
<p className="text-sm text-text-secondary">
|
||||
已选择 <span className="text-accent-indigo font-medium">{selectedProjects.length}</span> 个项目
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => { setAssignModal({ open: false, agency: null }); setSelectedProjects([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleConfirmAssign} disabled={selectedProjects.length === 0}>
|
||||
<FolderPlus size={16} />
|
||||
确认分配
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 点击其他地方关闭菜单 */}
|
||||
{openMenuId && (
|
||||
<div
|
||||
className="fixed inset-0 z-0"
|
||||
onClick={() => setOpenMenuId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { SuccessTag, PendingTag, WarningTag } from '@/components/ui/Tag'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
@ -14,11 +15,24 @@ import {
|
||||
Video,
|
||||
ChevronRight,
|
||||
Calendar,
|
||||
Users
|
||||
Users,
|
||||
Pencil
|
||||
} from 'lucide-react'
|
||||
|
||||
// 项目类型定义
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
deadline: string
|
||||
scriptCount: { total: number; passed: number; pending: number; rejected: number }
|
||||
videoCount: { total: number; passed: number; pending: number; rejected: number }
|
||||
agencyCount: number
|
||||
creatorCount: number
|
||||
}
|
||||
|
||||
// 模拟项目数据
|
||||
const mockProjects = [
|
||||
const initialProjects: Project[] = [
|
||||
{
|
||||
id: 'proj-001',
|
||||
name: 'XX品牌618推广',
|
||||
@ -57,7 +71,7 @@ function StatusTag({ status }: { status: string }) {
|
||||
return <WarningTag>暂停</WarningTag>
|
||||
}
|
||||
|
||||
function ProjectCard({ project }: { project: typeof mockProjects[0] }) {
|
||||
function ProjectCard({ project, onEditDeadline }: { project: Project; onEditDeadline: (project: Project) => void }) {
|
||||
const scriptProgress = Math.round((project.scriptCount.passed / project.scriptCount.total) * 100)
|
||||
const videoProgress = Math.round((project.videoCount.passed / project.videoCount.total) * 100)
|
||||
|
||||
@ -72,6 +86,18 @@ function ProjectCard({ project }: { project: typeof mockProjects[0] }) {
|
||||
<div className="flex items-center gap-2 mt-1 text-sm text-text-secondary">
|
||||
<Calendar size={14} />
|
||||
<span>截止 {project.deadline}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onEditDeadline(project)
|
||||
}}
|
||||
className="p-1 rounded hover:bg-bg-page transition-colors"
|
||||
title="修改截止日期"
|
||||
>
|
||||
<Pencil size={12} className="text-text-tertiary hover:text-accent-indigo" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<StatusTag status={project.status} />
|
||||
@ -145,8 +171,33 @@ function ProjectCard({ project }: { project: typeof mockProjects[0] }) {
|
||||
export default function BrandProjectsPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
const [projects, setProjects] = useState<Project[]>(initialProjects)
|
||||
|
||||
const filteredProjects = mockProjects.filter(project => {
|
||||
// 编辑截止日期相关状态
|
||||
const [showDeadlineModal, setShowDeadlineModal] = useState(false)
|
||||
const [editingProject, setEditingProject] = useState<Project | null>(null)
|
||||
const [newDeadline, setNewDeadline] = useState('')
|
||||
|
||||
// 打开编辑截止日期弹窗
|
||||
const handleEditDeadline = (project: Project) => {
|
||||
setEditingProject(project)
|
||||
setNewDeadline(project.deadline)
|
||||
setShowDeadlineModal(true)
|
||||
}
|
||||
|
||||
// 保存截止日期
|
||||
const handleSaveDeadline = () => {
|
||||
if (!editingProject || !newDeadline) return
|
||||
|
||||
setProjects(prev => prev.map(p =>
|
||||
p.id === editingProject.id ? { ...p, deadline: newDeadline } : p
|
||||
))
|
||||
setShowDeadlineModal(false)
|
||||
setEditingProject(null)
|
||||
setNewDeadline('')
|
||||
}
|
||||
|
||||
const filteredProjects = projects.filter(project => {
|
||||
const matchesSearch = project.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesStatus = statusFilter === 'all' || project.status === statusFilter
|
||||
return matchesSearch && matchesStatus
|
||||
@ -198,7 +249,7 @@ export default function BrandProjectsPage() {
|
||||
{/* 项目卡片网格 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredProjects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
<ProjectCard key={project.id} project={project} onEditDeadline={handleEditDeadline} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -216,6 +267,63 @@ export default function BrandProjectsPage() {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑截止日期弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDeadlineModal}
|
||||
onClose={() => {
|
||||
setShowDeadlineModal(false)
|
||||
setEditingProject(null)
|
||||
setNewDeadline('')
|
||||
}}
|
||||
title="修改截止日期"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{editingProject && (
|
||||
<div className="p-3 rounded-lg bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">项目名称</p>
|
||||
<p className="font-medium text-text-primary">{editingProject.name}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
新截止日期
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="date"
|
||||
value={newDeadline}
|
||||
onChange={(e) => setNewDeadline(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setShowDeadlineModal(false)
|
||||
setEditingProject(null)
|
||||
setNewDeadline('')
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
onClick={handleSaveDeadline}
|
||||
disabled={!newDeadline}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
564
frontend/app/brand/projects/[id]/config/page.tsx
Normal file
564
frontend/app/brand/projects/[id]/config/page.tsx
Normal file
@ -0,0 +1,564 @@
|
||||
'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 { Input } from '@/components/ui/Input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
Shield,
|
||||
Settings,
|
||||
Plus,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Video,
|
||||
Bot,
|
||||
Users,
|
||||
Save,
|
||||
Upload,
|
||||
Download,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟数据
|
||||
const mockData = {
|
||||
project: {
|
||||
id: 'proj-001',
|
||||
name: 'XX品牌618推广',
|
||||
},
|
||||
brief: {
|
||||
title: 'XX品牌618推广Brief',
|
||||
description: '本次618大促营销活动,需要达人围绕夏日护肤、美妆新品进行内容创作。',
|
||||
requirements: [
|
||||
'视频时长:60-90秒',
|
||||
'必须展示产品使用过程',
|
||||
'需要口播品牌slogan:"XX品牌,夏日焕新"',
|
||||
'背景音乐需使用品牌指定曲库',
|
||||
],
|
||||
keywords: ['夏日护肤', '清爽', '补水', '防晒', '焕新'],
|
||||
forbiddenWords: ['最好', '第一', '绝对', '100%'],
|
||||
referenceLinks: [
|
||||
{ title: '品牌视觉指南', url: 'https://example.com/brand-guide.pdf' },
|
||||
{ title: '产品资料包', url: 'https://example.com/product-pack.zip' },
|
||||
],
|
||||
deadline: '2026-06-10',
|
||||
},
|
||||
rules: {
|
||||
aiReview: {
|
||||
enabled: true,
|
||||
strictness: 'medium', // low, medium, high
|
||||
checkItems: [
|
||||
{ id: 'forbidden_words', name: '违禁词检测', enabled: true },
|
||||
{ id: 'competitor', name: '竞品提及检测', enabled: true },
|
||||
{ id: 'brand_tone', name: '品牌调性检测', enabled: true },
|
||||
{ id: 'duration', name: '视频时长检测', enabled: true },
|
||||
{ id: 'music', name: '背景音乐检测', enabled: false },
|
||||
],
|
||||
},
|
||||
manualReview: {
|
||||
scriptRequired: true,
|
||||
videoRequired: true,
|
||||
agencyCanApprove: true, // 代理商是否有终审权限
|
||||
brandFinalReview: true, // 品牌方是否需要终审
|
||||
},
|
||||
appealRules: {
|
||||
maxAppeals: 3, // 最大申诉次数
|
||||
appealDeadline: 48, // 申诉处理时限(小时)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 严格程度选项
|
||||
const strictnessOptions = [
|
||||
{ value: 'low', label: '宽松', description: '仅检测明显违规内容' },
|
||||
{ value: 'medium', label: '标准', description: '平衡检测,推荐使用' },
|
||||
{ value: 'high', label: '严格', description: '严格检测,可能有较多误判' },
|
||||
]
|
||||
|
||||
export default function ProjectConfigPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const projectId = params.id as string
|
||||
|
||||
const [brief, setBrief] = useState(mockData.brief)
|
||||
const [rules, setRules] = useState(mockData.rules)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [activeSection, setActiveSection] = useState<string | null>('brief')
|
||||
|
||||
// 新增需求
|
||||
const [newRequirement, setNewRequirement] = useState('')
|
||||
// 新增关键词
|
||||
const [newKeyword, setNewKeyword] = useState('')
|
||||
// 新增违禁词
|
||||
const [newForbiddenWord, setNewForbiddenWord] = useState('')
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('配置已保存')
|
||||
}
|
||||
|
||||
const addRequirement = () => {
|
||||
if (newRequirement.trim()) {
|
||||
setBrief({ ...brief, requirements: [...brief.requirements, newRequirement.trim()] })
|
||||
setNewRequirement('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeRequirement = (index: number) => {
|
||||
setBrief({ ...brief, requirements: brief.requirements.filter((_, i) => i !== index) })
|
||||
}
|
||||
|
||||
const addKeyword = () => {
|
||||
if (newKeyword.trim() && !brief.keywords.includes(newKeyword.trim())) {
|
||||
setBrief({ ...brief, keywords: [...brief.keywords, newKeyword.trim()] })
|
||||
setNewKeyword('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeKeyword = (keyword: string) => {
|
||||
setBrief({ ...brief, keywords: brief.keywords.filter(k => k !== keyword) })
|
||||
}
|
||||
|
||||
const addForbiddenWord = () => {
|
||||
if (newForbiddenWord.trim() && !brief.forbiddenWords.includes(newForbiddenWord.trim())) {
|
||||
setBrief({ ...brief, forbiddenWords: [...brief.forbiddenWords, newForbiddenWord.trim()] })
|
||||
setNewForbiddenWord('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeForbiddenWord = (word: string) => {
|
||||
setBrief({ ...brief, forbiddenWords: brief.forbiddenWords.filter(w => w !== word) })
|
||||
}
|
||||
|
||||
const toggleAiCheckItem = (itemId: string) => {
|
||||
setRules({
|
||||
...rules,
|
||||
aiReview: {
|
||||
...rules.aiReview,
|
||||
checkItems: rules.aiReview.checkItems.map(item =>
|
||||
item.id === itemId ? { ...item, enabled: !item.enabled } : item
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const SectionHeader = ({ title, icon: Icon, section }: { title: string; icon: React.ElementType; section: string }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveSection(activeSection === section ? null : section)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-bg-elevated/50 rounded-xl transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-2 font-semibold text-text-primary">
|
||||
<Icon size={18} className="text-accent-indigo" />
|
||||
{title}
|
||||
</span>
|
||||
{activeSection === section ? (
|
||||
<ChevronUp size={18} className="text-text-tertiary" />
|
||||
) : (
|
||||
<ChevronDown size={18} className="text-text-tertiary" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
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">Brief和规则配置</h1>
|
||||
<p className="text-sm text-text-secondary mt-0.5">
|
||||
{mockData.project.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? '保存中...' : (
|
||||
<>
|
||||
<Save size={16} />
|
||||
保存配置
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Brief配置 */}
|
||||
<Card>
|
||||
<SectionHeader title="Brief配置" icon={FileText} section="brief" />
|
||||
{activeSection === 'brief' && (
|
||||
<CardContent className="space-y-6 pt-0">
|
||||
{/* 基本信息 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">Brief标题</label>
|
||||
<Input
|
||||
value={brief.title}
|
||||
onChange={(e) => setBrief({ ...brief, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">截止日期</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={brief.deadline}
|
||||
onChange={(e) => setBrief({ ...brief, deadline: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">项目描述</label>
|
||||
<textarea
|
||||
value={brief.description}
|
||||
onChange={(e) => setBrief({ ...brief, description: e.target.value })}
|
||||
className="w-full h-24 p-3 rounded-xl bg-bg-elevated border border-border-subtle text-text-primary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 创作要求 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">创作要求</label>
|
||||
<div className="space-y-2">
|
||||
{brief.requirements.map((req, index) => (
|
||||
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-bg-elevated">
|
||||
<CheckCircle size={16} className="text-accent-green flex-shrink-0" />
|
||||
<span className="flex-1 text-text-primary">{req}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRequirement(index)}
|
||||
className="p-1 rounded hover:bg-bg-page text-text-tertiary hover:text-accent-coral transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newRequirement}
|
||||
onChange={(e) => setNewRequirement(e.target.value)}
|
||||
placeholder="添加新的创作要求"
|
||||
onKeyDown={(e) => e.key === 'Enter' && addRequirement()}
|
||||
/>
|
||||
<Button variant="secondary" onClick={addRequirement}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键词 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">推荐关键词</label>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{brief.keywords.map((keyword) => (
|
||||
<span
|
||||
key={keyword}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-accent-indigo/15 text-accent-indigo text-sm"
|
||||
>
|
||||
{keyword}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeKeyword(keyword)}
|
||||
className="hover:text-accent-coral transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder="添加关键词"
|
||||
onKeyDown={(e) => e.key === 'Enter' && addKeyword()}
|
||||
/>
|
||||
<Button variant="secondary" onClick={addKeyword}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 违禁词 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block flex items-center gap-2">
|
||||
<AlertTriangle size={14} className="text-accent-coral" />
|
||||
违禁词列表
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{brief.forbiddenWords.map((word) => (
|
||||
<span
|
||||
key={word}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-accent-coral/15 text-accent-coral text-sm"
|
||||
>
|
||||
{word}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeForbiddenWord(word)}
|
||||
className="hover:text-accent-coral/70 transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newForbiddenWord}
|
||||
onChange={(e) => setNewForbiddenWord(e.target.value)}
|
||||
placeholder="添加违禁词"
|
||||
onKeyDown={(e) => e.key === 'Enter' && addForbiddenWord()}
|
||||
/>
|
||||
<Button variant="secondary" onClick={addForbiddenWord}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 参考资料 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">参考资料</label>
|
||||
<div className="space-y-2">
|
||||
{brief.referenceLinks.map((link, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated">
|
||||
<FileText size={16} className="text-accent-indigo" />
|
||||
<span className="flex-1 text-text-primary">{link.title}</span>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent-indigo hover:underline text-sm"
|
||||
>
|
||||
下载
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="secondary" className="w-full">
|
||||
<Upload size={16} />
|
||||
上传参考资料
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* AI审核规则 */}
|
||||
<Card>
|
||||
<SectionHeader title="AI审核规则" icon={Bot} section="ai" />
|
||||
{activeSection === 'ai' && (
|
||||
<CardContent className="space-y-6 pt-0">
|
||||
{/* AI审核开关 */}
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">启用AI自动审核</p>
|
||||
<p className="text-sm text-text-secondary">开启后,内容将先经过AI预审</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, aiReview: { ...rules.aiReview, enabled: !rules.aiReview.enabled } })}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
rules.aiReview.enabled ? 'bg-accent-indigo' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
rules.aiReview.enabled ? 'left-7' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rules.aiReview.enabled && (
|
||||
<>
|
||||
{/* 严格程度 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">审核严格程度</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{strictnessOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, aiReview: { ...rules.aiReview, strictness: option.value } })}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
rules.aiReview.strictness === option.value
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-border-subtle/80'
|
||||
}`}
|
||||
>
|
||||
<p className={`font-medium ${rules.aiReview.strictness === option.value ? 'text-accent-indigo' : 'text-text-primary'}`}>
|
||||
{option.label}
|
||||
</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">{option.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 检测项目 */}
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-2 block">检测项目</label>
|
||||
<div className="space-y-2">
|
||||
{rules.aiReview.checkItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated"
|
||||
>
|
||||
<span className="text-text-primary">{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAiCheckItem(item.id)}
|
||||
className={`relative w-10 h-5 rounded-full transition-colors ${
|
||||
item.enabled ? 'bg-accent-green' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
item.enabled ? 'left-5' : 'left-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 人工审核规则 */}
|
||||
<Card>
|
||||
<SectionHeader title="人工审核规则" icon={Users} section="manual" />
|
||||
{activeSection === 'manual' && (
|
||||
<CardContent className="space-y-4 pt-0">
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">脚本需要人工审核</p>
|
||||
<p className="text-sm text-text-secondary">脚本提交后需要代理商/品牌方审核</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, scriptRequired: !rules.manualReview.scriptRequired } })}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
rules.manualReview.scriptRequired ? 'bg-accent-indigo' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
rules.manualReview.scriptRequired ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">视频需要人工审核</p>
|
||||
<p className="text-sm text-text-secondary">视频提交后需要代理商/品牌方审核</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, videoRequired: !rules.manualReview.videoRequired } })}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
rules.manualReview.videoRequired ? 'bg-accent-indigo' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
rules.manualReview.videoRequired ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">代理商终审权限</p>
|
||||
<p className="text-sm text-text-secondary">允许代理商直接通过/驳回内容,无需品牌方审核</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, agencyCanApprove: !rules.manualReview.agencyCanApprove } })}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
rules.manualReview.agencyCanApprove ? 'bg-accent-indigo' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
rules.manualReview.agencyCanApprove ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">品牌方终审</p>
|
||||
<p className="text-sm text-text-secondary">所有内容最终需要品牌方确认</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRules({ ...rules, manualReview: { ...rules.manualReview, brandFinalReview: !rules.manualReview.brandFinalReview } })}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
rules.manualReview.brandFinalReview ? 'bg-accent-indigo' : 'bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
rules.manualReview.brandFinalReview ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 申诉规则 */}
|
||||
<Card>
|
||||
<SectionHeader title="申诉规则" icon={Shield} section="appeal" />
|
||||
{activeSection === 'appeal' && (
|
||||
<CardContent className="space-y-4 pt-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">最大申诉次数</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={rules.appealRules.maxAppeals}
|
||||
onChange={(e) => setRules({
|
||||
...rules,
|
||||
appealRules: { ...rules.appealRules, maxAppeals: parseInt(e.target.value) || 1 }
|
||||
})}
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary mt-1">达人对同一内容最多可申诉的次数</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-text-secondary mb-1.5 block">申诉处理时限(小时)</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={168}
|
||||
value={rules.appealRules.appealDeadline}
|
||||
onChange={(e) => setRules({
|
||||
...rules,
|
||||
appealRules: { ...rules.appealRules, appealDeadline: parseInt(e.target.value) || 24 }
|
||||
})}
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary mt-1">代理商需要在此时间内处理申诉</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -5,18 +5,27 @@ import { useRouter, useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, PendingTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { SuccessTag, PendingTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
Users,
|
||||
FileText,
|
||||
Video,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Settings,
|
||||
Search,
|
||||
Building2,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
Check,
|
||||
Pencil
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟项目详情数据
|
||||
@ -27,7 +36,6 @@ const mockProject = {
|
||||
deadline: '2026-06-18',
|
||||
createdAt: '2026-02-01',
|
||||
description: '618大促活动营销内容审核项目',
|
||||
briefUrl: '/briefs/xx-brand-618.pdf',
|
||||
stats: {
|
||||
scriptTotal: 20,
|
||||
scriptPassed: 15,
|
||||
@ -39,18 +47,27 @@ const mockProject = {
|
||||
videoRejected: 3,
|
||||
},
|
||||
agencies: [
|
||||
{ id: 'agency-001', name: '星耀传媒', creatorCount: 8, passRate: 92 },
|
||||
{ id: 'agency-002', name: '创意无限', creatorCount: 5, passRate: 88 },
|
||||
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 2, passRate: 75 },
|
||||
{ id: 'AG789012', name: '星耀传媒', creatorCount: 8, passRate: 92 },
|
||||
{ id: 'AG456789', name: '创意无限', creatorCount: 5, passRate: 88 },
|
||||
],
|
||||
recentTasks: [
|
||||
{ id: 'task-001', type: 'video', creatorName: '小美护肤', status: 'pending', submittedAt: '2026-02-06 14:30' },
|
||||
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', status: 'approved', submittedAt: '2026-02-06 12:15' },
|
||||
{ id: 'task-003', type: 'video', creatorName: '健身王', status: 'rejected', submittedAt: '2026-02-06 10:00' },
|
||||
{ id: 'task-004', type: 'script', creatorName: '时尚达人', status: 'pending', submittedAt: '2026-02-05 16:45' },
|
||||
{ id: 'task-001', type: 'video', creatorName: '小美护肤', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'pending', submittedAt: '2026-02-06 14:30' },
|
||||
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'approved', submittedAt: '2026-02-06 12:15' },
|
||||
{ id: 'task-003', type: 'video', creatorName: '健身王', agencyId: 'AG456789', agencyName: '创意无限', status: 'rejected', submittedAt: '2026-02-06 10:00' },
|
||||
{ id: 'task-004', type: 'script', creatorName: '时尚达人', agencyId: 'AG456789', agencyName: '创意无限', status: 'pending', submittedAt: '2026-02-05 16:45' },
|
||||
],
|
||||
}
|
||||
|
||||
// 模拟品牌方已添加的代理商(来自代理商管理)
|
||||
const mockManagedAgencies = [
|
||||
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司' },
|
||||
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司' },
|
||||
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司' },
|
||||
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司' },
|
||||
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司' },
|
||||
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司' },
|
||||
]
|
||||
|
||||
function StatCard({ title, value, icon: Icon, color }: { title: string; value: number | string; icon: React.ElementType; color: string }) {
|
||||
return (
|
||||
<Card>
|
||||
@ -81,11 +98,85 @@ function TaskStatusTag({ status }: { status: string }) {
|
||||
export default function ProjectDetailPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const project = mockProject
|
||||
const projectId = params.id as string
|
||||
const [project, setProject] = useState(mockProject)
|
||||
|
||||
// 添加代理商相关状态
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
|
||||
|
||||
// 代理商操作菜单
|
||||
const [activeAgencyMenu, setActiveAgencyMenu] = useState<string | null>(null)
|
||||
|
||||
// 删除确认
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [agencyToDelete, setAgencyToDelete] = useState<typeof project.agencies[0] | null>(null)
|
||||
|
||||
// 编辑截止日期
|
||||
const [showDeadlineModal, setShowDeadlineModal] = useState(false)
|
||||
const [newDeadline, setNewDeadline] = useState(project.deadline)
|
||||
|
||||
// 保存截止日期
|
||||
const handleSaveDeadline = () => {
|
||||
if (!newDeadline) return
|
||||
setProject({ ...project, deadline: newDeadline })
|
||||
setShowDeadlineModal(false)
|
||||
}
|
||||
|
||||
const scriptPassRate = Math.round((project.stats.scriptPassed / project.stats.scriptTotal) * 100)
|
||||
const videoPassRate = Math.round((project.stats.videoPassed / project.stats.videoTotal) * 100)
|
||||
|
||||
// 过滤可添加的代理商(排除已在项目中的)
|
||||
const availableAgencies = mockManagedAgencies.filter(
|
||||
agency => !project.agencies.some(a => a.id === agency.id)
|
||||
)
|
||||
|
||||
// 搜索过滤
|
||||
const filteredAgencies = availableAgencies.filter(agency =>
|
||||
searchQuery === '' ||
|
||||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// 切换选择
|
||||
const toggleSelectAgency = (agencyId: string) => {
|
||||
setSelectedAgencies(prev =>
|
||||
prev.includes(agencyId)
|
||||
? prev.filter(id => id !== agencyId)
|
||||
: [...prev, agencyId]
|
||||
)
|
||||
}
|
||||
|
||||
// 确认添加
|
||||
const handleAddAgencies = () => {
|
||||
const newAgencies = mockManagedAgencies
|
||||
.filter(a => selectedAgencies.includes(a.id))
|
||||
.map(a => ({ id: a.id, name: a.name, creatorCount: 0, passRate: 0 }))
|
||||
|
||||
setProject({
|
||||
...project,
|
||||
agencies: [...project.agencies, ...newAgencies]
|
||||
})
|
||||
|
||||
setShowAddModal(false)
|
||||
setSelectedAgencies([])
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
// 移除代理商
|
||||
const handleRemoveAgency = async () => {
|
||||
if (!agencyToDelete) return
|
||||
|
||||
setProject({
|
||||
...project,
|
||||
agencies: project.agencies.filter(a => a.id !== agencyToDelete.id)
|
||||
})
|
||||
setShowDeleteModal(false)
|
||||
setAgencyToDelete(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部导航 */}
|
||||
@ -105,17 +196,44 @@ export default function ProjectDetailPage() {
|
||||
<span className="flex items-center gap-2">
|
||||
<Calendar size={16} />
|
||||
截止日期: {project.deadline}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewDeadline(project.deadline)
|
||||
setShowDeadlineModal(true)
|
||||
}}
|
||||
className="p-1 rounded hover:bg-bg-elevated transition-colors"
|
||||
title="修改截止日期"
|
||||
>
|
||||
<Pencil size={14} className="text-text-tertiary hover:text-accent-indigo" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock size={16} />
|
||||
创建时间: {project.createdAt}
|
||||
</span>
|
||||
<Link href={project.briefUrl} className="flex items-center gap-2 text-accent-indigo hover:underline">
|
||||
<FileText size={16} />
|
||||
查看 Brief
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Brief和规则配置 - 大按钮 */}
|
||||
<Link href={`/brand/projects/${projectId}/config`}>
|
||||
<Card className="hover:border-accent-indigo transition-colors cursor-pointer">
|
||||
<CardContent className="py-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Settings size={24} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-text-primary">Brief和规则配置</p>
|
||||
<p className="text-sm text-text-secondary">配置项目Brief、审核规则、AI检测项等</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-text-tertiary" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<StatCard title="脚本通过率" value={`${scriptPassRate}%`} icon={FileText} color="text-accent-green" />
|
||||
@ -197,20 +315,58 @@ export default function ProjectDetailPage() {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users size={16} />
|
||||
参与代理商
|
||||
<span className="text-sm font-normal text-text-tertiary">({project.agencies.length})</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-2">
|
||||
{project.agencies.map((agency) => (
|
||||
<div key={agency.id} className="p-3 rounded-lg bg-bg-elevated">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||||
<span className={`text-sm font-medium ${agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}`}>
|
||||
{agency.passRate}%
|
||||
</span>
|
||||
<div key={agency.id} className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<Building2 size={18} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary text-sm">{agency.name}</p>
|
||||
<p className="text-xs text-text-tertiary">{agency.creatorCount} 位达人 · 通过率 {agency.passRate}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveAgencyMenu(activeAgencyMenu === agency.id ? null : agency.id)}
|
||||
className="p-1.5 rounded hover:bg-bg-page transition-colors"
|
||||
>
|
||||
<MoreHorizontal size={16} className="text-text-tertiary" />
|
||||
</button>
|
||||
{activeAgencyMenu === agency.id && (
|
||||
<div className="absolute right-0 top-8 z-10 w-32 py-1 bg-bg-card rounded-lg shadow-lg border border-border-subtle">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAgencyToDelete(agency)
|
||||
setShowDeleteModal(true)
|
||||
setActiveAgencyMenu(null)
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-accent-coral hover:bg-bg-elevated flex items-center gap-2"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">{agency.creatorCount} 位达人参与</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 添加代理商按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="w-full p-3 rounded-lg border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-indigo"
|
||||
>
|
||||
<Plus size={18} />
|
||||
<span className="text-sm font-medium">添加代理商</span>
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -234,6 +390,7 @@ export default function ProjectDetailPage() {
|
||||
<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">达人</th>
|
||||
<th className="pb-3 font-medium">所属代理商</th>
|
||||
<th className="pb-3 font-medium">状态</th>
|
||||
<th className="pb-3 font-medium">提交时间</th>
|
||||
<th className="pb-3 font-medium">操作</th>
|
||||
@ -249,6 +406,12 @@ export default function ProjectDetailPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 text-text-primary">{task.creatorName}</td>
|
||||
<td className="py-4">
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-bg-elevated text-sm">
|
||||
<Building2 size={14} className="text-accent-indigo" />
|
||||
<span className="text-text-secondary">{task.agencyName}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4"><TaskStatusTag status={task.status} /></td>
|
||||
<td className="py-4 text-sm text-text-tertiary">{task.submittedAt}</td>
|
||||
<td className="py-4">
|
||||
@ -265,6 +428,176 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 添加代理商弹窗 */}
|
||||
<Modal
|
||||
isOpen={showAddModal}
|
||||
onClose={() => {
|
||||
setShowAddModal(false)
|
||||
setSearchQuery('')
|
||||
setSelectedAgencies([])
|
||||
}}
|
||||
title="添加代理商"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative">
|
||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索代理商名称或ID..."
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 代理商列表 */}
|
||||
<div className="max-h-80 overflow-y-auto space-y-2">
|
||||
{filteredAgencies.length > 0 ? (
|
||||
filteredAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleSelectAgency(agency.id)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-xl border-2 transition-all text-left ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/5'
|
||||
: 'border-transparent bg-bg-elevated hover:bg-bg-page'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
|
||||
}`}>
|
||||
{isSelected ? (
|
||||
<Check size={20} className="text-white" />
|
||||
) : (
|
||||
<Building2 size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-text-primary">{agency.name}</p>
|
||||
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary truncate">{agency.companyName}</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
{availableAgencies.length === 0 ? (
|
||||
<>
|
||||
<Users size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>所有代理商都已添加到此项目</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>未找到匹配的代理商</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 已选择提示 */}
|
||||
{selectedAgencies.length > 0 && (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
|
||||
<span className="text-sm text-text-secondary">
|
||||
已选择 <span className="text-accent-indigo font-medium">{selectedAgencies.length}</span> 个代理商
|
||||
</span>
|
||||
<Button variant="primary" onClick={handleAddAgencies}>
|
||||
<Plus size={16} />
|
||||
确认添加
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部提示 */}
|
||||
<p className="text-xs text-text-tertiary pt-2">
|
||||
仅显示已在"代理商管理"中添加的代理商,如需添加新代理商请先前往代理商管理
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}
|
||||
title="移除代理商"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary">
|
||||
确定要将 <span className="text-text-primary font-medium">{agencyToDelete?.name}</span> 从此项目中移除吗?
|
||||
</p>
|
||||
<p className="text-sm text-accent-coral">
|
||||
移除后,该代理商下的达人将无法继续参与此项目的任务。
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="secondary" className="flex-1" onClick={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1 bg-accent-coral hover:bg-accent-coral/80"
|
||||
onClick={handleRemoveAgency}
|
||||
>
|
||||
确认移除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑截止日期弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDeadlineModal}
|
||||
onClose={() => setShowDeadlineModal(false)}
|
||||
title="修改截止日期"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-lg bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">项目名称</p>
|
||||
<p className="font-medium text-text-primary">{project.name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
新截止日期
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="date"
|
||||
value={newDeadline}
|
||||
onChange={(e) => setNewDeadline(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => setShowDeadlineModal(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
onClick={handleSaveDeadline}
|
||||
disabled={!newDeadline}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -12,15 +12,19 @@ import {
|
||||
FileText,
|
||||
CheckCircle,
|
||||
X,
|
||||
Users
|
||||
Users,
|
||||
Search,
|
||||
Building2
|
||||
} from 'lucide-react'
|
||||
|
||||
// 模拟代理商列表
|
||||
// 模拟品牌方已添加的代理商(来自代理商管理)
|
||||
const mockAgencies = [
|
||||
{ id: 'agency-001', name: '星耀传媒', creatorCount: 50, passRate: 92 },
|
||||
{ id: 'agency-002', name: '创意无限', creatorCount: 35, passRate: 88 },
|
||||
{ id: 'agency-003', name: '美妆达人MCN', creatorCount: 28, passRate: 82 },
|
||||
{ id: 'agency-004', name: '时尚风向标', creatorCount: 42, passRate: 85 },
|
||||
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司', creatorCount: 50, passRate: 92 },
|
||||
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司', creatorCount: 35, passRate: 88 },
|
||||
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司', creatorCount: 28, passRate: 82 },
|
||||
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司', creatorCount: 42, passRate: 85 },
|
||||
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司', creatorCount: 30, passRate: 90 },
|
||||
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司', creatorCount: 25, passRate: 87 },
|
||||
]
|
||||
|
||||
export default function CreateProjectPage() {
|
||||
@ -30,6 +34,15 @@ export default function CreateProjectPage() {
|
||||
const [briefFile, setBriefFile] = useState<File | null>(null)
|
||||
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [agencySearch, setAgencySearch] = useState('')
|
||||
|
||||
// 搜索过滤代理商
|
||||
const filteredAgencies = mockAgencies.filter(agency =>
|
||||
agencySearch === '' ||
|
||||
agency.name.toLowerCase().includes(agencySearch.toLowerCase()) ||
|
||||
agency.id.toLowerCase().includes(agencySearch.toLowerCase()) ||
|
||||
agency.companyName.toLowerCase().includes(agencySearch.toLowerCase())
|
||||
)
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@ -145,41 +158,76 @@ export default function CreateProjectPage() {
|
||||
已选择 {selectedAgencies.length} 个
|
||||
</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mockAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleAgency(agency.id)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-text-primary">{agency.name}</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={14} />
|
||||
{agency.creatorCount} 达人
|
||||
</span>
|
||||
<span className={agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}>
|
||||
通过率 {agency.passRate}%
|
||||
</span>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="relative mb-4">
|
||||
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
|
||||
<input
|
||||
type="text"
|
||||
value={agencySearch}
|
||||
onChange={(e) => setAgencySearch(e.target.value)}
|
||||
placeholder="搜索代理商名称、ID或公司名..."
|
||||
className="w-full pl-11 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 代理商列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-80 overflow-y-auto">
|
||||
{filteredAgencies.length > 0 ? (
|
||||
filteredAgencies.map((agency) => {
|
||||
const isSelected = selectedAgencies.includes(agency.id)
|
||||
return (
|
||||
<button
|
||||
key={agency.id}
|
||||
type="button"
|
||||
onClick={() => toggleAgency(agency.id)}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
isSelected
|
||||
? 'border-accent-indigo bg-accent-indigo/10'
|
||||
: 'border-border-subtle hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
|
||||
}`}>
|
||||
{isSelected ? (
|
||||
<CheckCircle size={20} className="text-white" />
|
||||
) : (
|
||||
<Building2 size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-text-primary">{agency.name}</span>
|
||||
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary truncate mt-0.5">{agency.companyName}</p>
|
||||
<div className="flex items-center gap-4 mt-1.5 text-xs text-text-tertiary">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
{agency.creatorCount} 达人
|
||||
</span>
|
||||
<span className={agency.passRate >= 90 ? 'text-accent-green' : agency.passRate >= 80 ? 'text-accent-indigo' : 'text-orange-400'}>
|
||||
通过率 {agency.passRate}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckCircle size={20} className="text-accent-indigo" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-2 text-center py-8 text-text-tertiary">
|
||||
<Search size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>未找到匹配的代理商</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-text-tertiary mt-3">
|
||||
仅显示已在"代理商管理"中添加的代理商
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
||||
@ -1,23 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import {
|
||||
Bell,
|
||||
Shield,
|
||||
Download,
|
||||
Key,
|
||||
User,
|
||||
Mail,
|
||||
Smartphone,
|
||||
Globe,
|
||||
Moon,
|
||||
Sun,
|
||||
Check
|
||||
Check,
|
||||
LogOut,
|
||||
Monitor,
|
||||
Trash2,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
FileText,
|
||||
BarChart3,
|
||||
Users,
|
||||
CheckCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Phone
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function BrandSettingsPage() {
|
||||
const router = useRouter()
|
||||
const [notifications, setNotifications] = useState({
|
||||
email: true,
|
||||
push: true,
|
||||
@ -28,16 +42,126 @@ export default function BrandSettingsPage() {
|
||||
|
||||
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('dark')
|
||||
|
||||
// 退出登录弹窗
|
||||
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
||||
|
||||
// 修改密码弹窗
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false)
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
current: '',
|
||||
new: '',
|
||||
confirm: ''
|
||||
})
|
||||
const [showPasswords, setShowPasswords] = useState({
|
||||
current: false,
|
||||
new: false,
|
||||
confirm: false
|
||||
})
|
||||
|
||||
// 双因素认证弹窗
|
||||
const [show2FAModal, setShow2FAModal] = useState(false)
|
||||
const [twoFAEnabled, setTwoFAEnabled] = useState(false)
|
||||
|
||||
// 修改邮箱弹窗
|
||||
const [showEmailModal, setShowEmailModal] = useState(false)
|
||||
const [newEmail, setNewEmail] = useState('')
|
||||
const [emailCode, setEmailCode] = useState('')
|
||||
|
||||
// 修改手机弹窗
|
||||
const [showPhoneModal, setShowPhoneModal] = useState(false)
|
||||
const [newPhone, setNewPhone] = useState('')
|
||||
const [phoneCode, setPhoneCode] = useState('')
|
||||
|
||||
// 数据导出弹窗
|
||||
const [showExportModal, setShowExportModal] = useState(false)
|
||||
const [exportType, setExportType] = useState<string>('')
|
||||
const [exportRange, setExportRange] = useState('month')
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
// 登录设备管理
|
||||
const [showDevicesModal, setShowDevicesModal] = useState(false)
|
||||
|
||||
// 模拟登录设备数据
|
||||
const loginDevices = [
|
||||
{ id: 'd-1', name: 'Chrome - MacOS', location: '上海', lastActive: '当前设备', isCurrent: true },
|
||||
{ id: 'd-2', name: 'Safari - iPhone', location: '上海', lastActive: '2小时前', isCurrent: false },
|
||||
{ id: 'd-3', name: 'Chrome - Windows', location: '北京', lastActive: '3天前', isCurrent: false },
|
||||
]
|
||||
|
||||
// 模拟导出历史
|
||||
const exportHistory = [
|
||||
{ id: 'e-1', type: '审核记录', range: '2026年1月', status: 'completed', createdAt: '2026-02-01 10:30', size: '2.3MB' },
|
||||
{ id: 'e-2', type: '统计报告', range: '2025年Q4', status: 'completed', createdAt: '2026-01-15 14:20', size: '1.1MB' },
|
||||
]
|
||||
|
||||
const handleSave = () => {
|
||||
alert('设置已保存')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
// 模拟退出登录
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const handleChangePassword = () => {
|
||||
if (passwordForm.new !== passwordForm.confirm) {
|
||||
alert('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
alert('密码修改成功')
|
||||
setShowPasswordModal(false)
|
||||
setPasswordForm({ current: '', new: '', confirm: '' })
|
||||
}
|
||||
|
||||
const handleEnable2FA = () => {
|
||||
setTwoFAEnabled(true)
|
||||
setShow2FAModal(false)
|
||||
alert('双因素认证已启用')
|
||||
}
|
||||
|
||||
const handleChangeEmail = () => {
|
||||
alert(`邮箱已更新为 ${newEmail}`)
|
||||
setShowEmailModal(false)
|
||||
setNewEmail('')
|
||||
setEmailCode('')
|
||||
}
|
||||
|
||||
const handleChangePhone = () => {
|
||||
alert(`手机号已更新为 ${newPhone}`)
|
||||
setShowPhoneModal(false)
|
||||
setNewPhone('')
|
||||
setPhoneCode('')
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true)
|
||||
// 模拟导出过程
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setIsExporting(false)
|
||||
setShowExportModal(false)
|
||||
alert('导出任务已创建,完成后将通知您下载')
|
||||
}
|
||||
|
||||
const handleRemoveDevice = (deviceId: string) => {
|
||||
alert('已移除该设备的登录状态')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
{/* 页面标题 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">系统设置</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">管理账户和系统偏好设置</p>
|
||||
<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>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-accent-coral border-accent-coral/30 hover:bg-accent-coral/10"
|
||||
onClick={() => setShowLogoutModal(true)}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 通知设置 */}
|
||||
@ -181,7 +305,7 @@ export default function BrandSettingsPage() {
|
||||
<p className="text-sm text-text-secondary">定期更换密码以保护账户安全</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">修改</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowPasswordModal(true)}>修改</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-3 border-b border-border-subtle">
|
||||
@ -189,13 +313,19 @@ export default function BrandSettingsPage() {
|
||||
<Smartphone size={20} className="text-text-tertiary" />
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">双因素认证</p>
|
||||
<p className="text-sm text-text-secondary">启用后需要手机验证码登录</p>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{twoFAEnabled ? '已启用,登录时需要手机验证码' : '启用后需要手机验证码登录'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">启用</Button>
|
||||
{twoFAEnabled ? (
|
||||
<span className="px-3 py-1 rounded-full bg-accent-green/15 text-accent-green text-sm font-medium">已启用</span>
|
||||
) : (
|
||||
<Button variant="secondary" size="sm" onClick={() => setShow2FAModal(true)}>启用</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div className="flex items-center justify-between py-3 border-b border-border-subtle">
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail size={20} className="text-text-tertiary" />
|
||||
<div>
|
||||
@ -203,7 +333,29 @@ export default function BrandSettingsPage() {
|
||||
<p className="text-sm text-text-secondary">brand@example.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">修改</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowEmailModal(true)}>修改</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-3 border-b border-border-subtle">
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone size={20} className="text-text-tertiary" />
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">绑定手机</p>
|
||||
<p className="text-sm text-text-secondary">138****8888</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowPhoneModal(true)}>修改</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Monitor size={20} className="text-text-tertiary" />
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">登录设备管理</p>
|
||||
<p className="text-sm text-text-secondary">查看和管理已登录的设备</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowDevicesModal(true)}>管理</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -216,17 +368,78 @@ export default function BrandSettingsPage() {
|
||||
数据导出
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">导出您的审核数据和报告</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="secondary">
|
||||
<Download size={16} />
|
||||
导出审核记录
|
||||
</Button>
|
||||
<Button variant="secondary">
|
||||
<Download size={16} />
|
||||
导出统计报告
|
||||
</Button>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-text-secondary mb-4">导出您的审核数据和报告</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setExportType('review'); setShowExportModal(true); }}
|
||||
className="p-4 rounded-xl border border-border-subtle hover:border-accent-indigo/50 transition-colors text-left"
|
||||
>
|
||||
<FileText size={24} className="text-accent-indigo mb-2" />
|
||||
<p className="font-medium text-text-primary">审核记录</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">脚本和视频审核</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setExportType('stats'); setShowExportModal(true); }}
|
||||
className="p-4 rounded-xl border border-border-subtle hover:border-accent-indigo/50 transition-colors text-left"
|
||||
>
|
||||
<BarChart3 size={24} className="text-accent-green mb-2" />
|
||||
<p className="font-medium text-text-primary">统计报告</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">数据统计分析</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setExportType('agency'); setShowExportModal(true); }}
|
||||
className="p-4 rounded-xl border border-border-subtle hover:border-accent-indigo/50 transition-colors text-left"
|
||||
>
|
||||
<Users size={24} className="text-purple-400 mb-2" />
|
||||
<p className="font-medium text-text-primary">代理商数据</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">合作代理商信息</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setExportType('all'); setShowExportModal(true); }}
|
||||
className="p-4 rounded-xl border border-border-subtle hover:border-accent-indigo/50 transition-colors text-left"
|
||||
>
|
||||
<Download size={24} className="text-orange-400 mb-2" />
|
||||
<p className="font-medium text-text-primary">全部数据</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">导出所有数据</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导出历史 */}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-primary mb-3">导出历史</p>
|
||||
{exportHistory.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{exportHistory.map(item => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-green/15 flex items-center justify-center">
|
||||
<CheckCircle size={20} className="text-accent-green" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{item.type} - {item.range}</p>
|
||||
<p className="text-xs text-text-tertiary">{item.createdAt} · {item.size}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm">
|
||||
<Download size={14} />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
<Download size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>暂无导出记录</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -237,6 +450,361 @@ export default function BrandSettingsPage() {
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 退出登录确认弹窗 */}
|
||||
<Modal
|
||||
isOpen={showLogoutModal}
|
||||
onClose={() => setShowLogoutModal(false)}
|
||||
title="确认退出登录"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-accent-coral flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-text-primary font-medium">确定要退出登录吗?</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
退出后需要重新登录才能访问系统
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="ghost" onClick={() => setShowLogoutModal(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border-accent-coral text-accent-coral hover:bg-accent-coral/10"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
确认退出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 修改密码弹窗 */}
|
||||
<Modal
|
||||
isOpen={showPasswordModal}
|
||||
onClose={() => { setShowPasswordModal(false); setPasswordForm({ current: '', new: '', confirm: '' }); }}
|
||||
title="修改密码"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">当前密码</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPasswords.current ? 'text' : 'password'}
|
||||
value={passwordForm.current}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, current: e.target.value })}
|
||||
className="w-full px-4 py-2.5 pr-10 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords({ ...showPasswords, current: !showPasswords.current })}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary"
|
||||
>
|
||||
{showPasswords.current ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">新密码</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPasswords.new ? 'text' : 'password'}
|
||||
value={passwordForm.new}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, new: e.target.value })}
|
||||
className="w-full px-4 py-2.5 pr-10 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords({ ...showPasswords, new: !showPasswords.new })}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary"
|
||||
>
|
||||
{showPasswords.new ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary mt-1">密码需要8位以上,包含字母和数字</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">确认新密码</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPasswords.confirm ? 'text' : 'password'}
|
||||
value={passwordForm.confirm}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, confirm: e.target.value })}
|
||||
className="w-full px-4 py-2.5 pr-10 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords({ ...showPasswords, confirm: !showPasswords.confirm })}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary"
|
||||
>
|
||||
{showPasswords.confirm ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => { setShowPasswordModal(false); setPasswordForm({ current: '', new: '', confirm: '' }); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleChangePassword} disabled={!passwordForm.current || !passwordForm.new || !passwordForm.confirm}>
|
||||
确认修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 双因素认证弹窗 */}
|
||||
<Modal
|
||||
isOpen={show2FAModal}
|
||||
onClose={() => setShow2FAModal(false)}
|
||||
title="启用双因素认证"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary text-sm">
|
||||
启用双因素认证后,登录时除了密码还需要输入手机验证码,可以有效保护账户安全。
|
||||
</p>
|
||||
<div className="p-4 rounded-xl bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary mb-2">绑定手机号</p>
|
||||
<p className="font-medium text-text-primary">138****8888</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield size={20} className="text-accent-indigo flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-text-primary font-medium">安全提示</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
启用后,每次登录都需要输入发送到手机的验证码
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => setShow2FAModal(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleEnable2FA}>
|
||||
<Shield size={16} />
|
||||
确认启用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 修改邮箱弹窗 */}
|
||||
<Modal
|
||||
isOpen={showEmailModal}
|
||||
onClose={() => { setShowEmailModal(false); setNewEmail(''); setEmailCode(''); }}
|
||||
title="修改绑定邮箱"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-xl bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">当前邮箱</p>
|
||||
<p className="font-medium text-text-primary">brand@example.com</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">新邮箱地址</label>
|
||||
<input
|
||||
type="email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
placeholder="请输入新邮箱"
|
||||
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>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={emailCode}
|
||||
onChange={(e) => setEmailCode(e.target.value)}
|
||||
placeholder="请输入验证码"
|
||||
className="flex-1 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"
|
||||
/>
|
||||
<Button variant="secondary">发送验证码</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => { setShowEmailModal(false); setNewEmail(''); setEmailCode(''); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleChangeEmail} disabled={!newEmail || !emailCode}>
|
||||
确认修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 修改手机弹窗 */}
|
||||
<Modal
|
||||
isOpen={showPhoneModal}
|
||||
onClose={() => { setShowPhoneModal(false); setNewPhone(''); setPhoneCode(''); }}
|
||||
title="修改绑定手机"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-xl bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">当前手机号</p>
|
||||
<p className="font-medium text-text-primary">138****8888</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">新手机号</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={newPhone}
|
||||
onChange={(e) => setNewPhone(e.target.value)}
|
||||
placeholder="请输入新手机号"
|
||||
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>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={phoneCode}
|
||||
onChange={(e) => setPhoneCode(e.target.value)}
|
||||
placeholder="请输入验证码"
|
||||
className="flex-1 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"
|
||||
/>
|
||||
<Button variant="secondary">发送验证码</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => { setShowPhoneModal(false); setNewPhone(''); setPhoneCode(''); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleChangePhone} disabled={!newPhone || !phoneCode}>
|
||||
确认修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 登录设备管理弹窗 */}
|
||||
<Modal
|
||||
isOpen={showDevicesModal}
|
||||
onClose={() => setShowDevicesModal(false)}
|
||||
title="登录设备管理"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-text-secondary text-sm">
|
||||
以下是您账号已登录的设备,您可以移除不需要的设备。
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{loginDevices.map(device => (
|
||||
<div key={device.id} className="flex items-center justify-between p-4 rounded-xl bg-bg-elevated">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
device.isCurrent ? 'bg-accent-green/15' : 'bg-bg-page'
|
||||
}`}>
|
||||
<Monitor size={20} className={device.isCurrent ? 'text-accent-green' : 'text-text-tertiary'} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-text-primary">{device.name}</p>
|
||||
{device.isCurrent && (
|
||||
<span className="px-2 py-0.5 rounded bg-accent-green/15 text-accent-green text-xs">当前</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-text-tertiary">{device.location} · {device.lastActive}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!device.isCurrent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-accent-coral hover:bg-accent-coral/10"
|
||||
onClick={() => handleRemoveDevice(device.id)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
移除
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => setShowDevicesModal(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 数据导出弹窗 */}
|
||||
<Modal
|
||||
isOpen={showExportModal}
|
||||
onClose={() => { setShowExportModal(false); setExportType(''); }}
|
||||
title="导出数据"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-xl bg-bg-elevated">
|
||||
<p className="text-sm text-text-secondary">导出类型</p>
|
||||
<p className="font-medium text-text-primary">
|
||||
{exportType === 'review' && '审核记录'}
|
||||
{exportType === 'stats' && '统计报告'}
|
||||
{exportType === 'agency' && '代理商数据'}
|
||||
{exportType === 'all' && '全部数据'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">选择时间范围</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ value: 'week', label: '最近一周' },
|
||||
{ value: 'month', label: '最近一月' },
|
||||
{ value: 'quarter', label: '最近三月' },
|
||||
{ value: 'year', label: '最近一年' },
|
||||
].map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setExportRange(option.value)}
|
||||
className={`p-3 rounded-xl border-2 text-sm font-medium transition-colors ${
|
||||
exportRange === option.value
|
||||
? 'border-accent-indigo bg-accent-indigo/10 text-accent-indigo'
|
||||
: 'border-border-subtle text-text-secondary hover:border-accent-indigo/50'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">导出格式</label>
|
||||
<div className="flex gap-2">
|
||||
<span className="px-3 py-2 rounded-lg bg-accent-indigo/15 text-accent-indigo text-sm font-medium">Excel (.xlsx)</span>
|
||||
<span className="px-3 py-2 rounded-lg bg-bg-elevated text-text-tertiary text-sm">CSV</span>
|
||||
<span className="px-3 py-2 rounded-lg bg-bg-elevated text-text-tertiary text-sm">PDF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<Button variant="ghost" onClick={() => { setShowExportModal(false); setExportType(''); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={isExporting}>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<Clock size={16} className="animate-spin" />
|
||||
导出中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download size={16} />
|
||||
开始导出
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ const agencyNavItems: NavItem[] = [
|
||||
{ icon: Users, label: '达人管理', href: '/agency/creators' },
|
||||
{ icon: BarChart3, label: '数据报表', href: '/agency/reports' },
|
||||
{ icon: Bell, label: '消息中心', href: '/agency/messages' },
|
||||
{ icon: User, label: '个人中心', href: '/agency/profile' },
|
||||
]
|
||||
|
||||
// 品牌方端导航项
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user