Files
botgroup.chat/src/pages/chat/components/UserSection.tsx
maojindao55 3eeaa9c514 update it
2025-04-01 08:56:49 +08:00

220 lines
7.4 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { cn } from "@/lib/utils";
import { Edit2Icon, LogOutIcon, CheckIcon, XIcon } from 'lucide-react';
import { request } from '@/utils/request';
import { useUserStore } from '@/store/userStore';
import { getAvatarData } from '@/utils/avatar';
interface UserSectionProps {
isOpen: boolean;
}
export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
const [isHovering, setIsHovering] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [newNickname, setNewNickname] = useState('');
const [uploadingAvatar, setUploadingAvatar] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const userStore = useUserStore();
// 添加更新昵称的函数
const updateNickname = async () => {
if (!newNickname.trim()) return;
try {
setIsLoading(true);
const response = await request('/api/user/update', {
method: 'POST',
body: JSON.stringify({ nickname: newNickname.trim() })
});
const { data } = await response.json();
console.log('更新用户信息', data);
//更新用户信息
userStore.setUserInfo(data);
setIsEditing(false);
} catch (error) {
console.error('更新昵称失败:', error);
} finally {
setIsLoading(false);
}
};
// 添加上传头像的处理函数
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
setUploadingAvatar(true);
// 1. 首先从后端获取上传 URL
const response = await request('/api/user/upload', {
method: 'POST'
});
const { uploadURL, id } = await response.json();
// 2. 上传图片到 Cloudflare Images
const formData = new FormData();
formData.append('file', file); // 使用 'file' 作为字段名
await fetch(uploadURL, {
method: 'POST',
body: formData
});
// 3. 更新用户头像信息
const updateResponse = await request('/api/user/update', {
method: 'POST',
body: JSON.stringify({ avatar_url: id })
});
const { data } = await updateResponse.json();
userStore.setUserInfo(data);
} catch (error) {
console.error('上传头像失败:', error);
} finally {
setUploadingAvatar(false);
}
};
if (!isOpen) return null;
return (
<div
className={cn(
"px-3 py-3 border-t border-b border-border/40 h-20",
"flex items-center gap-3 hover:bg-accent/50 transition-colors"
)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{/* 头像区域 */}
<div className="relative group cursor-pointer">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleAvatarUpload}
/>
<div
className="w-10 h-10 rounded-full flex items-center justify-center shadow-sm overflow-hidden"
style={{ backgroundColor: getAvatarData(userStore.userInfo?.nickname || '我').backgroundColor }}
onClick={() => !uploadingAvatar && fileInputRef.current?.click()}
>
{uploadingAvatar ? (
<div className="flex items-center justify-center w-full h-full bg-black/20">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
) : userStore.userInfo?.avatar_url ? (
<img
src={`${userStore.userInfo.avatar_url}`}
alt="avatar"
className="w-full h-full object-cover"
/>
) : (
<span
className="text-base font-medium text-white"
>
{getAvatarData(userStore.userInfo?.nickname || '我').text}
</span>
)}
</div>
{/* 头像hover效果 */}
<div
className={cn(
"absolute inset-0 rounded-full bg-black/40 flex items-center justify-center transition-opacity",
uploadingAvatar ? 'opacity-0' : 'opacity-0 group-hover:opacity-100'
)}
onClick={() => !uploadingAvatar && fileInputRef.current?.click()}
>
<Edit2Icon className="w-4 h-4 text-white" />
</div>
</div>
{/* 用户信息区域 */}
<div className="flex flex-col relative flex-1">
<div className="flex items-center group cursor-pointer">
{isEditing ? (
<div className="flex flex-col">
<input
type="text"
value={newNickname}
onChange={(e) => setNewNickname(e.target.value)}
className="text-sm px-2 border rounded-md w-full"
placeholder={userStore.userInfo?.nickname || '输入新昵称'}
onKeyDown={(e) => {
if (e.key === 'Enter') updateNickname();
if (e.key === 'Escape') setIsEditing(false);
}}
autoFocus
/>
<div className="flex items-center gap-1">
<button
onClick={updateNickname}
className="p-1 hover:bg-emerald-50 rounded-md transition-colors"
title="保存"
>
<CheckIcon className="w-4 h-4 text-emerald-600 hover:text-emerald-500" />
</button>
<button
onClick={() => setIsEditing(false)}
className="p-1 hover:bg-rose-50 rounded-md transition-colors"
title="取消"
>
<XIcon className="w-4 h-4 text-rose-600 hover:text-rose-500" />
</button>
</div>
</div>
) : (
<>
<span className="text-sm font-semibold group-hover:text-primary transition-colors">
{isLoading ? '加载中...' : userStore.userInfo?.nickname || '游客用户'}
</span>
<Edit2Icon
className={cn(
"w-3 h-3 text-muted-foreground/50",
"opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
)}
onClick={() => {
setIsEditing(true);
setNewNickname(userStore.userInfo?.nickname || '');
}}
/>
</>
)}
</div>
{/* 退出登录按钮 */}
{!isEditing && (
<div
className={cn(
"flex items-center gap-0.5 mt-1 text-xs text-muted-foreground/70",
"hover:text-rose-500 transition-all duration-200 group",
"rounded-md cursor-pointer"
)}
onClick={() => {
localStorage.removeItem('token');
window.location.href = '/login';
}}
>
<LogOutIcon
className={cn(
"w-3 h-3",
"group-hover:animate-pulse"
)}
/>
<span className="group-hover:tracking-wide transition-all duration-200">
退
</span>
</div>
)}
</div>
</div>
);
};