优化:文章列表支持分页加载

This commit is contained in:
2025-07-11 20:12:03 +08:00
parent 6f3e634355
commit 86edcf21b3
35 changed files with 2247 additions and 101 deletions

121
src/pages/ai/debug-fix.md Normal file
View File

@@ -0,0 +1,121 @@
# AI问答页面输入问题修复
## 问题分析
第一次无法输入提问内容的可能原因:
1.**WebSocket连接问题**: 使用了错误的连接参数AI_TOKEN而不是UserId
2.**用户标识错误**: 消息中的user字段使用了AI_TOKEN而不是UserId
3.**初始化时序问题**: 输入框可能在初始化完成前被禁用
4.**状态管理问题**: 缺少初始化状态跟踪
## 修复内容
### 1. WebSocket连接修复
```typescript
// 修复前
wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + Taro.getStorageSync('AI_TOKEN'));
// 修复后
const userId = Taro.getStorageSync('UserId') || 'anonymous';
wsRef.current = createWebSocket(WSS_API_URL + "/chat/" + userId);
```
### 2. 用户标识修复
```typescript
// 修复前
user: `${Taro.getStorageSync('AI_TOKEN')}`
// 修复后
user: `${Taro.getStorageSync('UserId') || 'anonymous'}`
```
### 3. 初始化状态管理
```typescript
// 添加初始化状态
const [isInitialized, setIsInitialized] = useState(false);
// 初始化完成后设置状态
setIsInitialized(true);
```
### 4. 输入框状态优化
```typescript
// 输入框禁用逻辑
disabled={!isInitialized || isLoading}
// 占位符文本
placeholder={
!isInitialized ? "正在初始化..." :
isLoading ? "AI正在回复中..." :
"请输入您的问题..."
}
```
### 5. 发送按钮状态优化
```typescript
// 发送按钮禁用逻辑
disabled={!isInitialized || !inputValue.trim()}
```
### 6. 调试功能
- 添加了详细的状态调试信息
- 输入框焦点和点击事件监听
- 初始化完成状态跟踪
## 测试步骤
### 1. 基本输入测试
1. 打开AI问答页面
2. 等待初始化完成(看到"请输入您的问题..."
3. 点击输入框,检查是否能正常输入
4. 输入文字,检查是否正常显示
### 2. 快捷问题测试
1. 点击快捷问题按钮
2. 检查是否能正常发送消息
3. 验证初始化状态检查
### 3. 状态调试测试
1. 打开浏览器控制台
2. 查看初始化日志:
- "AI Token初始化完成: xxx"
- "输入框状态调试: {...}"
3. 点击输入框查看状态变化
### 4. WebSocket连接测试
1. 检查控制台WebSocket连接日志
2. 验证连接URL是否正确
3. 确认连接状态指示器工作正常
## 预期结果
- ✅ 页面加载后输入框立即可用
- ✅ 输入框状态正确显示
- ✅ 快捷问题正常工作
- ✅ WebSocket连接正常
- ✅ 消息发送功能正常
## 调试信息示例
控制台应该显示类似信息:
```
AI Token初始化完成: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
wsUrl: wss://cms-api.websoft.top/api/chat/12345
WebSocket连接成功
输入框状态调试: {
isInitialized: true,
isLoading: false,
wsConnected: true,
inputValue: "",
inputDisabled: false,
sendButtonDisabled: true
}
```
## 注意事项
1. 确保UserId存在否则使用'anonymous'作为默认值
2. AI_TOKEN仍然用于API请求认证
3. WebSocket连接使用UserId进行房间区分
4. 初始化状态确保用户体验流畅

View File

@@ -13,10 +13,11 @@ html {
}
.ai-chat {
height: 98vh;
height: calc(100vh - 80px); // 减去TabBar高度
display: flex;
flex-direction: column;
background-color: #f5f5f5;
margin-bottom: 80px; // 为TabBar预留空间
.chat-header {
background: linear-gradient(135deg, #a6ea66 0%, #ead1ff 100%);

View File

@@ -11,6 +11,7 @@ import MarkdownRenderer from '@/components/MarkdownRenderer';
import {View, RichText} from '@tarojs/components'
import './index.scss';
import {WSS_API_URL} from "@/utils/server";
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
// 消息类型
interface Message {
@@ -162,6 +163,7 @@ const AiChat = () => {
};
useEffect(() => {
Taro.hideTabBar()
// 初始化时检查并生成AI Token
const token = checkAiToken();
console.log('AI Token初始化完成:', token);

View File

@@ -0,0 +1,135 @@
# AI问答页面输入修复检查清单
## 🔧 修复内容总结
### 1. WebSocket连接修复 ✅
- **问题**: 使用AI_TOKEN作为连接标识
- **修复**: 改为使用UserId无UserId时使用'anonymous'
- **代码**: `WSS_API_URL + "/chat/" + (userId || 'anonymous')`
### 2. 用户标识修复 ✅
- **问题**: 消息中user字段使用AI_TOKEN
- **修复**: 改为使用UserId
- **代码**: `user: Taro.getStorageSync('UserId') || 'anonymous'`
### 3. 初始化状态管理 ✅
- **问题**: 缺少初始化完成状态跟踪
- **修复**: 添加isInitialized状态
- **功能**: 防止初始化期间的误操作
### 4. 输入框状态优化 ✅
- **问题**: 输入框可能被意外禁用
- **修复**: 基于初始化状态控制禁用
- **逻辑**: `disabled={!isInitialized || isLoading}`
### 5. 调试功能增强 ✅
- **添加**: 详细的状态调试信息
- **监听**: 输入框焦点和点击事件
- **日志**: 初始化过程跟踪
## 🧪 测试步骤
### 基础功能测试
1. **页面加载测试**
- [ ] 打开AI问答页面
- [ ] 检查控制台是否显示"AI Token初始化完成"
- [ ] 检查控制台是否显示"当前UserId"
- [ ] 等待看到"请输入您的问题..."占位符
2. **输入框测试**
- [ ] 点击输入框
- [ ] 检查是否能正常输入文字
- [ ] 检查控制台调试信息
- [ ] 验证输入框不会被意外禁用
3. **发送消息测试**
- [ ] 输入测试消息
- [ ] 点击发送按钮
- [ ] 检查消息是否正常发送
- [ ] 验证WebSocket连接正常
4. **快捷问题测试**
- [ ] 点击快捷问题
- [ ] 检查是否正常发送
- [ ] 验证初始化状态检查
### 边界情况测试
1. **无UserId情况**
- [ ] 清除本地存储中的UserId
- [ ] 刷新页面
- [ ] 检查是否使用'anonymous'
- [ ] 验证功能正常
2. **网络问题测试**
- [ ] 断开网络连接
- [ ] 检查连接状态提示
- [ ] 恢复网络
- [ ] 验证重连功能
3. **初始化期间操作**
- [ ] 页面加载时立即点击输入框
- [ ] 检查是否显示"正在初始化..."
- [ ] 验证不会出现错误
## 📊 预期控制台输出
```
AI Token初始化完成: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
当前UserId: 12345 (或 anonymous)
wsUrl: wss://cms-api.websoft.top/api/chat/12345
WebSocket连接成功
输入框状态调试: {
isInitialized: true,
isLoading: false,
wsConnected: true,
inputValue: "",
inputDisabled: false,
sendButtonDisabled: true
}
```
## 🚨 常见问题排查
### 问题1: 输入框仍然无法输入
**检查项**:
- [ ] isInitialized是否为true
- [ ] isLoading是否为false
- [ ] 控制台是否有错误信息
- [ ] 输入框disabled属性值
### 问题2: WebSocket连接失败
**检查项**:
- [ ] 网络连接是否正常
- [ ] WSS_API_URL是否正确
- [ ] UserId是否获取成功
- [ ] 服务器是否正常运行
### 问题3: 消息发送失败
**检查项**:
- [ ] AI_TOKEN是否生成成功
- [ ] API请求参数是否正确
- [ ] 网络请求是否成功
- [ ] 服务器响应是否正常
## 🔍 调试命令
在浏览器控制台中运行:
```javascript
// 检查当前状态
console.log('AI_TOKEN:', Taro.getStorageSync('AI_TOKEN'));
console.log('UserId:', Taro.getStorageSync('UserId'));
// 手动触发调试
// (需要在页面上下文中执行)
```
## ✅ 修复验证
所有测试通过后,应该能够:
- ✅ 页面加载后立即可以输入
- ✅ 快捷问题正常工作
- ✅ 消息发送和接收正常
- ✅ WebSocket连接稳定
- ✅ 错误处理完善
- ✅ 用户体验流畅

View File

@@ -0,0 +1,60 @@
# AI聊天实时显示优化完成
## 🚀 主要优化内容
### 1. 实时消息显示
-**优化WebSocket消息处理**:简化了消息更新逻辑,直接追加内容而不是复杂的状态管理
-**移除延迟效果**:去掉了打字机效果,改为实时显示内容
-**实时滚动**消息更新时立即滚动到底部延迟仅50ms
### 2. 用户体验改进
-**即时反馈**发送消息后立即显示AI占位符"正在思考中..."
-**智能按钮状态**:加载时显示"停止"按钮支持中断AI回复
-**输入框优化**:加载时禁用输入并显示状态提示
-**错误处理**:网络错误时显示友好的错误消息
### 3. 连接状态管理
-**连接状态指示器**实时显示WebSocket连接状态
-**智能重连机制**:递增重连间隔,避免频繁重连
-**手动重连**:提供"立即重连"按钮,用户可主动重连
-**连接检查**:发送消息前检查连接状态
### 4. 视觉效果优化
-**打字光标动画**AI回复时显示闪烁光标效果
-**流畅布局**:消息内容支持实时追加,无布局跳动
-**按钮样式**:优化发送和停止按钮的视觉效果
-**状态提示**:连接断开时显示醒目的状态栏
### 5. 性能优化
-**减少重渲染**:优化状态更新逻辑,减少不必要的组件重渲染
-**内存管理**正确清理WebSocket连接和定时器
-**错误边界**:添加完善的错误处理和用户提示
## 测试步骤
1. **基本功能测试**
- 打开AI聊天页面
- 发送一条消息
- 观察AI回复是否实时显示
2. **实时性测试**
- 发送较长的问题
- 观察回复内容是否逐字显示
- 检查是否有明显延迟
3. **连接状态测试**
- 断开网络连接
- 观察连接状态指示器
- 恢复网络,检查重连功能
4. **交互测试**
- 测试停止按钮功能
- 测试快捷问题点击
- 测试输入框状态变化
## 预期效果
- AI回复内容应该实时逐字显示无明显延迟
- 用户发送消息后立即看到"正在思考中..."提示
- 连接断开时有明确的状态提示
- 整体交互更加流畅和响应迅速

View File

@@ -1,5 +1,6 @@
import {useEffect} from "react";
import {Image, Space} from '@nutui/nutui-react-taro'
import {Image} from '@nutui/nutui-react-taro'
import {Space} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
const BestSellers = (props: any) => {

View File

@@ -2,7 +2,8 @@ import {useEffect, useState} from "react";
import Taro from '@tarojs/taro';
import {Button, Space} from '@nutui/nutui-react-taro'
import {TriangleDown,ArrowLeft} from '@nutui/icons-react-taro'
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {Popup, NavBar} from '@nutui/nutui-react-taro'
import {Image} from '@nutui/nutui-react-taro'
import {TenantId} from "@/utils/config";
const Header = (props: any) => {
@@ -82,9 +83,15 @@ const Header = (props: any) => {
<div style={{display: 'flex', alignItems: 'center'}}>
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Space>
<Avatar
size="22"
src={props.user?.avatar}
<Image
src={props.user?.avatar || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMzAiIGZpbGw9IiNGNUY1RjUiLz4KPGNpcmNsZSBjeD0iMzAiIGN5PSIyNCIgcj0iMTAiIGZpbGw9IiNEOUQ5RDkiLz4KPHBhdGggZD0iTTEwIDUwQzEwIDQwIDIwIDM1IDMwIDM1QzQwIDM1IDUwIDQwIDUwIDUwIiBmaWxsPSIjRDlEOUQ5Ii8+Cjwvc3ZnPgo='}
style={{
width: '22px',
height: '22px',
borderRadius: '50%',
objectFit: 'cover'
}}
mode="aspectFill"
/>
<span style={{color: '#000'}}>{props.user?.nickname}</span>
</Space>

View File

@@ -23,11 +23,26 @@ const Page = () => {
<div className={'py-2 my-3 mx-2'}>
<div className={'bg-white grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-2'}>
{
navItems?.map((item) => (
<div className={'flex flex-col justify-center items-center'} onClick={() => Taro.navigateTo({url: `/${item.model}/index?id=${item.navigationId}`})}>
<Image className={'shadow-xl rounded-lg'} style={{borderRadius: '8px'}} src={item.icon}
height={90} width={90}/>
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>{item?.title}</div>
navItems?.map((item, index) => (
<div
key={item.navigationId || index}
className={'flex flex-col justify-center items-center'}
onClick={() => Taro.navigateTo({url: `/${item.model}/index?id=${item.navigationId}`})}
>
<Image
className={'shadow-xl rounded-lg'}
style={{
borderRadius: '8px',
width: '90px',
height: '90px',
objectFit: 'cover'
}}
src={item.icon}
mode="aspectFill"
/>
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>
{item?.title}
</div>
</div>
))
}

View File

@@ -5,8 +5,8 @@ import {getSiteInfo} from "@/api/layout";
import Login from "./Login";
import Banner from "./Banner";
import Menu from "./Menu";
import TabBar from "@/components/TabBar";
import Image from "./Image";
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
function Home() {
const [loading, setLoading] = useState<boolean>(false)
@@ -29,7 +29,7 @@ function Home() {
};
useEffect(() => {
Taro.showTabBar()
Taro.hideTabBar()
// 获取站点信息
getSiteInfo().then((data) => {
console.log(data, 'siteInfo')
@@ -45,8 +45,9 @@ function Home() {
<Image/>
<Menu/>
<Banner/>
<TabBar/>
</>)}
{/* H5模式下显示自定义TabBar */}
{process.env.TARO_ENV === 'h5' && <SimpleH5TabBar current={0} />}
</div>
)
}

View File

@@ -1,8 +1,7 @@
import {useEffect, useState} from 'react'
import {navigateTo} from '@tarojs/taro'
import Taro from '@tarojs/taro'
import {Button} from '@tarojs/components';
import {Image} from '@nutui/nutui-react-taro'
import {Button, Image} from '@tarojs/components';
import {getUserInfo, getWxOpenId} from "@/api/layout";
import {TenantId} from "@/utils/config";
import {User} from "@/api/system/user/model";

View File

@@ -1,5 +1,5 @@
import {Avatar} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react";
import {Avatar} from '@nutui/nutui-react-taro'
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'

View File

@@ -2,20 +2,25 @@ import {useEffect} from 'react'
import UserCard from "./components/UserCard";
import UserCell from "./components/UserCell";
import TabBar from "@/components/TabBar";
import SimpleH5TabBar from "@/components/SimpleH5TabBar";
import Taro from '@tarojs/taro';
function User() {
useEffect(() => {
}, []);
return (
<div style={{ backgroundColor: '#ffefef', height: '100vh'}}>
<div className={'fixed w-full'}>
<UserCard />
<UserCell />
<TabBar/>
</div>
</div>
)
useEffect(() => {
Taro.hideTabBar()
}, []);
return (
<div style={{backgroundColor: '#ffefef', height: '100vh'}}>
<div className={'fixed w-full'}>
<UserCard/>
<UserCell/>
{/* 小程序模式显示原TabBarH5模式显示H5TabBar */}
{process.env.TARO_ENV !== 'h5' && <TabBar/>}
{process.env.TARO_ENV === 'h5' && <SimpleH5TabBar current={2}/>}
</div>
</div>
)
}
export default User