Your Name f02b3f4098 feat: 前端对接 Profile/Messages/Settings 页面 API
- 3 消息页 + 2 资料编辑页 + 3 设置页 + 2 资料展示页
- api.ts 新增 Profile/Messages/ChangePassword 等类型和方法
- SSEContext 事件映射修复 + 断线重连修复
- 剩余页面加 USE_MOCK 双模式,52/55 页面已完成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:27:59 +08:00

286 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useState, useEffect, useCallback } 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'
import { useToast } from '@/components/ui/Toast'
import { USE_MOCK } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
// 模拟用户数据
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 toast = useToast()
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,
})
const [creatorId, setCreatorId] = useState(mockUser.creatorId)
const loadData = useCallback(async () => {
if (USE_MOCK) return
try {
const profile = await api.getProfile()
setFormData({
name: profile.name || '',
phone: profile.phone || '',
email: profile.email || '',
douyinAccount: profile.creator?.douyin_account || '',
bio: profile.creator?.bio || '',
})
if (profile.creator?.id) setCreatorId(profile.creator.id)
} catch {}
}, [])
useEffect(() => { loadData() }, [loadData])
// 复制达人ID
const handleCopyId = async () => {
try {
await navigator.clipboard.writeText(creatorId)
setIdCopied(true)
setTimeout(() => setIdCopied(false), 2000)
} catch {
toast.error('复制失败,请重试')
}
}
// 处理输入变化
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
// 保存
const handleSave = async () => {
setIsSaving(true)
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 1000))
} else {
try {
await api.updateProfile({
name: formData.name,
phone: formData.phone,
bio: formData.bio,
douyin_account: formData.douyinAccount,
})
} catch (err: any) {
toast.error(err.message || '保存失败')
setIsSaving(false)
return
}
}
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">{formData.name?.[0] || '?'}</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">
JPGPNG 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">{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>
)
}