Commit da7791dc by pengshun

fix: page chat analysis

parent 2005e2c8
No preview for this file type
...@@ -3,25 +3,22 @@ ...@@ -3,25 +3,22 @@
<mapper namespace="cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper"> <mapper namespace="cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper">
<select id="selectPageLatestByChatId" resultType="cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO"> <select id="selectPageLatestByChatId" resultType="cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO">
SELECT a.* SELECT *
FROM (
SELECT a.*,
ROW_NUMBER() OVER (
PARTITION BY a.chat_id
ORDER BY a.chat_time DESC, a.id DESC -- 按时间升序取最早,若时间相同则取id最小的
) AS rn
FROM ai_chat_analysis a FROM ai_chat_analysis a
INNER JOIN (
SELECT chat_id, MAX(chat_time) AS max_time
FROM ai_chat_analysis
<where> <where>
<if test="reqVO.user != null and reqVO.user != ''">
AND `user` = #{reqVO.user}
</if>
<if test="reqVO.question != null and reqVO.question != ''">
AND question LIKE CONCAT('%', #{reqVO.question}, '%')
</if>
<if test="reqVO.chatTime != null and reqVO.chatTime.length == 2"> <if test="reqVO.chatTime != null and reqVO.chatTime.length == 2">
AND chat_time BETWEEN #{reqVO.chatTime[0]} AND #{reqVO.chatTime[1]} AND a.chat_time BETWEEN #{reqVO.chatTime[0]} AND #{reqVO.chatTime[1]}
</if> </if>
</where> </where>
GROUP BY chat_id ) t
) b ON a.chat_id = b.chat_id AND a.chat_time = b.max_time WHERE t.rn = 1
ORDER BY a.chat_time DESC ORDER BY t.chat_time DESC, t.id DESC -- 最终按时间倒序排序,满足分页需求
</select> </select>
</mapper> </mapper>
\ No newline at end of file
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<div class="stat-main"> <div class="stat-main">
<span class="num">{{ statsData?.totalCount || 0 }}</span> <span class="num">{{ statsData?.totalCount || 0 }}</span>
<span :class="['growth', (statsData?.percentQAPrevious || 0) >= 0 ? 'up' : 'down']"> <span :class="['growth', (statsData?.percentQAPrevious || 0) >= 0 ? 'up' : 'down']">
{{ (statsData?.percentQAPrevious || 0) >= 0 ? '↑' : '↓' }} {{ (statsData?.percentQAPrevious || 100) >= 0 ? '↑' : '↓' }}
{{ Math.abs(statsData?.percentQAPrevious || 0).toFixed(2) }}% {{ Math.abs(statsData?.percentQAPrevious || 100).toFixed(2) }}%
</span> </span>
</div> </div>
<div class="compare-text">较前一{{ config.compareUnit }}</div> <div class="compare-text">较前一{{ config.compareUnit }}</div>
......
<template> <template>
<el-aside width="260px" style="height: 100vh" class="conversation-container h-100%"> <el-aside width="260px" class="conversation-container">
<div class="h-100%"> <div class="header-section">
<el-date-picker <el-date-picker
v-model="searchDate" v-model="searchDate"
type="date" type="date"
...@@ -11,30 +11,24 @@ ...@@ -11,30 +11,24 @@
:editable="false" :editable="false"
@change="searchConversation" @change="searchConversation"
/> />
</div>
<div class="conversation-list" ref="conversationListRef" @scroll="handleScroll"> <div class="conversation-list" ref="conversationListRef">
<!-- 加载中(首次) -->
<div v-if="loading" class="loading-wrapper"> <div v-if="loading" class="loading-wrapper">
<el-icon class="is-loading"><Loading /></el-icon> <el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span> <span>加载中...</span>
</div> </div>
<!-- 无数据 -->
<el-empty v-else-if="conversationList.length === 0" description="暂无对话记录" /> <el-empty v-else-if="conversationList.length === 0" description="暂无对话记录" />
<!-- 列表 -->
<div v-else class="conversation-items"> <div v-else class="conversation-items">
<div <div
class="conversation-item" class="conversation-item"
v-for="conversation in conversationList" v-for="conversation in conversationList"
:key="conversation.id" :key="conversation.id"
@click="handleConversationClick(conversation.id)" @click="handleConversationClick(conversation.id)"
@mouseover="hoverConversationId = conversation.id"
@mouseout="hoverConversationId = null"
>
<div
:class="
conversation.id === activeConversationId ? 'conversation active' : 'conversation'
"
> >
<div :class="['conversation', { active: conversation.id === activeConversationId }]">
<div class="title-wrapper"> <div class="title-wrapper">
<img class="avatar" :src="roleAvatarDefaultImg" /> <img class="avatar" :src="roleAvatarDefaultImg" />
<div class="title-info"> <div class="title-info">
...@@ -44,18 +38,22 @@ ...@@ -44,18 +38,22 @@
</div> </div>
</div> </div>
</div> </div>
</div> <div v-if="hasMore" class="load-more-btn-wrapper">
<!-- 加载更多指示器 --> <div class="load-more-btn" @click="loadMore">
<div v-if="loadingMore" class="loading-wrapper"> <template v-if="!loadingMore">
<span>加载更多</span>
</template>
<template v-else>
<el-icon class="is-loading"><Loading /></el-icon> <el-icon class="is-loading"><Loading /></el-icon>
<span>加载更多...</span> <span>加载中...</span>
</template>
</div>
</div> </div>
<div class="h-160px w-100%"></div> <div v-else class="no-more-tip">没有更多了~</div>
</div> </div>
</div> </div>
</el-aside> </el-aside>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, toRefs, watch, reactive } from 'vue' import { ref, onMounted, toRefs, watch, reactive } from 'vue'
import { ChatAnalysisApi, ChatAnalysisVO } from '@/api/ai/chat/analysis' import { ChatAnalysisApi, ChatAnalysisVO } from '@/api/ai/chat/analysis'
...@@ -154,8 +152,8 @@ const loadData = async (append = false) => { ...@@ -154,8 +152,8 @@ const loadData = async (append = false) => {
if (append) { if (append) {
// 追加数据,需去重(根据 id) // 追加数据,需去重(根据 id)
const existingIds = new Set(conversationList.value.map(item => item.id)) const existingIds = new Set(conversationList.value.map(item => item.chatId))
const uniqueNewList = newList.filter(item => !existingIds.has(item.id)) const uniqueNewList = newList.filter(item => !existingIds.has(item.chatId))
conversationList.value.push(...uniqueNewList) conversationList.value.push(...uniqueNewList)
} else { } else {
conversationList.value = newList conversationList.value = newList
...@@ -189,23 +187,18 @@ const loadData = async (append = false) => { ...@@ -189,23 +187,18 @@ const loadData = async (append = false) => {
} }
/** 加载下一页 */ /** 加载下一页 */
/** const loadMore = async () => {
if (!hasMore.value || loadingMore.value || loading.value) return
queryParams.pageNo++
await loadData(true)
} */
const loadMore = async () => { const loadMore = async () => {
if (!hasMore.value || loadingMore.value || loading.value) return if (!hasMore.value || loadingMore.value || loading.value) return
queryParams.pageNo++ queryParams.pageNo++
await loadData(true) await loadData(true)
} }
/** 滚动监听 */
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
const scrollTop = target.scrollTop
const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight
// 滚动到底部(预留 10px 阈值)
if (scrollTop + clientHeight + 10 >= scrollHeight) {
loadMore()
}
}
/** 触发搜索(重置分页) */ /** 触发搜索(重置分页) */
const searchConversation = async () => { const searchConversation = async () => {
...@@ -282,96 +275,120 @@ onMounted(() => { ...@@ -282,96 +275,120 @@ onMounted(() => {
.conversation-container { .conversation-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; height: 100%; // 锁定侧边栏高度为视口高度
overflow: hidden; background-color: #fff;
height: 100%; border-right: 1px solid #e6e6e6;
overflow: hidden; // 禁止 aside 出现滚动条
box-sizing: border-box;
.btn-new-conversation { .header-section {
padding: 18px 0; padding: 10px;
width: 100%; flex-shrink: 0; // 确保筛选框不被压缩
margin-bottom: 10px;
} }
.search-date-picker { .search-date-picker {
width: 100% !important; width: 100% !important;
margin-bottom: 10px;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
width: 100%; width: 100%;
box-sizing: border-box;
} }
} }
.conversation-list { .conversation-list {
flex: 1; flex: 1; // 核心:自动填满剩下的所有垂直空间
overflow-y: auto; overflow-y: auto; // 核心:内容超出时在这里产生滚动条
height: 100%; padding: 0 10px;
// 自定义滚动条美化(可选)
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 10px;
}
.loading-wrapper { .loading-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
padding: 20px; padding: 20px;
gap: 8px;
color: #909399; color: #909399;
} }
}
.conversation-items { .conversation-items {
.conversation-item { .conversation-item {
margin-top: 5px; margin-bottom: 8px;
.conversation { .conversation {
display: flex; display: flex;
justify-content: space-between; padding: 10px;
align-items: center; border-radius: 8px;
padding: 8px;
border-radius: 5px;
cursor: pointer; cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
&:hover {
background-color: #f5f7fa;
}
&.active { &.active {
background-color: #e6e6e6; background-color: #eef5fe;
border-color: #409eff;
} }
.title-wrapper { .title-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 10px;
}
.title-info { .title-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 10px;
.user { .user {
font-size: 14px; font-size: 14px;
max-width: 140px; font-weight: 500;
overflow: hidden; color: #303133;
text-overflow: ellipsis;
white-space: nowrap;
} }
.time { .time {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
line-height: 1.4;
} }
} }
.avatar {
width: 25px;
height: 25px;
border-radius: 4px;
} }
} }
.button-wrapper {
display: flex;
.btn {
margin: 0;
padding: 4px;
} }
} }
.load-more-btn-wrapper {
padding: 15px 0;
.load-more-btn {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
padding: 8px;
background: #f0f2f5;
border-radius: 4px;
font-size: 13px;
color: #606266;
cursor: pointer;
&:hover {
background: #e4e7ed;
} }
} }
} }
.no-more-tip {
text-align: center;
padding: 15px;
color: #c0c4cc;
font-size: 12px;
} }
.tool-box { .bottom-padding {
padding: 10px 20px; height: 20px;
background: #f9f9f9;
display: flex;
justify-content: space-between;
cursor: pointer;
} }
} }
</style> </style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment