From cc4997fb28c6924015a417331f46cff574a43c3f Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Wed, 12 Mar 2025 18:18:26 +0800 Subject: [PATCH 01/13] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5518781..4491166 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ web_modules/ .env.production.local .env.local .dev.vars +.dev.vars.my # parcel-bundler cache (https://parceljs.org/) .cache From 6c495ac631e7268e26fc648cc7081849ea89ad6c Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 25 Mar 2025 06:36:14 +0800 Subject: [PATCH 02/13] add login --- .gitignore | 2 +- functions/api/login.ts | 157 +++++++++++++++++++++++++++++++++ functions/api/sendCode.ts | 72 ++++++++++++++++ functions/middleware/auth.ts | 60 +++++++++++++ src/App.tsx | 90 ++++++++++++++++--- src/components/PhoneLogin.tsx | 158 ++++++++++++++++++++++++++++++++++ wrangler.toml | 4 + 7 files changed, 532 insertions(+), 11 deletions(-) create mode 100644 functions/api/login.ts create mode 100644 functions/api/sendCode.ts create mode 100644 functions/middleware/auth.ts create mode 100644 src/components/PhoneLogin.tsx create mode 100644 wrangler.toml diff --git a/.gitignore b/.gitignore index 0ac13c4..6406656 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* package-lock.json - +.wrangler/ # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/functions/api/login.ts b/functions/api/login.ts new file mode 100644 index 0000000..d89b6c5 --- /dev/null +++ b/functions/api/login.ts @@ -0,0 +1,157 @@ +interface Env { + bgkv: KVNamespace; + JWT_SECRET: string; +} + +export const onRequestPost: PagesFunction = async (context) => { + try { + const { request, env } = context; + + // 获取请求体 + const body = await request.json(); + const { phone, code } = body; + + // 验证手机号格式 + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + return new Response( + JSON.stringify({ + success: false, + message: '无效的手机号码' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 验证码格式检查 + if (!code || !/^\d{6}$/.test(code)) { + return new Response( + JSON.stringify({ + success: false, + message: '验证码格式错误' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 从 KV 中获取存储的验证码 + const storedCode = await env.bgkv.get(`sms:${phone}`); + + if (!storedCode || storedCode !== code) { + return new Response( + JSON.stringify({ + success: false, + message: '验证码错误或已过期' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 验证成功,生成 JWT token,传入 env + const token = await generateToken(phone, env); + + // 删除验证码 + await env.bgkv.delete(`sms:${phone}`); + + return new Response( + JSON.stringify({ + success: true, + message: '登录成功', + data: { + token, + phone + } + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; + +// 修改为 async 函数 +async function generateToken(phone: string, env: Env): Promise { + const header = { + alg: 'HS256', + typ: 'JWT' + }; + + const payload = { + phone, + exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7天过期 + iat: Math.floor(Date.now() / 1000) + }; + + // Base64Url 编码 + const encodeBase64Url = (data: object) => { + return btoa(JSON.stringify(data)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + // 生成签名 + const generateSignature = async (input: string, secret: string) => { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + encoder.encode(input) + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + const headerEncoded = encodeBase64Url(header); + const payloadEncoded = encodeBase64Url(payload); + + const signature = await generateSignature( + `${headerEncoded}.${payloadEncoded}`, + env.JWT_SECRET + ); + + return `${headerEncoded}.${payloadEncoded}.${signature}`; +} \ No newline at end of file diff --git a/functions/api/sendCode.ts b/functions/api/sendCode.ts new file mode 100644 index 0000000..c3990b1 --- /dev/null +++ b/functions/api/sendCode.ts @@ -0,0 +1,72 @@ +interface Env { + bgkv: KVNamespace; +} + +export const onRequestPost: PagesFunction = async (context) => { + try { + const { request, env } = context; + + // 获取请求体 + const body = await request.json(); + const { phone } = body; + + // 验证手机号格式 + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + return new Response( + JSON.stringify({ + success: false, + message: '无效的手机号码' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 生成6位随机验证码 + let verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); + // 使用 CF_PAGES_ENVIRONMENT 判断环境 + // 值为 'production' 或 'preview' + if (env.CF_PAGES_ENVIRONMENT !== 'production') { + verificationCode = '123456'; + } + // 将验证码存储到 KV 中,设置5分钟过期 + await env.bgkv.put(`sms:${phone}`, verificationCode, { + expirationTtl: 300 // 5分钟过期 + }); + console.log(env.CF_PAGES_ENVIRONMENT, await env.bgkv.get(`sms:${phone}`)); + + return new Response( + JSON.stringify({ + success: true, + message: '验证码发送成功', + // 注意:实际生产环境不应该返回验证码 + code: verificationCode // 仅用于测试 + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; diff --git a/functions/middleware/auth.ts b/functions/middleware/auth.ts new file mode 100644 index 0000000..05b2450 --- /dev/null +++ b/functions/middleware/auth.ts @@ -0,0 +1,60 @@ +interface Env { + JWT_SECRET: string; +} + +export async function verifyToken(token: string, env: Env) { + try { + const [headerB64, payloadB64, signature] = token.split('.'); + + // 验证签名 + const input = `${headerB64}.${payloadB64}`; + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(env.JWT_SECRET), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['verify'] + ); + + const signatureBytes = Uint8Array.from(atob( + signature.replace(/-/g, '+').replace(/_/g, '/') + ), c => c.charCodeAt(0)); + + const isValid = await crypto.subtle.verify( + 'HMAC', + key, + signatureBytes, + encoder.encode(input) + ); + + if (!isValid) { + throw new Error('Invalid signature'); + } + + // 解码 payload + const payload = JSON.parse(atob( + payloadB64.replace(/-/g, '+').replace(/_/g, '/') + )); + + // 检查过期时间 + if (payload.exp < Math.floor(Date.now() / 1000)) { + throw new Error('Token expired'); + } + + return payload; + } catch (error) { + throw new Error('Invalid token'); + } +} + +// 中间件函数 +export const authMiddleware = async (request: Request, env: Env) => { + const authHeader = request.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new Error('No token provided'); + } + + const token = authHeader.split(' ')[1]; + return await verifyToken(token, env); +}; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7154bbe..bfbf30e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,83 @@ -import ChatUI from './components/ChatUI' -import './App.css' -import Layout from './components/Layout' +import React, { useState, useEffect } from 'react'; +import ChatUI from './components/ChatUI'; +import PhoneLogin from './components/PhoneLogin'; +import './App.css'; +import Layout from './components/Layout'; + +const App: React.FC = () => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + // 在组件加载时检查登录状态 + useEffect(() => { + checkLoginStatus(); + }, []); + + // 检查登录状态 + const checkLoginStatus = async () => { + try { + // 从 localStorage 获取 token + const token = localStorage.getItem('token'); + if (!token) { + setIsLoggedIn(false); + setIsLoading(false); + return; + } + + // 验证 token 有效性 + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/verify-token`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + setIsLoggedIn(true); + } else { + // token 无效,清除存储的 token + localStorage.removeItem('token'); + setIsLoggedIn(false); + } + } catch (error) { + console.error('验证登录状态失败:', error); + setIsLoggedIn(false); + } finally { + setIsLoading(false); + } + }; + + + // 处理登出 + const handleLogout = () => { + localStorage.removeItem('token'); + setIsLoggedIn(false); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } -function App() { return ( - - - - ) -} + <> + {isLoggedIn ? ( +
+ + +
+ ) : ( + + )} + + ); +}; -export default App +export default App; diff --git a/src/components/PhoneLogin.tsx b/src/components/PhoneLogin.tsx new file mode 100644 index 0000000..61868b4 --- /dev/null +++ b/src/components/PhoneLogin.tsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +interface PhoneLoginProps { + onLogin: (phone: string, code: string) => void; +} + +const PhoneLogin: React.FC = ({ onLogin }) => { + const [phone, setPhone] = useState(''); + const [code, setCode] = useState(''); + const [countdown, setCountdown] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + // 发送验证码 + const handleSendCode = async () => { + if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { + alert('请输入正确的手机号'); + return; + } + + //setIsLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/sendcode`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ phone }), + }); + + if (!response.ok) { + throw new Error('发送验证码失败'); + } + + // 开始倒计时 + setCountdown(60); + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + } catch (error) { + console.error('发送验证码失败:', error); + alert('发送验证码失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + // 提交登录 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!phone || !code) { + alert('请输入手机号和验证码'); + return; + } + + setIsLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/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 || '登录失败'); + } + + // 保存 token 到 localStorage + localStorage.setItem('token', data.data.token); + localStorage.setItem('phone', data.data.phone); + + } catch (error) { + console.error('登录失败:', error); + alert(error instanceof Error ? error.message : '登录失败,请重试'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+ botgroup.chat +
+ + +
+ 仅支持中国大陆手机号登录 +
+ +
+
+
+ +86 + setPhone(e.target.value)} + maxLength={11} + className="border-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none p-0" + /> +
+
+ +
+
+ setCode(e.target.value)} + maxLength={6} + className="border rounded-lg p-3 h-[46px] focus:border-[#ff6600] focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none" + /> + +
+
+ + +
+
+
+ ); +}; + +export default PhoneLogin; \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..9d9d2a5 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,4 @@ +[[kv_namespaces]] +binding = "bgkv" +id = "cbd11575c3504e6bb043c1c250d2f7ed" +preview_id = "cbd11575c3504e6bb043c1c250d2f7ed" # 本地开发环境使用 \ No newline at end of file From 487621a645eec96da2bacfdf9e9a7612fe1538c1 Mon Sep 17 00:00:00 2001 From: hobby Date: Tue, 25 Mar 2025 08:25:40 +0800 Subject: [PATCH 03/13] update loyout --- .../auth.ts => api/_middleware.js} | 42 ++++++--- package-lock.json | 62 +++++++++++++ package.json | 1 + src/App.tsx | 86 ++----------------- src/components/AuthGuard.jsx | 16 ++++ src/layouts/BasicLayout.tsx | 22 +++++ src/{ => pages/chat}/components/AdSection.tsx | 2 +- src/{ => pages/chat}/components/ChatUI.tsx | 4 +- src/{ => pages/chat}/components/Header.tsx | 0 src/{ => pages/chat}/components/Layout.tsx | 0 .../chat}/components/MembersManagement.tsx | 0 .../chat}/components/SharePoster.tsx | 0 src/{ => pages/chat}/components/Sidebar.tsx | 0 src/pages/chat/index.tsx | 7 ++ .../login/comonents}/PhoneLogin.tsx | 5 +- src/pages/login/index.jsx | 17 ++++ src/routes.tsx | 26 ++++++ src/utils/request.ts | 36 ++++++++ warngler.toml | 3 - 19 files changed, 229 insertions(+), 100 deletions(-) rename functions/{middleware/auth.ts => api/_middleware.js} (55%) create mode 100644 src/components/AuthGuard.jsx create mode 100644 src/layouts/BasicLayout.tsx rename src/{ => pages/chat}/components/AdSection.tsx (99%) rename src/{ => pages/chat}/components/ChatUI.tsx (99%) rename src/{ => pages/chat}/components/Header.tsx (100%) rename src/{ => pages/chat}/components/Layout.tsx (100%) rename src/{ => pages/chat}/components/MembersManagement.tsx (100%) rename src/{ => pages/chat}/components/SharePoster.tsx (100%) rename src/{ => pages/chat}/components/Sidebar.tsx (100%) create mode 100644 src/pages/chat/index.tsx rename src/{components => pages/login/comonents}/PhoneLogin.tsx (96%) create mode 100644 src/pages/login/index.jsx create mode 100644 src/routes.tsx create mode 100644 src/utils/request.ts delete mode 100644 warngler.toml diff --git a/functions/middleware/auth.ts b/functions/api/_middleware.js similarity index 55% rename from functions/middleware/auth.ts rename to functions/api/_middleware.js index 05b2450..de21585 100644 --- a/functions/middleware/auth.ts +++ b/functions/api/_middleware.js @@ -1,8 +1,4 @@ -interface Env { - JWT_SECRET: string; -} - -export async function verifyToken(token: string, env: Env) { +async function verifyToken(token, env) { try { const [headerB64, payloadB64, signature] = token.split('.'); @@ -49,12 +45,32 @@ export async function verifyToken(token: string, env: Env) { } // 中间件函数 -export const authMiddleware = async (request: Request, env: Env) => { - const authHeader = request.headers.get('Authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new Error('No token provided'); +export async function onRequest(context) { + try { + const request = context.request; + const env = context.env; + //跳过登录页面 + if (request.url.includes('/login') || request.url.includes('/sendcode') || request.url.includes('/login')) { + return await context.next(); + } + const authHeader = request.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new Error('No token provided'); + } + + const token = authHeader.split(' ')[1]; + const payload = await verifyToken(token, env); + + // 将用户信息添加到上下文中 + context.user = payload; + + return await context.next(); + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + status: 401, + headers: { + 'Content-Type': 'application/json' + } + }); } - - const token = authHeader.split(' ')[1]; - return await verifyToken(token, env); -}; \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index c558cb6..cfea23a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", "react-markdown": "^9.0.3", + "react-router-dom": "^7.4.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -2405,6 +2406,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", @@ -6060,6 +6066,52 @@ } } }, + "node_modules/react-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.0.tgz", + "integrity": "sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.0.tgz", + "integrity": "sha512-VlksBPf3n2bijPvnA7nkTsXxMAKOj+bWp4R9c3i+bnwlSOFAGOkJkKhzy/OsRkWaBMICqcAl1JDzh9ZSOze9CA==", + "dependencies": { + "react-router": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -6449,6 +6501,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", @@ -6918,6 +6975,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", diff --git a/package.json b/package.json index 8fa2622..27240bc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-dom": "^18.2.0", "react-github-btn": "^1.4.0", "react-markdown": "^9.0.3", + "react-router-dom": "^7.4.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", diff --git a/src/App.tsx b/src/App.tsx index bfbf30e..957ceec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,83 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import ChatUI from './components/ChatUI'; -import PhoneLogin from './components/PhoneLogin'; -import './App.css'; -import Layout from './components/Layout'; - -const App: React.FC = () => { - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - // 在组件加载时检查登录状态 - useEffect(() => { - checkLoginStatus(); - }, []); - - // 检查登录状态 - const checkLoginStatus = async () => { - try { - // 从 localStorage 获取 token - const token = localStorage.getItem('token'); - if (!token) { - setIsLoggedIn(false); - setIsLoading(false); - return; - } - - // 验证 token 有效性 - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/verify-token`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (response.ok) { - setIsLoggedIn(true); - } else { - // token 无效,清除存储的 token - localStorage.removeItem('token'); - setIsLoggedIn(false); - } - } catch (error) { - console.error('验证登录状态失败:', error); - setIsLoggedIn(false); - } finally { - setIsLoading(false); - } - }; - - - // 处理登出 - const handleLogout = () => { - localStorage.removeItem('token'); - setIsLoggedIn(false); - }; - - if (isLoading) { - return ( -
-
-
- ); - } +import { RouterProvider } from 'react-router-dom'; +import { router } from './routes'; +function App() { + console.log("App rendering"); // 添加日志 return ( - <> - {isLoggedIn ? ( -
- - -
- ) : ( - - )} - + ); -}; +} -export default App; +export default App; \ No newline at end of file diff --git a/src/components/AuthGuard.jsx b/src/components/AuthGuard.jsx new file mode 100644 index 0000000..738133e --- /dev/null +++ b/src/components/AuthGuard.jsx @@ -0,0 +1,16 @@ +import { Navigate, useLocation } from 'react-router-dom'; + +export default function AuthGuard({ children }) { + const location = useLocation(); + const token = localStorage.getItem('token'); + + if (!token && location.pathname !== '/login') { + return ; + } + + if (token && location.pathname === '/login') { + return ; + } + + return children; +} \ No newline at end of file diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx new file mode 100644 index 0000000..12e32c0 --- /dev/null +++ b/src/layouts/BasicLayout.tsx @@ -0,0 +1,22 @@ +import { Outlet, useNavigate } from 'react-router-dom'; + +export default function BasicLayout() { + const navigate = useNavigate(); + + const handleLogout = () => { + localStorage.removeItem('token'); + navigate('/login'); + }; + + return ( +
+
+
AI Chat
+ +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/AdSection.tsx b/src/pages/chat/components/AdSection.tsx similarity index 99% rename from src/components/AdSection.tsx rename to src/pages/chat/components/AdSection.tsx index a36ce32..1bff3b7 100644 --- a/src/components/AdSection.tsx +++ b/src/pages/chat/components/AdSection.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { cn } from '../lib/utils'; +import { cn } from '@/lib/utils'; import { Popover, PopoverContent, diff --git a/src/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx similarity index 99% rename from src/components/ChatUI.tsx rename to src/pages/chat/components/ChatUI.tsx index 5e21286..89541d8 100644 --- a/src/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -17,8 +17,8 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' -import { SharePoster } from '@/components/SharePoster'; -import { MembersManagement } from '@/components/MembersManagement'; +import { SharePoster } from '@/pages/chat/components/SharePoster'; +import { MembersManagement } from '@/pages/chat/components/MembersManagement'; import Sidebar from './Sidebar'; import { AdBanner, AdBannerMobile } from './AdSection'; // 使用本地头像数据,避免外部依赖 diff --git a/src/components/Header.tsx b/src/pages/chat/components/Header.tsx similarity index 100% rename from src/components/Header.tsx rename to src/pages/chat/components/Header.tsx diff --git a/src/components/Layout.tsx b/src/pages/chat/components/Layout.tsx similarity index 100% rename from src/components/Layout.tsx rename to src/pages/chat/components/Layout.tsx diff --git a/src/components/MembersManagement.tsx b/src/pages/chat/components/MembersManagement.tsx similarity index 100% rename from src/components/MembersManagement.tsx rename to src/pages/chat/components/MembersManagement.tsx diff --git a/src/components/SharePoster.tsx b/src/pages/chat/components/SharePoster.tsx similarity index 100% rename from src/components/SharePoster.tsx rename to src/pages/chat/components/SharePoster.tsx diff --git a/src/components/Sidebar.tsx b/src/pages/chat/components/Sidebar.tsx similarity index 100% rename from src/components/Sidebar.tsx rename to src/pages/chat/components/Sidebar.tsx diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx new file mode 100644 index 0000000..e7d9216 --- /dev/null +++ b/src/pages/chat/index.tsx @@ -0,0 +1,7 @@ +import ChatUI from './components/ChatUI'; + +export default function Chat() { + return ( + + ); +} diff --git a/src/components/PhoneLogin.tsx b/src/pages/login/comonents/PhoneLogin.tsx similarity index 96% rename from src/components/PhoneLogin.tsx rename to src/pages/login/comonents/PhoneLogin.tsx index 61868b4..44c49e4 100644 --- a/src/components/PhoneLogin.tsx +++ b/src/pages/login/comonents/PhoneLogin.tsx @@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"; interface PhoneLoginProps { onLogin: (phone: string, code: string) => void; } +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const PhoneLogin: React.FC = ({ onLogin }) => { const [phone, setPhone] = useState(''); @@ -21,7 +22,7 @@ const PhoneLogin: React.FC = ({ onLogin }) => { //setIsLoading(true); try { - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/sendcode`, { + const response = await fetch(`${API_BASE_URL}/api/sendcode`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -64,7 +65,7 @@ const PhoneLogin: React.FC = ({ onLogin }) => { setIsLoading(true); try { - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/login`, { + const response = await fetch(`${API_BASE_URL}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/pages/login/index.jsx b/src/pages/login/index.jsx new file mode 100644 index 0000000..b5aef5d --- /dev/null +++ b/src/pages/login/index.jsx @@ -0,0 +1,17 @@ +import { useNavigate } from 'react-router-dom'; +import PhoneLogin from './comonents/PhoneLogin'; + +export default function Login() { + const navigate = useNavigate(); + + const handleLoginSuccess = (token) => { + localStorage.setItem('token', token); + navigate('/'); + }; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx new file mode 100644 index 0000000..162afa5 --- /dev/null +++ b/src/routes.tsx @@ -0,0 +1,26 @@ +import { createBrowserRouter } from 'react-router-dom'; +import Login from './pages/login'; +import Chat from './pages/chat'; +import BasicLayout from './layouts/BasicLayout'; +import AuthGuard from './components/AuthGuard'; + +export const router = createBrowserRouter([ + { + path: '/login', + element: , + }, + { + path: '/', + element: ( + + + + ), + children: [ + { + path: '', + element: , + }, + ], + }, +]); \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..b309ad7 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,36 @@ +export async function request(url: string, options: RequestInit = {}) { + const token = localStorage.getItem('token'); + + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const response = await fetch(url, { + ...options, + headers, + }); + + // 如果返回 401,清除 token 并跳转到登录页 + if (response.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + throw new Error('Unauthorized'); + } + + if (!response.ok) { + throw new Error('Request failed'); + } + + return response.json(); + } catch (error) { + // 如果是网络错误或其他错误,也可以处理 + console.error('Request error:', error); + throw error; + } +} \ No newline at end of file diff --git a/warngler.toml b/warngler.toml deleted file mode 100644 index 829ef71..0000000 --- a/warngler.toml +++ /dev/null @@ -1,3 +0,0 @@ -[observability] -enabled = true -head_sampling_rate = 1 # optional. default = 1. \ No newline at end of file From 051a7da4c614c174acf8f09d36506daff34d12de Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 25 Mar 2025 19:58:39 +0800 Subject: [PATCH 04/13] add sms --- functions/api/_middleware.js | 9 + functions/api/login.ts | 42 ++++- functions/api/sendCode.ts | 92 ++++++---- functions/utils/sms.ts | 125 +++++++++++++ package-lock.json | 220 +++++++++++++++++++++++ package.json | 2 + src/components/AuthGuard.jsx | 19 +- src/pages/chat/components/ChatUI.tsx | 10 +- src/pages/login/comonents/PhoneLogin.tsx | 14 +- src/pages/login/index.jsx | 2 +- src/utils/request.ts | 6 +- wrangler.toml | 7 +- 12 files changed, 482 insertions(+), 66 deletions(-) create mode 100644 functions/utils/sms.ts diff --git a/functions/api/_middleware.js b/functions/api/_middleware.js index de21585..fe638db 100644 --- a/functions/api/_middleware.js +++ b/functions/api/_middleware.js @@ -47,6 +47,14 @@ async function verifyToken(token, env) { // 中间件函数 export async function onRequest(context) { try { + //获取环境变量中的AUTH_ACCESS + const authAccess = context.env.AUTH_ACCESS; + console.log('authAccess', authAccess); + //如果AUTH_ACCESS为0则跳过权限校验 + if (!authAccess || authAccess === '0') { + console.log('跳过权限校验'); + return await context.next(); + } const request = context.request; const env = context.env; //跳过登录页面 @@ -66,6 +74,7 @@ export async function onRequest(context) { return await context.next(); } catch (error) { + console.error(error.message, context.request.url); return new Response(JSON.stringify({ error: error.message }), { status: 401, headers: { diff --git a/functions/api/login.ts b/functions/api/login.ts index d89b6c5..b97c933 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -1,6 +1,7 @@ interface Env { bgkv: KVNamespace; JWT_SECRET: string; + DB: D1Database; } export const onRequestPost: PagesFunction = async (context) => { @@ -61,19 +62,56 @@ export const onRequestPost: PagesFunction = async (context) => { ); } - // 验证成功,生成 JWT token,传入 env + // 验证成功后,处理用户数据 + const db = env.DB; // 假设你的 D1 数据库实例名为 DB + + // 查询用户是否存在 + const existingUser = await db.prepare( + "SELECT id, phone, nickname FROM users WHERE phone = ?" + ).bind(phone).first(); + + let userId; + if (!existingUser) { + // 用户不存在,创建新用户 + const result = await db.prepare(` + INSERT INTO users (phone, nickname, status, created_at, updated_at, last_login_at) + VALUES (?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).bind(phone, `用户${phone.substring(7)}`).run(); + + userId = result.lastRowId; + } else { + // 用户存在,更新登录时间 + await db.prepare(` + UPDATE users + SET last_login_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE phone = ? + `).bind(phone).run(); + + userId = existingUser.id; + } + + // 获取完整的用户信息 + const userInfo = await db.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE phone = ? + `).bind(phone).first(); + + // 生成 token const token = await generateToken(phone, env); // 删除验证码 await env.bgkv.delete(`sms:${phone}`); + // 返回用户信息和token return new Response( JSON.stringify({ success: true, message: '登录成功', data: { token, - phone + user: userInfo } }), { diff --git a/functions/api/sendCode.ts b/functions/api/sendCode.ts index c3990b1..3710215 100644 --- a/functions/api/sendCode.ts +++ b/functions/api/sendCode.ts @@ -1,71 +1,85 @@ +import { sendSMS } from '../utils/sms'; + interface Env { - bgkv: KVNamespace; + ALIYUN_ACCESS_KEY_ID: string; + ALIYUN_ACCESS_KEY_SECRET: string; + ALIYUN_SMS_SIGN_NAME: string; + ALIYUN_SMS_TEMPLATE_CODE: string; + bgkv: KVNamespace; } export const onRequestPost: PagesFunction = async (context) => { + const { request, env } = context; + try { - const { request, env } = context; + const { phone } = await request.json(); - // 获取请求体 - const body = await request.json(); - const { phone } = body; - - // 验证手机号格式 if (!phone || !/^1[3-9]\d{9}$/.test(phone)) { return new Response( JSON.stringify({ - success: false, - message: '无效的手机号码' - }), + success: false, + message: '请输入正确的手机号' + }), { status: 400, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, } ); } - // 生成6位随机验证码 - let verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); - // 使用 CF_PAGES_ENVIRONMENT 判断环境 - // 值为 'production' 或 'preview' - if (env.CF_PAGES_ENVIRONMENT !== 'production') { - verificationCode = '123456'; + // 开发环境使用固定验证码 + const verificationCode = env.CF_PAGES_ENVIRONMENT === 'preview' + ? '123456' + : Math.random().toString().slice(-6); + + if (env.CF_PAGES_ENVIRONMENT !== 'preview') { + try { + await sendSMS(phone, verificationCode, { + accessKeyId: env.ALIYUN_ACCESS_KEY_ID, + accessKeySecret: env.ALIYUN_ACCESS_KEY_SECRET, + signName: env.ALIYUN_SMS_SIGN_NAME, + templateCode: env.ALIYUN_SMS_TEMPLATE_CODE + }); + } catch (error) { + console.error('SMS Error:', error); + return new Response( + JSON.stringify({ + success: false, + message: error instanceof Error ? error.message : '发送验证码失败,请重试' + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } } - // 将验证码存储到 KV 中,设置5分钟过期 + + // 存储验证码到 KV,设置 5 分钟过期 await env.bgkv.put(`sms:${phone}`, verificationCode, { - expirationTtl: 300 // 5分钟过期 + expirationTtl: 5 * 60 // 5分钟 }); - console.log(env.CF_PAGES_ENVIRONMENT, await env.bgkv.get(`sms:${phone}`)); return new Response( JSON.stringify({ - success: true, - message: '验证码发送成功', - // 注意:实际生产环境不应该返回验证码 - code: verificationCode // 仅用于测试 - }), + success: true, + message: '验证码发送成功' + }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, } ); } catch (error) { - console.error(error); + console.error('Request Error:', error); return new Response( JSON.stringify({ - success: false, - message: '服务器错误' - }), + success: false, + message: '请求格式错误' + }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, + status: 400, + headers: { 'Content-Type': 'application/json' }, } ); } diff --git a/functions/utils/sms.ts b/functions/utils/sms.ts new file mode 100644 index 0000000..9e4e8a9 --- /dev/null +++ b/functions/utils/sms.ts @@ -0,0 +1,125 @@ +interface AliyunSMSConfig { + accessKeyId: string; + accessKeySecret: string; + signName: string; + templateCode: string; +} + +// 辅助函数:生成随机字符串 +function generateNonce(length: number): string { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +// 辅助函数:计算 HMAC-SHA256(V3 使用 SHA256) +async function calculateHmacSha256(message: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(message); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + messageData + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} + +export async function sendSMS(phone: string, code: string, config: AliyunSMSConfig) { + const API_URL = 'https://dysmsapi.aliyuncs.com/'; + const API_VERSION = '2017-05-25'; + + // 准备请求参数 + const params = { + AccessKeyId: config.accessKeyId, + Action: 'SendSms', + Format: 'JSON', // 明确指定返回 JSON 格式 + PhoneNumbers: phone, + SignName: config.signName, + TemplateCode: config.templateCode, + TemplateParam: JSON.stringify({ code }), + Version: API_VERSION, + SignatureMethod: 'HMAC-SHA1', + SignatureVersion: '1.0', + SignatureNonce: generateNonce(16), + Timestamp: new Date().toISOString() + }; + + // 参数排序 + const sortedParams = Object.keys(params) + .sort() + .reduce((acc, key) => ({ + ...acc, + [key]: params[key] + }), {}); + + // 构建签名字符串 + const canonicalizedQueryString = Object.entries(sortedParams) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`) + .join('&'); + + const stringToSign = `GET&${encodeURIComponent('/')}&${encodeURIComponent(canonicalizedQueryString)}`; + + // 计算签名 + const signature = await calculateHmacSha1(stringToSign, `${config.accessKeySecret}&`); + + // 构建最终的 URL + const finalUrl = `${API_URL}?${canonicalizedQueryString}&Signature=${encodeURIComponent(signature)}`; + + // 发送请求 + const response = await fetch(finalUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + const responseText = await response.text(); + let responseData; + + try { + responseData = JSON.parse(responseText); + } catch (e) { + console.error('Response:', responseText); + throw new Error('Invalid response format from SMS service'); + } + //console.log(responseData, finalUrl) + if (!response.ok || responseData.Code !== 'OK') { + throw new Error(responseData.Message || 'SMS send failed'); + } + + return responseData; +} + +// 辅助函数:计算 HMAC-SHA1 +async function calculateHmacSha1(message: string, secret: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const messageData = encoder.encode(message); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-1' }, + false, + ['sign'] + ); + + const signature = await crypto.subtle.sign( + 'HMAC', + key, + messageData + ); + + return btoa(String.fromCharCode(...new Uint8Array(signature))); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cfea23a..becc9b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "chat-ui", "version": "1.0.0", "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/dysmsapi20170525": "^3.1.1", "@fontsource/audiowide": "^5.1.1", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", @@ -55,6 +57,114 @@ "vite": "^5.0.0" } }, + "node_modules/@alicloud/credentials": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.2.tgz", + "integrity": "sha512-UbqUYlwOWKNxOemXM545HzQyCaChhyrne9cab4f67EqAkgrTjeMiTA7QK6sHtmcmJYowYQxxXoKPSe5GZstvbA==", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@alicloud/dysmsapi20170525/-/dysmsapi20170525-3.1.1.tgz", + "integrity": "sha512-UvrQo9p1b7A/JH209jPFLdtuYGywMrn4vWl48LwGxgZOH21i/LQXJKGhIUkeN9/CbdWsW709lkJ9kWvzmQZ5gQ==", + "dependencies": { + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/openapi-client": "^0.4.12", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.9" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.13", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.13.tgz", + "integrity": "sha512-APi5NwzY6IMxGxM3bTehCzfFVsvY11ljbERhuO2Q4Lfemo4yF23ph38nXLmpv92exDmc4Xe93qKDhnVYMVEdiQ==", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "1.4.9", + "@alicloud/tea-xml": "0.0.3" + } + }, + "node_modules/@alicloud/openapi-client/node_modules/@alicloud/tea-util": { + "version": "1.4.9", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz", + "integrity": "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.2.tgz", + "integrity": "sha512-EC2JvxdcOgMlBAEG0+joOh2IB1um8CPz9EdYuRfTfd1uP8Yc9D8QRUWVGjP6scnj6fWSOaHFlit9H6PrJSyFow==", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-typescript/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.10", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.10.tgz", + "integrity": "sha512-VEsXWP2dlJLvsY2THj+sH++zwxQRz3Y5BQ8EkfnFems36RkngQKYOLsoto5nR6ej1Gf6I+0IOgBXrkRdpNCQ1g==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "@darabonba/typescript": "^1.0.0", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", + "integrity": "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.6.0" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -440,6 +550,19 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@darabonba/typescript": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.3.tgz", + "integrity": "sha512-/y2y6wf5TsxD7pCPIm0OvTC+5qV0Tk7HQYxwpIuWRLXQLB0CRDvr6qk4bR6rTLO/JglJa8z2uCGZsaLYpQNqFQ==", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "xml2js": "^0.6.2" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -2539,6 +2662,14 @@ "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3994,6 +4125,28 @@ "node": ">=8.0.0" } }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.17.27", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/httpx/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -4057,6 +4210,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -4315,6 +4473,14 @@ "node": ">= 12" } }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "dependencies": { + "@types/node": "^22.5.4" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4340,6 +4506,11 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmmirror.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz", @@ -5392,6 +5563,25 @@ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6484,6 +6674,11 @@ } ] }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -6606,6 +6801,11 @@ "node": ">=8" } }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==" + }, "node_modules/sonner": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/sonner/-/sonner-2.0.0.tgz", @@ -7902,6 +8102,26 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 27240bc..bb6a51a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "preview": "vite preview" }, "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/dysmsapi20170525": "^3.1.1", "@fontsource/audiowide": "^5.1.1", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", diff --git a/src/components/AuthGuard.jsx b/src/components/AuthGuard.jsx index 738133e..c85aeaf 100644 --- a/src/components/AuthGuard.jsx +++ b/src/components/AuthGuard.jsx @@ -1,16 +1,21 @@ import { Navigate, useLocation } from 'react-router-dom'; export default function AuthGuard({ children }) { - const location = useLocation(); - const token = localStorage.getItem('token'); + //判断环境变量中的AUTH_ACCESS是否为1开启权限校验 + const authAccess = import.meta.env.AUTH_ACCESS; + if (authAccess === '1') { + const location = useLocation(); + const token = localStorage.getItem('token'); + + if (!token && location.pathname !== '/login') { + return ; + } - if (!token && location.pathname !== '/login') { - return ; + if (token && location.pathname === '/login') { + return ; + } } - if (token && location.pathname === '/login') { - return ; - } return children; } \ No newline at end of file diff --git a/src/pages/chat/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx index 89541d8..9b2206a 100644 --- a/src/pages/chat/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; - +import { request } from '@/utils/request'; import { Tooltip, TooltipContent, @@ -137,8 +137,6 @@ const KaTeXStyle = () => ( `}} /> ); -// Vite环境变量访问方式 -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const ChatUI = () => { //获取url参数 @@ -182,7 +180,7 @@ const ChatUI = () => { const initData = async () => { try { - const response = await fetch(`${API_BASE_URL}/api/init`); + const response = await request(`/api/init`); if (!response.ok) { throw new Error('初始化数据失败'); } @@ -297,7 +295,7 @@ const ChatUI = () => { })); let selectedGroupAiCharacters = groupAiCharacters; if (!isGroupDiscussionMode) { - const shedulerResponse = await fetch(`${API_BASE_URL}/api/scheduler`, { + const shedulerResponse = await request(`/api/scheduler`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -325,7 +323,7 @@ const ChatUI = () => { setMessages(prev => [...prev, aiMessage]); try { - const response = await fetch(`${API_BASE_URL}/api/chat`, { + const response = await request(`/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/pages/login/comonents/PhoneLogin.tsx b/src/pages/login/comonents/PhoneLogin.tsx index 44c49e4..d599124 100644 --- a/src/pages/login/comonents/PhoneLogin.tsx +++ b/src/pages/login/comonents/PhoneLogin.tsx @@ -1,13 +1,12 @@ import React, { useState } from 'react'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; - +import { request } from '@/utils/request'; interface PhoneLoginProps { onLogin: (phone: string, code: string) => void; } -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; -const PhoneLogin: React.FC = ({ onLogin }) => { +const PhoneLogin: React.FC = ({ handleLoginSuccess }) => { const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [countdown, setCountdown] = useState(0); @@ -22,7 +21,7 @@ const PhoneLogin: React.FC = ({ onLogin }) => { //setIsLoading(true); try { - const response = await fetch(`${API_BASE_URL}/api/sendcode`, { + const response = await fetch(`/api/sendcode`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -65,7 +64,7 @@ const PhoneLogin: React.FC = ({ onLogin }) => { setIsLoading(true); try { - const response = await fetch(`${API_BASE_URL}/api/login`, { + const response = await request(`/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -80,9 +79,8 @@ const PhoneLogin: React.FC = ({ onLogin }) => { throw new Error(data.message || '登录失败'); } - // 保存 token 到 localStorage - localStorage.setItem('token', data.data.token); - localStorage.setItem('phone', data.data.phone); + //执行登录成功回调 + handleLoginSuccess(data.data.token); } catch (error) { console.error('登录失败:', error); diff --git a/src/pages/login/index.jsx b/src/pages/login/index.jsx index b5aef5d..35f7b8b 100644 --- a/src/pages/login/index.jsx +++ b/src/pages/login/index.jsx @@ -11,7 +11,7 @@ export default function Login() { return (
- +
); } \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts index b309ad7..90d542b 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -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'); @@ -10,8 +11,9 @@ export async function request(url: string, options: RequestInit = {}) { headers['Authorization'] = `Bearer ${token}`; } + try { - const response = await fetch(url, { + const response = await fetch(`${API_BASE_URL}${url}`, { ...options, headers, }); @@ -27,7 +29,7 @@ export async function request(url: string, options: RequestInit = {}) { throw new Error('Request failed'); } - return response.json(); + return response; } catch (error) { // 如果是网络错误或其他错误,也可以处理 console.error('Request error:', error); diff --git a/wrangler.toml b/wrangler.toml index 9d9d2a5..4812b4f 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,4 +1,9 @@ [[kv_namespaces]] binding = "bgkv" id = "cbd11575c3504e6bb043c1c250d2f7ed" -preview_id = "cbd11575c3504e6bb043c1c250d2f7ed" # 本地开发环境使用 \ No newline at end of file +preview_id = "cbd11575c3504e6bb043c1c250d2f7ed" # 本地开发环境使用 + +[[d1_databases]] +binding = "bgdb" # available in your Worker on env.DB +database_name = "friends" +database_id = "f68e9fa5-4aea-45db-9516-2d7052e936fa" \ No newline at end of file From 04d6a2f9af11ac295b1a08c402851ea63ff0f008 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Wed, 26 Mar 2025 20:00:31 +0800 Subject: [PATCH 05/13] add user db --- devrun.sh | 1 + functions/api/_middleware.js | 2 +- functions/api/login.ts | 7 +-- functions/api/sendCode.ts | 5 +- functions/api/test-db.ts | 32 ++++++++++ init-db.sh | 41 +++++++++++++ src/pages/chat/components/Sidebar.tsx | 4 ++ src/pages/chat/components/UserSection.tsx | 73 +++++++++++++++++++++++ 8 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 functions/api/test-db.ts create mode 100644 init-db.sh create mode 100644 src/pages/chat/components/UserSection.tsx diff --git a/devrun.sh b/devrun.sh index 40cad01..c573936 100644 --- a/devrun.sh +++ b/devrun.sh @@ -1 +1,2 @@ +# 启动开发服务器 wrangler pages dev -- npm run dev \ No newline at end of file diff --git a/functions/api/_middleware.js b/functions/api/_middleware.js index fe638db..cfa42fb 100644 --- a/functions/api/_middleware.js +++ b/functions/api/_middleware.js @@ -58,7 +58,7 @@ export async function onRequest(context) { const request = context.request; const env = context.env; //跳过登录页面 - if (request.url.includes('/login') || request.url.includes('/sendcode') || request.url.includes('/login')) { + if (request.url.includes('/login') || request.url.includes('/sendcode') || request.url.includes('/login') || request.url.includes('/test-db')) { return await context.next(); } const authHeader = request.headers.get('Authorization'); diff --git a/functions/api/login.ts b/functions/api/login.ts index b97c933..288d049 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -1,7 +1,7 @@ interface Env { bgkv: KVNamespace; JWT_SECRET: string; - DB: D1Database; + bgdb: D1Database; } export const onRequestPost: PagesFunction = async (context) => { @@ -46,7 +46,6 @@ export const onRequestPost: PagesFunction = async (context) => { // 从 KV 中获取存储的验证码 const storedCode = await env.bgkv.get(`sms:${phone}`); - if (!storedCode || storedCode !== code) { return new Response( JSON.stringify({ @@ -63,13 +62,13 @@ export const onRequestPost: PagesFunction = async (context) => { } // 验证成功后,处理用户数据 - const db = env.DB; // 假设你的 D1 数据库实例名为 DB - + const db = env.bgdb; // 假设你的 D1 数据库实例名为 DB // 查询用户是否存在 const existingUser = await db.prepare( "SELECT id, phone, nickname FROM users WHERE phone = ?" ).bind(phone).first(); + console.log('existingUser', existingUser); let userId; if (!existingUser) { // 用户不存在,创建新用户 diff --git a/functions/api/sendCode.ts b/functions/api/sendCode.ts index 3710215..2cca299 100644 --- a/functions/api/sendCode.ts +++ b/functions/api/sendCode.ts @@ -28,11 +28,10 @@ export const onRequestPost: PagesFunction = async (context) => { } // 开发环境使用固定验证码 - const verificationCode = env.CF_PAGES_ENVIRONMENT === 'preview' + const verificationCode = env.CF_PAGES_ENVIRONMENT !== 'production' ? '123456' : Math.random().toString().slice(-6); - - if (env.CF_PAGES_ENVIRONMENT !== 'preview') { + if (env.CF_PAGES_ENVIRONMENT === 'production') { try { await sendSMS(phone, verificationCode, { accessKeyId: env.ALIYUN_ACCESS_KEY_ID, diff --git a/functions/api/test-db.ts b/functions/api/test-db.ts new file mode 100644 index 0000000..e816d3f --- /dev/null +++ b/functions/api/test-db.ts @@ -0,0 +1,32 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestGet: PagesFunction = async (context) => { + try { + const { env } = context; + const db = env.bgdb; + + // 测试数据库连接 + const result = await db.prepare( + "SELECT * FROM users" + ).all(); + + return new Response(JSON.stringify({ + success: true, + tables: result, + env: Object.keys(env) + }), { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ + success: false, + error: error.message, + stack: error.stack + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; \ No newline at end of file diff --git a/init-db.sh b/init-db.sh new file mode 100644 index 0000000..ef79f74 --- /dev/null +++ b/init-db.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# 文件名: init-db.sh + +rm -rf .local-db + +# 重新创建目录 +mkdir -p .local-db + +# 删除旧数据库(如果存在) +wrangler d1 delete friends --local + +# 创建新数据库 +wrangler d1 create friends --local --persist-to=.local-db + +# 创建表 +wrangler d1 execute friends --local --persist-to=.local-db --command=" +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + phone VARCHAR(11) NOT NULL UNIQUE, + nickname VARCHAR(50), + avatar_url TEXT, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +);" + +# 插入测试数据 +wrangler d1 execute friends --local --persist-to=.local-db --command=" +INSERT OR IGNORE INTO users (phone, nickname) +VALUES ('13800138000', '测试用户');" + +# 验证表是否创建成功 +echo "验证表结构:" +wrangler d1 execute friends --local --persist-to=.local-db --command="PRAGMA table_info(users);" + +echo "验证数据:" +wrangler d1 execute friends --local --persist-to=.local-db --command="SELECT * FROM users;" + +echo "数据库初始化完成。使用以下命令启动开发服务器:" +echo "wrangler pages dev --d1 bgdb=friends --persist-to=.local-db -- npm run dev" diff --git a/src/pages/chat/components/Sidebar.tsx b/src/pages/chat/components/Sidebar.tsx index 9415f0a..70b960f 100644 --- a/src/pages/chat/components/Sidebar.tsx +++ b/src/pages/chat/components/Sidebar.tsx @@ -6,6 +6,7 @@ import GitHubButton from 'react-github-btn'; import '@fontsource/audiowide'; //import { groups } from "@/config/groups"; import { AdSection } from './AdSection'; +import { UserSection } from './UserSection'; import { Tooltip, TooltipContent, @@ -123,6 +124,9 @@ const Sidebar = ({ isOpen, toggleSidebar, selectedGroupIndex = 0, onSelectGroup, {/* 广告位 */} + {/* 用户信息模块 */} + + {/* GitHub Star Button - 只在侧边栏打开时显示,放在底部 */}
{/* 标题移至底部 */} diff --git a/src/pages/chat/components/UserSection.tsx b/src/pages/chat/components/UserSection.tsx new file mode 100644 index 0000000..03af019 --- /dev/null +++ b/src/pages/chat/components/UserSection.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { cn } from "@/lib/utils"; +import { Edit2Icon, LogOutIcon } from 'lucide-react'; + +interface UserSectionProps { + isOpen: boolean; +} + +export const UserSection: React.FC = ({ isOpen }) => { + const [isHovering, setIsHovering] = useState(false); + + if (!isOpen) return null; + + return ( +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {/* 头像区域 */} +
+
+ +
+ {/* 头像hover效果 */} +
+ +
+
+ + {/* 用户信息区域 */} +
+
+ + 游客用户 + + +
+ + {/* 退出登录按钮 */} +
{ + // 这里添加退出登录的处理逻辑 + console.log('退出登录'); + }} + > + + + 退出登录 + +
+
+
+ ); +}; \ No newline at end of file From 413b120c404fa3e20f8339a758b1f89365207864 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Thu, 27 Mar 2025 19:24:55 +0800 Subject: [PATCH 06/13] update --- functions/api/_middleware.js | 27 ++-- functions/api/init.ts | 4 +- functions/api/login.ts | 6 +- functions/api/user/info.ts | 60 ++++++++ functions/api/user/update.ts | 88 ++++++++++++ src/pages/chat/components/Sidebar.tsx | 2 +- src/pages/chat/components/UserSection.tsx | 164 +++++++++++++++++----- src/pages/login/index.jsx | 8 ++ 8 files changed, 302 insertions(+), 57 deletions(-) create mode 100644 functions/api/user/info.ts create mode 100644 functions/api/user/update.ts diff --git a/functions/api/_middleware.js b/functions/api/_middleware.js index cfa42fb..170f1bd 100644 --- a/functions/api/_middleware.js +++ b/functions/api/_middleware.js @@ -47,32 +47,27 @@ async function verifyToken(token, env) { // 中间件函数 export async function onRequest(context) { try { - //获取环境变量中的AUTH_ACCESS const authAccess = context.env.AUTH_ACCESS; console.log('authAccess', authAccess); - //如果AUTH_ACCESS为0则跳过权限校验 - if (!authAccess || authAccess === '0') { + + if (!authAccess || authAccess === '0' || context.request.url.includes('/login') || context.request.url.includes('/sendcode') || context.request.url.includes('/test-db')) { console.log('跳过权限校验'); - return await context.next(); + context.data = { user: null }; + return context.next(); } - const request = context.request; - const env = context.env; - //跳过登录页面 - if (request.url.includes('/login') || request.url.includes('/sendcode') || request.url.includes('/login') || request.url.includes('/test-db')) { - return await context.next(); - } - const authHeader = request.headers.get('Authorization'); + + const authHeader = context.request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new Error('No token provided'); } const token = authHeader.split(' ')[1]; - const payload = await verifyToken(token, env); + const payload = await verifyToken(token, context.env); - // 将用户信息添加到上下文中 - context.user = payload; - - return await context.next(); + // 直接在原有的 request 对象上添加 context + context.data = { user: payload }; + console.log('context.request.user', context.data); + return context.next(); } catch (error) { console.error(error.message, context.request.url); return new Response(JSON.stringify({ error: error.message }), { diff --git a/functions/api/init.ts b/functions/api/init.ts index 68495b0..2aee297 100644 --- a/functions/api/init.ts +++ b/functions/api/init.ts @@ -1,13 +1,13 @@ import {generateAICharacters } from '../../src/config/aiCharacters'; import { groups } from '../../src/config/groups'; -export async function onRequestGet({ env, request }) { - console.log('init'); +export async function onRequestGet(context) { try { return Response.json({ code: 200, data: { groups: groups, characters: generateAICharacters('#groupName#', '#allTags#'), + user: context.data.user || null } }); } catch (error) { diff --git a/functions/api/login.ts b/functions/api/login.ts index 288d049..b213faf 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -98,7 +98,7 @@ export const onRequestPost: PagesFunction = async (context) => { `).bind(phone).first(); // 生成 token - const token = await generateToken(phone, env); + const token = await generateToken(userId, env); // 删除验证码 await env.bgkv.delete(`sms:${phone}`); @@ -139,14 +139,14 @@ export const onRequestPost: PagesFunction = async (context) => { }; // 修改为 async 函数 -async function generateToken(phone: string, env: Env): Promise { +async function generateToken(userId: string, env: Env): Promise { const header = { alg: 'HS256', typ: 'JWT' }; const payload = { - phone, + userId, exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7天过期 iat: Math.floor(Date.now() / 1000) }; diff --git a/functions/api/user/info.ts b/functions/api/user/info.ts new file mode 100644 index 0000000..e7de286 --- /dev/null +++ b/functions/api/user/info.ts @@ -0,0 +1,60 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestGet: PagesFunction = async (context) => { + try { + const { env, data } = context; + + // 从数据库获取用户信息 + const db = env.bgdb; + const userInfo = await db.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE id = ? + `).bind(data.user.userId).first(); + + if (!userInfo) { + return new Response( + JSON.stringify({ + success: false, + message: '用户不存在' + }), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + return new Response( + JSON.stringify({ + success: true, + data: userInfo + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; \ No newline at end of file diff --git a/functions/api/user/update.ts b/functions/api/user/update.ts new file mode 100644 index 0000000..e82b28c --- /dev/null +++ b/functions/api/user/update.ts @@ -0,0 +1,88 @@ +interface Env { + bgdb: D1Database; +} + +export const onRequestPost: PagesFunction = async (context) => { + try { + const { env, data, request } = context; + + // 解析请求体 + const body = await request.json(); + const { nickname } = body; + + // 验证昵称 + if (!nickname || typeof nickname !== 'string' || nickname.length > 32) { + return new Response( + JSON.stringify({ + success: false, + message: '昵称格式不正确' + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 更新数据库中的昵称 + 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 (!result.success) { + return new Response( + JSON.stringify({ + success: false, + message: '更新失败' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } + + // 获取更新后的用户信息 + const userInfo = await db.prepare(` + SELECT id, phone, nickname, avatar_url, status + FROM users + WHERE id = ? + `).bind(data.user.userId).first(); + + return new Response( + JSON.stringify({ + success: true, + data: userInfo + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + } catch (error) { + console.error(error); + return new Response( + JSON.stringify({ + success: false, + message: '服务器错误' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } +}; \ No newline at end of file diff --git a/src/pages/chat/components/Sidebar.tsx b/src/pages/chat/components/Sidebar.tsx index 70b960f..e054bf5 100644 --- a/src/pages/chat/components/Sidebar.tsx +++ b/src/pages/chat/components/Sidebar.tsx @@ -145,7 +145,7 @@ const Sidebar = ({ isOpen, toggleSidebar, selectedGroupIndex = 0, onSelectGroup,
{isOpen && ( -
+
= ({ 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 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(); + setUserInfo(data); + setIsEditing(false); + } catch (error) { + console.error('更新昵称失败:', error); + } finally { + setIsLoading(false); + } + }; + if (!isOpen) return null; return (
setIsHovering(true)} @@ -23,7 +74,9 @@ export const UserSection: React.FC = ({ isOpen }) => { {/* 头像区域 */}
- + + {isLoading ? '...' : userInfo?.nickname?.[0] || '我'} +
{/* 头像hover效果 */}
@@ -33,40 +86,81 @@ export const UserSection: React.FC = ({ isOpen }) => { {/* 用户信息区域 */}
-
- - 游客用户 - - +
+ {isEditing ? ( +
+ setNewNickname(e.target.value)} + className="text-sm px-2 border rounded-md w-full" + placeholder={userInfo?.nickname || '输入新昵称'} + onKeyDown={(e) => { + if (e.key === 'Enter') updateNickname(); + if (e.key === 'Escape') setIsEditing(false); + }} + autoFocus + /> +
+ + +
+
+ ) : ( + <> + + {isLoading ? '加载中...' : userInfo?.nickname || '游客用户'} + + { + setIsEditing(true); + setNewNickname(userInfo?.nickname || ''); + }} + /> + + )}
{/* 退出登录按钮 */} -
{ - // 这里添加退出登录的处理逻辑 - console.log('退出登录'); - }} - > - - - 退出登录 - -
+ "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'; + }} + > + + + 退出登录 + +
+ )}
); diff --git a/src/pages/login/index.jsx b/src/pages/login/index.jsx index 35f7b8b..aa4a2a4 100644 --- a/src/pages/login/index.jsx +++ b/src/pages/login/index.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { useNavigate } from 'react-router-dom'; import PhoneLogin from './comonents/PhoneLogin'; @@ -9,6 +10,13 @@ export default function Login() { navigate('/'); }; + React.useEffect(() => { + const isLogin = localStorage.getItem('token'); + if (isLogin) { + window.location.href = '/'; // 由于是 Vite 多页面,这里使用 window.location.href + } + }, []); + return (
From 86b045026f928c41d39422a08fdf9f438a962042 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Fri, 28 Mar 2025 07:49:02 +0800 Subject: [PATCH 07/13] add migrations --- migrations/0001_init_users.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 migrations/0001_init_users.sql diff --git a/migrations/0001_init_users.sql b/migrations/0001_init_users.sql new file mode 100644 index 0000000..cec8201 --- /dev/null +++ b/migrations/0001_init_users.sql @@ -0,0 +1 @@ +-- Migration number: 0001 2025-03-27T12:27:07.602Z From b75e0daaf09d02ceae8b2ab4ed30ad366929d039 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Fri, 28 Mar 2025 08:47:45 +0800 Subject: [PATCH 08/13] fix login bug --- functions/api/login.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/api/login.ts b/functions/api/login.ts index b213faf..a5ced7b 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -97,6 +97,8 @@ export const onRequestPost: PagesFunction = async (context) => { WHERE phone = ? `).bind(phone).first(); + userId = userInfo.id + // 生成 token const token = await generateToken(userId, env); From 0f983f0e5e14ef1f698203eaa6c570bcf3904e6d Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Fri, 28 Mar 2025 15:00:27 +0800 Subject: [PATCH 09/13] update migrateions --- migrations/0002_create_users.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 migrations/0002_create_users.sql diff --git a/migrations/0002_create_users.sql b/migrations/0002_create_users.sql new file mode 100644 index 0000000..c7a3979 --- /dev/null +++ b/migrations/0002_create_users.sql @@ -0,0 +1 @@ +-- Migration number: 0002 2025-03-28T06:06:16.353Z From 4a5a2240568cc84673ad59fd59bb9d9f9783602c Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Mon, 31 Mar 2025 18:54:51 +0800 Subject: [PATCH 10/13] update --- functions/api/user/info.ts | 2 + functions/api/user/update.ts | 67 ++++++++----- package-lock.json | 31 +++++- package.json | 3 +- src/pages/chat/components/ChatUI.tsx | 65 ++++++------ src/pages/chat/components/Sidebar.tsx | 1 - src/pages/chat/components/UserSection.tsx | 116 ++++++++++++++++------ 7 files changed, 189 insertions(+), 96 deletions(-) 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 || ''); }} /> From 6929aa8a47203487d284072a121fc77313ec4342 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 1 Apr 2025 07:26:40 +0800 Subject: [PATCH 11/13] support user --- friends.sql | 13 +++++++++++++ functions/api/user/upload.ts | 24 ++++++++++++++++++++++++ src/store/userStore.ts | 25 +++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 friends.sql create mode 100644 functions/api/user/upload.ts create mode 100644 src/store/userStore.ts diff --git a/friends.sql b/friends.sql new file mode 100644 index 0000000..872897a --- /dev/null +++ b/friends.sql @@ -0,0 +1,13 @@ +PRAGMA defer_foreign_keys=TRUE; +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone VARCHAR(11) NOT NULL UNIQUE, + nickname VARCHAR(50), + avatar_url TEXT, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO users VALUES(1,'13800138000','测试用户',NULL,1,'2025-03-26 08:39:15','2025-03-26 08:39:15','2025-03-26 08:39:15'); +INSERT INTO users VALUES(2,'13391881866','Hobby',NULL,1,'2025-03-26 09:42:30','2025-03-27 05:50:04','2025-03-27 03:21:40'); \ No newline at end of file diff --git a/functions/api/user/upload.ts b/functions/api/user/upload.ts new file mode 100644 index 0000000..698688d --- /dev/null +++ b/functions/api/user/upload.ts @@ -0,0 +1,24 @@ +export const onRequestPost: PagesFunction = async (context) =>{ + const { env, data } = context; + + // 创建 FormData 对象 + const formData = new FormData(); + formData.append('requireSignedURLs', 'false'); + formData.append('metadata', JSON.stringify({ + user: data.user.userId + })); + + // 调用 Cloudflare Images API 获取直接上传 URL + const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/images/v2/direct_upload`, { + method: "POST", + headers: { + "Authorization": `Bearer ${env.CF_API_TOKEN}`, + }, + body: formData + }); + + const data1 = await response.json(); + console.log('上传头像', data1); + //下发image前缀 + return Response.json(data1.result); + } \ No newline at end of file diff --git a/src/store/userStore.ts b/src/store/userStore.ts new file mode 100644 index 0000000..ff7ce01 --- /dev/null +++ b/src/store/userStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand' + +interface UserInfo { + id: number; + phone: string; + nickname: string; + avatar_url: string | null; + status: number; +} + +interface UserStore { + userInfo: UserInfo; + setUserInfo: (userInfo: UserInfo) => void; +} + +export const useUserStore = create((set) => ({ + userInfo: { + id: 0, + phone: '', + nickname: '', + avatar_url: null, + status: 0 + }, + setUserInfo: (userInfo: UserInfo) => set({ userInfo }) +})); \ No newline at end of file From 3eeaa9c514ecfb006a3275287bd53f9d42fa39d3 Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 1 Apr 2025 08:56:49 +0800 Subject: [PATCH 12/13] update it --- functions/api/user/info.ts | 4 +- functions/api/user/update.ts | 4 +- src/pages/chat/components/ChatUI.tsx | 92 +-------------------- src/pages/chat/components/UserSection.tsx | 15 ++-- src/utils/avatar.ts | 97 +++++++++++++++++++++++ 5 files changed, 113 insertions(+), 99 deletions(-) create mode 100644 src/utils/avatar.ts diff --git a/functions/api/user/info.ts b/functions/api/user/info.ts index 93723a6..4b4697c 100644 --- a/functions/api/user/info.ts +++ b/functions/api/user/info.ts @@ -29,7 +29,9 @@ export const onRequestGet: PagesFunction = async (context) => { ); } //处理avatar_url - userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`; + if (userInfo.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 9f8447d..dd5f999 100644 --- a/functions/api/user/update.ts +++ b/functions/api/user/update.ts @@ -73,7 +73,9 @@ export const onRequestPost: PagesFunction = async (context) => { WHERE id = ? `).bind(data.user.userId).first(); //处理avatar_url - userInfo.avatar_url = `${env.NEXT_PUBLIC_CF_IMAGES_DELIVERY_URL}/${userInfo.avatar_url}/public`; + if (userInfo.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/src/pages/chat/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx index 7e3fb55..bbf8d5b 100644 --- a/src/pages/chat/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -22,94 +22,8 @@ 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']; - const index = (name.charCodeAt(0) + (name.charCodeAt(1) || 0 )) % colors.length; - return { - backgroundColor: colors[index], - text: name[0], - }; -}; +import { getAvatarData } from '@/utils/avatar'; -// 单个完整头像 -const SingleAvatar = ({ user }: { user: User | AICharacter }) => { - // 如果有头像就使用头像,否则使用默认的文字头像 - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; - -// 左右分半头像 -const HalfAvatar = ({ user, isFirst }: { user: User, isFirst: boolean }) => { - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; - -// 四分之一头像 -const QuarterAvatar = ({ user, index }: { user: User, index: number }) => { - if ('avatar' in user && user.avatar) { - return ( -
- {user.name} -
- ); - } - const avatarData = getAvatarData(user.name); - return ( -
- {avatarData.text} -
- ); -}; // 修改 KaTeXStyle 组件 const KaTeXStyle = () => ( @@ -212,7 +126,7 @@ const ChatUI = () => { userStore.setUserInfo(userInfo.data); setUsers([ - { id: 1, name: userInfo.data.nickname, avatar: userInfo.data.avatar_url }, + { id: 1, name: userInfo.data.nickname, avatar: userInfo.data.avatar_url? userInfo.data.avatar_url : null }, ...groupAiCharacters ]); } catch (error) { @@ -531,7 +445,7 @@ const ChatUI = () => { - {'avatar' in user && user.avatar ? ( + {'avatar' in user && user.avatar && user.avatar !== null ? ( ) : ( diff --git a/src/pages/chat/components/UserSection.tsx b/src/pages/chat/components/UserSection.tsx index 08f6cd2..e452103 100644 --- a/src/pages/chat/components/UserSection.tsx +++ b/src/pages/chat/components/UserSection.tsx @@ -3,17 +3,13 @@ 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; } -// 添加用户信息接口 -interface UserInfo { - nickname: string; - avatar_url?: string; -} export const UserSection: React.FC = ({ isOpen }) => { const [isHovering, setIsHovering] = useState(false); @@ -107,7 +103,8 @@ export const UserSection: React.FC = ({ isOpen }) => { onChange={handleAvatarUpload} />
!uploadingAvatar && fileInputRef.current?.click()} > {uploadingAvatar ? ( @@ -121,8 +118,10 @@ export const UserSection: React.FC = ({ isOpen }) => { className="w-full h-full object-cover" /> ) : ( - - {userStore.userInfo?.nickname?.[0] || '我'} + + {getAvatarData(userStore.userInfo?.nickname || '我').text} )}
diff --git a/src/utils/avatar.ts b/src/utils/avatar.ts new file mode 100644 index 0000000..c0ab36a --- /dev/null +++ b/src/utils/avatar.ts @@ -0,0 +1,97 @@ +import React from 'react'; + +interface User { + id: number | string; + name: string; + avatar?: string; +} + +interface AICharacter { + id: string; + name: string; + personality: string; + model: string; + avatar?: string; + custom_prompt?: string; + tags?: string[]; +} + +export const getAvatarData = (name: string) => { + const colors = ['#1abc9c', '#3498db', '#9b59b6', '#f1c40f', '#e67e22']; + const index = (name.charCodeAt(0) + (name.charCodeAt(1) || 0)) % colors.length; + return { + backgroundColor: colors[index], + text: name[0], + }; +}; + +// 获取单个头像的样式和内容 +export const getSingleAvatarData = (user: User | AICharacter) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover' + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'w-full h-full flex items-center justify-center text-xs text-white font-medium', + style: { backgroundColor: avatarData.backgroundColor } + }; +}; + +// 获取半头像的样式和内容 +export const getHalfAvatarData = (user: User, isFirst: boolean) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover', + containerStyle: { + borderRight: isFirst ? '1px solid white' : 'none' + } + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'w-1/2 h-full flex items-center justify-center text-xs text-white font-medium', + style: { + backgroundColor: avatarData.backgroundColor, + borderRight: isFirst ? '1px solid white' : 'none' + } + }; +}; + +// 获取四分之一头像的样式和内容 +export const getQuarterAvatarData = (user: User, index: number) => { + if ('avatar' in user && user.avatar) { + return { + type: 'image', + src: user.avatar, + alt: user.name, + className: 'w-full h-full object-cover', + containerStyle: { + borderRight: index % 2 === 0 ? '1px solid white' : 'none', + borderBottom: index < 2 ? '1px solid white' : 'none' + } + }; + } + const avatarData = getAvatarData(user.name); + return { + type: 'text', + text: avatarData.text, + className: 'aspect-square flex items-center justify-center text-[8px] text-white font-medium', + style: { + backgroundColor: avatarData.backgroundColor, + borderRight: index % 2 === 0 ? '1px solid white' : 'none', + borderBottom: index < 2 ? '1px solid white' : 'none' + } + }; +}; \ No newline at end of file From f7a9ec24515c149d8aad736cdf16ce1d62666bff Mon Sep 17 00:00:00 2001 From: maojindao55 Date: Tue, 1 Apr 2025 19:49:23 +0800 Subject: [PATCH 13/13] fix some issue --- src/pages/chat/components/ChatUI.tsx | 27 +++++++++++++++++------ src/pages/chat/components/UserSection.tsx | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/pages/chat/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx index bbf8d5b..6fbf749 100644 --- a/src/pages/chat/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -118,15 +118,28 @@ const ChatUI = () => { setGroupAiCharacters(groupAiCharacters); const allNames = groupAiCharacters.map(character => character.name); allNames.push('user'); + let avatar_url = null; + let nickname = '我'; setAllNames(allNames); - - const response1 = await request('/api/user/info'); - const userInfo = await response1.json(); - //设置store - userStore.setUserInfo(userInfo.data); - + if (data.user && data.user != null) { + const response1 = await request('/api/user/info'); + const userInfo = await response1.json(); + //设置store + userStore.setUserInfo(userInfo.data); + avatar_url = userInfo.data.avatar_url; + nickname = userInfo.data.nickname; + } else { + // 设置空的用户信息 + userStore.setUserInfo({ + id: 0, + phone: '', + nickname: nickname, + avatar_url: null, + status: 0 + }); + } setUsers([ - { id: 1, name: userInfo.data.nickname, avatar: userInfo.data.avatar_url? userInfo.data.avatar_url : null }, + { id: 1, name: nickname, avatar: avatar_url }, ...groupAiCharacters ]); } catch (error) { diff --git a/src/pages/chat/components/UserSection.tsx b/src/pages/chat/components/UserSection.tsx index e452103..868a13f 100644 --- a/src/pages/chat/components/UserSection.tsx +++ b/src/pages/chat/components/UserSection.tsx @@ -82,7 +82,7 @@ export const UserSection: React.FC = ({ isOpen }) => { } }; - if (!isOpen) return null; + if (!isOpen || !userStore.userInfo || !userStore.userInfo.status) return null; return (