Commit 45a26348 authored by 水玉婷's avatar 水玉婷
Browse files

feat:历史记录添加上拉刷新下拉加载

parent 00898816
......@@ -36,7 +36,33 @@
</div>
</div>
<div class="history-list">
<div
class="history-list"
@scroll="handleScroll"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
>
<!-- 下拉刷新指示器 -->
<div
v-if="isPullingDown || isRefreshing"
class="refresh-indicator"
>
<div
class="refresh-spinner"
:class="{ 'refreshing': isRefreshing }"
></div>
<span v-if="!isRefreshing">
下拉刷新
</span>
<span v-else>刷新中...</span>
</div>
<!-- 历史记录列表 -->
<div
v-for="session in recentSessions"
:key="session.id"
......@@ -51,7 +77,25 @@
</button>
</div>
<div v-if="recentSessions.length === 0" class="empty-state">
<!-- 上拉加载更多提示 -->
<div v-if="hasMore && !isLoading && recentSessions.length > 0" class="pull-up-hint">
<div class="pull-up-icon"></div>
<span>上拉加载更多历史记录</span>
</div>
<!-- 加载更多指示器 -->
<div v-if="isLoading && !isRefreshing" class="load-more-indicator">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 没有更多数据提示 -->
<div v-if="!hasMore && recentSessions.length > 0" class="no-more-data">
<span>没有更多历史记录了</span>
</div>
<!-- 空状态 -->
<div v-if="recentSessions.length === 0 && !isLoading " class="empty-state">
<file-search-outlined class="empty-icon" />
<p>暂无历史记录</p>
</div>
......@@ -79,7 +123,7 @@
:token="userToken"
:appCode="appCode"
customClass="chat-history"
:key="currentSessionId+new Date().getTime()"
:key="currentSessionId"
/>
</div>
</div>
......@@ -105,6 +149,19 @@ const searchKeyword = ref('');
const currentSessionId = ref('');
const currentSessionDetail = ref({ title: '' });
// 分页相关数据
const currentPage = ref(1);
const pageSize = ref(20);
const hasMore = ref(true);
const isLoading = ref(false);
const isRefreshing = ref(false);
// 下拉刷新相关数据
const pullDownStartY = ref(0);
const pullDownDistance = ref(0);
const isPullingDown = ref(false);
const isMouseDown = ref(false);
// API配置
const getApiBaseUrl = () => {
......@@ -133,28 +190,166 @@ interface Session {
}
const historyList = ref<Session[]>([]);
// 获取对话记录
const getChatRecordList = async () => {
// 延时函数
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// 获取对话记录(支持分页)
const getChatRecordList = async (isLoadMore = false) => {
if (isLoading.value) return;
isLoading.value = true;
if (!isLoadMore) {
// 下拉刷新:重置分页,但不清空数据
currentPage.value = 1;
isRefreshing.value = true;
}
try {
// 添加延时效果,让加载过程更平滑
await delay(500);
let res = await axios.post(`${apiBaseUrl}/aiService/ask/history`, {
pageNum: 1,
pageSize: 20,
queryObject: {
appId: chatParams.appId,
offsetDay: 365,
},
pageNum: currentPage.value,
pageSize: pageSize.value,
queryObject: {
appId: chatParams.appId,
offsetDay: 365,
},
});
if (res.data.code === 0) {
let { data = [], total = 0, totalInfo = {} } = res.data.data || [];
historyList.value = (data.reverse() || []).map((item) => ({
let { data = [], total = 0, totalInfo = {} } = res.data.data || [];
// 反转数据,最新的在前面
const newData = (data.reverse() || []).map((item: any) => ({
...item,
isEdit: false,
}));
totalCount.value = total;
appName.value = totalInfo?.app_name || '';
}));
if (isLoadMore) {
// 上拉加载更多:追加数据
historyList.value = [...historyList.value, ...newData];
} else {
// 下拉刷新:替换数据
historyList.value = newData;
}
totalCount.value = total;
appName.value = totalInfo?.app_name || '';
// 判断是否还有更多数据
hasMore.value = historyList.value.length < total;
if (isLoadMore) {
currentPage.value++;
}
}
} catch (error) {
console.error('获取历史记录失败:', error);
} finally {
isLoading.value = false;
isRefreshing.value = false;
}
};
// 通用的开始事件处理
const handleStart = (clientY: number, target: HTMLElement) => {
// 只有在顶部且没有在滚动时才允许下拉刷新
if (target.scrollTop === 0 && !isRefreshing.value) {
pullDownStartY.value = clientY;
isPullingDown.value = true;
}
};
// 下拉刷新触摸事件处理
const handleTouchStart = (event: TouchEvent) => {
const target = event.currentTarget as HTMLElement;
handleStart(event.touches[0].clientY, target);
};
// 通用的移动事件处理
const handleMove = (clientY: number) => {
const isActive = isPullingDown.value || isMouseDown.value;
if (!isActive) return;
const pullDownDistance = Math.max(0, clientY - pullDownStartY.value);
// 移除最大下拉距离限制,让动画更自然
// 使用缓动函数让下拉效果更平滑
if (pullDownDistance > 100) {
// 超过100px后增加阻尼效果
// 这里可以添加其他逻辑,但不再存储到响应式变量
}
};
const handleTouchMove = (event: TouchEvent) => {
handleMove(event.touches[0].clientY);
};
// 通用的结束事件处理
const handleEnd = () => {
// 直接触发刷新,不再检查下拉距离
handleRefresh();
// 重置状态
isPullingDown.value = false;
isMouseDown.value = false;
};
const handleTouchEnd = () => {
if (!isPullingDown.value) return;
handleEnd();
};
// PC端鼠标事件处理
const handleMouseDown = (event: MouseEvent) => {
const target = event.currentTarget as HTMLElement;
handleStart(event.clientY, target);
isMouseDown.value = true;
};
const handleMouseMove = (event: MouseEvent) => {
handleMove(event.clientY);
};
const handleMouseUp = () => {
if (!isMouseDown.value) return;
handleEnd();
};
const handleMouseLeave = () => {
// 鼠标离开时取消下拉状态
if (isMouseDown.value) {
isMouseDown.value = false;
isPullingDown.value = false;
}
};
// 下拉刷新
const handleRefresh = () => {
getChatRecordList(false);
};
// 上拉加载更多
const handleLoadMore = () => {
if (!hasMore.value || isLoading.value) return;
getChatRecordList(true);
};
// 滚动事件处理
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = target;
// 检查是否滚动到底部(距离底部50px时触发加载)
if (scrollHeight - scrollTop - clientHeight < 50 && hasMore.value && !isLoading.value) {
handleLoadMore();
}
};
// 显示限制
const displayLimit = 20;
const displayLimit = computed(() => historyList.value.length);
// 计算属性:过滤后的会话列表
const filteredSessions = computed(() => {
......@@ -169,7 +364,7 @@ const filteredSessions = computed(() => {
// 计算属性:最近显示的会话列表
const recentSessions = computed(() => {
const sessions = filteredSessions.value;
return sessions.slice(0, displayLimit);
return sessions;
});
// 方法
const toggleHistoryPanel = () => {
......@@ -411,6 +606,88 @@ onMounted(() => {
flex: 1;
overflow-y: auto;
padding: 8px 0;
max-height: calc(100vh - 184px); // 设置固定高度,确保可以滚动
// 下拉刷新指示器
.refresh-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #666;
font-size: 14px;
background: #ffffff;
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 1;
.refresh-spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #1890ff;
border-radius: 50%;
margin-right: 8px;
transition: transform 0.3s ease;
&.refreshing {
animation: spin 1s linear infinite;
}
}
}
// 加载更多指示器
.load-more-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #666;
font-size: 14px;
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
}
// 上拉加载更多提示
.pull-up-hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #999;
font-size: 14px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
.pull-up-icon {
font-size: 20px;
margin-bottom: 8px;
animation: bounce 2s infinite;
color: #1890ff;
}
span {
font-size: 12px;
}
}
// 没有更多数据提示
.no-more-data {
text-align: center;
padding: 16px;
color: #999;
font-size: 14px;
}
.history-item {
display: flex;
......@@ -516,6 +793,19 @@ onMounted(() => {
}
}
// 弹跳动画
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-3px);
}
}
// 主内容区域
.main-content {
flex: 1;
......@@ -523,6 +813,12 @@ onMounted(() => {
flex-direction: column;
}
// 旋转动画
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// 响应式设计
@media (max-width: 768px) {
.history-sidebar {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment