feat: 完善品牌方和代理商前端功能

品牌方功能:
- 项目看板: 添加截止日期编辑功能
- 项目详情: 添加代理商管理、截止日期编辑、最近任务显示代理商
- 项目创建: 代理商选择支持搜索(名称/ID/公司名)
- 代理商管理: 通过ID邀请、添加备注/分配项目/移除操作
- Brief配置: 新增项目级Brief和规则配置页面
- 系统设置: 完善账户安全(密码/2FA/邮箱/手机/设备管理)、数据导出、退出登录

代理商功能:
- 个人中心: 新增代理商ID展示、公司信息(企业验证)、个人信息编辑
- 账户设置: 密码修改、手机/邮箱绑定、两步验证
- 通知设置: 分类型和渠道的通知开关
- 审核历史: 搜索筛选和统计展示
- 帮助反馈: FAQ分类搜索和客服联系

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-06 17:40:11 +08:00
co-authored by Claude Opus 4.5
parent ae74c515c7
commit 964797d2e9
14 changed files with 4349 additions and 154 deletions
+292
View 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>
)
}
@@ -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
View 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"> JPGPNG 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
View 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
View 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>
)
}
@@ -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>
)
}
@@ -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>
)
}