Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
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
482ce623
authored
May 16, 2024
by
cherishsince
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【优化】Ai 对话解耦
parent
91593d5d
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
522 additions
and
341 deletions
+522
-341
src/views/ai/chat/Conversation.vue
+393
-0
src/views/ai/chat/index.vue
+129
-341
No files found.
src/views/ai/chat/Conversation.vue
0 → 100644
View file @
482ce623
<!-- AI 对话 -->
<
template
>
<el-aside
width=
"260px"
class=
"conversation-container"
>
<!-- 左顶部:对话 -->
<div>
<el-button
class=
"w-1/1 btn-new-conversation"
type=
"primary"
@
click=
"createConversation"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
新建对话
</el-button>
<!-- 左顶部:搜索对话 -->
<el-input
v-model=
"searchName"
size=
"large"
class=
"mt-10px search-input"
placeholder=
"搜索历史记录"
@
keyup=
"searchConversation"
>
<template
#
prefix
>
<Icon
icon=
"ep:search"
/>
</
template
>
</el-input>
<!-- 左中间:对话列表 -->
<div
class=
"conversation-list"
>
<!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
<div
v-for=
"conversationKey in Object.keys(conversationMap)"
:key=
"conversationKey"
>
<div
v-if=
"conversationMap[conversationKey].length"
>
<el-text
class=
"mx-1"
size=
"small"
tag=
"b"
>
{{ conversationKey }}
</el-text>
</div>
<el-row
v-for=
"conversation in conversationMap[conversationKey]"
:key=
"conversation.id"
@
click=
"handleConversationClick(conversation.id)"
>
<div
:class=
"conversation.id === activeConversationId ? 'conversation active' : 'conversation'"
>
<div
class=
"title-wrapper"
>
<img
class=
"avatar"
:src=
"conversation.roleAvatar"
/>
<span
class=
"title"
>
{{ conversation.title }}
</span>
</div>
<!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
<div
class=
"button-wrapper"
>
<el-icon
title=
"编辑"
@
click=
"updateConversationTitle(conversation)"
>
<Icon
icon=
"ep:edit"
/>
</el-icon>
<el-icon
title=
"删除会话"
@
click=
"deleteChatConversation(conversation)"
>
<Icon
icon=
"ep:delete"
/>
</el-icon>
</div>
</div>
</el-row>
</div>
</div>
</div>
<!-- 左底部:工具栏 -->
<div
class=
"tool-box"
>
<div
@
click=
"handleRoleRepository"
>
<Icon
icon=
"ep:user"
/>
<el-text
size=
"small"
>
角色仓库
</el-text>
</div>
<div
@
click=
"handleClearConversation"
>
<Icon
icon=
"ep:delete"
/>
<el-text
size=
"small"
>
清空未置顶对话
</el-text>
</div>
</div>
<!-- ============= 额外组件 ============= -->
<!-- 角色仓库抽屉 -->
<el-drawer
v-model=
"drawer"
title=
"角色仓库"
size=
"50%"
>
<Role/>
</el-drawer>
</el-aside>
</template>
<
script
setup
lang=
"ts"
>
import
{
ChatConversationApi
,
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
{
ref
}
from
"vue"
;
import
ChatConversationUpdateForm
from
"@/views/ai/chat/components/ChatConversationUpdateForm.vue"
;
import
Role
from
"@/views/ai/chat/role/index.vue"
;
const
message
=
useMessage
()
// 消息弹窗
// 定义属性
const
searchName
=
ref
<
string
>
(
''
)
// 对话搜索
const
activeConversationId
=
ref
<
number
|
null
>
(
null
)
// 选中的对话,默认为 null
const
conversationList
=
ref
([]
as
ChatConversationVO
[])
// 对话列表
const
conversationMap
=
ref
<
any
>
({})
// 对话分组 (置顶、今天、三天前、一星期前、一个月前)
const
drawer
=
ref
<
boolean
>
(
false
)
// 角色仓库抽屉
// 定义组件 props
const
props
=
defineProps
({
activeId
:
{
type
:
Number
||
null
,
required
:
true
}
})
// 定义钩子
const
emits
=
defineEmits
([
'onConversationClick'
,
'onConversationClear'
])
/**
* 对话 - 搜索
*/
const
searchConversation
=
()
=>
{
// TODO fan:待实现
}
/**
* 对话 - 点击
*/
const
handleConversationClick
=
async
(
id
:
number
)
=>
{
// 切换对话
activeConversationId
.
value
=
id
const
filterConversation
=
conversationList
.
value
.
filter
(
item
=>
{
return
item
.
id
!==
id
})
// 回调 onConversationClick
emits
(
'onConversationClick'
,
filterConversation
[
0
])
}
/**
* 对话 - 获取列表
*/
const
getChatConversationList
=
async
()
=>
{
// 1、获取 对话数据
conversationList
.
value
=
await
ChatConversationApi
.
getChatConversationMyList
()
// 2、没有 任何对话情况
if
(
conversationList
.
value
.
length
===
0
)
{
activeConversationId
.
value
=
null
conversationMap
.
value
=
{}
return
}
// 3、对话根据时间分组(置顶、今天、一天前、三天前、七天前、30天前)
conversationMap
.
value
=
await
conversationTimeGroup
(
conversationList
.
value
)
}
const
conversationTimeGroup
=
async
(
list
:
ChatConversationVO
[])
=>
{
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
const
groupMap
=
{
'置顶'
:
[],
'今天'
:
[],
'一天前'
:
[],
'三天前'
:
[],
'七天前'
:
[],
'三十天前'
:
[]
}
// 当前时间的时间戳
const
now
=
Date
.
now
();
// 定义时间间隔常量(单位:毫秒)
const
oneDay
=
24
*
60
*
60
*
1000
;
const
threeDays
=
3
*
oneDay
;
const
sevenDays
=
7
*
oneDay
;
const
thirtyDays
=
30
*
oneDay
;
console
.
log
(
'listlistlist'
,
list
)
for
(
const
conversation
:
ChatConversationVO
of
list
)
{
// 置顶
if
(
conversation
.
pinned
)
{
groupMap
[
'置顶'
].
push
(
conversation
)
continue
}
// 计算时间差(单位:毫秒)
const
diff
=
now
-
conversation
.
updateTime
;
// 根据时间间隔判断
if
(
diff
<
oneDay
)
{
groupMap
[
'今天'
].
push
(
conversation
)
}
else
if
(
diff
<
threeDays
)
{
groupMap
[
'一天前'
].
push
(
conversation
)
}
else
if
(
diff
<
sevenDays
)
{
groupMap
[
'三天前'
].
push
(
conversation
)
}
else
if
(
diff
<
thirtyDays
)
{
groupMap
[
'七天前'
].
push
(
conversation
)
}
else
{
groupMap
[
'三十天前'
].
push
(
conversation
)
}
}
return
groupMap
}
/**
* 对话 - 新建
*/
const
createConversation
=
async
()
=>
{
// 1、新建对话
const
conversationId
=
await
ChatConversationApi
.
createChatConversationMy
(
{}
as
unknown
as
ChatConversationVO
)
// 2、选中对话
await
handleConversationClick
(
conversationId
)
// 3、获取对话内容
await
getChatConversationList
()
}
/**
* 对话 - 更新标题
*/
const
updateConversationTitle
=
async
(
conversation
:
ChatConversationVO
)
=>
{
// 1、二次确认
const
{
value
}
=
await
ElMessageBox
.
prompt
(
'修改标题'
,
{
inputPattern
:
/^
[\s\S]
*.*
\S[\s\S]
*$/
,
// 判断非空,且非空格
inputErrorMessage
:
'标题不能为空'
,
inputValue
:
conversation
.
title
})
// 2、发起修改
await
ChatConversationApi
.
updateChatConversationMy
({
id
:
conversation
.
id
,
title
:
value
}
as
ChatConversationVO
)
message
.
success
(
'重命名成功'
)
// 刷新列表
await
getChatConversationList
()
}
/**
* 删除聊天会话
*/
const
deleteChatConversation
=
async
(
conversation
:
ChatConversationVO
)
=>
{
try
{
// 删除的二次确认
await
message
.
delConfirm
(
`是否确认删除会话 -
${
conversation
.
title
}
?`
)
// 发起删除
await
ChatConversationApi
.
deleteChatConversationMy
(
conversation
.
id
)
message
.
success
(
'会话已删除'
)
// 刷新列表
await
getChatConversationList
()
}
catch
{
}
}
// ============ 角色仓库
/**
* 角色仓库抽屉
*/
const
handleRoleRepository
=
async
()
=>
{
drawer
.
value
=
!
drawer
.
value
}
// ============= 清空对话
/**
* 清空对话
*/
const
handleClearConversation
=
async
()
=>
{
ElMessageBox
.
confirm
(
'确认后对话会全部清空,置顶的对话除外。'
,
'确认提示'
,
{
confirmButtonText
:
'确认'
,
cancelButtonText
:
'取消'
,
type
:
'warning'
,
})
.
then
(
async
()
=>
{
await
ChatConversationApi
.
deleteMyAllExceptPinned
()
ElMessage
({
message
:
'操作成功!'
,
type
:
'success'
})
// 清空 对话 和 对话内容
activeConversationId
.
value
=
null
// 获取 对话列表
await
getChatConversationList
()
// 回调 方法
emits
(
'onConversationClear'
)
})
.
catch
(()
=>
{
})
}
// ============ 组件 onMounted
onMounted
(
async
()
=>
{
//
if
(
props
.
activeId
!=
null
)
{
}
// 获取 对话列表
await
getChatConversationList
()
})
</
script
>
<
style
scoped
lang=
"scss"
>
.conversation-container
{
position
:
relative
;
display
:
flex
;
flex-direction
:
column
;
justify-content
:
space-between
;
padding
:
0
10px
;
padding-top
:
10px
;
.btn-new-conversation
{
padding
:
18px
0
;
}
.search-input
{
margin-top
:
20px
;
}
.conversation-list
{
margin-top
:
20px
;
.conversation
{
display
:
flex
;
flex-direction
:
row
;
justify-content
:
space-between
;
flex
:
1
;
padding
:
0
5px
;
margin-top
:
10px
;
cursor
:
pointer
;
border-radius
:
5px
;
align-items
:
center
;
line-height
:
30px
;
&.active
{
background-color
:
#e6e6e6
;
.button
{
display
:
inline-block
;
}
}
.title-wrapper
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
center
;
}
.title
{
padding
:
5px
10px
;
max-width
:
220px
;
font-size
:
14px
;
overflow
:
hidden
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
;
}
.avatar
{
width
:
28px
;
height
:
28px
;
display
:
flex
;
flex-direction
:
row
;
justify-items
:
center
;
}
//
对话编辑、删除
.button-wrapper
{
right
:
2px
;
display
:
flex
;
flex-direction
:
row
;
justify-items
:
center
;
color
:
#606266
;
.el-icon
{
margin-right
:
5px
;
}
}
}
}
//
角色仓库、清空未设置对话
.tool-box
{
line-height
:
35px
;
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
color
:
var
(
--el-text-color
);
>
div
{
display
:
flex
;
align-items
:
center
;
color
:
#606266
;
padding
:
0
;
margin
:
0
;
cursor
:
pointer
;
>
span
{
margin-left
:
5px
;
}
}
}
}
</
style
>
src/views/ai/chat/index.vue
View file @
482ce623
<
template
>
<el-container
class=
"ai-layout"
>
<!-- 左侧:会话列表 -->
<el-aside
width=
"260px"
class=
"conversation-container"
>
<div>
<!-- 左顶部:新建对话 -->
<el-button
class=
"w-1/1 btn-new-conversation"
type=
"primary"
@
click=
"createConversation"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
新建对话
</el-button>
<!-- 左顶部:搜索对话 -->
<el-input
v-model=
"searchName"
size=
"large"
class=
"mt-10px search-input"
placeholder=
"搜索历史记录"
@
keyup=
"searchConversation"
>
<template
#
prefix
>
<Icon
icon=
"ep:search"
/>
</
template
>
</el-input>
<!-- 左中间:对话列表 -->
<div
class=
"conversation-list"
>
<!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
<div
v-for=
"conversationKey in Object.keys(conversationMap)"
:key=
"conversationKey"
>
<div
v-if=
"conversationMap[conversationKey].length"
>
<el-text
class=
"mx-1"
size=
"small"
tag=
"b"
>
{{conversationKey}}
</el-text>
</div>
<el-row
v-for=
"conversation in conversationMap[conversationKey]"
:key=
"conversation.id"
@
click=
"handleConversationClick(conversation.id)"
>
<div
:class=
"conversation.id === conversationId ? 'conversation active' : 'conversation'"
@
click=
"changeConversation(conversation.id)"
>
<div
class=
"title-wrapper"
>
<img
class=
"avatar"
:src=
"conversation.roleAvatar"
/>
<span
class=
"title"
>
{{ conversation.title }}
</span>
</div>
<!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
<div
class=
"button-wrapper"
>
<el-icon
title=
"编辑"
@
click=
"updateConversationTitle(conversation)"
>
<Icon
icon=
"ep:edit"
/>
</el-icon>
<el-icon
title=
"删除会话"
@
click=
"deleteChatConversation(conversation)"
>
<Icon
icon=
"ep:delete"
/>
</el-icon>
</div>
</div>
</el-row>
</div>
</div>
</div>
<!-- 左底部:工具栏 -->
<div
class=
"tool-box"
>
<div
@
click=
"handleRoleRepository"
>
<Icon
icon=
"ep:user"
/>
<el-text
size=
"small"
>
角色仓库
</el-text>
</div>
<div
@
click=
"handleClearConversation"
>
<Icon
icon=
"ep:delete"
/>
<el-text
size=
"small"
>
清空未置顶对话
</el-text>
</div>
</div>
</el-aside>
<Conversation
@
onConversationClick=
"handleConversationClick"
@
onConversationClear=
"handlerConversationClear"
/>
<!-- 右侧:会话详情 -->
<el-container
class=
"detail-container"
>
<!-- 右顶部 TODO 芋艿:右对齐 -->
<el-header
class=
"header"
>
<div
class=
"title"
>
{{
us
eConversation?.title }}
{{
activ
eConversation
?.
title
}}
</div>
<div>
<!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
<el-button
type=
"primary"
@
click=
"openChatConversationUpdateForm"
>
<span
v-html=
"
us
eConversation?.modelName"
></span>
<span
v-html=
"
activ
eConversation?.modelName"
></span>
<Icon
icon=
"ep:setting"
style=
"margin-left: 10px"
/>
</el-button>
<el-button>
...
...
@@ -107,8 +45,6 @@
<el-text
class=
"time"
>
{{
formatDate
(
item
.
createTime
)
}}
</el-text>
</div>
<div
class=
"left-text-container"
ref=
"markdownViewRef"
>
<!-- <div class="left-text markdown-view" v-html="item.content"></div>-->
<!-- <mdPreview :content="item.content" :delay="false" />-->
<MarkdownView
class=
"left-text"
:content=
"item.content"
/>
</div>
<div
class=
"left-btns"
>
...
...
@@ -136,7 +72,6 @@
</div>
<div
class=
"right-text-container"
>
<div
class=
"right-text"
>
{{
item
.
content
}}
</div>
<!-- <MarkdownView class="right-text" :content="item.content" />-->
</div>
<div
class=
"right-btns"
>
<div
class=
"btn-cus"
@
click=
"noCopy(item.content)"
>
...
...
@@ -152,10 +87,6 @@
</div>
</div>
</div>
<!-- 角色仓库抽屉 -->
<el-drawer
v-model=
"drawer"
title=
"角色仓库"
size=
"50%"
>
<Role/>
</el-drawer>
</el-main>
<el-footer
class=
"footer-container"
>
<form
@
submit
.
prevent=
"onSend"
class=
"prompt-from"
>
...
...
@@ -191,38 +122,35 @@
</form>
</el-footer>
</el-container>
</el-container>
<ChatConversationUpdateForm
ref=
"chatConversationUpdateFormRef"
@
success=
"getChatConversationList"
/>
<!-- ========= 额外组件 ========== -->
<!-- 更新对话 form -->
<ChatConversationUpdateForm
ref=
"chatConversationUpdateFormRef"
@
success=
"handlerTitleSuccess"
/>
</el-container>
</
template
>
<
script
setup
lang=
"ts"
>
import
MarkdownView
from
'@/components/MarkdownView/index.vue'
import
Conversation
from
'./Conversation.vue'
import
{
ChatMessageApi
,
ChatMessageVO
}
from
'@/api/ai/chat/message'
import
{
ChatConversationApi
,
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
ChatConversationUpdateForm
from
'./components/ChatConversationUpdateForm.vue'
import
Role
from
'@/views/ai/chat/role/index.vue'
import
{
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
useClipboard
}
from
'@vueuse/core'
import
ChatConversationUpdateForm
from
"@/views/ai/chat/components/ChatConversationUpdateForm.vue"
;
const
route
=
useRoute
()
// 路由
const
message
=
useMessage
()
// 消息弹窗
const
{
copy
}
=
useClipboard
()
// 初始化 copy 到粘贴板
const
conversationList
=
ref
([]
as
ChatConversationVO
[])
const
conversationMap
=
ref
<
any
>
({})
// 初始化 copy 到粘贴板
const
{
copy
}
=
useClipboard
()
const
drawer
=
ref
<
boolean
>
(
false
)
// 角色仓库抽屉
const
searchName
=
ref
(
''
)
// 查询的内容
const
inputTimeout
=
ref
<
any
>
()
// 处理输入中回车的定时器
const
conversationId
=
ref
<
number
|
null
>
(
null
)
// 选中的对话编号
// ref 属性定义
const
activeConversationId
=
ref
<
number
|
null
>
(
null
)
// 选中的对话编号
const
activeConversation
=
ref
<
ChatConversationVO
|
null
>
(
null
)
// 选中的 Conversation
const
conversationInProgress
=
ref
(
false
)
// 对话进行中
const
conversationInAbortController
=
ref
<
any
>
()
// 对话进行中 abort 控制器(控制 stream 对话)
const
inputTimeout
=
ref
<
any
>
()
// 处理输入中回车的定时器
const
prompt
=
ref
<
string
>
()
// prompt
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
...
...
@@ -231,66 +159,73 @@ const isScrolling = ref(false) //用于判断用户是否在滚动
const
isComposing
=
ref
(
false
)
// 判断用户是否在输入
/** chat message 列表 */
// defineOptions({ name: 'chatMessageList' })
const
list
=
ref
<
ChatMessageVO
[]
>
([])
// 列表的数据
const
useConversation
=
ref
<
ChatConversationVO
|
null
>
(
null
)
// 使用的 Conversation
/** 新建对话 */
const
createConversation
=
async
()
=>
{
// 新建对话
const
conversationId
=
await
ChatConversationApi
.
createChatConversationMy
(
{}
as
unknown
as
ChatConversationVO
)
changeConversation
(
conversationId
)
// 刷新对话列表
await
getChatConversationList
()
}
const
changeConversation
=
(
id
:
number
)
=>
{
// 切换对话
conversationId
.
value
=
id
// TODO 芋艿:待实现
// 刷新 message 列表
messageList
()
}
// ============ 处理对话滚动 ==============
/** 更新聊天会话的标题 */
const
updateConversationTitle
=
async
(
conversation
:
ChatConversationVO
)
=>
{
// 二次确认
const
{
value
}
=
await
ElMessageBox
.
prompt
(
'修改标题'
,
{
inputPattern
:
/^
[\s\S]
*.*
\S[\s\S]
*$/
,
// 判断非空,且非空格
inputErrorMessage
:
'标题不能为空'
,
inputValue
:
conversation
.
title
function
scrollToBottom
()
{
nextTick
(()
=>
{
//注意要使用nexttick以免获取不到dom
console
.
log
(
'isScrolling.value'
,
isScrolling
.
value
)
if
(
!
isScrolling
.
value
)
{
messageContainer
.
value
.
scrollTop
=
messageContainer
.
value
.
scrollHeight
-
messageContainer
.
value
.
offsetHeight
}
})
// 发起修改
await
ChatConversationApi
.
updateChatConversationMy
({
id
:
conversation
.
id
,
title
:
value
}
as
ChatConversationVO
)
message
.
success
(
'重命名成功'
)
// 刷新列表
await
getChatConversationList
()
}
/** 删除聊天会话 */
const
deleteChatConversation
=
async
(
conversation
:
ChatConversationVO
)
=>
{
try
{
// 删除的二次确认
await
message
.
delConfirm
(
`是否确认删除会话 -
${
conversation
.
title
}
?`
)
// 发起删除
await
ChatConversationApi
.
deleteChatConversationMy
(
conversation
.
id
)
message
.
success
(
'会话已删除'
)
// 刷新列表
await
getChatConversationList
()
}
catch
{
function
handleScroll
()
{
const
scrollContainer
=
messageContainer
.
value
const
scrollTop
=
scrollContainer
.
scrollTop
const
scrollHeight
=
scrollContainer
.
scrollHeight
const
offsetHeight
=
scrollContainer
.
offsetHeight
if
(
scrollTop
+
offsetHeight
<
scrollHeight
)
{
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
isScrolling
.
value
=
true
}
else
{
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
isScrolling
.
value
=
false
}
}
const
searchConversation
=
()
=>
{
// TODO fan:待实现
// ============= 处理聊天输入回车发送 =============
const
onCompositionstart
=
()
=>
{
isComposing
.
value
=
true
}
/** send */
const
onCompositionend
=
()
=>
{
// console.log('输入结束...')
setTimeout
(()
=>
{
isComposing
.
value
=
false
},
200
)
}
const
onPromptInput
=
(
event
)
=>
{
// 非输入法 输入设置为 true
if
(
!
isComposing
.
value
)
{
// 回车 event data 是 null
if
(
event
.
data
==
null
)
{
return
}
isComposing
.
value
=
true
}
// 清理定时器
if
(
inputTimeout
.
value
)
{
clearTimeout
(
inputTimeout
.
value
)
}
// 重置定时器
inputTimeout
.
value
=
setTimeout
(()
=>
{
isComposing
.
value
=
false
},
400
)
}
// ============== 对话消息相关 =================
/**
* 发送消息
*/
const
onSend
=
async
()
=>
{
// 判断用户是否在输入
if
(
isComposing
.
value
)
{
...
...
@@ -311,21 +246,15 @@ const onSend = async () => {
// TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
// 清空输入框
prompt
.
value
=
''
// const requestParams = {
// conversationId: conversationId.value,
// content: content
// } as unknown as ChatMessageSendVO
// // 添加 message
const
userMessage
=
{
conversationId
:
c
onversationId
.
value
,
conversationId
:
activeC
onversationId
.
value
,
content
:
content
}
as
ChatMessageVO
// list.value.push(userMessage)
//
//
滚动到住下面
//
scrollToBottom()
// 滚动到住下面
scrollToBottom
()
// stream
await
doSendStream
(
userMessage
)
//
}
const
doSendStream
=
async
(
userMessage
:
ChatMessageVO
)
=>
{
...
...
@@ -387,48 +316,35 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
}
}
/** 查询列表 */
const
messageList
=
async
()
=>
{
const
stopStream
=
async
()
=>
{
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
if
(
conversationInAbortController
.
value
)
{
conversationInAbortController
.
value
.
abort
()
}
// 设置为 false
conversationInProgress
.
value
=
false
}
// ============== message 数据 =================
/**
* 获取 - message 列表
*/
const
getMessageList
=
async
()
=>
{
try
{
if
(
c
onversationId
.
value
===
null
)
{
if
(
activeC
onversationId
.
value
===
null
)
{
return
}
// 获取列表数据
const
res
=
await
ChatMessageApi
.
messageList
(
conversationId
.
value
)
list
.
value
=
res
list
.
value
=
await
ChatMessageApi
.
messageList
(
activeConversationId
.
value
)
// 滚动到最下面
scrollToBottom
()
await
nextTick
(()
=>
{
scrollToBottom
()
})
}
finally
{
}
}
function
scrollToBottom
()
{
nextTick
(()
=>
{
//注意要使用nexttick以免获取不到dom
console
.
log
(
'isScrolling.value'
,
isScrolling
.
value
)
if
(
!
isScrolling
.
value
)
{
messageContainer
.
value
.
scrollTop
=
messageContainer
.
value
.
scrollHeight
-
messageContainer
.
value
.
offsetHeight
}
})
}
function
handleScroll
()
{
const
scrollContainer
=
messageContainer
.
value
const
scrollTop
=
scrollContainer
.
scrollTop
const
scrollHeight
=
scrollContainer
.
scrollHeight
const
offsetHeight
=
scrollContainer
.
offsetHeight
if
(
scrollTop
+
offsetHeight
<
scrollHeight
)
{
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
isScrolling
.
value
=
true
}
else
{
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
isScrolling
.
value
=
false
}
}
function
noCopy
(
content
)
{
copy
(
content
)
ElMessage
({
...
...
@@ -445,186 +361,57 @@ const onDelete = async (id) => {
type
:
'success'
})
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
stopStream
()
await
stopStream
()
// 重新获取 message 列表
await
m
essageList
()
await
getM
essageList
()
}
const
stopStream
=
async
()
=>
{
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
if
(
conversationInAbortController
.
value
)
{
conversationInAbortController
.
value
.
abort
()
}
// 设置为 false
conversationInProgress
.
value
=
false
}
/** 修改聊天会话 */
const
chatConversationUpdateFormRef
=
ref
()
const
openChatConversationUpdateForm
=
async
()
=>
{
chatConversationUpdateFormRef
.
value
.
open
(
c
onversationId
.
value
)
chatConversationUpdateFormRef
.
value
.
open
(
activeC
onversationId
.
value
)
}
// 输入
const
onCompositionstart
=
()
=>
{
console
.
log
(
'onCompositionstart。。。.'
)
isComposing
.
value
=
true
}
const
onCompositionend
=
()
=>
{
// console.log('输入结束...')
setTimeout
(()
=>
{
console
.
log
(
'输入结束...'
)
isComposing
.
value
=
false
},
200
)
/**
* 对话 - 标题修改成功
*/
const
handlerTitleSuccess
=
async
()
=>
{
// TODO 需要刷新 对话列表
}
const
onPromptInput
=
(
event
)
=>
{
// 非输入法 输入设置为 true
if
(
!
isComposing
.
value
)
{
// 回车 event data 是 null
if
(
event
.
data
==
null
)
{
return
}
console
.
log
(
'setTimeout 输入开始...'
)
isComposing
.
value
=
true
}
// 清理定时器
if
(
inputTimeout
.
value
)
{
clearTimeout
(
inputTimeout
.
value
)
}
// 重置定时器
inputTimeout
.
value
=
setTimeout
(()
=>
{
console
.
log
(
'setTimeout 输入结束...'
)
isComposing
.
value
=
false
},
400
)
}
const
getConversation
=
async
(
conversationId
:
number
|
null
)
=>
{
if
(
!
conversationId
)
{
return
}
// 获取对话信息
useConversation
.
value
=
await
ChatConversationApi
.
getChatConversationMy
(
conversationId
)
console
.
log
(
'useConversation.value'
,
useConversation
.
value
)
}
/** 获得聊天会话列表 */
const
getChatConversationList
=
async
()
=>
{
conversationList
.
value
=
await
ChatConversationApi
.
getChatConversationMyList
()
// 默认选中第一条
if
(
conversationList
.
value
.
length
===
0
)
{
conversationId
.
value
=
null
list
.
value
=
[]
}
else
{
if
(
conversationId
.
value
===
null
)
{
conversationId
.
value
=
conversationList
.
value
[
0
].
id
changeConversation
(
conversationList
.
value
[
0
].
id
)
}
}
// map
const
groupRes
=
await
conversationTimeGroup
(
conversationList
.
value
)
conversationMap
.
value
=
groupRes
}
const
conversationTimeGroup
=
async
(
list
:
ChatConversationVO
[])
=>
{
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
const
groupMap
=
{
'置顶'
:
[],
'今天'
:
[],
'一天前'
:
[],
'三天前'
:
[],
'七天前'
:
[],
'三十天前'
:
[]
}
// 当前时间的时间戳
const
now
=
Date
.
now
();
// 定义时间间隔常量(单位:毫秒)
const
oneDay
=
24
*
60
*
60
*
1000
;
const
threeDays
=
3
*
oneDay
;
const
sevenDays
=
7
*
oneDay
;
const
thirtyDays
=
30
*
oneDay
;
console
.
log
(
'listlistlist'
,
list
)
for
(
const
conversation
:
ChatConversationVO
of
list
)
{
// 置顶
if
(
conversation
.
pinned
)
{
groupMap
[
'置顶'
].
push
(
conversation
)
continue
}
// 计算时间差(单位:毫秒)
const
diff
=
now
-
conversation
.
updateTime
;
// 根据时间间隔判断
if
(
diff
<
oneDay
)
{
groupMap
[
'今天'
].
push
(
conversation
)
}
else
if
(
diff
<
threeDays
)
{
groupMap
[
'一天前'
].
push
(
conversation
)
}
else
if
(
diff
<
sevenDays
)
{
groupMap
[
'三天前'
].
push
(
conversation
)
}
else
if
(
diff
<
thirtyDays
)
{
groupMap
[
'七天前'
].
push
(
conversation
)
}
else
{
groupMap
[
'三十天前'
].
push
(
conversation
)
}
}
return
groupMap
}
// 对话点击
const
handleConversationClick
=
async
(
id
:
number
)
=>
{
// 切换对话
conversationId
.
value
=
id
console
.
log
(
'conversationId.value'
,
conversationId
.
value
)
// 获取列表数据
await
messageList
()
}
// 角色仓库
const
handleRoleRepository
=
async
()
=>
{
drawer
.
value
=
!
drawer
.
value
/**
* 对话 - 点击
*/
const
handleConversationClick
=
async
(
conversation
:
ChatConversationVO
)
=>
{
// 更新选中的对话 id
activeConversationId
.
value
=
conversation
.
id
// 刷新 message 列表
await
getMessageList
()
}
// 清空对话
const
handleClearConversation
=
async
()
=>
{
ElMessageBox
.
confirm
(
'确认后对话会全部清空,置顶的对话除外。'
,
'确认提示'
,
{
confirmButtonText
:
'确认'
,
cancelButtonText
:
'取消'
,
type
:
'warning'
,
}
)
.
then
(
async
()
=>
{
await
ChatConversationApi
.
deleteMyAllExceptPinned
()
ElMessage
({
message
:
'操作成功!'
,
type
:
'success'
})
// 清空选中的对话
useConversation
.
value
=
null
conversationId
.
value
=
null
list
.
value
=
[]
// 获得聊天会话列表
await
getChatConversationList
()
})
.
catch
(()
=>
{
})
/**
* 对话 - 清理全部对话
*/
const
handlerConversationClear
=
async
()
=>
{
activeConversationId
.
value
=
null
activeConversation
.
value
=
null
list
.
value
=
[]
}
/** 初始化 **/
onMounted
(
async
()
=>
{
// 设置当前对话
if
(
route
.
query
.
conversationId
)
{
conversationId
.
value
=
route
.
query
.
conversationId
as
number
}
// 设置当前对话
TODO 角色仓库过来的,自带 conversationId 需要选中
//
if (route.query.conversationId) {
//
conversationId.value = route.query.conversationId as number
//
}
// 获得聊天会话列表
await
getChatConversationList
()
//
await getChatConversationList()
// 获取对话信息
await
getConversation
(
conversationId
.
value
)
//
await getConversation(conversationId.value)
// 获取列表数据
await
m
essageList
()
await
getM
essageList
()
// scrollToBottom();
// await nextTick
// 监听滚动事件,判断用户滚动状态
...
...
@@ -642,6 +429,7 @@ onMounted(async () => {
})
})
</
script
>
<
style
lang=
"scss"
scoped
>
.ai-layout
{
//
TODO
@范
这里height不能
100%
先这样临时处理
...
...
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