Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Shucai Chen
/
lufa-ai
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
f63ea6ae
authored
Mar 21, 2026
by
pengshun
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 对话页面调整
parent
0d09257b
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
300 additions
and
117 deletions
+300
-117
.DS_Store
+0
-0
ruoyi-vue-pro-master-jdk17/.DS_Store
+0
-0
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatAnalysisMapper.java
+5
-0
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatAnalysisServiceImpl.java
+9
-1
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/resources/mapper/chat/AiChatAnalysisMapper.xml
+28
-0
ruoyi-vue-pro-master-jdk17/yudao-server/.DS_Store
+0
-0
yudao-ui-admin-vue3/.DS_Store
+0
-0
yudao-ui-admin-vue3/src/types/auto-components.d.ts
+0
-2
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/components/conversation/ConversationList.vue
+176
-99
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/components/message/MessageList.vue
+52
-11
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/index.vue
+30
-4
No files found.
.DS_Store
View file @
f63ea6ae
No preview for this file type
ruoyi-vue-pro-master-jdk17/.DS_Store
View file @
f63ea6ae
No preview for this file type
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatAnalysisMapper.java
View file @
f63ea6ae
...
@@ -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
)
...
...
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatAnalysisServiceImpl.java
View file @
f63ea6ae
...
@@ -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
...
...
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/resources/mapper/chat/AiChatAnalysisMapper.xml
0 → 100644
View file @
f63ea6ae
<?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
ruoyi-vue-pro-master-jdk17/yudao-server/.DS_Store
View file @
f63ea6ae
No preview for this file type
yudao-ui-admin-vue3/.DS_Store
View file @
f63ea6ae
No preview for this file type
yudao-ui-admin-vue3/src/types/auto-components.d.ts
View file @
f63ea6ae
...
@@ -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'
]
...
...
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/components/conversation/ConversationList.vue
View file @
f63ea6ae
<
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 conversation
Map[conversationKey]
"
v-for=
"conversation in conversation
List
"
: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
,
ElMessage
Box
}
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
})
}
}
//
处理分页返回或数组返回
//
调用分页接口
con
versationList
.
value
=
res
?.
list
?
res
.
list
:
(
res
||
[]
)
con
st
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
{
...
...
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/components/message/MessageList.vue
View file @
f63ea6ae
...
@@ -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
yudao-ui-admin-vue3/src/views/ai/chat_analysis/message/index.vue
View file @
f63ea6ae
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment