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
+589 -21
View File
@@ -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>
)
}