|
|
@@ -0,0 +1,314 @@
|
|
|
+import { useState } from 'react';
|
|
|
+import { Button } from '@/client/components/ui/button';
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
+import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
|
|
|
+import { Badge } from '@/client/components/ui/badge';
|
|
|
+import { Separator } from '@/client/components/ui/separator';
|
|
|
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/client/components/ui/tabs';
|
|
|
+import { Input } from '@/client/components/ui/input';
|
|
|
+import { Label } from '@/client/components/ui/label';
|
|
|
+import { Switch } from '@/client/components/ui/switch';
|
|
|
+import { Alert, AlertDescription } from '@/client/components/ui/alert';
|
|
|
+import {
|
|
|
+ User,
|
|
|
+ Key,
|
|
|
+ CreditCard,
|
|
|
+ Clock,
|
|
|
+ CheckCircle,
|
|
|
+ AlertCircle,
|
|
|
+ Eye,
|
|
|
+ EyeOff
|
|
|
+} from 'lucide-react';
|
|
|
+import { format } from 'date-fns';
|
|
|
+import { zhCN } from 'date-fns/locale';
|
|
|
+import { useAuth } from '@/client/home-shadcn/hooks/AuthProvider';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import RechargeRecords from './RechargeRecords';
|
|
|
+
|
|
|
+interface UserInfoModalProps {
|
|
|
+ isOpen: boolean;
|
|
|
+ onClose: () => void;
|
|
|
+}
|
|
|
+
|
|
|
+// PaymentRecord类型已在RechargeRecords中定义
|
|
|
+
|
|
|
+export default function UserInfoModal({ isOpen, onClose }: UserInfoModalProps) {
|
|
|
+ const { user, logout } = useAuth();
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const [activeTab, setActiveTab] = useState('profile');
|
|
|
+ const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
|
+ const [showNewPassword, setShowNewPassword] = useState(false);
|
|
|
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
+ const [passwordForm, setPasswordForm] = useState({
|
|
|
+ currentPassword: '',
|
|
|
+ newPassword: '',
|
|
|
+ confirmPassword: ''
|
|
|
+ });
|
|
|
+ const [passwordError, setPasswordError] = useState('');
|
|
|
+ const [passwordSuccess, setPasswordSuccess] = useState('');
|
|
|
+
|
|
|
+ // 使用实际的充值记录组件
|
|
|
+
|
|
|
+ if (!isOpen || !user) return null;
|
|
|
+
|
|
|
+ const handlePasswordChange = () => {
|
|
|
+ setPasswordError('');
|
|
|
+ setPasswordSuccess('');
|
|
|
+
|
|
|
+ if (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
|
|
+ setPasswordError('请填写所有必填字段');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
|
|
+ setPasswordError('新密码与确认密码不匹配');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (passwordForm.newPassword.length < 6) {
|
|
|
+ setPasswordError('新密码至少6位字符');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 这里应该调用修改密码的API
|
|
|
+ setPasswordSuccess('密码修改成功!');
|
|
|
+ setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ setPasswordSuccess('');
|
|
|
+ }, 3000);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleLogout = async () => {
|
|
|
+ await logout();
|
|
|
+ onClose();
|
|
|
+ navigate('/');
|
|
|
+ };
|
|
|
+
|
|
|
+ const getStatusBadge = (status: string) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'completed':
|
|
|
+ return <Badge className="bg-green-100 text-green-800">已完成</Badge>;
|
|
|
+ case 'pending':
|
|
|
+ return <Badge className="bg-yellow-100 text-yellow-800">处理中</Badge>;
|
|
|
+ case 'failed':
|
|
|
+ return <Badge className="bg-red-100 text-red-800">失败</Badge>;
|
|
|
+ default:
|
|
|
+ return <Badge>{status}</Badge>;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
|
+ <div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-hidden shadow-2xl">
|
|
|
+ <div className="flex justify-between items-center p-6 border-b">
|
|
|
+ <h2 className="text-2xl font-bold">用户中心</h2>
|
|
|
+ <Button variant="ghost" size="sm" onClick={onClose} className="text-gray-500">
|
|
|
+ ✕
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="overflow-y-auto" style={{ maxHeight: 'calc(90vh - 80px)' }}>
|
|
|
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
|
+ <TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0">
|
|
|
+ <TabsTrigger value="profile" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
|
|
|
+ <User className="h-4 w-4 mr-2" />
|
|
|
+ 个人信息
|
|
|
+ </TabsTrigger>
|
|
|
+ <TabsTrigger value="security" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
|
|
|
+ <Key className="h-4 w-4 mr-2" />
|
|
|
+ 密码修改
|
|
|
+ </TabsTrigger>
|
|
|
+ <TabsTrigger value="recharge" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
|
|
|
+ <CreditCard className="h-4 w-4 mr-2" />
|
|
|
+ 充值记录
|
|
|
+ </TabsTrigger>
|
|
|
+ </TabsList>
|
|
|
+
|
|
|
+ <TabsContent value="profile" className="p-6">
|
|
|
+ <div className="space-y-6">
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
+ <Avatar className="h-20 w-20">
|
|
|
+ <AvatarImage
|
|
|
+ src={user.avatar || `https://avatar.vercel.sh/${user.username}`}
|
|
|
+ alt={user.nickname || user.username}
|
|
|
+ />
|
|
|
+ <AvatarFallback className="text-2xl">
|
|
|
+ {user.username?.charAt(0).toUpperCase()}
|
|
|
+ </AvatarFallback>
|
|
|
+ </Avatar>
|
|
|
+ <div>
|
|
|
+ <h3 className="text-xl font-semibold">{user.nickname || user.username}</h3>
|
|
|
+ <p className="text-gray-500">@{user.username}</p>
|
|
|
+ <Badge className={user.userType === 'premium' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'}>
|
|
|
+ {user.userType === 'premium' ? '高级会员' : '普通会员'}
|
|
|
+ </Badge>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-lg">账户信息</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent className="space-y-4">
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <Label className="text-sm text-gray-500">邮箱</Label>
|
|
|
+ <p className="font-medium">{user.email || '未设置'}</p>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <Label className="text-sm text-gray-500">手机号</Label>
|
|
|
+ <p className="font-medium">{user.phone || '未设置'}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <Label className="text-sm text-gray-500">注册时间</Label>
|
|
|
+ <p className="font-medium">
|
|
|
+ {format(new Date(user.createdAt), 'yyyy年MM月dd日 HH:mm', { locale: zhCN })}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-lg">账户余额</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <p className="text-3xl font-bold text-blue-600">{user.remainingCount || 0}</p>
|
|
|
+ <p className="text-sm text-gray-500">剩余处理次数</p>
|
|
|
+ </div>
|
|
|
+ {user.expireAt && (
|
|
|
+ <div className="text-right">
|
|
|
+ <p className="text-sm text-gray-500">会员到期</p>
|
|
|
+ <p className="font-semibold">
|
|
|
+ {format(new Date(user.expireAt), 'yyyy年MM月dd日', { locale: zhCN })}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ className="w-full mt-4"
|
|
|
+ onClick={() => {
|
|
|
+ onClose();
|
|
|
+ navigate('/recharge');
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <CreditCard className="h-4 w-4 mr-2" />
|
|
|
+ 立即充值
|
|
|
+ </Button>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ className="w-full text-red-600 hover:text-red-700"
|
|
|
+ onClick={handleLogout}
|
|
|
+ >
|
|
|
+ 退出登录
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ <TabsContent value="security" className="p-6">
|
|
|
+ <div className="space-y-6">
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-lg">修改密码</CardTitle>
|
|
|
+ <CardDescription>定期修改密码可以提高账户安全性</CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent className="space-y-4">
|
|
|
+ {passwordError && (
|
|
|
+ <Alert variant="destructive">
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
+ <AlertDescription>{passwordError}</AlertDescription>
|
|
|
+ </Alert>
|
|
|
+ )}
|
|
|
+ {passwordSuccess && (
|
|
|
+ <Alert className="bg-green-50 text-green-800">
|
|
|
+ <CheckCircle className="h-4 w-4" />
|
|
|
+ <AlertDescription>{passwordSuccess}</AlertDescription>
|
|
|
+ </Alert>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="currentPassword">当前密码</Label>
|
|
|
+ <div className="relative">
|
|
|
+ <Input
|
|
|
+ id="currentPassword"
|
|
|
+ type={showCurrentPassword ? 'text' : 'password'}
|
|
|
+ value={passwordForm.currentPassword}
|
|
|
+ onChange={(e) => setPasswordForm({...passwordForm, currentPassword: e.target.value})}
|
|
|
+ placeholder="请输入当前密码"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
|
+ onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
|
|
+ >
|
|
|
+ {showCurrentPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="newPassword">新密码</Label>
|
|
|
+ <div className="relative">
|
|
|
+ <Input
|
|
|
+ id="newPassword"
|
|
|
+ type={showNewPassword ? 'text' : 'password'}
|
|
|
+ value={passwordForm.newPassword}
|
|
|
+ onChange={(e) => setPasswordForm({...passwordForm, newPassword: e.target.value})}
|
|
|
+ placeholder="请输入新密码(至少6位)"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
|
+ onClick={() => setShowNewPassword(!showNewPassword)}
|
|
|
+ >
|
|
|
+ {showNewPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="confirmPassword">确认密码</Label>
|
|
|
+ <div className="relative">
|
|
|
+ <Input
|
|
|
+ id="confirmPassword"
|
|
|
+ type={showConfirmPassword ? 'text' : 'password'}
|
|
|
+ value={passwordForm.confirmPassword}
|
|
|
+ onChange={(e) => setPasswordForm({...passwordForm, confirmPassword: e.target.value})}
|
|
|
+ placeholder="请再次输入新密码"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
|
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
|
+ >
|
|
|
+ {showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Button onClick={handlePasswordChange} className="w-full">
|
|
|
+ 确认修改
|
|
|
+ </Button>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ <TabsContent value="recharge" className="p-6">
|
|
|
+ <RechargeRecords userId={user.id} />
|
|
|
+ </TabsContent>
|
|
|
+ </Tabs>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|