feat(creator): 完成达人端前端页面开发
- 新增申诉中心页面(列表、详情、新建申诉) - 新增申诉次数管理页面(按任务显示配额,支持向代理商申请) - 新增个人中心页面(达人ID复制、菜单导航) - 新增个人信息编辑、账户设置、消息通知设置页面 - 新增帮助中心和历史记录页面 - 新增脚本提交和视频提交页面 - 优化消息中心页面(消息详情跳转) - 优化任务详情页面布局和交互 - 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
1011e73643
commit
2f9b7f05fd
@@ -0,0 +1,249 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, Camera, Check, Copy } from 'lucide-react'
|
||||
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUser = {
|
||||
name: '李小红',
|
||||
initial: '李',
|
||||
creatorId: 'CR123456', // 达人ID(系统生成,不可修改)
|
||||
phone: '138****8888',
|
||||
email: 'lixiaohong@example.com',
|
||||
douyinAccount: '@xiaohong_creator',
|
||||
bio: '专注美妆护肤分享,与你一起变美~',
|
||||
}
|
||||
|
||||
export default function ProfileEditPage() {
|
||||
const router = useRouter()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [idCopied, setIdCopied] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
name: mockUser.name,
|
||||
phone: mockUser.phone,
|
||||
email: mockUser.email,
|
||||
douyinAccount: mockUser.douyinAccount,
|
||||
bio: mockUser.bio,
|
||||
})
|
||||
|
||||
// 复制达人ID
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(mockUser.creatorId)
|
||||
setIdCopied(true)
|
||||
setTimeout(() => setIdCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
// 模拟保存
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
router.back()
|
||||
}
|
||||
|
||||
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 lg:flex-row gap-6 flex-1 min-h-0 overflow-y-auto lg:overflow-visible">
|
||||
{/* 头像编辑卡片 */}
|
||||
<div className="lg:w-[360px] lg:flex-shrink-0">
|
||||
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex flex-col items-center gap-5">
|
||||
{/* 头像 */}
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-24 h-24 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366F1 0%, #4F46E5 100%)',
|
||||
}}
|
||||
>
|
||||
<span className="text-[40px] font-bold text-white">{mockUser.initial}</span>
|
||||
</div>
|
||||
{/* 相机按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute bottom-0 right-0 w-8 h-8 rounded-full bg-accent-indigo flex items-center justify-center shadow-lg hover:bg-accent-indigo/90 transition-colors"
|
||||
>
|
||||
<Camera size={16} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">点击更换头像</p>
|
||||
|
||||
{/* 提示 */}
|
||||
<div className="w-full p-4 rounded-xl bg-bg-elevated">
|
||||
<p className="text-[13px] text-text-tertiary leading-relaxed">
|
||||
支持 JPG、PNG 格式,文件大小不超过 5MB,建议使用正方形图片以获得最佳显示效果。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单卡片 */}
|
||||
<div className="flex-1 flex flex-col gap-5">
|
||||
<div className="bg-bg-card rounded-2xl p-6 card-shadow flex flex-col gap-5">
|
||||
{/* 达人ID(只读) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">达人ID</label>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 px-4 py-3 rounded-xl border border-border-default bg-bg-elevated/50 flex items-center justify-between">
|
||||
<span className="font-mono font-medium text-accent-indigo">{mockUser.creatorId}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyId}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-bg-elevated transition-colors"
|
||||
>
|
||||
{idCopied ? (
|
||||
<>
|
||||
<Check size={14} className="text-accent-green" />
|
||||
<span className="text-xs text-accent-green">已复制</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} className="text-text-tertiary" />
|
||||
<span className="text-xs text-text-tertiary">复制</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary">系统自动生成的唯一标识,供代理商邀请时使用</p>
|
||||
</div>
|
||||
|
||||
{/* 昵称 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">昵称</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 手机号 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">手机号</label>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="请输入手机号"
|
||||
className="flex-1"
|
||||
disabled
|
||||
/>
|
||||
<Button variant="secondary" size="md">
|
||||
更换
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary">手机号用于登录和接收重要通知</p>
|
||||
</div>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">邮箱</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 抖音账号 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">抖音账号</label>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={formData.douyinAccount}
|
||||
onChange={(e) => handleInputChange('douyinAccount', e.target.value)}
|
||||
placeholder="请输入抖音账号"
|
||||
className="flex-1"
|
||||
disabled
|
||||
/>
|
||||
<Button variant="secondary" size="md">
|
||||
重新绑定
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-tertiary">已认证的抖音账号,用于发布视频</p>
|
||||
</div>
|
||||
|
||||
{/* 个人简介 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-text-primary">个人简介</label>
|
||||
<textarea
|
||||
value={formData.bio}
|
||||
onChange={(e) => handleInputChange('bio', e.target.value)}
|
||||
placeholder="介绍一下自己吧..."
|
||||
rows={3}
|
||||
className={cn(
|
||||
'w-full px-4 py-3 rounded-xl border border-border-default',
|
||||
'bg-bg-elevated text-text-primary text-[15px]',
|
||||
'placeholder:text-text-tertiary',
|
||||
'focus:outline-none focus:border-accent-indigo focus:ring-2 focus:ring-accent-indigo/20',
|
||||
'transition-all resize-none'
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-text-tertiary text-right">{formData.bio.length}/100</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="secondary" size="lg" onClick={() => router.back()}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
{isSaving ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
保存中...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
<Check size={18} />
|
||||
保存修改
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
CircleUser,
|
||||
Settings,
|
||||
BellRing,
|
||||
History,
|
||||
MessageCircleQuestion,
|
||||
PlusCircle,
|
||||
ChevronRight,
|
||||
LogOut,
|
||||
Copy,
|
||||
Check
|
||||
} from 'lucide-react'
|
||||
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// 用户数据
|
||||
const mockUser = {
|
||||
name: '李小红',
|
||||
initial: '李',
|
||||
creatorId: 'CR123456', // 达人ID
|
||||
role: '抖音达人 · 已认证',
|
||||
stats: {
|
||||
completed: 28,
|
||||
passRate: 92,
|
||||
inProgress: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// 菜单项数据
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'personal',
|
||||
icon: CircleUser,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo',
|
||||
title: '个人信息',
|
||||
subtitle: '头像、昵称、绑定账号',
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
icon: Settings,
|
||||
iconColor: 'text-accent-green',
|
||||
bgColor: 'bg-accent-green',
|
||||
title: '账户设置',
|
||||
subtitle: '修改密码、账号安全',
|
||||
},
|
||||
{
|
||||
id: 'notification',
|
||||
icon: BellRing,
|
||||
iconColor: 'text-accent-blue',
|
||||
bgColor: 'bg-accent-blue',
|
||||
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: '常见问题、联系客服',
|
||||
},
|
||||
{
|
||||
id: 'appeal',
|
||||
icon: PlusCircle,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo',
|
||||
title: '申诉次数',
|
||||
subtitle: '查看各任务申诉次数 · 申请增加',
|
||||
},
|
||||
]
|
||||
|
||||
// 用户卡片组件
|
||||
function UserCard() {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// 复制达人ID
|
||||
const handleCopyId = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(mockUser.creatorId)
|
||||
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">{mockUser.initial}</span>
|
||||
</div>
|
||||
{/* 用户信息 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xl font-semibold text-text-primary">{mockUser.name}</span>
|
||||
<span className="text-sm text-text-secondary">{mockUser.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">{mockUser.creatorId}</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 justify-around pt-4 border-t border-border-subtle">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-bold text-text-primary">{mockUser.stats.completed}</span>
|
||||
<span className="text-xs text-text-secondary">完成任务</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-bold text-accent-green">{mockUser.stats.passRate}%</span>
|
||||
<span className="text-xs text-text-secondary">通过率</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-bold text-accent-indigo">{mockUser.stats.inProgress}</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 CreatorProfilePage() {
|
||||
const router = useRouter()
|
||||
|
||||
// 菜单项点击处理
|
||||
const handleMenuClick = (menuId: string) => {
|
||||
const routes: Record<string, string> = {
|
||||
personal: '/creator/profile/edit',
|
||||
account: '/creator/settings/account',
|
||||
notification: '/creator/settings/notification',
|
||||
history: '/creator/history',
|
||||
help: '/creator/help',
|
||||
appeal: '/creator/appeal-quota',
|
||||
}
|
||||
const route = routes[menuId]
|
||||
if (route) {
|
||||
router.push(route)
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
// TODO: 实际退出逻辑
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveLayout role="creator">
|
||||
<div className="flex flex-col gap-6 h-full">
|
||||
{/* 顶部栏 */}
|
||||
<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 className="flex flex-col lg:flex-row gap-6 flex-1 min-h-0 overflow-y-auto lg:overflow-visible">
|
||||
{/* 用户卡片 */}
|
||||
<div className="lg:w-[360px] lg:flex-shrink-0">
|
||||
<UserCard />
|
||||
</div>
|
||||
|
||||
{/* 菜单和退出 */}
|
||||
<div className="flex-1 flex flex-col gap-5 lg:overflow-y-auto lg:pr-2">
|
||||
<MenuCard onMenuClick={handleMenuClick} />
|
||||
<LogoutCard onLogout={handleLogout} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user