diff --git a/functions/api/user/info.ts b/functions/api/user/info.ts index e7de286..93723a6 100644 --- a/functions/api/user/info.ts +++ b/functions/api/user/info.ts @@ -28,6 +28,8 @@ export const onRequestGet: PagesFunction = async (context) => { } ); } + //处理avatar_url + userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`; return new Response( JSON.stringify({ diff --git a/functions/api/user/update.ts b/functions/api/user/update.ts index e82b28c..9f8447d 100644 --- a/functions/api/user/update.ts +++ b/functions/api/user/update.ts @@ -8,32 +8,48 @@ export const onRequestPost: PagesFunction = 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 = 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, diff --git a/package-lock.json b/package-lock.json index becc9b4..72549f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bb6a51a..7ab24b1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/chat/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx index 9b2206a..7e3fb55 100644 --- a/src/pages/chat/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -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 = () => {
{messages.map((message) => (
- {message.sender.name !== "我" && ( + className={`flex items-start gap-2 ${message.sender.name === userStore.userInfo.nickname ? "justify-end" : ""}`}> + {message.sender.name !== userStore.userInfo.nickname && ( {'avatar' in message.sender && message.sender.avatar ? ( @@ -591,16 +582,16 @@ const ChatUI = () => { )} )} -
+
{message.sender.name}
{ )}
- {message.sender.name === "我" && ( + {message.sender.name === userStore.userInfo.nickname && ( {'avatar' in message.sender && message.sender.avatar ? ( diff --git a/src/pages/chat/components/Sidebar.tsx b/src/pages/chat/components/Sidebar.tsx index e054bf5..887bc46 100644 --- a/src/pages/chat/components/Sidebar.tsx +++ b/src/pages/chat/components/Sidebar.tsx @@ -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 { diff --git a/src/pages/chat/components/UserSection.tsx b/src/pages/chat/components/UserSection.tsx index d14fa1c..08f6cd2 100644 --- a/src/pages/chat/components/UserSection.tsx +++ b/src/pages/chat/components/UserSection.tsx @@ -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 = ({ isOpen }) => { const [isHovering, setIsHovering] = useState(false); - const [userInfo, setUserInfo] = useState(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(null); + const userStore = useUserStore(); // 添加更新昵称的函数 const updateNickname = async () => { @@ -51,7 +35,10 @@ export const UserSection: React.FC = ({ 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 = ({ isOpen }) => { } }; + // 添加上传头像的处理函数 + const handleAvatarUpload = async (event: React.ChangeEvent) => { + 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 = ({ isOpen }) => { > {/* 头像区域 */}
-
- - {isLoading ? '...' : userInfo?.nickname?.[0] || '我'} - + +
!uploadingAvatar && fileInputRef.current?.click()} + > + {uploadingAvatar ? ( +
+
+
+ ) : userStore.userInfo?.avatar_url ? ( + avatar + ) : ( + + {userStore.userInfo?.nickname?.[0] || '我'} + + )}
{/* 头像hover效果 */} -
+
!uploadingAvatar && fileInputRef.current?.click()} + >
@@ -94,7 +148,7 @@ export const UserSection: React.FC = ({ 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 = ({ isOpen }) => { ) : ( <> - {isLoading ? '加载中...' : userInfo?.nickname || '游客用户'} + {isLoading ? '加载中...' : userStore.userInfo?.nickname || '游客用户'} = ({ isOpen }) => { )} onClick={() => { setIsEditing(true); - setNewNickname(userInfo?.nickname || ''); + setNewNickname(userStore.userInfo?.nickname || ''); }} />