Commit f63ea6ae by pengshun

fix: 对话页面调整

parent 0d09257b
No preview for this file type
...@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl ...@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime; import java.time.LocalDateTime;
...@@ -32,6 +34,9 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> { ...@@ -32,6 +34,9 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> {
.orderByDesc(AiChatAnalysisDO::getChatTime)); .orderByDesc(AiChatAnalysisDO::getChatTime));
} }
IPage<AiChatAnalysisDO> selectPageLatestByChatId(IPage<AiChatAnalysisDO> page,
@Param("reqVO") AiChatAnalysisPageReqVO reqVO);
default List<AiChatAnalysisDO> selectByChatId(String chatId) { default List<AiChatAnalysisDO> selectByChatId(String chatId) {
return selectList(new LambdaQueryWrapperX<AiChatAnalysisDO>() return selectList(new LambdaQueryWrapperX<AiChatAnalysisDO>()
.eqIfPresent(AiChatAnalysisDO::getChatId, chatId) .eqIfPresent(AiChatAnalysisDO::getChatId, chatId)
......
...@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; ...@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.analysis.*; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.analysis.*;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -87,7 +89,13 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService { ...@@ -87,7 +89,13 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
@Override @Override
public PageResult<AiChatAnalysisDO> getChatAnalysisPage(AiChatAnalysisPageReqVO pageReqVO) { public PageResult<AiChatAnalysisDO> getChatAnalysisPage(AiChatAnalysisPageReqVO pageReqVO) {
return chatAnalysisMapper.selectPageAll(pageReqVO); // return chatAnalysisMapper.selectPageAll(pageReqVO);
// 创建分页对象
Page<AiChatAnalysisDO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
// 执行自定义分组查询
IPage<AiChatAnalysisDO> pageResult = chatAnalysisMapper.selectPageLatestByChatId(page, pageReqVO);
// 封装返回
return new PageResult<>(pageResult.getRecords(), pageResult.getTotal());
} }
@Override @Override
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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 a.*
FROM ai_chat_analysis a
INNER JOIN (
SELECT chat_id, MAX(chat_time) AS max_time
FROM ai_chat_analysis
<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.size() == 2">
AND chat_time BETWEEN #{reqVO.chatTime[0]} AND #{reqVO.chatTime[1]}
</if>
</where>
GROUP BY chat_id
) b ON a.chat_id = b.chat_id AND a.chat_time = b.max_time
ORDER BY a.chat_time DESC
</select>
</mapper>
\ No newline at end of file
...@@ -10,8 +10,6 @@ declare module 'vue' { ...@@ -10,8 +10,6 @@ declare module 'vue' {
AddNode: typeof import('./../components/SimpleProcessDesigner/src/addNode.vue')['default'] AddNode: typeof import('./../components/SimpleProcessDesigner/src/addNode.vue')['default']
AppLinkInput: typeof import('./../components/AppLinkInput/index.vue')['default'] AppLinkInput: typeof import('./../components/AppLinkInput/index.vue')['default']
AppLinkSelectDialog: typeof import('./../components/AppLinkInput/AppLinkSelectDialog.vue')['default'] AppLinkSelectDialog: typeof import('./../components/AppLinkInput/AppLinkSelectDialog.vue')['default']
'AutoComponents.d': typeof import('./auto-components.d.ts')['default']
'AutoImports.d': typeof import('./auto-imports.d.ts')['default']
Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default'] Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default']
CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default'] CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default']
ColorInput: typeof import('./../components/ColorInput/index.vue')['default'] ColorInput: typeof import('./../components/ColorInput/index.vue')['default']
......
<template> <template>
<el-aside width="260px" class="conversation-container h-100%"> <el-aside width="260px" style="height: 100vh" class="conversation-container h-100%">
<div class="h-100%"> <div class="h-100%">
<el-date-picker <el-date-picker
v-model="searchDate" v-model="searchDate"
type="date" type="date"
...@@ -13,18 +12,19 @@ ...@@ -13,18 +12,19 @@
@change="searchConversation" @change="searchConversation"
/> />
<div class="conversation-list"> <div class="conversation-list" ref="conversationListRef" @scroll="handleScroll">
<el-empty v-if="loading" description="." :v-loading="loading" /> <!-- 加载中(首次) -->
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey"> <div v-if="loading" class="loading-wrapper">
<div <el-icon class="is-loading"><Loading /></el-icon>
class="conversation-item classify-title" <span>加载中...</span>
v-if="conversationMap[conversationKey].length"
>
<el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
</div> </div>
<!-- 无数据 -->
<el-empty v-else-if="conversationList.length === 0" description="暂无对话记录" />
<!-- 列表 -->
<div v-else class="conversation-items">
<div <div
class="conversation-item" class="conversation-item"
v-for="conversation in conversationMap[conversationKey]" v-for="conversation in conversationList"
:key="conversation.id" :key="conversation.id"
@click="handleConversationClick(conversation.id)" @click="handleConversationClick(conversation.id)"
@mouseover="hoverConversationId = conversation.id" @mouseover="hoverConversationId = conversation.id"
...@@ -37,27 +37,19 @@ ...@@ -37,27 +37,19 @@
> >
<div class="title-wrapper"> <div class="title-wrapper">
<img class="avatar" :src="roleAvatarDefaultImg" /> <img class="avatar" :src="roleAvatarDefaultImg" />
<span class="title">{{ conversation.question || conversation.user }}</span> <div class="title-info">
<span class="user">{{ conversation.user || '匿名用户' }}</span>
<span class="time">{{ formatChatTime(conversation.chatTime) }}</span>
</div> </div>
<div class="button-wrapper" v-show="hoverConversationId === conversation.id">
<el-button class="btn" link @click.stop="handleTop(conversation)">
<el-icon title="置顶" v-if="!(conversation as any).pinned"><Top /></el-icon>
<el-icon title="取消置顶" v-if="(conversation as any).pinned"><Bottom /></el-icon>
</el-button>
<el-button class="btn" link @click.stop="updateConversationUser(conversation)">
<el-icon title="编辑">
<Icon icon="ep:edit" />
</el-icon>
</el-button>
<el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
<el-icon title="删除对话">
<Icon icon="ep:delete" />
</el-icon>
</el-button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 加载更多指示器 -->
<div v-if="loadingMore" class="loading-wrapper">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载更多...</span>
</div>
<div class="h-160px w-100%"></div> <div class="h-160px w-100%"></div>
</div> </div>
</div> </div>
...@@ -65,24 +57,33 @@ ...@@ -65,24 +57,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, toRefs, watch } from 'vue' import { ref, onMounted, toRefs, watch, reactive } from 'vue'
// 导入新的接口和VO
import { ChatAnalysisApi, ChatAnalysisVO } from '@/api/ai/chat/analysis' import { ChatAnalysisApi, ChatAnalysisVO } from '@/api/ai/chat/analysis'
import RoleRepository from '../role/RoleRepository.vue'
import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { Loading } from '@element-plus/icons-vue'
const message = useMessage() const message = useMessage()
// 分页查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
chatTime: [] as string[],
})
// 定义属性 // 定义属性
const searchDate = ref<string | null>(null) // 筛选日期 const searchDate = ref<string | null>(null) // 筛选日期
const activeConversationId = ref<number | string | null>(null) const activeConversationId = ref<number | string | null>(null)
const hoverConversationId = ref<number | string | null>(null) const hoverConversationId = ref<number | string | null>(null)
const conversationList = ref([] as ChatAnalysisVO[]) const conversationList = ref<ChatAnalysisVO[]>([])
const conversationMap = ref<any>({})
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false) // 是否正在加载更多
const total = ref<number>(0) // 总记录数
const hasMore = ref<boolean>(true) // 是否还有更多数据
const conversationListRef = ref<HTMLElement | null>(null)
// 定义组件 props // 定义组件 props
const props = defineProps({ const props = defineProps({
...@@ -99,71 +100,118 @@ const emits = defineEmits([ ...@@ -99,71 +100,118 @@ const emits = defineEmits([
'onConversationDelete' 'onConversationDelete'
]) ])
/** 核心数据加载逻辑 */ /** 时间格式化函数 */
const loadData = async () => { const formatChatTime = (time: string | Date | undefined) => {
if (!time) return '未知时间'
const date = new Date(time)
if (isNaN(date.getTime())) return '无效时间'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
/** 核心数据加载逻辑(支持重置或追加) */
const loadData = async (append = false) => {
try { try {
if (!append) {
loading.value = true loading.value = true
let res: any // 重置分页参数
queryParams.pageNo = 1
if (!searchDate.value) { conversationList.value = []
// 默认查询 total.value = 0
res = await ChatAnalysisApi.getChatConversationMyList() hasMore.value = true
} else { } else {
// 适配后端:LocalDateTime[] 格式,发送该日期的全天范围 loadingMore.value = true
const chatTime = [ }
// 构建请求参数
const params: any = {
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
}
// 日期筛选
if (searchDate.value) {
params.chatTime = [
`${searchDate.value} 00:00:00`, `${searchDate.value} 00:00:00`,
`${searchDate.value} 23:59:59` `${searchDate.value} 23:59:59`
] ]
res = await ChatAnalysisApi.getChatAnalysisSearch({ } else {
chatTime, // 不传日期范围时,后端可能返回全部
pageNo: 1, // 如果后端支持不传,则省略 chatTime 字段
pageSize: 100
})
} }
// 处理分页返回或数组返回 // 调用分页接口
conversationList.value = res?.list ? res.list : (res || []) const res = await ChatAnalysisApi.getChatAnalysisPage(params)
// 排序 // 处理返回数据(假设后端返回格式为 { list: [], total: number })
conversationList.value.sort((a, b) => { const newList = res?.list || []
const tA = a.chatTime ? new Date(a.chatTime).getTime() : 0 total.value = res?.total || 0
const tB = b.chatTime ? new Date(b.chatTime).getTime() : 0
return tB - tA if (append) {
}) // 追加数据,需去重(根据 id)
const existingIds = new Set(conversationList.value.map(item => item.id))
const uniqueNewList = newList.filter(item => !existingIds.has(item.id))
conversationList.value.push(...uniqueNewList)
} else {
conversationList.value = newList
}
// 分组渲染 // 判断是否还有更多
conversationMap.value = await getConversationGroupByChatTime(conversationList.value) hasMore.value = conversationList.value.length < total.value
} catch (error) {
console.error('加载对话列表失败', error)
if (!append) {
conversationList.value = []
total.value = 0
hasMore.value = false
}
} finally { } finally {
if (!append) {
loading.value = false
// 如果有数据,默认选中第一条
if (conversationList.value.length) { if (conversationList.value.length) {
activeConversationId.value = conversationList.value[0].id activeConversationId.value = conversationList.value[0].id
// 回调 onConversationClick
await emits('onConversationClick', conversationList.value[0]) await emits('onConversationClick', conversationList.value[0])
} else {
// 无数据时清空选中状态
activeConversationId.value = null
await emits('onConversationClick', null)
}
} else {
loadingMore.value = false
} }
loading.value = false
} }
} }
/** 触发搜索 */ /** 加载下一页 */
const searchConversation = async () => { const loadMore = async () => {
await loadData() if (!hasMore.value || loadingMore.value || loading.value) return
queryParams.pageNo++
await loadData(true)
} }
/** 按照 chatTime 分组 */ /** 滚动监听 */
const getConversationGroupByChatTime = async (list: ChatAnalysisVO[]) => { const handleScroll = (e: Event) => {
const groupMap = { 置顶: [], 今天: [], 一天前: [], 三天前: [], 七天前: [], 三十天前: [] } const target = e.target as HTMLElement
const now = Date.now() const scrollTop = target.scrollTop
const oneDay = 24 * 60 * 60 * 1000 const scrollHeight = target.scrollHeight
for (const conv of list) { const clientHeight = target.clientHeight
if ((conv as any).pinned) { groupMap['置顶'].push(conv); continue } // 滚动到底部(预留 10px 阈值)
const cTime = conv.chatTime ? new Date(conv.chatTime).getTime() : 0 if (scrollTop + clientHeight + 10 >= scrollHeight) {
const diff = now - cTime loadMore()
if (diff < oneDay) groupMap['今天'].push(conv)
else if (diff < 3 * oneDay) groupMap['一天前'].push(conv)
else if (diff < 7 * oneDay) groupMap['三天前'].push(conv)
else if (diff < 30 * oneDay) groupMap['七天前'].push(conv)
else groupMap['三十天前'].push(conv)
} }
return groupMap }
/** 触发搜索(重置分页) */
const searchConversation = async () => {
// 重置页码
queryParams.pageNo = 1
await loadData(false)
} }
/** 点击对话 */ /** 点击对话 */
...@@ -175,11 +223,11 @@ const handleConversationClick = async (id: number | string) => { ...@@ -175,11 +223,11 @@ const handleConversationClick = async (id: number | string) => {
} }
} }
/** 新建对话 */ /** 新建对话(保持原有逻辑) */
const createConversation = async () => { const createConversation = async () => {
const res = await ChatAnalysisApi.createChatAnalysis({ user: '新用户' } as ChatAnalysisVO) const res = await ChatAnalysisApi.createChatAnalysis({ user: '新用户' } as ChatAnalysisVO)
const id = (res as any).id || res const id = (res as any).id || res
await loadData() await searchConversation() // 刷新列表并重置分页
handleConversationClick(id) handleConversationClick(id)
emits('onConversationCreate') emits('onConversationCreate')
} }
...@@ -193,30 +241,30 @@ const updateConversationUser = async (conversation: ChatAnalysisVO) => { ...@@ -193,30 +241,30 @@ const updateConversationUser = async (conversation: ChatAnalysisVO) => {
}) })
await ChatAnalysisApi.updateChatAnalysis({ id: conversation.id, user: value } as ChatAnalysisVO) await ChatAnalysisApi.updateChatAnalysis({ id: conversation.id, user: value } as ChatAnalysisVO)
message.success('修改成功') message.success('修改成功')
await loadData() await searchConversation() // 刷新列表
} }
/** 删除对话 */ /** 删除对话 */
const deleteChatConversation = async (conversation: ChatAnalysisVO) => { const deleteChatConversation = async (conversation: ChatAnalysisVO) => {
await message.delConfirm(`确认删除 - ${conversation.user}?`) await message.delConfirm(`确认删除 - ${conversation.user}?`)
await ChatAnalysisApi.deleteChatAnalysis(conversation.id.toString()) await ChatAnalysisApi.deleteChatAnalysis(conversation.id.toString())
await loadData() await searchConversation() // 刷新列表
emits('onConversationDelete', conversation) emits('onConversationDelete', conversation)
} }
/** 清空对话 */ /** 清空对话(本示例未实现清空逻辑,保留原有结构) */
const handleClearConversation = async () => { const handleClearConversation = async () => {
await message.confirm('确认清空?') await message.confirm('确认清空?')
// 这里需根据后端是否有对应接口调用,暂时模拟刷新 // 实际清空操作应调用后端接口
await loadData() await searchConversation()
emits('onConversationClear') emits('onConversationClear')
} }
/** 置顶 */ /** 置顶(如需保留置顶功能,可重新实现) */
const handleTop = async (conversation: ChatAnalysisVO) => { const handleTop = async (conversation: ChatAnalysisVO) => {
;(conversation as any).pinned = !(conversation as any).pinned ;(conversation as any).pinned = !(conversation as any).pinned
await ChatAnalysisApi.updateChatAnalysis(conversation) await ChatAnalysisApi.updateChatAnalysis(conversation)
await loadData() await searchConversation()
} }
const roleRepositoryOpen = ref(false) const roleRepositoryOpen = ref(false)
...@@ -225,28 +273,28 @@ const handleRoleRepository = () => roleRepositoryOpen.value = true ...@@ -225,28 +273,28 @@ const handleRoleRepository = () => roleRepositoryOpen.value = true
const { activeId } = toRefs(props) const { activeId } = toRefs(props)
watch(activeId, (val) => activeConversationId.value = val) watch(activeId, (val) => activeConversationId.value = val)
onMounted(() => loadData()) onMounted(() => {
loadData(false) // 首次加载
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.conversation-container { .conversation-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; // 统一侧边栏边距 padding: 10px;
overflow: hidden; overflow: hidden;
height: 100%;
.btn-new-conversation { .btn-new-conversation {
padding: 18px 0; padding: 18px 0;
width: 100%; // 确保宽度铺满 width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
} }
// 关键修改:使 DatePicker 与按钮完全一致
.search-date-picker { .search-date-picker {
width: 100% !important; width: 100% !important;
margin-bottom: 10px; margin-bottom: 10px;
// 穿透修改 Element Plus 内部样式
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
...@@ -256,7 +304,18 @@ onMounted(() => loadData()) ...@@ -256,7 +304,18 @@ onMounted(() => loadData())
.conversation-list { .conversation-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
height: 100%;
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
gap: 8px;
color: #909399;
}
.conversation-items {
.conversation-item { .conversation-item {
margin-top: 5px; margin-top: 5px;
.conversation { .conversation {
...@@ -266,27 +325,45 @@ onMounted(() => loadData()) ...@@ -266,27 +325,45 @@ onMounted(() => loadData())
padding: 8px; padding: 8px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
&.active { background-color: #e6e6e6; } &.active {
background-color: #e6e6e6;
}
.title-wrapper { .title-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
.title { .title-info {
padding-left: 10px; display: flex;
flex-direction: column;
margin-left: 10px;
.user {
font-size: 14px; font-size: 14px;
max-width: 140px; max-width: 140px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.avatar { width: 25px; height: 25px; border-radius: 4px; } .time {
font-size: 12px;
color: #999;
line-height: 1.4;
}
}
.avatar {
width: 25px;
height: 25px;
border-radius: 4px;
}
} }
.button-wrapper { .button-wrapper {
display: flex; display: flex;
.btn { margin: 0; padding: 4px; } .btn {
margin: 0;
padding: 4px;
}
}
} }
} }
} }
.classify-title { padding: 10px 0 5px; }
} }
.tool-box { .tool-box {
......
...@@ -22,13 +22,21 @@ ...@@ -22,13 +22,21 @@
</div> </div>
<div class="message"> <div class="message">
<div> <div>
<el-text class="time">AI 助手 · 耗时 {{ item.duration || 0 }}s</el-text> <el-text class="time">AI 助手</el-text>
</div> </div>
<div class="left-text-container"> <div class="left-text-container">
<MarkdownView class="left-text" :content="item.answer || '思考中...'" /> <MarkdownView class="left-text" :content="item.answer || '思考中...'" />
</div> </div>
<div class="analysis-dashboard mt-12px"> <!-- 新增工具栏:按钮 + 耗时文本,向左排列 -->
<div class="analysis-toolbar">
<el-button size="small" @click="toggleAnalysis(item.id)">
{{ analysisExpanded[item.id] ? '收起分析' : '查看分析' }}
</el-button>
<span class="time">响应耗时:{{ item.duration || 0 }}s</span>
</div>
<div class="analysis-dashboard mt-12px" v-show="analysisExpanded[item.id]">
<div v-if="item.intentInsight" class="analysis-item"> <div v-if="item.intentInsight" class="analysis-item">
<el-tag type="info" effect="plain" size="small">意图洞察</el-tag> <el-tag type="info" effect="plain" size="small">意图洞察</el-tag>
<span class="analysis-content">{{ item.intentInsight }}</span> <span class="analysis-content">{{ item.intentInsight }}</span>
...@@ -45,15 +53,11 @@ ...@@ -45,15 +53,11 @@
<el-tag type="success" effect="plain" size="small">优化建议</el-tag> <el-tag type="success" effect="plain" size="small">优化建议</el-tag>
<div class="analysis-content suggest-box">{{ item.optimizeSuggest }}</div> <div class="analysis-content suggest-box">{{ item.optimizeSuggest }}</div>
</div> </div>
</div>
<div class="left-btns"> <!-- 空状态展示 -->
<el-button class="btn-cus" link @click="copyContent(item.answer)"> <div v-if="!item.intentInsight && (!item.reviewDiagnosis || item.reviewDiagnosis.length === 0) && !item.optimizeSuggest" class="analysis-empty">
<img class="btn-image" src="@/assets/ai/copy.svg" /> <el-empty description="暂无分析数据" :image-size="60" />
</el-button> </div>
<el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
<img class="btn-image h-17px" src="@/assets/ai/delete.svg" />
</el-button>
</div> </div>
</div> </div>
</div> </div>
...@@ -100,6 +104,26 @@ const props = defineProps({ ...@@ -100,6 +104,26 @@ const props = defineProps({
} }
}) })
// 新增:存储每个消息的分析面板展开状态,默认为 true
const analysisExpanded = ref<Record<number, boolean>>({})
// 切换展开/收起
const toggleAnalysis = (itemId: number) => {
analysisExpanded.value[itemId] = !analysisExpanded.value[itemId]
}
// 或者使用一个辅助函数获取状态(如果未初始化则默认为 true)
const isAnalysisExpanded = (itemId: number) => {
return analysisExpanded.value[itemId] ?? true
}
// 新增:批量切换所有分析面板的展开/收起状态
const toggleAllAnalysis = (expanded: boolean) => {
// 遍历 list 中的每个 item,设置其展开状态
list.value.forEach((item: ChatAnalysisVO) => {
analysisExpanded.value[item.id] = expanded
})
}
const { list } = toRefs(props) const { list } = toRefs(props)
const emits = defineEmits(['onDeleteSuccess']) const emits = defineEmits(['onDeleteSuccess'])
...@@ -131,7 +155,7 @@ const handlerGoTop = () => { ...@@ -131,7 +155,7 @@ const handlerGoTop = () => {
messageContainer.value.scrollTop = 0 messageContainer.value.scrollTop = 0
} }
defineExpose({ scrollToBottom, handlerGoTop }) defineExpose({ scrollToBottom, handlerGoTop, toggleAllAnalysis })
/** 操作 */ /** 操作 */
const copyContent = async (content: string | undefined) => { const copyContent = async (content: string | undefined) => {
...@@ -148,6 +172,9 @@ const onDelete = async (id: number) => { ...@@ -148,6 +172,9 @@ const onDelete = async (id: number) => {
onMounted(() => { onMounted(() => {
messageContainer.value?.addEventListener('scroll', handleScroll) messageContainer.value?.addEventListener('scroll', handleScroll)
if (messageContainer.value) {
messageContainer.value.scrollTop = 0
}
}) })
</script> </script>
...@@ -233,4 +260,17 @@ onMounted(() => { ...@@ -233,4 +260,17 @@ onMounted(() => {
right: 20px; right: 20px;
z-index: 10; z-index: 10;
} }
.analysis-toolbar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin-top: 8px; // 与上方 left-text-container 保持间距
margin-bottom: 4px; // 与下方分析面板保持适当间距(mt-12px 已提供)
.time {
font-size: 12px;
color: #999;
line-height: 24px;
}
}
</style> </style>
\ No newline at end of file
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
<!-- 右侧:对话详情 --> <!-- 右侧:对话详情 -->
<el-container class="detail-container"> <el-container class="detail-container">
<el-header class="header"> <el-header class="header">
<div class="title"> <div class="header-left">
{{ activeConversation?.user ? '用户:' + activeConversation?.user : '对话' }} <el-button size="small" @click="handleExpandAllAnalysis">一键展开</el-button>
<el-button size="small" @click="handleCollapseAllAnalysis">一键收起</el-button>
</div> </div>
</el-header> </el-header>
...@@ -98,6 +99,23 @@ const enableContext = ref<boolean>(true) // 是否开启上下文 ...@@ -98,6 +99,23 @@ const enableContext = ref<boolean>(true) // 是否开启上下文
const receiveMessageFullText = ref('') const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('') const receiveMessageDisplayedText = ref('')
// 获取子组件 ref
// const messageRef = ref<InstanceType<typeof MessageList>>()
// 一键展开所有分析面板
const handleExpandAllAnalysis = () => {
if (messageRef.value) {
messageRef.value.toggleAllAnalysis(true)
}
}
// 一键收起所有分析面板
const handleCollapseAllAnalysis = () => {
if (messageRef.value) {
messageRef.value.toggleAllAnalysis(false)
}
}
// =========== 【聊天对话】相关 =========== // =========== 【聊天对话】相关 ===========
/** 获取对话信息 */ /** 获取对话信息 */
...@@ -127,7 +145,8 @@ const handleConversationClick = async (conversation: ChatAnalysisVO) => { ...@@ -127,7 +145,8 @@ const handleConversationClick = async (conversation: ChatAnalysisVO) => {
// 刷新 message 列表 // 刷新 message 列表
await getMessageList() await getMessageList()
// 滚动底部 // 滚动底部
scrollToBottom(true) // scrollToBottom(true)
scrollToTop(true)
// 清空输入框 // 清空输入框
prompt.value = '' prompt.value = ''
return true return true
...@@ -193,7 +212,7 @@ const getMessageList = async () => { ...@@ -193,7 +212,7 @@ const getMessageList = async () => {
// 滚动到最下面 // 滚动到最下面
await nextTick() await nextTick()
await scrollToBottom() // await scrollToBottom()
} finally { } finally {
// time 定时器,如果加载速度很快,就不进入加载中 // time 定时器,如果加载速度很快,就不进入加载中
if (activeMessageListLoadingTimer.value) { if (activeMessageListLoadingTimer.value) {
...@@ -442,6 +461,13 @@ const scrollToBottom = async (isIgnore?: boolean) => { ...@@ -442,6 +461,13 @@ const scrollToBottom = async (isIgnore?: boolean) => {
} }
} }
const scrollToTop = async (isIgnore?: boolean) => {
await nextTick()
if (messageRef.value) {
messageRef.value.handlerGoTop()
}
}
/** 自提滚动效果 */ /** 自提滚动效果 */
const textRoll = async () => { const textRoll = async () => {
let index = 0 let index = 0
......
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