- 3 消息页 + 2 资料编辑页 + 3 设置页 + 2 资料展示页 - api.ts 新增 Profile/Messages/ChangePassword 等类型和方法 - SSEContext 事件映射修复 + 断线重连修复 - 剩余页面加 USE_MOCK 双模式,52/55 页面已完成 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
406 lines
14 KiB
TypeScript
406 lines
14 KiB
TypeScript
'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>
|
||
)
|
||
}
|