v1
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user