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,51 +11,49 @@ ...@@ -11,51 +11,49 @@
: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" <div :class="['conversation', { active: conversation.id === activeConversationId }]">
@mouseout="hoverConversationId = null" <div class="title-wrapper">
> <img class="avatar" :src="roleAvatarDefaultImg" />
<div <div class="title-info">
:class=" <span class="user">{{ conversation.user || '匿名用户' }}</span>
conversation.id === activeConversationId ? 'conversation active' : 'conversation' <span class="time">{{ formatChatTime(conversation.chatTime) }}</span>
"
>
<div class="title-wrapper">
<img class="avatar" :src="roleAvatarDefaultImg" />
<div class="title-info">
<span class="user">{{ conversation.user || '匿名用户' }}</span>
<span class="time">{{ formatChatTime(conversation.chatTime) }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 加载更多指示器 --> <div v-if="hasMore" class="load-more-btn-wrapper">
<div v-if="loadingMore" class="loading-wrapper"> <div class="load-more-btn" @click="loadMore">
<el-icon class="is-loading"><Loading /></el-icon> <template v-if="!loadingMore">
<span>加载更多...</span> <span>加载更多</span>
</template>
<template v-else>
<el-icon class="is-loading"><Loading /></el-icon>
<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,83 +275,85 @@ onMounted(() => { ...@@ -282,83 +275,85 @@ 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 出现滚动条
.btn-new-conversation { box-sizing: border-box;
padding: 18px 0;
width: 100%; .header-section {
margin-bottom: 10px; padding: 10px;
flex-shrink: 0; // 确保筛选框不被压缩
} }
.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-item {
margin-bottom: 8px;
.conversation {
display: flex;
padding: 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
&:hover {
background-color: #f5f7fa;
}
&.active {
background-color: #eef5fe;
border-color: #409eff;
}
.conversation-items { .title-wrapper {
.conversation-item {
margin-top: 5px;
.conversation {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 8px; .avatar {
border-radius: 5px; width: 32px;
cursor: pointer; height: 32px;
&.active { border-radius: 50%;
background-color: #e6e6e6; margin-right: 10px;
} }
.title-wrapper { .title-info {
display: flex; display: flex;
align-items: center; flex-direction: column;
.title-info { .user {
display: flex; font-size: 14px;
flex-direction: column; font-weight: 500;
margin-left: 10px; color: #303133;
.user {
font-size: 14px;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time {
font-size: 12px;
color: #999;
line-height: 1.4;
}
} }
.avatar { .time {
width: 25px; font-size: 12px;
height: 25px; color: #999;
border-radius: 4px;
}
}
.button-wrapper {
display: flex;
.btn {
margin: 0;
padding: 4px;
} }
} }
} }
...@@ -366,12 +361,34 @@ onMounted(() => { ...@@ -366,12 +361,34 @@ onMounted(() => {
} }
} }
.tool-box { .load-more-btn-wrapper {
padding: 10px 20px; padding: 15px 0;
background: #f9f9f9; .load-more-btn {
display: flex; display: flex;
justify-content: space-between; justify-content: center;
cursor: pointer; 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;
}
.bottom-padding {
height: 20px;
} }
} }
</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