update
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
31
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 || '');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user