2
0

UserInfoModal.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import { useState } from 'react';
  2. import { Button } from '@/client/components/ui/button';
  3. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
  4. import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
  5. import { Badge } from '@/client/components/ui/badge';
  6. import { Separator } from '@/client/components/ui/separator';
  7. import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/client/components/ui/tabs';
  8. import { Input } from '@/client/components/ui/input';
  9. import { Label } from '@/client/components/ui/label';
  10. import { Switch } from '@/client/components/ui/switch';
  11. import { Alert, AlertDescription } from '@/client/components/ui/alert';
  12. import {
  13. User,
  14. Key,
  15. CreditCard,
  16. Clock,
  17. CheckCircle,
  18. AlertCircle,
  19. Eye,
  20. EyeOff
  21. } from 'lucide-react';
  22. import { format } from 'date-fns';
  23. import { zhCN } from 'date-fns/locale';
  24. import { useAuth } from '@/client/home/hooks/AuthProvider';
  25. import { useNavigate } from 'react-router-dom';
  26. import RechargeRecords from './RechargeRecords';
  27. interface UserInfoModalProps {
  28. isOpen: boolean;
  29. onClose: () => void;
  30. }
  31. // PaymentRecord类型已在RechargeRecords中定义
  32. export default function UserInfoModal({ isOpen, onClose }: UserInfoModalProps) {
  33. const { user, logout } = useAuth();
  34. const navigate = useNavigate();
  35. const [activeTab, setActiveTab] = useState('profile');
  36. const [showCurrentPassword, setShowCurrentPassword] = useState(false);
  37. const [showNewPassword, setShowNewPassword] = useState(false);
  38. const [showConfirmPassword, setShowConfirmPassword] = useState(false);
  39. const [passwordForm, setPasswordForm] = useState({
  40. currentPassword: '',
  41. newPassword: '',
  42. confirmPassword: ''
  43. });
  44. const [passwordError, setPasswordError] = useState('');
  45. const [passwordSuccess, setPasswordSuccess] = useState('');
  46. // 使用实际的充值记录组件
  47. if (!isOpen || !user) return null;
  48. const handlePasswordChange = () => {
  49. setPasswordError('');
  50. setPasswordSuccess('');
  51. if (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
  52. setPasswordError('请填写所有必填字段');
  53. return;
  54. }
  55. if (passwordForm.newPassword !== passwordForm.confirmPassword) {
  56. setPasswordError('新密码与确认密码不匹配');
  57. return;
  58. }
  59. if (passwordForm.newPassword.length < 6) {
  60. setPasswordError('新密码至少6位字符');
  61. return;
  62. }
  63. // 这里应该调用修改密码的API
  64. setPasswordSuccess('密码修改成功!');
  65. setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
  66. setTimeout(() => {
  67. setPasswordSuccess('');
  68. }, 3000);
  69. };
  70. const handleLogout = async () => {
  71. await logout();
  72. onClose();
  73. navigate('/');
  74. };
  75. const getStatusBadge = (status: string) => {
  76. switch (status) {
  77. case 'completed':
  78. return <Badge className="bg-green-100 text-green-800">已完成</Badge>;
  79. case 'pending':
  80. return <Badge className="bg-yellow-100 text-yellow-800">处理中</Badge>;
  81. case 'failed':
  82. return <Badge className="bg-red-100 text-red-800">失败</Badge>;
  83. default:
  84. return <Badge>{status}</Badge>;
  85. }
  86. };
  87. return (
  88. <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
  89. <div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-hidden shadow-2xl">
  90. <div className="flex justify-between items-center p-6 border-b">
  91. <h2 className="text-2xl font-bold">用户中心</h2>
  92. <Button variant="ghost" size="sm" onClick={onClose} className="text-gray-500">
  93. </Button>
  94. </div>
  95. <div className="overflow-y-auto" style={{ maxHeight: 'calc(90vh - 80px)' }}>
  96. <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
  97. <TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0">
  98. <TabsTrigger value="profile" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
  99. <User className="h-4 w-4 mr-2" />
  100. 个人信息
  101. </TabsTrigger>
  102. <TabsTrigger value="security" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
  103. <Key className="h-4 w-4 mr-2" />
  104. 密码修改
  105. </TabsTrigger>
  106. <TabsTrigger value="recharge" className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500">
  107. <CreditCard className="h-4 w-4 mr-2" />
  108. 充值记录
  109. </TabsTrigger>
  110. </TabsList>
  111. <TabsContent value="profile" className="p-6">
  112. <div className="space-y-6">
  113. <div className="flex items-center space-x-4">
  114. <Avatar className="h-20 w-20">
  115. <AvatarImage
  116. src={user.avatar || `https://avatar.vercel.sh/${user.username}`}
  117. alt={user.nickname || user.username}
  118. />
  119. <AvatarFallback className="text-2xl">
  120. {user.username?.charAt(0).toUpperCase()}
  121. </AvatarFallback>
  122. </Avatar>
  123. <div>
  124. <h3 className="text-xl font-semibold">{user.nickname || user.username}</h3>
  125. <p className="text-gray-500">@{user.username}</p>
  126. <Badge className={user.userType === 'premium' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'}>
  127. {user.userType === 'premium' ? '高级会员' : '普通会员'}
  128. </Badge>
  129. </div>
  130. </div>
  131. <Card>
  132. <CardHeader>
  133. <CardTitle className="text-lg">账户信息</CardTitle>
  134. </CardHeader>
  135. <CardContent className="space-y-4">
  136. <div className="grid grid-cols-2 gap-4">
  137. <div>
  138. <Label className="text-sm text-gray-500">邮箱</Label>
  139. <p className="font-medium">{user.email || '未设置'}</p>
  140. </div>
  141. <div>
  142. <Label className="text-sm text-gray-500">手机号</Label>
  143. <p className="font-medium">{user.phone || '未设置'}</p>
  144. </div>
  145. </div>
  146. <div>
  147. <Label className="text-sm text-gray-500">注册时间</Label>
  148. <p className="font-medium">
  149. {format(new Date(user.createdAt), 'yyyy年MM月dd日 HH:mm', { locale: zhCN })}
  150. </p>
  151. </div>
  152. </CardContent>
  153. </Card>
  154. <Card>
  155. <CardHeader>
  156. <CardTitle className="text-lg">会员到期时间</CardTitle>
  157. </CardHeader>
  158. <CardContent>
  159. <div className="flex items-center justify-between">
  160. <div>
  161. <p className="text-3xl font-bold text-blue-600">
  162. {user.expireAt ? format(new Date(user.expireAt), 'yyyy年MM月dd日', { locale: zhCN }) : '未开通'}
  163. </p>
  164. <p className="text-sm text-gray-500">
  165. {user.expireAt ? `剩余 ${Math.ceil((new Date(user.expireAt).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))} 天` : '请开通会员'}
  166. </p>
  167. </div>
  168. <div className="text-right">
  169. <p className="text-sm text-gray-500">剩余次数</p>
  170. <p className="font-semibold">{user.remainingCount || 0} 次</p>
  171. </div>
  172. </div>
  173. <Button
  174. className="w-full mt-4"
  175. onClick={() => {
  176. onClose();
  177. navigate('/recharge');
  178. }}
  179. >
  180. <CreditCard className="h-4 w-4 mr-2" />
  181. {user.expireAt ? '续费会员' : '立即开通'}
  182. </Button>
  183. </CardContent>
  184. </Card>
  185. <Button
  186. variant="outline"
  187. className="w-full text-red-600 hover:text-red-700"
  188. onClick={handleLogout}
  189. >
  190. 退出登录
  191. </Button>
  192. </div>
  193. </TabsContent>
  194. <TabsContent value="security" className="p-6">
  195. <div className="space-y-6">
  196. <Card>
  197. <CardHeader>
  198. <CardTitle className="text-lg">修改密码</CardTitle>
  199. <CardDescription>定期修改密码可以提高账户安全性</CardDescription>
  200. </CardHeader>
  201. <CardContent className="space-y-4">
  202. {passwordError && (
  203. <Alert variant="destructive">
  204. <AlertCircle className="h-4 w-4" />
  205. <AlertDescription>{passwordError}</AlertDescription>
  206. </Alert>
  207. )}
  208. {passwordSuccess && (
  209. <Alert className="bg-green-50 text-green-800">
  210. <CheckCircle className="h-4 w-4" />
  211. <AlertDescription>{passwordSuccess}</AlertDescription>
  212. </Alert>
  213. )}
  214. <div className="space-y-4">
  215. <div className="space-y-2">
  216. <Label htmlFor="currentPassword">当前密码</Label>
  217. <div className="relative">
  218. <Input
  219. id="currentPassword"
  220. type={showCurrentPassword ? 'text' : 'password'}
  221. value={passwordForm.currentPassword}
  222. onChange={(e) => setPasswordForm({...passwordForm, currentPassword: e.target.value})}
  223. placeholder="请输入当前密码"
  224. />
  225. <button
  226. type="button"
  227. className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
  228. onClick={() => setShowCurrentPassword(!showCurrentPassword)}
  229. >
  230. {showCurrentPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  231. </button>
  232. </div>
  233. </div>
  234. <div className="space-y-2">
  235. <Label htmlFor="newPassword">新密码</Label>
  236. <div className="relative">
  237. <Input
  238. id="newPassword"
  239. type={showNewPassword ? 'text' : 'password'}
  240. value={passwordForm.newPassword}
  241. onChange={(e) => setPasswordForm({...passwordForm, newPassword: e.target.value})}
  242. placeholder="请输入新密码(至少6位)"
  243. />
  244. <button
  245. type="button"
  246. className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
  247. onClick={() => setShowNewPassword(!showNewPassword)}
  248. >
  249. {showNewPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  250. </button>
  251. </div>
  252. </div>
  253. <div className="space-y-2">
  254. <Label htmlFor="confirmPassword">确认密码</Label>
  255. <div className="relative">
  256. <Input
  257. id="confirmPassword"
  258. type={showConfirmPassword ? 'text' : 'password'}
  259. value={passwordForm.confirmPassword}
  260. onChange={(e) => setPasswordForm({...passwordForm, confirmPassword: e.target.value})}
  261. placeholder="请再次输入新密码"
  262. />
  263. <button
  264. type="button"
  265. className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
  266. onClick={() => setShowConfirmPassword(!showConfirmPassword)}
  267. >
  268. {showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  269. </button>
  270. </div>
  271. </div>
  272. </div>
  273. <Button onClick={handlePasswordChange} className="w-full">
  274. 确认修改
  275. </Button>
  276. </CardContent>
  277. </Card>
  278. </div>
  279. </TabsContent>
  280. <TabsContent value="recharge" className="p-6">
  281. <RechargeRecords userId={user.id} />
  282. </TabsContent>
  283. </Tabs>
  284. </div>
  285. </div>
  286. </div>
  287. );
  288. }