From 5deb2e96b5637c2b2cdedfa690d35bdbf6652fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 9 Jul 2025 10:19:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9AAI=E9=97=AE=E7=AD=94?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.scss | 34 +++ src/pages/ai/index.scss | 43 ++-- src/pages/ai/index.tsx | 480 +++++++++++++++++++------------------ src/pages/index/Header.tsx | 29 +-- src/passport/sms-login.tsx | 1 + src/utils/server.ts | 2 + 6 files changed, 319 insertions(+), 270 deletions(-) diff --git a/src/app.scss b/src/app.scss index 46e83e0..d46d9be 100644 --- a/src/app.scss +++ b/src/app.scss @@ -161,3 +161,37 @@ taro-rich-text-core[space]{ white-space: normal !important; line-height: 1.8; } + +html { + font-size: calc(100vw / 375 * 16); // 假设设计稿宽度为375px,设置基础字体大小为16px + + @media screen and (max-width: 375px) { + font-size: 16px; // 最小字体大小 + } + + @media screen and (min-width: 768px) { + font-size: 24px; // 平板或大屏设备上的字体大小 + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes typing { + 0%, 80%, 100% { + transform: scale(0); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} diff --git a/src/pages/ai/index.scss b/src/pages/ai/index.scss index c9028af..b57db42 100644 --- a/src/pages/ai/index.scss +++ b/src/pages/ai/index.scss @@ -1,5 +1,19 @@ +// ... existing code ... + +html { + font-size: calc(100vw / 375 * 16); // 假设设计稿宽度为375px,设置基础字体大小为16px + + @media screen and (max-width: 375px) { + font-size: 16px; // 最小字体大小 + } + + @media screen and (min-width: 768px) { + font-size: 24px; // 平板或大屏设备上的字体大小 + } +} + .ai-chat { - height: 94vh; + height: 98vh; display: flex; flex-direction: column; background-color: #f5f5f5; @@ -9,7 +23,7 @@ color: white; padding: 16px; text-align: center; - font-size: 18px; + font-size: 1.2rem; // 使用 rem 单位 font-weight: bold; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } @@ -71,13 +85,13 @@ } .avatar { - width: 40px; - height: 40px; + width: 60px; + height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 18px; + font-size: 18px; // 可以保持不变或调整为 rem 单位 flex-shrink: 0; &.user-avatar { @@ -97,7 +111,7 @@ border-radius: 18px; word-wrap: break-word; line-height: 1.5; - font-size: 14px; + font-size: 1rem; // 使用 rem 单位 position: relative; .typing-indicator { @@ -136,16 +150,15 @@ .message-input { width: 100%; - min-height: 40px; + min-height: 90px; max-height: 120px; padding: 10px 16px; border: 1px solid #e0e0e0; border-radius: 20px; - font-size: 14px; - line-height: 1.5; + font-size: 1.2rem; // 使用 rem 单位 + line-height: 1.8; resize: none; outline: none; - background: #f9f9f9; transition: all 0.3s ease; &:focus { @@ -197,7 +210,7 @@ border-top: 1px solid #e0e0e0; .quick-title { - font-size: 14px; + font-size: 1rem; // 使用 rem 单位 color: #666; margin-bottom: 12px; display: flex; @@ -215,7 +228,7 @@ background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 12px; - font-size: 14px; + font-size: 1rem; // 使用 rem 单位 color: #333; cursor: pointer; transition: all 0.3s ease; @@ -246,20 +259,20 @@ text-align: center; .empty-icon { - font-size: 64px; + font-size: 4rem; // 使用 rem 单位 margin-bottom: 16px; opacity: 0.5; } .empty-title { - font-size: 18px; + font-size: 1.5rem; // 使用 rem 单位 font-weight: bold; color: #333; margin-bottom: 8px; } .empty-desc { - font-size: 14px; + font-size: 1rem; // 使用 rem 单位 color: #666; line-height: 1.5; } diff --git a/src/pages/ai/index.tsx b/src/pages/ai/index.tsx index c4c6657..5d6826f 100644 --- a/src/pages/ai/index.tsx +++ b/src/pages/ai/index.tsx @@ -1,269 +1,273 @@ -import { useEffect, useState, useRef } from "react"; -import { View, Textarea } from '@tarojs/components'; -// import Toast from '@tarojs/taro'; +import {useEffect, useState, useRef} from "react"; +import {View, Textarea} from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { Button } from '@nutui/nutui-react-taro'; -import { User, ArrowUp } from '@nutui/icons-react-taro'; -import { sendAiMessage, stopAiMessage } from '@/api/ai'; -import { createWebSocket } from '@/utils/websocket'; +import {Button} from '@nutui/nutui-react-taro'; +import {User, ArrowUp} from '@nutui/icons-react-taro'; +import {sendAiMessage, stopAiMessage} from '@/api/ai'; +import {createWebSocket} from '@/utils/websocket'; import './index.scss'; -import {APP_API_URL} from "@/utils/server"; +import Header from "../index/Header"; +import {WSS_API_URL} from "@/utils/server"; // 消息类型 interface Message { - id?: string; - type?: 'user' | 'ai'; - query?: string; - timestamp?: number; - isTyping?: boolean; - user?: string; - responseMode?: string; + id?: string; + type?: 'user' | 'ai'; + query?: string; + timestamp?: number; + isTyping?: boolean; + user?: string; + responseMode?: string; } /** * AI问答页面 */ const AiChat = () => { - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [currentTaskId, setCurrentTaskId] = useState(''); - const messagesEndRef = useRef(null); - const wsRef = useRef(null); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [currentTaskId, setCurrentTaskId] = useState(''); + const messagesEndRef = useRef(null); + const wsRef = useRef(null); - // 快捷问题 - const quickQuestions = [ - "当兵的注意事项?", - "男兵应征报名对象:", - "当兵公务员考试?", - "请问您还有什么问题吗?" - ]; + // 快捷问题 + const quickQuestions = [ + "当兵的注意事项?", + "男兵应征报名对象:", + "当兵公务员考试?", + "请问您还有什么问题吗?" + ]; - // 滚动到底部 - const scrollToBottom = () => { - setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, 100); - }; - - // 初始化WebSocket连接 - const initWebSocket = () => { - const wsUrl = APP_API_URL.replace('https', 'ws') + '/chat/' + Taro.getStorageSync('UserId') || 'token'; - // const wsUrl = 'ws://127.0.0.1:9000/chat/test' - wsRef.current = createWebSocket(wsUrl); - - wsRef.current.onMessage((data: any) => { - console.log('收到WebSocket消息:', data); - - if (data.answer) { - if (data.answer === '__END__') { - // 消息结束,移除typing状态 - setMessages(prev => prev.map(msg => - msg.isTyping ? { ...msg, isTyping: false } : msg - )); - setIsLoading(false); - setCurrentTaskId(''); - } else { - // 更新AI回复消息 - setMessages(prev => { - const lastMessage = prev[prev.length - 1]; - if (lastMessage && lastMessage.type === 'ai' && lastMessage.isTyping) { - // 更新最后一条AI消息 - return prev.map((msg, index) => - index === prev.length - 1 - ? { ...msg, query: msg.query + data.answer } - : msg - ); - } else { - // 创建新的AI消息 - return [...prev, { - id: Date.now().toString(), - type: 'ai', - query: data.answer, - timestamp: Date.now(), - isTyping: true - }]; - } - }); - - if (data.taskId) { - setCurrentTaskId(data.taskId); - } + // 滚动到底部 + const scrollToBottom = () => { + if (!Taro.getStorageSync('UserId')) { + Taro.showToast({ + title: '请先登录', + icon: 'none', + duration: 2000 + }); + Taro.navigateTo({ + url: '/passport/sms-login', + }) + return false; } + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({behavior: 'smooth'}); + }, 100); + }; + + // 初始化WebSocket连接 + const initWebSocket = () => { + // const wsUrl = APP_API_URL.replace('http', 'ws') + '/chat/' + Taro.getStorageSync('UserId') || 'token'; + console.log(WSS_API_URL + "/chat/" + Taro.getStorageSync('UserId'), 'wsUrl') + wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + Taro.getStorageSync('UserId')); + + wsRef.current.onMessage((data: any) => { + console.log('收到WebSocket消息:', data); + + if (data.answer) { + if (data.answer === '__END__') { + // 消息结束,移除typing状态 + setMessages(prev => prev.map(msg => + msg.isTyping ? {...msg, isTyping: false} : msg + )); + setIsLoading(false); + setCurrentTaskId(''); + } else { + // 更新AI回复消息 + setMessages(prev => { + const lastMessage = prev[prev.length - 1]; + if (lastMessage && lastMessage.type === 'ai' && lastMessage.isTyping) { + // 更新最后一条AI消息 + return prev.map((msg, index) => + index === prev.length - 1 + ? {...msg, query: msg.query + data.answer} + : msg + ); + } else { + // 创建新的AI消息 + return [...prev, { + id: Date.now().toString(), + type: 'ai', + query: data.answer, + timestamp: Date.now(), + isTyping: true + }]; + } + }); + + if (data.taskId) { + setCurrentTaskId(data.taskId); + } + } + scrollToBottom(); + } + }); + + wsRef.current.onOpen(() => { + console.log('WebSocket连接成功'); + }); + + wsRef.current.onError((error: any) => { + console.error('WebSocket连接错误:', error); + }); + + wsRef.current.connect().catch((error: any) => { + console.error('WebSocket连接失败:', error); + }); + }; + + useEffect(() => { + initWebSocket(); + Taro.hideTabBar(); + return () => { + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, []); + + useEffect(() => { scrollToBottom(); - } - }); + }, [messages]); - wsRef.current.onOpen(() => { - console.log('WebSocket连接成功'); - }); + // 发送消息 + const handleSendMessage = async (content: string) => { + if (!content.trim() || isLoading) return; - wsRef.current.onError((error: any) => { - console.error('WebSocket连接错误:', error); - // Toast.showToast({ title: '连接失败,请检查网络', icon: 'error'}); - }); + const userMessage: Message = { + id: Date.now().toString(), + type: 'user', + query: content.trim(), + timestamp: Date.now(), + user: `${Taro.getStorageSync('UserId')}` + }; + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setIsLoading(true); - wsRef.current.connect().catch((error: any) => { - console.error('WebSocket连接失败:', error); - }); - }; - - useEffect(() => { - initWebSocket(); - Taro.hideTabBar(); - return () => { - if (wsRef.current) { - wsRef.current.close(); - } + try { + await sendAiMessage({ + query: content.trim(), + user: `${Taro.getStorageSync('UserId')}`, + responseMode: 'streaming', + }); + } catch (error) { + console.error('发送消息失败:', error); + setIsLoading(false); + } }; - }, []); - useEffect(() => { - scrollToBottom(); - }, [messages]); - - // 发送消息 - const handleSendMessage = async (content: string) => { - if (!content.trim() || isLoading) return; - - const userMessage: Message = { - id: Date.now().toString(), - type: 'user', - query: content.trim(), - timestamp: Date.now(), - user: `${Taro.getStorageSync('UserId')}` + // 停止AI回复 + const handleStopMessage = async () => { + if (currentTaskId) { + try { + await stopAiMessage({taskId: currentTaskId}); + setIsLoading(false); + setCurrentTaskId(''); + setMessages(prev => prev.map(msg => + msg.isTyping ? {...msg, isTyping: false} : msg + )); + } catch (error) { + console.error('停止消息失败:', error); + } + } }; - setMessages(prev => [...prev, userMessage]); - setInputValue(''); - setIsLoading(true); - try { - await sendAiMessage({ - query: content.trim(), - user: `${Taro.getStorageSync('UserId')}`, - responseMode: 'streaming', - }); - } catch (error) { - console.error('发送消息失败:', error); - // Toast.showToast({ title: '发送失败,请重试', icon: 'error'}); - setIsLoading(false); - } - }; + // 处理快捷问题点击 + const handleQuickQuestion = (question: string) => { + handleSendMessage(question); + }; - // 停止AI回复 - const handleStopMessage = async () => { - if (currentTaskId) { - try { - await stopAiMessage({ taskId: currentTaskId }); - setIsLoading(false); - setCurrentTaskId(''); - setMessages(prev => prev.map(msg => - msg.isTyping ? { ...msg, isTyping: false } : msg - )); - } catch (error) { - console.error('停止消息失败:', error); - } - } - }; - - // 处理快捷问题点击 - const handleQuickQuestion = (question: string) => { - console.log(question,'qqq') - handleSendMessage(question); - }; - - return ( - - {/**/} - {/* AI智能问答*/} - {/**/} - - - - {messages.length === 0 ? ( - - 🤖 - 您好!我是AI助手 - - 我可以为您解答各种问题,请输入您想了解的内容 - - - ) : ( - messages.map((message) => ( - - - {message.type === 'user' ? : '🤖'} + return ( + +
+ + + {messages.length === 0 ? ( + + 🤖 + 您好!我是AI助手 + + 我可以为您解答各种问题,请输入您想了解的内容 + + + ) : ( + messages.map((message) => ( + + + {message.type === 'user' ? : '🤖'} + + + {message.isTyping ? ( + + + + + + ) : ( + message.query + )} + + + )) + )} + - - {message.isTyping ? ( - - - - + + {messages.length === 0 && ( + + + 💡 您可以问我: + + + {quickQuestions.map((question, index) => ( + handleQuickQuestion(question)} + > + {question} + + ))} + - ) : ( - message.query - )} + )} + + + +