支持用户换头像

This commit is contained in:
maojindao55
2025-07-14 19:04:02 +08:00
parent af2eabddf0
commit b63534895a
10 changed files with 94 additions and 135 deletions

View File

@@ -29,7 +29,7 @@
2. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
- 进入Workers > Workers & Pages 页面
- 点击 "创建" 按钮
- 选择 "Pages">"Connect to Git"
- 选择 "Pages">"Connect to Git (导入现有 Git 存储库)"
3. 配置部署选项
- 选择你 fork 的仓库
@@ -141,7 +141,7 @@ APIKEY|对应角色|服务商|申请地址|
## 贡献指南
欢迎提交 Pull Request 或提出 Issue。
加官方微信:`botgroup` 拉你进微信群
加官方微信`botgroup` 拉你进微信群
当然也可以加共建QQ群交流922322461群号
## 跪谢赞助商ORZ

View File

@@ -20,5 +20,5 @@ export const onRequestPost: PagesFunction<Env> = async (context) =>{
const data1 = await response.json();
console.log('上传头像', data1);
//下发image前缀
return Response.json(data1.result);
return Response.json({data :data1.result});
}

View File

@@ -5,6 +5,8 @@
<link rel="icon" type="image/x-icon" href="/chat_bubble.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>AI机器人群聊</title>
<!-- 运行时配置文件 - 在应用加载前加载配置 -->
<script src="/config.js"></script>
<script>
var _hmt = _hmt || [];
(function() {

6
public/config.js Normal file
View File

@@ -0,0 +1,6 @@
// 运行时配置文件
// 部署时可以直接修改这个文件来改变配置,无需重新编译
window.APP_CONFIG = {
// 权限验证开关:'1' 开启,'0' 关闭
AUTH_ACCESS: '1',
};

View File

@@ -1,10 +1,24 @@
import { RouterProvider } from 'react-router-dom';
import { Toaster } from 'sonner';
import { router } from './routes';
function App() {
console.log("App rendering"); // 添加日志
return (
<RouterProvider router={router} />
<>
<RouterProvider router={router} />
<Toaster
position="top-center"
richColors
toastOptions={{
style: {
fontSize: '14px',
fontWeight: '500',
},
}}
theme="light"
/>
</>
);
}

View File

@@ -2,7 +2,11 @@ import { Navigate, useLocation } from 'react-router-dom';
export default function AuthGuard({ children }) {
//判断环境变量中的AUTH_ACCESS是否为1开启权限校验
const authAccess = import.meta.env.AUTH_ACCESS;
//const authAccess = import.meta.env.AUTH_ACCESS;
const authAccess = window.APP_CONFIG?.AUTH_ACCESS ||
import.meta.env.AUTH_ACCESS ||
'0';
console.log(authAccess, 'authAccess');
if (authAccess === '1') {
const location = useLocation();
const token = localStorage.getItem('token');

View File

@@ -2,143 +2,63 @@
@tailwind components;
@tailwind utilities;
/* ... */
@import url('https://fonts.googleapis.com/css2?family=Audiowide:wght@400&display=swap');
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 9% 11%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
--radius: 0.5rem
;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%}
.dark {
--background: 0 0% 3.9%;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%}
--chart-5: 340 75% 55%;
}
}
@layer base {
@@ -150,6 +70,18 @@
}
}
[data-sonner-toaster] [data-type="error"] {
background-color: #fef2f2 !important;
border-color: #fecaca !important;
color: #dc2626 !important;
}
[data-sonner-toaster] [data-icon] {
color: inherit !important;
}
.typing-indicator {
display: inline-block;
animation: blink 1s step-end infinite;

View File

@@ -4,7 +4,7 @@ import { Edit2Icon, LogOutIcon, CheckIcon, XIcon } from 'lucide-react';
import { request } from '@/utils/request';
import { useUserStore } from '@/store/userStore';
import { getAvatarData } from '@/utils/avatar';
import { toast } from 'sonner';
interface UserSectionProps {
isOpen: boolean;
@@ -16,6 +16,7 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
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();
@@ -31,13 +32,12 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
body: JSON.stringify({ nickname: newNickname.trim() })
});
const { data } = await response.json();
console.log('更新用户信息', data);
//更新用户信息
userStore.setUserInfo(data);
toast.success('更新昵称成功');
setIsEditing(false);
} catch (error) {
console.error('更新昵称失败:', error);
toast.error(error instanceof Error ? error.message : '更新昵称失败');
} finally {
setIsLoading(false);
}
@@ -55,7 +55,10 @@ export const UserSection: React.FC<UserSectionProps> = ({ isOpen }) => {
const response = await request('/api/user/upload', {
method: 'POST'
});
const { uploadURL, id } = await response.json();
const { data: uploadData } = await response.json();
const uploadURL = uploadData.uploadURL;
const id = uploadData.id;
// 2. 上传图片到 Cloudflare Images
const formData = new FormData();

View File

@@ -1,7 +1,9 @@
import React, { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from 'sonner';
import { request } from '@/utils/request';
interface PhoneLoginProps {
onLogin: (phone: string, code: string) => void;
}
@@ -15,13 +17,13 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
// 发送验证码
const handleSendCode = async () => {
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
alert('请输入正确的手机号');
toast.error('请输入正确的手机号');
return;
}
//setIsLoading(true);
setIsLoading(true);
try {
const response = await fetch(`/api/sendcode`, {
const response = await request(`/api/sendcode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -33,6 +35,8 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
throw new Error('发送验证码失败');
}
toast.success('验证码已发送');
// 开始倒计时
setCountdown(60);
const timer = setInterval(() => {
@@ -47,7 +51,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
} catch (error) {
console.error('发送验证码失败:', error);
alert('发送验证码失败,请重试');
toast.error('发送验证码失败,请重试');
} finally {
setIsLoading(false);
}
@@ -58,7 +62,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
e.preventDefault();
if (!phone || !code) {
alert('请输入手机号和验证码');
toast.error('请输入手机号和验证码');
return;
}
@@ -66,25 +70,17 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
try {
const response = await request(`/api/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone, code }),
});
const data = await response.json();
console.log(data);
if (!response.ok) {
throw new Error(data.message || '登录失败');
}
//执行登录成功回调
handleLoginSuccess(data.data.token);
} catch (error) {
console.error('登录失败:', error);
alert(error instanceof Error ? error.message : '登录失败,请重试');
toast.error(error instanceof Error ? error.message : '登录失败,请重试');
} finally {
setIsLoading(false);
}
@@ -98,7 +94,6 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
<span style={{fontFamily: 'Audiowide, system-ui', color: '#ff6600'}} className="text-3xl ml-2">botgroup.chat</span>
</div>
<div className="text-gray-500 mb-4 text-center">
</div>

View File

@@ -1,3 +1,4 @@
// 优先使用运行时配置,降级到构建时配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
export async function request(url: string, options: RequestInit = {}) {
const token = localStorage.getItem('token');
@@ -26,7 +27,9 @@ export async function request(url: string, options: RequestInit = {}) {
}
if (!response.ok) {
throw new Error('Request failed');
// 解析错误信息用于抛出异常
const data = await response.json();
throw new Error(data.message || 'Request failed');
}
return response;