567 lines
21 KiB
TypeScript
567 lines
21 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { User, Mail, Phone, MapPin, Save, Edit, Key } from 'lucide-react'
|
||
|
||
interface UserProfile {
|
||
id: string
|
||
email: string
|
||
username: string
|
||
name?: string
|
||
phone?: string
|
||
address?: string
|
||
}
|
||
|
||
interface UserStats {
|
||
user: {
|
||
id: string
|
||
username: string
|
||
email: string
|
||
memberSince: string
|
||
}
|
||
orderStats: {
|
||
total: number
|
||
byStatus: {
|
||
PENDING: number
|
||
CONFIRMED: number
|
||
SHIPPED: number
|
||
DELIVERED: number
|
||
CANCELLED: number
|
||
}
|
||
totalSpent: number
|
||
}
|
||
recentOrders: Array<{
|
||
id: string
|
||
status: string
|
||
totalAmount: number
|
||
createdAt: string
|
||
}>
|
||
favoriteComponentType: string | null
|
||
summary: {
|
||
totalOrders: number
|
||
totalSpent: number
|
||
pendingOrders: number
|
||
completedOrders: number
|
||
cartItems: number
|
||
}
|
||
}
|
||
|
||
export default function ProfilePage() {
|
||
const [user, setUser] = useState<UserProfile | null>(null)
|
||
const [userStats, setUserStats] = useState<UserStats | null>(null)
|
||
const [isEditing, setIsEditing] = useState(false)
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [isSaving, setIsSaving] = useState(false)
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
phone: '',
|
||
address: ''
|
||
})
|
||
const [passwordForm, setPasswordForm] = useState({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
const [showPasswordForm, setShowPasswordForm] = useState(false)
|
||
useEffect(() => {
|
||
loadUserProfile()
|
||
loadUserStats()
|
||
}, [])
|
||
|
||
const loadUserProfile = async () => {
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
if (!token) {
|
||
window.location.href = '/login'
|
||
return
|
||
}
|
||
|
||
const response = await fetch('/api/user/profile', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
|
||
if (response.ok) {
|
||
const userData = await response.json()
|
||
setUser(userData)
|
||
setFormData({
|
||
name: userData.name || '',
|
||
phone: userData.phone || '',
|
||
address: userData.address || ''
|
||
})
|
||
} else if (response.status === 401) {
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('user')
|
||
window.location.href = '/login'
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户资料失败:', error)
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
const loadUserStats = async () => {
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
if (!token) {
|
||
return
|
||
}
|
||
|
||
const response = await fetch('/api/user/stats', {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
|
||
if (response.ok) {
|
||
const stats = await response.json()
|
||
setUserStats(stats)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户统计失败:', error)
|
||
}
|
||
}
|
||
|
||
const handleSaveProfile = async () => {
|
||
setIsSaving(true)
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch('/api/user/profile', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify(formData)
|
||
})
|
||
|
||
if (response.ok) {
|
||
const updatedUser = await response.json()
|
||
setUser(updatedUser)
|
||
setIsEditing(false)
|
||
|
||
// 更新localStorage中的用户信息
|
||
localStorage.setItem('user', JSON.stringify(updatedUser))
|
||
|
||
alert('资料更新成功!')
|
||
} else {
|
||
alert('更新失败,请重试')
|
||
}
|
||
} catch (error) {
|
||
console.error('更新用户资料失败:', error)
|
||
alert('更新失败,请重试')
|
||
} finally {
|
||
setIsSaving(false)
|
||
}
|
||
}
|
||
|
||
const handleChangePassword = async () => {
|
||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||
alert('新密码和确认密码不匹配')
|
||
return
|
||
}
|
||
|
||
if (passwordForm.newPassword.length < 6) {
|
||
alert('新密码长度至少6位')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch('/api/user/change-password', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({
|
||
currentPassword: passwordForm.currentPassword,
|
||
newPassword: passwordForm.newPassword
|
||
})
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('密码修改成功!')
|
||
setPasswordForm({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
setShowPasswordForm(false)
|
||
} else {
|
||
const error = await response.json()
|
||
alert(error.message || '密码修改失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('修改密码失败:', error)
|
||
alert('修改失败,请重试')
|
||
}
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
setFormData({
|
||
name: user?.name || '',
|
||
phone: user?.phone || '',
|
||
address: user?.address || ''
|
||
})
|
||
setIsEditing(false)
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||
<p className="text-gray-600">加载中...</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Header */}
|
||
<div className="text-center mb-8">
|
||
<div className="w-24 h-24 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<User className="h-12 w-12 text-white" />
|
||
</div>
|
||
<h1 className="text-3xl font-bold text-gray-900">{user?.name || user?.username}</h1>
|
||
<p className="text-gray-600">{user?.email}</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
{/* Profile Info */}
|
||
<div className="lg:col-span-2">
|
||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h2 className="text-xl font-semibold text-gray-900">个人资料</h2>
|
||
{!isEditing ? (
|
||
<button
|
||
onClick={() => setIsEditing(true)}
|
||
className="inline-flex items-center text-blue-600 hover:text-blue-800"
|
||
>
|
||
<Edit className="h-4 w-4 mr-2" />
|
||
编辑资料
|
||
</button>
|
||
) : (
|
||
<div className="flex space-x-2">
|
||
<button
|
||
onClick={handleSaveProfile}
|
||
disabled={isSaving}
|
||
className="inline-flex items-center bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
<Save className="h-4 w-4 mr-2" />
|
||
{isSaving ? '保存中...' : '保存'}
|
||
</button>
|
||
<button
|
||
onClick={handleCancel}
|
||
className="text-gray-600 hover:text-gray-800 px-4 py-2"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{/* Email (readonly) */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
<Mail className="h-4 w-4 inline mr-2" />
|
||
邮箱地址
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={user?.email || ''}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500"
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">邮箱地址不可修改</p>
|
||
</div>
|
||
|
||
{/* Username (readonly) */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
<User className="h-4 w-4 inline mr-2" />
|
||
用户名
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={user?.username || ''}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500"
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">用户名不可修改</p>
|
||
</div>
|
||
|
||
{/* Name */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
姓名
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||
disabled={!isEditing}
|
||
className={`w-full px-3 py-2 border border-gray-300 rounded-lg ${
|
||
isEditing ? 'focus:ring-2 focus:ring-blue-500 focus:border-blue-500' : 'bg-gray-50'
|
||
}`}
|
||
placeholder="请输入真实姓名"
|
||
/>
|
||
</div>
|
||
|
||
{/* Phone */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
<Phone className="h-4 w-4 inline mr-2" />
|
||
手机号码
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData({...formData, phone: e.target.value})}
|
||
disabled={!isEditing}
|
||
className={`w-full px-3 py-2 border border-gray-300 rounded-lg ${
|
||
isEditing ? 'focus:ring-2 focus:ring-blue-500 focus:border-blue-500' : 'bg-gray-50'
|
||
}`}
|
||
placeholder="请输入手机号码"
|
||
/>
|
||
</div>
|
||
|
||
{/* Address */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
<MapPin className="h-4 w-4 inline mr-2" />
|
||
收货地址
|
||
</label>
|
||
<textarea
|
||
value={formData.address}
|
||
onChange={(e) => setFormData({...formData, address: e.target.value})}
|
||
disabled={!isEditing}
|
||
rows={3}
|
||
className={`w-full px-3 py-2 border border-gray-300 rounded-lg ${
|
||
isEditing ? 'focus:ring-2 focus:ring-blue-500 focus:border-blue-500' : 'bg-gray-50'
|
||
}`}
|
||
placeholder="请输入详细地址"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Password Change */}
|
||
<div className="bg-white rounded-lg shadow-sm p-6 mt-6">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h2 className="text-xl font-semibold text-gray-900">安全设置</h2>
|
||
<button
|
||
onClick={() => setShowPasswordForm(!showPasswordForm)}
|
||
className="inline-flex items-center text-blue-600 hover:text-blue-800"
|
||
>
|
||
<Key className="h-4 w-4 mr-2" />
|
||
修改密码
|
||
</button>
|
||
</div>
|
||
|
||
{showPasswordForm && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
当前密码
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={passwordForm.currentPassword}
|
||
onChange={(e) => setPasswordForm({...passwordForm, currentPassword: e.target.value})}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="请输入当前密码"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
新密码
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={passwordForm.newPassword}
|
||
onChange={(e) => setPasswordForm({...passwordForm, newPassword: e.target.value})}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="请输入新密码(至少6位)"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
确认新密码
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={passwordForm.confirmPassword}
|
||
onChange={(e) => setPasswordForm({...passwordForm, confirmPassword: e.target.value})}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="请再次输入新密码"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex space-x-3">
|
||
<button
|
||
onClick={handleChangePassword}
|
||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||
>
|
||
修改密码
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowPasswordForm(false)
|
||
setPasswordForm({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
}}
|
||
className="text-gray-600 hover:text-gray-800 px-4 py-2"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Actions */}
|
||
<div className="lg:col-span-1">
|
||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">快捷操作</h3>
|
||
<div className="space-y-3">
|
||
<a
|
||
href="/orders"
|
||
className="block w-full text-left px-4 py-3 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
||
>
|
||
查看我的订单
|
||
</a>
|
||
<a
|
||
href="/cart"
|
||
className="block w-full text-left px-4 py-3 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
||
>
|
||
查看购物车
|
||
</a>
|
||
<a
|
||
href="/build"
|
||
className="block w-full text-left px-4 py-3 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
||
>
|
||
装机配置
|
||
</a>
|
||
<a
|
||
href="/components"
|
||
className="block w-full text-left px-4 py-3 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
||
>
|
||
浏览商品
|
||
</a>
|
||
</div>
|
||
</div> {/* Account Stats */}
|
||
<div className="bg-white rounded-lg shadow-sm p-6 mt-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">账户统计</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">注册时间:</span>
|
||
<span className="font-medium text-gray-900">
|
||
{userStats?.user?.memberSince ?
|
||
new Date(userStats.user.memberSince).toLocaleDateString('zh-CN') :
|
||
'--'
|
||
}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">订单数量:</span>
|
||
<span className="font-medium text-gray-900">
|
||
{userStats?.summary?.totalOrders || 0}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">累计消费:</span>
|
||
<span className="font-medium text-red-600">
|
||
¥{userStats?.summary?.totalSpent?.toFixed(2) || '0.00'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">待处理订单:</span>
|
||
<span className="font-medium text-orange-600">
|
||
{userStats?.summary?.pendingOrders || 0}
|
||
</span>
|
||
</div> <div className="flex justify-between">
|
||
<span className="text-gray-600">已完成订单:</span>
|
||
<span className="font-medium text-green-600">
|
||
{userStats?.summary?.completedOrders || 0}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">购物车商品:</span>
|
||
<span className="font-medium text-blue-600">
|
||
{userStats?.summary?.cartItems || 0}
|
||
</span>
|
||
</div>
|
||
{userStats?.favoriteComponentType && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">偏好配件:</span>
|
||
<span className="font-medium text-blue-600">
|
||
{userStats.favoriteComponentType}
|
||
</span>
|
||
</div> )}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recent Orders */}
|
||
{userStats?.recentOrders && userStats.recentOrders.length > 0 && (
|
||
<div className="bg-white rounded-lg shadow-sm p-6 mt-6">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-4">最近订单</h3>
|
||
<div className="space-y-3">
|
||
{userStats.recentOrders.map((order) => (
|
||
<div key={order.id} className="flex justify-between items-center p-3 border border-gray-200 rounded-lg">
|
||
<div>
|
||
<p className="font-medium text-gray-900">订单 #{order.id.slice(-8)}</p>
|
||
<p className="text-sm text-gray-500">
|
||
{new Date(order.createdAt).toLocaleDateString('zh-CN')}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="font-medium text-red-600">¥{order.totalAmount.toFixed(2)}</p>
|
||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||
order.status === 'PENDING' ? 'bg-yellow-100 text-yellow-800' :
|
||
order.status === 'CONFIRMED' ? 'bg-blue-100 text-blue-800' :
|
||
order.status === 'SHIPPED' ? 'bg-purple-100 text-purple-800' :
|
||
order.status === 'PROCESSING' ? 'bg-purple-100 text-purple-800' :
|
||
order.status === 'DELIVERED' ? 'bg-green-100 text-green-800' :
|
||
'bg-red-100 text-red-800'
|
||
}`}>
|
||
{order.status === 'PENDING' ? '待确认' :
|
||
order.status === 'CONFIRMED' ? '已确认' :
|
||
order.status === 'SHIPPED' ? '已发货' :
|
||
order.status === 'DELIVERED' ? '已送达' :
|
||
order.status === 'PROCESSING' ? '处理中' :
|
||
'已取消'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
<div className="text-center">
|
||
<a
|
||
href="/orders"
|
||
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||
>
|
||
查看全部订单 →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|