2025-06-22 11:34:32 +08:00

567 lines
21 KiB
TypeScript
Raw 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 { 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>
)
}