This commit is contained in:
maojindao55
2025-03-31 18:54:51 +08:00
parent 0f983f0e5e
commit 4a5a224056
7 changed files with 189 additions and 96 deletions

View File

@@ -28,6 +28,8 @@ export const onRequestGet: PagesFunction<Env> = async (context) => {
}
);
}
//处理avatar_url
userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`;
return new Response(
JSON.stringify({

View File

@@ -8,32 +8,48 @@ export const onRequestPost: PagesFunction<Env> = async (context) => {
// 解析请求体
const body = await request.json();
const { nickname } = body;
const { nickname, avatar_url } = body;
// 验证昵称
if (!nickname || typeof nickname !== 'string' || nickname.length > 32) {
return new Response(
JSON.stringify({
success: false,
message: '昵称格式不正确'
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
// 构建 SQL 更新语句和参数
let sql = 'UPDATE users SET updated_at = DATETIME(\'now\')';
const params = [];
// 如果有昵称更新
if (nickname !== undefined) {
if (typeof nickname !== 'string' || nickname.length > 32) {
return new Response(
JSON.stringify({
success: false,
message: '昵称格式不正确'
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
sql += ', nickname = ?';
params.push(nickname);
}
// 更新数据库中的昵称
const db = env.bgdb;
const result = await db.prepare(`
UPDATE users
SET nickname = ?,
updated_at = DATETIME('now')
WHERE id = ?
`).bind(nickname, data.user.userId).run();
// 如果有头像更新
if (avatar_url !== undefined) {
if (typeof avatar_url !== 'string') {
return new Response(
JSON.stringify({
success: false,
message: '头像URL格式不正确'
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
sql += ', avatar_url = ?';
params.push(avatar_url);
}
// 添加 WHERE 条件
sql += ' WHERE id = ?';
params.push(data.user.userId);
// 执行更新
const result = await env.bgdb.prepare(sql).bind(...params).run();
if (!result.success) {
return new Response(
@@ -51,12 +67,13 @@ export const onRequestPost: PagesFunction<Env> = async (context) => {
}
// 获取更新后的用户信息
const userInfo = await db.prepare(`
const userInfo = await env.bgdb.prepare(`
SELECT id, phone, nickname, avatar_url, status
FROM users
WHERE id = ?
`).bind(data.user.userId).first();
//处理avatar_url
userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`;
return new Response(
JSON.stringify({
success: true,

31
package-lock.json generated
View File

@@ -40,7 +40,8 @@
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"wrangler": "^3.112.0"
"wrangler": "^3.112.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@shadcn/ui": "^0.0.4",
@@ -8158,6 +8159,34 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": {
"version": "5.0.3",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.3.tgz",
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -40,7 +40,8 @@
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"wrangler": "^3.112.0"
"wrangler": "^3.112.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@shadcn/ui": "^0.0.4",

View File

@@ -21,6 +21,7 @@ import { SharePoster } from '@/pages/chat/components/SharePoster';
import { MembersManagement } from '@/pages/chat/components/MembersManagement';
import Sidebar from './Sidebar';
import { AdBanner, AdBannerMobile } from './AdSection';
import { useUserStore } from '@/store/userStore';
// 使用本地头像数据,避免外部依赖
const getAvatarData = (name: string) => {
const colors = ['#1abc9c', '#3498db', '#9b59b6', '#f1c40f', '#e67e22'];
@@ -139,6 +140,8 @@ const KaTeXStyle = () => (
const ChatUI = () => {
const userStore = useUserStore();
//获取url参数
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id')? parseInt(urlParams.get('id')!) : 0;
@@ -202,8 +205,14 @@ const ChatUI = () => {
const allNames = groupAiCharacters.map(character => character.name);
allNames.push('user');
setAllNames(allNames);
const response1 = await request('/api/user/info');
const userInfo = await response1.json();
//设置store
userStore.setUserInfo(userInfo.data);
setUsers([
{ id: 1, name: "我" },
{ id: 1, name: userInfo.data.nickname, avatar: userInfo.data.avatar_url },
...groupAiCharacters
]);
} catch (error) {
@@ -215,7 +224,7 @@ const ChatUI = () => {
initData();
// 标记为已初始化
isInitialized.current = true;
}, []); // 依赖数组保持为空
}, [userStore]);
useEffect(() => {
scrollToBottom();
@@ -235,6 +244,16 @@ const ChatUI = () => {
};
}, []);
// 添加一个新的 useEffect 来监听 userStore.userInfo 的变化
useEffect(() => {
if (userStore.userInfo && users.length > 0) {
setUsers(prev => [
{ id: 1, name: userStore.userInfo.nickname, avatar: userStore.userInfo.avatar_url? userStore.userInfo.avatar_url : null },
...prev.slice(1) // 保留其他 AI 角色
]);
}
}, [userStore.userInfo]); // 当 userInfo 变化时更新 users
// 4. 工具函数
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -290,7 +309,7 @@ const ChatUI = () => {
// 构建历史消息数组
let messageHistory = messages.map(msg => ({
role: 'user',
content: msg.sender.name == "我" ? 'user' + msg.content : msg.sender.name + '' + msg.content,
content: msg.sender.name == userStore.userInfo.nickname ? 'user' + msg.content : msg.sender.name + '' + msg.content,
name: msg.sender.name
}));
let selectedGroupAiCharacters = groupAiCharacters;
@@ -464,34 +483,6 @@ const ChatUI = () => {
//进行跳转到?id=index
window.location.href = `?id=${index}`;
return;
/*
//跳转后,关闭当前页面
setSelectedGroupIndex(index);
const newGroup = groups[index];
setGroup(newGroup);
// 重新生成当前群组的 AI 角色,并按照 members 数组的顺序排序
const newGroupAiCharacters = generateAICharacters(newGroup.name)
.filter(character => newGroup.members.includes(character.id))
.sort((a, b) => {
return newGroup.members.indexOf(a.id) - newGroup.members.indexOf(b.id);
});
// 更新用户列表
setUsers([
{ id: 1, name: "我" },
...newGroupAiCharacters
]);
setIsGroupDiscussionMode(newGroup.isGroupDiscussionMode);
// 重置消息
setMessages([]);
// 可选:关闭侧边栏(在移动设备上)
if (window.innerWidth < 768) {
setSidebarOpen(false);
}
*/
};
return (
@@ -579,8 +570,8 @@ const ChatUI = () => {
<div className="space-y-4">
{messages.map((message) => (
<div key={message.id}
className={`flex items-start gap-2 ${message.sender.name === "我" ? "justify-end" : ""}`}>
{message.sender.name !== "我" && (
className={`flex items-start gap-2 ${message.sender.name === userStore.userInfo.nickname ? "justify-end" : ""}`}>
{message.sender.name !== userStore.userInfo.nickname && (
<Avatar>
{'avatar' in message.sender && message.sender.avatar ? (
<AvatarImage src={message.sender.avatar} className="w-10 h-10" />
@@ -591,16 +582,16 @@ const ChatUI = () => {
)}
</Avatar>
)}
<div className={message.sender.name === "我" ? "text-right" : ""}>
<div className={message.sender.name === userStore.userInfo.nickname ? "text-right" : ""}>
<div className="text-sm text-gray-500">{message.sender.name}</div>
<div className={`mt-1 p-3 rounded-lg shadow-sm chat-message ${
message.sender.name === "我" ? "bg-blue-500 text-white text-left" : "bg-white"
message.sender.name === userStore.userInfo.nickname ? "bg-blue-500 text-white text-left" : "bg-white"
}`}>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
className={`prose dark:prose-invert max-w-none ${
message.sender.name === "我" ? "text-white [&_*]:text-white" : ""
message.sender.name === userStore.userInfo.nickname ? "text-white [&_*]:text-white" : ""
}
[&_h2]:py-1
[&_h2]:m-0
@@ -638,7 +629,7 @@ const ChatUI = () => {
)}
</div>
</div>
{message.sender.name === "我" && (
{message.sender.name === userStore.userInfo.nickname && (
<Avatar>
{'avatar' in message.sender && message.sender.avatar ? (
<AvatarImage src={message.sender.avatar} className="w-10 h-10" />

View File

@@ -4,7 +4,6 @@ import { MessageSquareIcon, PlusCircleIcon, MenuIcon, PanelLeftCloseIcon } from
import { cn } from "@/lib/utils";
import GitHubButton from 'react-github-btn';
import '@fontsource/audiowide';
//import { groups } from "@/config/groups";
import { AdSection } from './AdSection';
import { UserSection } from './UserSection';
import {

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
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';
interface UserSectionProps {
isOpen: boolean;
@@ -15,30 +17,12 @@ interface UserInfo {
export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
const [isHovering, setIsHovering] = useState(false);
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [newNickname, setNewNickname] = useState('');
useEffect(() => {
const fetchUserInfo = async () => {
if (!isOpen) return;
try {
setIsLoading(true);
const response = await request('/api/user/info');
const { data } = await response.json();
console.log('data', data);
setUserInfo(data);
} catch (error) {
console.error('获取用户信息失败:', error);
} finally {
setIsLoading(false);
}
};
fetchUserInfo();
}, [isOpen]);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const userStore = useUserStore();
// 添加更新昵称的函数
const updateNickname = async () => {
@@ -51,7 +35,10 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
body: JSON.stringify({ nickname: newNickname.trim() })
});
const { data } = await response.json();
setUserInfo(data);
console.log('更新用户信息', data);
//更新用户信息
userStore.setUserInfo(data);
setIsEditing(false);
} catch (error) {
console.error('更新昵称失败:', error);
@@ -60,6 +47,45 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
}
};
// 添加上传头像的处理函数
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 (
@@ -73,13 +99,41 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
>
{/* 头像区域 */}
<div className="relative group cursor-pointer">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-rose-400 flex items-center justify-center shadow-sm">
<span className="text-base font-medium text-white">
{isLoading ? '...' : userInfo?.nickname?.[0] || '我'}
</span>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleAvatarUpload}
/>
<div
className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-rose-400 flex items-center justify-center shadow-sm overflow-hidden"
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">
{userStore.userInfo?.nickname?.[0] || '我'}
</span>
)}
</div>
{/* 头像hover效果 */}
<div className="absolute inset-0 rounded-full bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<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>
@@ -94,7 +148,7 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
value={newNickname}
onChange={(e) => setNewNickname(e.target.value)}
className="text-sm px-2 border rounded-md w-full"
placeholder={userInfo?.nickname || '输入新昵称'}
placeholder={userStore.userInfo?.nickname || '输入新昵称'}
onKeyDown={(e) => {
if (e.key === 'Enter') updateNickname();
if (e.key === 'Escape') setIsEditing(false);
@@ -121,7 +175,7 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
) : (
<>
<span className="text-sm font-semibold group-hover:text-primary transition-colors">
{isLoading ? '加载中...' : userInfo?.nickname || '游客用户'}
{isLoading ? '加载中...' : userStore.userInfo?.nickname || '游客用户'}
</span>
<Edit2Icon
className={cn(
@@ -130,7 +184,7 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
)}
onClick={() => {
setIsEditing(true);
setNewNickname(userInfo?.nickname || '');
setNewNickname(userStore.userInfo?.nickname || '');
}}
/>
</>