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

406 lines
14 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 } 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'
import { USE_MOCK } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import { useToast } from '@/components/ui/Toast'
// 模拟登录设备数据
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 toast = useToast()
const handleSubmit = async () => {
if (formData.newPassword !== formData.confirmPassword) {
toast.error('两次输入的密码不一致')
return
}
setIsSaving(true)
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 1500))
setIsSaving(false)
setStep(2)
return
}
try {
await api.changePassword({
old_password: formData.currentPassword,
new_password: formData.newPassword,
})
setIsSaving(false)
setStep(2)
} catch (err: any) {
setIsSaving(false)
toast.error(err.message || '密码修改失败')
}
}
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>
)
}