From 6b36f83861f137068df71c0bde20ca06be5e3894 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 12:39:29 +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/api/ai/index.ts | 3 + src/article/index.tsx | 2 +- src/expert/index.tsx | 2 +- src/honor/index.tsx | 2 +- src/honor/list.tsx | 2 +- src/pages/ai/index.scss | 95 +++++++++++-- src/pages/ai/index.tsx | 186 +++++++++++++++++++------ src/pages/user/components/UserCell.tsx | 2 +- src/photo/index.tsx | 2 +- src/utils/websocket.ts | 8 +- 10 files changed, 247 insertions(+), 57 deletions(-) diff --git a/src/api/ai/index.ts b/src/api/ai/index.ts index 47bb114..43f0a3b 100644 --- a/src/api/ai/index.ts +++ b/src/api/ai/index.ts @@ -11,6 +11,7 @@ export interface AiChatMessage { conversationId?: string; type?: string; requestType?: number; + aiToken?: string; } /** @@ -48,3 +49,5 @@ export async function stopAiMessage(data: { taskId: string; type?: string }) { } return Promise.reject(new Error(res.message)); } + + diff --git a/src/article/index.tsx b/src/article/index.tsx index 17a051f..405b85b 100644 --- a/src/article/index.tsx +++ b/src/article/index.tsx @@ -25,7 +25,7 @@ const Index = () => { // 二级栏目 const childCateogry = await pageCmsNavigation({parentId: categoryId}); // 终极新闻列表 - const articles = await pageCmsArticle({categoryId}); + const articles = await pageCmsArticle({categoryId,limit: 50}); // 当前栏目信息 if (navs) { diff --git a/src/expert/index.tsx b/src/expert/index.tsx index 306fdb6..e2fdf23 100644 --- a/src/expert/index.tsx +++ b/src/expert/index.tsx @@ -22,7 +22,7 @@ const Index = () => { // 当前栏目信息 const navs = await getCmsNavigation(categoryId); // 终极新闻列表 - const articles = await pageCmsArticle({categoryId}); + const articles = await pageCmsArticle({categoryId,limit: 50}); // 当前栏目信息 if (navs) { diff --git a/src/honor/index.tsx b/src/honor/index.tsx index e662183..ecc8bf1 100644 --- a/src/honor/index.tsx +++ b/src/honor/index.tsx @@ -25,7 +25,7 @@ const Index = () => { // 二级栏目 const childCateogry = await pageCmsNavigation({parentId: categoryId}); // 终极新闻列表 - const articles = await pageCmsArticle({categoryId}); + const articles = await pageCmsArticle({categoryId,limit: 50}); // 当前栏目信息 if (navs) { diff --git a/src/honor/list.tsx b/src/honor/list.tsx index b7dfbc7..f0b4227 100644 --- a/src/honor/list.tsx +++ b/src/honor/list.tsx @@ -22,7 +22,7 @@ const List = () => { // 当前栏目信息 const navs = await getCmsNavigation(categoryId); // 终极新闻列表 - const articles = await pageCmsArticle({categoryId}); + const articles = await pageCmsArticle({categoryId,limit: 50}); // 当前栏目信息 if (navs) { diff --git a/src/pages/ai/index.scss b/src/pages/ai/index.scss index b57db42..39e932f 100644 --- a/src/pages/ai/index.scss +++ b/src/pages/ai/index.scss @@ -23,11 +23,40 @@ html { color: white; padding: 16px; text-align: center; - font-size: 1.2rem; // 使用 rem 单位 + font-size: 1rem; // 使用 rem 单位 font-weight: bold; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } + .connection-status { + background: #fff3cd; + border-bottom: 1px solid #ffeaa7; + padding: 8px 16px; + text-align: center; + + .status-indicator { + font-size: 0.9rem; + color: #856404; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + &.offline::before { + content: '⚠️'; + font-size: 1rem; + } + + .reconnect-button { + margin-left: 12px; + height: 28px; + padding: 0 12px; + font-size: 0.8rem; + border-radius: 14px; + } + } + } + .chat-container { flex: 1; display: flex; @@ -111,8 +140,23 @@ html { border-radius: 18px; word-wrap: break-word; line-height: 1.5; - font-size: 1rem; // 使用 rem 单位 + font-size: .8rem; // 使用 rem 单位 position: relative; + display: flex; + align-items: flex-end; + gap: 4px; + + .message-text { + flex: 1; + white-space: pre-wrap; + } + + .typing-cursor { + color: #666; + font-weight: bold; + animation: blink 1s infinite; + margin-left: 2px; + } .typing-indicator { display: flex; @@ -139,7 +183,7 @@ html { padding: 16px; border-top: 1px solid #e0e0e0; display: flex; - align-items: flex-end; + align-items: center; gap: 12px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); @@ -155,7 +199,7 @@ html { padding: 10px 16px; border: 1px solid #e0e0e0; border-radius: 20px; - font-size: 1.2rem; // 使用 rem 单位 + font-size: 1rem; // 使用 rem 单位 line-height: 1.8; resize: none; outline: none; @@ -202,6 +246,32 @@ html { box-shadow: none; } } + + .stop-button { + min-width: 60px; + height: 40px; + border-radius: 20px; + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); + border: none; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + flex-shrink: 0; + font-size: 0.8rem; + padding: 0 12px; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3); + } + + &:active { + transform: scale(0.95); + } + } } .quick-questions { @@ -210,7 +280,7 @@ html { border-top: 1px solid #e0e0e0; .quick-title { - font-size: 1rem; // 使用 rem 单位 + font-size: .8rem; // 使用 rem 单位 color: #666; margin-bottom: 12px; display: flex; @@ -228,7 +298,7 @@ html { background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 12px; - font-size: 1rem; // 使用 rem 单位 + font-size: .8rem; // 使用 rem 单位 color: #333; cursor: pointer; transition: all 0.3s ease; @@ -259,7 +329,7 @@ html { text-align: center; .empty-icon { - font-size: 4rem; // 使用 rem 单位 + font-size: 3rem; // 使用 rem 单位 margin-bottom: 16px; opacity: 0.5; } @@ -272,7 +342,7 @@ html { } .empty-desc { - font-size: 1rem; // 使用 rem 单位 + font-size: .8rem; // 使用 rem 单位 color: #666; line-height: 1.5; } @@ -300,3 +370,12 @@ html { opacity: 1; } } + +@keyframes blink { + 0%, 50% { + opacity: 1; + } + 51%, 100% { + opacity: 0; + } +} diff --git a/src/pages/ai/index.tsx b/src/pages/ai/index.tsx index 5d6826f..eac60f6 100644 --- a/src/pages/ai/index.tsx +++ b/src/pages/ai/index.tsx @@ -5,8 +5,8 @@ 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 {getAiToken} from '@/utils/aiToken'; import './index.scss'; -import Header from "../index/Header"; import {WSS_API_URL} from "@/utils/server"; // 消息类型 @@ -28,6 +28,7 @@ const AiChat = () => { const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [currentTaskId, setCurrentTaskId] = useState(''); + const [wsConnected, setWsConnected] = useState(false); const messagesEndRef = useRef(null); const wsRef = useRef(null); @@ -39,19 +40,16 @@ const AiChat = () => { "请问您还有什么问题吗?" ]; + // 检查并获取AI Token + const checkAiToken = () => { + return getAiToken(); + }; + // 滚动到底部 const scrollToBottom = () => { - if (!Taro.getStorageSync('UserId')) { - Taro.showToast({ - title: '请先登录', - icon: 'none', - duration: 2000 - }); - Taro.navigateTo({ - url: '/passport/sms-login', - }) - return false; - } + // 检查并确保AI_TOKEN存在 + checkAiToken(); + setTimeout(() => { messagesEndRef.current?.scrollIntoView({behavior: 'smooth'}); }, 100); @@ -59,9 +57,7 @@ const AiChat = () => { // 初始化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 = createWebSocket(WSS_API_URL + "/chat/" + Taro.getStorageSync('AI_TOKEN')); wsRef.current.onMessage((data: any) => { console.log('收到WebSocket消息:', data); @@ -75,52 +71,84 @@ const AiChat = () => { setIsLoading(false); setCurrentTaskId(''); } else { - // 更新AI回复消息 + // 实时更新AI回复消息 setMessages(prev => { - const lastMessage = prev[prev.length - 1]; + const newMessages = [...prev]; + const lastMessage = newMessages[newMessages.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 - ); + // 直接追加内容到现有AI消息 + lastMessage.query = (lastMessage.query || '') + data.answer; } else { // 创建新的AI消息 - return [...prev, { + newMessages.push({ id: Date.now().toString(), type: 'ai', query: data.answer, timestamp: Date.now(), isTyping: true - }]; + }); } + + return newMessages; }); if (data.taskId) { setCurrentTaskId(data.taskId); } } - scrollToBottom(); + + // 实时滚动到底部 + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({behavior: 'smooth'}); + }, 50); } }); wsRef.current.onOpen(() => { console.log('WebSocket连接成功'); + setWsConnected(true); }); wsRef.current.onError((error: any) => { console.error('WebSocket连接错误:', error); + setWsConnected(false); + // Taro.showToast({ + // title: 'WebSocket连接失败', + // icon: 'none', + // duration: 2000 + // }); + }); + + wsRef.current.onClose(() => { + console.log('WebSocket连接关闭'); + setWsConnected(false); + // 如果正在加载中,显示连接断开提示 + if (isLoading) { + Taro.showToast({ + title: '连接已断开,正在重连...', + icon: 'none', + duration: 2000 + }); + } }); wsRef.current.connect().catch((error: any) => { console.error('WebSocket连接失败:', error); + // Taro.showToast({ + // title: 'WebSocket连接失败', + // icon: 'none', + // duration: 2000 + // }); }); }; useEffect(() => { + // 初始化时检查并生成AI Token + checkAiToken(); initWebSocket(); Taro.hideTabBar(); + return () => { if (wsRef.current) { wsRef.current.close(); @@ -136,26 +164,78 @@ const AiChat = () => { const handleSendMessage = async (content: string) => { if (!content.trim() || isLoading) return; + // 检查并确保AI Token存在 + const aiToken = checkAiToken(); + if (!aiToken) { + Taro.showToast({ + title: '初始化失败,请重试', + icon: 'none', + duration: 2000 + }); + return; + } + + // 检查WebSocket连接状态 + if (!wsConnected) { + Taro.showToast({ + title: '连接已断开,请稍后重试', + icon: 'none', + duration: 2000 + }); + return; + } + const userMessage: Message = { id: Date.now().toString(), type: 'user', query: content.trim(), timestamp: Date.now(), - user: `${Taro.getStorageSync('UserId')}` + user: `${Taro.getStorageSync('AI_TOKEN')}` }; + + // 立即添加用户消息 setMessages(prev => [...prev, userMessage]); setInputValue(''); setIsLoading(true); + // 立即添加一个空的AI消息占位符,准备接收流式回复 + const aiPlaceholder: Message = { + id: (Date.now() + 1).toString(), + type: 'ai', + query: '', + timestamp: Date.now(), + isTyping: true + }; + setMessages(prev => [...prev, aiPlaceholder]); + try { await sendAiMessage({ query: content.trim(), - user: `${Taro.getStorageSync('UserId')}`, + user: `${Taro.getStorageSync('AI_TOKEN')}`, responseMode: 'streaming', + aiToken: aiToken, // 包含AI Token }); } catch (error) { console.error('发送消息失败:', error); setIsLoading(false); + // 移除AI占位符消息 + setMessages(prev => prev.filter(msg => msg.id !== aiPlaceholder.id)); + + // 添加错误消息 + const errorMessage: Message = { + id: (Date.now() + 2).toString(), + type: 'ai', + query: '抱歉,发送消息时出现错误,请检查网络连接后重试。', + timestamp: Date.now(), + isTyping: false + }; + setMessages(prev => [...prev, errorMessage]); + + Taro.showToast({ + title: '发送失败,请重试', + icon: 'none', + duration: 2000 + }); } }; @@ -180,9 +260,31 @@ const AiChat = () => { handleSendMessage(question); }; + // 手动重连WebSocket + const handleReconnect = () => { + if (wsRef.current) { + wsRef.current.close(); + } + initWebSocket(); + }; + return ( -
+ {!wsConnected && ( + + + 连接已断开,正在重连... + + + + )} {messages.length === 0 ? ( @@ -200,14 +302,11 @@ const AiChat = () => { {message.type === 'user' ? : '🤖'} - {message.isTyping ? ( - - - - - - ) : ( - message.query + + {message.query || (message.isTyping && message.type === 'ai' ? '正在思考中...' : '')} + + {message.isTyping && message.type === 'ai' && message.query && ( + | )} @@ -240,9 +339,10 @@ const AiChat = () => {