feat(creator): 完成达人端前端页面开发

- 新增申诉中心页面(列表、详情、新建申诉)
- 新增申诉次数管理页面(按任务显示配额,支持向代理商申请)
- 新增个人中心页面(达人ID复制、菜单导航)
- 新增个人信息编辑、账户设置、消息通知设置页面
- 新增帮助中心和历史记录页面
- 新增脚本提交和视频提交页面
- 优化消息中心页面(消息详情跳转)
- 优化任务详情页面布局和交互
- 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-06 15:38:01 +08:00
co-authored by Claude Opus 4.5
parent 1011e73643
commit 2f9b7f05fd
18 changed files with 5638 additions and 1145 deletions
@@ -0,0 +1,383 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import {
ArrowLeft,
Key,
ShieldCheck,
Smartphone,
ChevronRight,
Check,
X,
Eye,
EyeOff,
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/ui/Modal'
import { cn } from '@/lib/utils'
// 模拟登录设备数据
const mockDevices = [
{
id: '1',
name: 'iPhone 15 Pro',
type: 'mobile',
location: '北京',
lastActive: '当前设备',
isCurrent: true,
},
{
id: '2',
name: 'MacBook Pro',
type: 'desktop',
location: '北京',
lastActive: '2小时前',
isCurrent: false,
},
{
id: '3',
name: 'iPad Air',
type: 'tablet',
location: '上海',
lastActive: '3天前',
isCurrent: false,
},
]
// 设置项组件
function SettingItem({
icon: Icon,
iconColor,
title,
description,
badge,
onClick,
showArrow = true,
}: {
icon: React.ElementType
iconColor: string
title: string
description: string
badge?: React.ReactNode
onClick?: () => void
showArrow?: boolean
}) {
return (
<button
type="button"
onClick={onClick}
className="flex items-center justify-between py-5 px-5 w-full text-left hover:bg-bg-elevated/30 transition-colors border-b border-border-subtle last:border-b-0"
>
<div className="flex items-center gap-4">
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', `${iconColor}/15`)}>
<Icon size={20} className={iconColor} />
</div>
<div className="flex flex-col gap-1">
<span className="text-[15px] font-medium text-text-primary">{title}</span>
<span className="text-[13px] text-text-tertiary">{description}</span>
</div>
</div>
<div className="flex items-center gap-2">
{badge}
{showArrow && <ChevronRight size={20} className="text-text-tertiary" />}
</div>
</button>
)
}
// 修改密码弹窗
function ChangePasswordModal({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) {
const [step, setStep] = useState(1)
const [showPasswords, setShowPasswords] = useState({
current: false,
new: false,
confirm: false,
})
const [formData, setFormData] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
})
const [isSaving, setIsSaving] = useState(false)
const handleSubmit = async () => {
setIsSaving(true)
await new Promise(resolve => setTimeout(resolve, 1500))
setIsSaving(false)
setStep(2)
}
const handleClose = () => {
setStep(1)
setFormData({ currentPassword: '', newPassword: '', confirmPassword: '' })
onClose()
}
return (
<Modal isOpen={isOpen} onClose={handleClose} title="修改密码">
{step === 1 ? (
<div className="flex flex-col gap-5">
{/* 当前密码 */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-text-primary"></label>
<div className="relative">
<Input
type={showPasswords.current ? 'text' : 'password'}
value={formData.currentPassword}
onChange={(e) => setFormData(prev => ({ ...prev, currentPassword: e.target.value }))}
placeholder="请输入当前密码"
/>
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, current: !prev.current }))}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
>
{showPasswords.current ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{/* 新密码 */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-text-primary"></label>
<div className="relative">
<Input
type={showPasswords.new ? 'text' : 'password'}
value={formData.newPassword}
onChange={(e) => setFormData(prev => ({ ...prev, newPassword: e.target.value }))}
placeholder="请输入新密码(8-20位)"
/>
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, new: !prev.new }))}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
>
{showPasswords.new ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<p className="text-xs text-text-tertiary">8-20</p>
</div>
{/* 确认密码 */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-text-primary"></label>
<div className="relative">
<Input
type={showPasswords.confirm ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={(e) => setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))}
placeholder="请再次输入新密码"
/>
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, confirm: !prev.confirm }))}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary"
>
{showPasswords.confirm ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{/* 按钮 */}
<div className="flex gap-3 pt-2">
<Button variant="secondary" size="lg" className="flex-1" onClick={handleClose}>
</Button>
<Button
variant="primary"
size="lg"
className="flex-1"
onClick={handleSubmit}
disabled={!formData.currentPassword || !formData.newPassword || !formData.confirmPassword || isSaving}
>
{isSaving ? '提交中...' : '确认修改'}
</Button>
</div>
</div>
) : (
<div className="flex flex-col items-center gap-4 py-6">
<div className="w-16 h-16 rounded-full bg-accent-green/15 flex items-center justify-center">
<Check size={32} className="text-accent-green" />
</div>
<div className="text-center">
<p className="text-lg font-semibold text-text-primary"></p>
<p className="text-sm text-text-secondary mt-1">使</p>
</div>
<Button variant="primary" size="lg" className="w-full mt-2" onClick={handleClose}>
</Button>
</div>
)}
</Modal>
)
}
// 设备管理弹窗
function DeviceManageModal({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) {
const [devices, setDevices] = useState(mockDevices)
const handleRemoveDevice = (deviceId: string) => {
setDevices(prev => prev.filter(d => d.id !== deviceId))
}
return (
<Modal isOpen={isOpen} onClose={onClose} title="登录设备管理">
<div className="flex flex-col gap-4">
<p className="text-sm text-text-secondary">使</p>
<div className="flex flex-col gap-3">
{devices.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-xl bg-bg-card flex items-center justify-center">
<Smartphone size={20} className="text-text-secondary" />
</div>
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
<span className="text-[15px] font-medium text-text-primary">{device.name}</span>
{device.isCurrent && (
<span className="px-2 py-0.5 rounded-full bg-accent-green/15 text-accent-green text-xs font-medium">
</span>
)}
</div>
<span className="text-[13px] text-text-tertiary">
{device.location} · {device.lastActive}
</span>
</div>
</div>
{!device.isCurrent && (
<button
type="button"
onClick={() => handleRemoveDevice(device.id)}
className="text-accent-coral hover:text-accent-coral/80 text-sm font-medium"
>
</button>
)}
</div>
))}
</div>
<Button variant="secondary" size="lg" className="w-full mt-2" onClick={onClose}>
</Button>
</div>
</Modal>
)
}
export default function AccountSettingsPage() {
const router = useRouter()
const [showPasswordModal, setShowPasswordModal] = useState(false)
const [showDeviceModal, setShowDeviceModal] = useState(false)
const [twoFactorEnabled, setTwoFactorEnabled] = useState(true)
return (
<ResponsiveLayout role="creator">
<div className="flex flex-col gap-6 h-full">
{/* 顶部栏 */}
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => router.back()}
className="w-10 h-10 rounded-xl bg-bg-elevated flex items-center justify-center hover:bg-bg-elevated/80 transition-colors"
>
<ArrowLeft size={20} className="text-text-secondary" />
</button>
<div className="flex flex-col gap-1">
<h1 className="text-2xl lg:text-[28px] font-bold text-text-primary"></h1>
<p className="text-sm lg:text-[15px] text-text-secondary"></p>
</div>
</div>
{/* 内容区 */}
<div className="flex flex-col gap-5 flex-1 min-h-0 overflow-y-auto lg:max-w-2xl">
{/* 账户安全 */}
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold text-text-primary px-1"></h2>
<div className="bg-bg-card rounded-2xl card-shadow overflow-hidden">
<SettingItem
icon={Key}
iconColor="text-accent-indigo"
title="修改密码"
description="定期更新密码以保障账户安全"
onClick={() => setShowPasswordModal(true)}
/>
<SettingItem
icon={ShieldCheck}
iconColor="text-accent-green"
title="两步验证"
description="启用双因素认证增强安全性"
badge={
<span
className={cn(
'px-2 py-1 rounded text-xs font-medium',
twoFactorEnabled
? 'bg-accent-green/15 text-accent-green'
: 'bg-bg-elevated text-text-tertiary'
)}
>
{twoFactorEnabled ? '已开启' : '未开启'}
</span>
}
onClick={() => setTwoFactorEnabled(!twoFactorEnabled)}
/>
<SettingItem
icon={Smartphone}
iconColor="text-accent-blue"
title="登录设备管理"
description="查看和管理已登录的设备"
onClick={() => setShowDeviceModal(true)}
/>
</div>
</div>
{/* 危险区域 */}
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold text-accent-coral px-1"></h2>
<div className="bg-bg-card rounded-2xl card-shadow p-5">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex flex-col gap-1">
<span className="text-[15px] font-medium text-text-primary"></span>
<span className="text-[13px] text-text-tertiary">
</span>
</div>
<Button
variant="secondary"
size="md"
className="border-accent-coral text-accent-coral hover:bg-accent-coral/10 lg:flex-shrink-0"
>
</Button>
</div>
</div>
</div>
</div>
</div>
{/* 弹窗 */}
<ChangePasswordModal isOpen={showPasswordModal} onClose={() => setShowPasswordModal(false)} />
<DeviceManageModal isOpen={showDeviceModal} onClose={() => setShowDeviceModal(false)} />
</ResponsiveLayout>
)
}
@@ -0,0 +1,279 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import {
ArrowLeft,
Mail,
Bell,
MessageSquare,
Clock,
AlertTriangle,
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
// 开关组件
function Toggle({
enabled,
onChange,
}: {
enabled: boolean
onChange: (enabled: boolean) => void
}) {
return (
<button
type="button"
onClick={() => onChange(!enabled)}
className={cn(
'w-12 h-7 rounded-full p-0.5 transition-colors',
enabled ? 'bg-accent-indigo' : 'bg-bg-elevated'
)}
>
<div
className={cn(
'w-6 h-6 rounded-full bg-white shadow-sm transition-transform',
enabled ? 'translate-x-5' : 'translate-x-0'
)}
/>
</button>
)
}
// 设置项组件
function NotificationSettingItem({
icon: Icon,
iconColor,
title,
description,
enabled,
onChange,
isLast = false,
}: {
icon: React.ElementType
iconColor: string
title: string
description: string
enabled: boolean
onChange: (enabled: boolean) => void
isLast?: boolean
}) {
return (
<div
className={cn(
'flex items-center justify-between py-5 px-5',
!isLast && 'border-b border-border-subtle'
)}
>
<div className="flex items-center gap-4">
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', `${iconColor}/15`)}>
<Icon size={20} className={iconColor} />
</div>
<div className="flex flex-col gap-1">
<span className="text-[15px] font-medium text-text-primary">{title}</span>
<span className="text-[13px] text-text-tertiary">{description}</span>
</div>
</div>
<Toggle enabled={enabled} onChange={onChange} />
</div>
)
}
// 时段选择组件
function TimeRangeSelector({
startTime,
endTime,
onChange,
disabled,
}: {
startTime: string
endTime: string
onChange: (start: string, end: string) => void
disabled: boolean
}) {
return (
<div className={cn('flex items-center gap-3', disabled && 'opacity-50')}>
<select
value={startTime}
onChange={(e) => onChange(e.target.value, endTime)}
disabled={disabled}
className="px-3 py-2 rounded-lg bg-bg-elevated border border-border-default text-text-primary text-sm focus:outline-none focus:border-accent-indigo"
>
{Array.from({ length: 24 }, (_, i) => (
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
{i.toString().padStart(2, '0')}:00
</option>
))}
</select>
<span className="text-text-tertiary"></span>
<select
value={endTime}
onChange={(e) => onChange(startTime, e.target.value)}
disabled={disabled}
className="px-3 py-2 rounded-lg bg-bg-elevated border border-border-default text-text-primary text-sm focus:outline-none focus:border-accent-indigo"
>
{Array.from({ length: 24 }, (_, i) => (
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
{i.toString().padStart(2, '0')}:00
</option>
))}
</select>
</div>
)
}
export default function NotificationSettingsPage() {
const router = useRouter()
// 通知设置状态
const [settings, setSettings] = useState({
emailNotification: true,
pushNotification: true,
smsNotification: false,
reviewResult: true,
taskReminder: true,
urgentAlert: true,
quietMode: false,
quietStart: '22:00',
quietEnd: '08:00',
})
const handleToggle = (key: keyof typeof settings) => {
setSettings(prev => ({ ...prev, [key]: !prev[key] }))
}
const handleTimeChange = (start: string, end: string) => {
setSettings(prev => ({ ...prev, quietStart: start, quietEnd: end }))
}
return (
<ResponsiveLayout role="creator">
<div className="flex flex-col gap-6 h-full">
{/* 顶部栏 */}
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => router.back()}
className="w-10 h-10 rounded-xl bg-bg-elevated flex items-center justify-center hover:bg-bg-elevated/80 transition-colors"
>
<ArrowLeft size={20} className="text-text-secondary" />
</button>
<div className="flex flex-col gap-1">
<h1 className="text-2xl lg:text-[28px] font-bold text-text-primary"></h1>
<p className="text-sm lg:text-[15px] text-text-secondary"></p>
</div>
</div>
{/* 内容区 */}
<div className="flex flex-col gap-5 flex-1 min-h-0 overflow-y-auto lg:max-w-2xl">
{/* 通知渠道 */}
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold text-text-primary px-1"></h2>
<div className="bg-bg-card rounded-2xl card-shadow overflow-hidden">
<NotificationSettingItem
icon={Mail}
iconColor="text-accent-indigo"
title="邮件通知"
description="接收审核结果和系统通知的邮件提醒"
enabled={settings.emailNotification}
onChange={() => handleToggle('emailNotification')}
/>
<NotificationSettingItem
icon={Bell}
iconColor="text-accent-blue"
title="推送通知"
description="接收移动端实时推送消息"
enabled={settings.pushNotification}
onChange={() => handleToggle('pushNotification')}
/>
<NotificationSettingItem
icon={MessageSquare}
iconColor="text-accent-green"
title="短信通知"
description="接收重要消息的短信提醒"
enabled={settings.smsNotification}
onChange={() => handleToggle('smsNotification')}
isLast
/>
</div>
</div>
{/* 通知类型 */}
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold text-text-primary px-1"></h2>
<div className="bg-bg-card rounded-2xl card-shadow overflow-hidden">
<NotificationSettingItem
icon={Bell}
iconColor="text-accent-indigo"
title="审核结果通知"
description="视频审核通过或驳回时收到通知"
enabled={settings.reviewResult}
onChange={() => handleToggle('reviewResult')}
/>
<NotificationSettingItem
icon={Clock}
iconColor="text-accent-coral"
title="任务提醒"
description="任务截止日期临近时收到提醒"
enabled={settings.taskReminder}
onChange={() => handleToggle('taskReminder')}
/>
<NotificationSettingItem
icon={AlertTriangle}
iconColor="text-status-error"
title="紧急通知"
description="紧急事项和系统公告即时推送"
enabled={settings.urgentAlert}
onChange={() => handleToggle('urgentAlert')}
isLast
/>
</div>
</div>
{/* 免打扰模式 */}
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold text-text-primary px-1"></h2>
<div className="bg-bg-card rounded-2xl card-shadow p-5 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="text-[15px] font-medium text-text-primary"></span>
<span className="text-[13px] text-text-tertiary"></span>
</div>
<Toggle enabled={settings.quietMode} onChange={() => handleToggle('quietMode')} />
</div>
{/* 时间段选择 */}
<div
className={cn(
'flex flex-col lg:flex-row lg:items-center gap-3 pt-3 border-t border-border-subtle',
!settings.quietMode && 'opacity-50'
)}
>
<span className="text-sm text-text-secondary whitespace-nowrap"></span>
<TimeRangeSelector
startTime={settings.quietStart}
endTime={settings.quietEnd}
onChange={handleTimeChange}
disabled={!settings.quietMode}
/>
</div>
</div>
</div>
{/* 提示卡片 */}
<div className="bg-accent-indigo/10 rounded-2xl p-5 flex gap-4">
<div className="w-10 h-10 rounded-xl bg-accent-indigo/15 flex items-center justify-center flex-shrink-0">
<Bell size={20} className="text-accent-indigo" />
</div>
<div className="flex flex-col gap-1">
<span className="text-[15px] font-medium text-text-primary"></span>
<span className="text-[13px] text-text-secondary leading-relaxed">
便
</span>
</div>
</div>
</div>
</div>
</ResponsiveLayout>
)
}