This commit is contained in:
2025-06-22 11:34:32 +08:00
parent 142992a6c9
commit 7d139b22db
55 changed files with 11035 additions and 105 deletions
+566
View File
@@ -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>
)
}