9 changed files with 376 additions and 86 deletions
@ -1,6 +1,3 @@ |
|||||||
export interface PictureType { |
import type { ConversationData } from '@/store/moules/messageStore/index.d' |
||||||
id: string |
|
||||||
image: string |
export interface PictureType extends ConversationData {} |
||||||
name: string |
|
||||||
type: string |
|
||||||
} |
|
||||||
|
@ -1,48 +1,347 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { ref } from 'vue' |
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' |
||||||
|
import { onBeforeRouteLeave } from 'vue-router' |
||||||
|
import { Button, Spin, message } from 'ant-design-vue' |
||||||
import { AppContainerBox } from '@/components/AppContainerBox' |
import { AppContainerBox } from '@/components/AppContainerBox' |
||||||
import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' |
import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' |
||||||
import { AppSubMenuList } from '@/components/AppSubMenuList' |
import { AppSubMenuList } from '@/components/AppSubMenuList' |
||||||
import { AppRoleDefault } from '@/components/AppRoleDefault' |
import { AppRoleDefault } from '@/components/AppRoleDefault' |
||||||
import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' |
|
||||||
import { AppUserInfo } from '@/components/AppUserInfo' |
import { AppUserInfo } from '@/components/AppUserInfo' |
||||||
|
import { SubMenuActionEnum } from '@/components/AppSubMenuList/index.d' |
||||||
|
import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' |
||||||
|
|
||||||
const subMenuActive = ref(0) |
import { AppTextarea } from '@/components/AppTextarea' |
||||||
const subMenuList = ref<SubMenuItem[]>([ |
import { AppMessage } from '@/components/AppMessage' |
||||||
{ |
import type { MessageItem } from '@/components/AppMessage/index.d' |
||||||
title: '新对话1', |
import { useMessageStore } from '@/store/moules/messageStore/index' |
||||||
content: '这是一个新的对话哦;啦啦啦', |
import { MenuTypeEnum } from '@/enums/menuEnum' |
||||||
id: '1', |
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' |
||||||
}, |
import { addMessage, conversationList, historyMessage, removeMessage, sendTextToText, updateMessage } from '@/api/base/message' |
||||||
{ |
import { useMqtt } from '@/hooks/useMqtt' |
||||||
title: '新对话2', |
import { useMessage } from '@/hooks/useMessage' |
||||||
content: '这是一个新的对话哦', |
|
||||||
id: '2', |
const { createConfirm } = useMessage() |
||||||
|
|
||||||
|
const messageStore = useMessageStore() |
||||||
|
|
||||||
|
const sendBtnLoading = ref(false) |
||||||
|
const subMenuActiveIndex = ref(0) // 当前会话索引 |
||||||
|
const subMenuActionIndex = ref(-1) // 会话操作索引 |
||||||
|
const subMenuList = ref<SubMenuItem[]>([]) |
||||||
|
const subMenuInputValue = ref<string>('') |
||||||
|
const appMessageRef = ref() |
||||||
|
const messageList = computed(() => messageStore.getMessageList) |
||||||
|
const messageStatus = computed(() => messageStore.getMessageStatus) |
||||||
|
const conversationData = computed(() => messageStore.getConversationData) |
||||||
|
const historyMessageParams = ref({ |
||||||
|
conversationId: '', |
||||||
|
current: 1, |
||||||
|
size: 10, |
||||||
|
total: 0, |
||||||
|
}) |
||||||
|
|
||||||
|
const conversationDefaultShow = ref(false) |
||||||
|
const appMessageShow = ref(true) |
||||||
|
const spinning = ref(true) |
||||||
|
|
||||||
|
watch( |
||||||
|
() => messageStatus.value, |
||||||
|
(val) => { |
||||||
|
if (val === MessageStatusEnum.END) { |
||||||
|
sendBtnLoading.value = false |
||||||
|
} |
||||||
}, |
}, |
||||||
]) |
) |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 点击会话操作项 |
||||||
|
*/ |
||||||
|
function handlesubMenuActionIndex(type: SubMenuActionEnum, item: SubMenuItem, index: number) { |
||||||
|
subMenuActionIndex.value = index |
||||||
|
if (type === SubMenuActionEnum.EDIT) { |
||||||
|
subMenuList.value.forEach((item) => { |
||||||
|
item.actionType = SubMenuActionEnum.NOT |
||||||
|
}) |
||||||
|
subMenuList.value[index].actionType = SubMenuActionEnum.EDIT |
||||||
|
subMenuInputValue.value = item.title |
||||||
|
} |
||||||
|
|
||||||
|
if (type === SubMenuActionEnum.DELETE) { |
||||||
|
createConfirm({ |
||||||
|
title: '删除', |
||||||
|
content: `确定要删除${item.title}会话吗?`, |
||||||
|
iconType: 'warning', |
||||||
|
onOk: () => { |
||||||
|
removeMessage(item.id).then(() => { |
||||||
|
message.success('删除成功') |
||||||
|
getConversationList() |
||||||
|
}) |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
function handleSubMenuChange(index: number) { |
/** |
||||||
subMenuActive.value = index |
* @description: 切换会话 |
||||||
|
*/ |
||||||
|
async function handleSubMenuChange(index: number) { |
||||||
|
if (messageStatus.value !== MessageStatusEnum.END) { |
||||||
|
message.warn('请先结束对话') |
||||||
|
return |
||||||
|
} |
||||||
|
subMenuActiveIndex.value = index |
||||||
|
historyMessageParams.value.current = 1 |
||||||
|
historyMessageParams.value.total = 0 |
||||||
|
messageStore.setConversationData(subMenuList.value[subMenuActiveIndex.value]) |
||||||
|
messageStore.setMessageClear() |
||||||
|
useMqtt().end() |
||||||
|
useMqtt().connect() |
||||||
|
getHistoryMessage() |
||||||
|
|
||||||
|
// 因为会话切换时滚动底部有问题,所以重新加载下 |
||||||
|
appMessageShow.value = false |
||||||
|
setTimeout(() => { |
||||||
|
appMessageShow.value = true |
||||||
|
}, 0) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 发送消息 |
||||||
|
*/ |
||||||
|
async function handleSend(value: string) { |
||||||
|
sendBtnLoading.value = true |
||||||
|
conversationDefaultShow.value = false |
||||||
|
if (!conversationData.value) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (subMenuActiveIndex.value === -1) { |
||||||
|
try { |
||||||
|
await addMessage({ type: MenuTypeEnum.ROLE, title: '新的对话' }) |
||||||
|
await getConversationList() |
||||||
|
await nextTick() |
||||||
|
sendMessage(conversationData.value.id, value) |
||||||
|
} |
||||||
|
catch (error) { |
||||||
|
message.error('新建对话失败,请稍后重试!') |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
sendMessage(conversationData.value.id, value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 获取会话列表 |
||||||
|
*/ |
||||||
|
async function getConversationList() { |
||||||
|
const res = await conversationList(MenuTypeEnum.ROLE) |
||||||
|
res.forEach((item: SubMenuItem) => { |
||||||
|
item.actionType = SubMenuActionEnum.NOT |
||||||
|
}) |
||||||
|
console.log(res) |
||||||
|
|
||||||
|
subMenuList.value = res |
||||||
|
subMenuActiveIndex.value = 0 |
||||||
|
if (subMenuList.value.length) { |
||||||
|
await handleSubMenuChange(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 获取历史对话记录 |
||||||
|
*/ |
||||||
|
async function getHistoryMessage() { |
||||||
|
if (!conversationData.value) { |
||||||
|
return |
||||||
|
} |
||||||
|
spinning.value = true |
||||||
|
historyMessageParams.value.conversationId = conversationData.value.id |
||||||
|
const res = await historyMessage(historyMessageParams.value) |
||||||
|
console.log(res) |
||||||
|
|
||||||
|
spinning.value = false |
||||||
|
if (!res || !res.records || !res.records.length) { |
||||||
|
return |
||||||
|
} |
||||||
|
res.records.forEach((item: any) => { |
||||||
|
const itemData: MessageItem = { |
||||||
|
messageType: item.roleType === MessageTypeEnum.USER ? MessageTypeEnum.USER : MessageTypeEnum.AI, |
||||||
|
content: item.messageContent, |
||||||
|
time: item.messageTime, |
||||||
|
avatar: '', |
||||||
|
messageStatus: MessageStatusEnum.END, |
||||||
|
} |
||||||
|
historyMessageParams.value.total = res.total |
||||||
|
messageStore.setMessageUnshiftItem(itemData) |
||||||
|
}) |
||||||
|
|
||||||
|
conversationDefaultShow.value = false |
||||||
} |
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 滚动监听 |
||||||
|
*/ |
||||||
|
async function onScrollTop(scrollTop: number) { |
||||||
|
if (scrollTop !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (historyMessageParams.value.current * historyMessageParams.value.size > historyMessageParams.value.total) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (historyMessageParams.value.current < historyMessageParams.value.total) { |
||||||
|
historyMessageParams.value.current++ |
||||||
|
await getHistoryMessage() |
||||||
|
|
||||||
|
// 无缝滚动 |
||||||
|
appMessageRef.value.seamlessScrollToTop() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 发送消息hook |
||||||
|
*/ |
||||||
|
async function sendMessage(conversationId: string, question: string): Promise<void> { |
||||||
|
messageStore.setMessageStatus(MessageStatusEnum.LOADING) |
||||||
|
messageStore.setMessagePushItem({ |
||||||
|
messageType: MessageTypeEnum.USER, |
||||||
|
content: question, |
||||||
|
time: String(new Date().getTime()), |
||||||
|
avatar: '', |
||||||
|
}) |
||||||
|
messageStore.setMessagePushItem({ |
||||||
|
messageType: MessageTypeEnum.AI, |
||||||
|
content: '正在思考中...', |
||||||
|
time: String(new Date().getTime()), |
||||||
|
avatar: '', |
||||||
|
messageStatus: MessageStatusEnum.LOADING, |
||||||
|
}) |
||||||
|
if (!messageStore.getConversationData) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
sendTextToText({ |
||||||
|
conversationId, |
||||||
|
question, |
||||||
|
}).catch(() => { |
||||||
|
messageStore.getMessageList.splice(-2) |
||||||
|
messageStore.setMessageStatus(MessageStatusEnum.END) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 重新回答 |
||||||
|
*/ |
||||||
|
function reloadMessage() { |
||||||
|
if (!conversationData.value) { |
||||||
|
return |
||||||
|
} |
||||||
|
const question = messageList.value[messageList.value.length - 2]?.content |
||||||
|
sendMessage(conversationData.value.id, question) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 点击新建会话按钮 |
||||||
|
*/ |
||||||
|
function handleAddMessage() { |
||||||
|
conversationDefaultShow.value = true |
||||||
|
subMenuActiveIndex.value = -1 |
||||||
|
} |
||||||
|
|
||||||
|
// 下面是子菜单操作项 |
||||||
|
function handleSubMenuInputAffirm(index: number, item: SubMenuItem, inputValue: string) { |
||||||
|
updateMessage({ ...item, title: inputValue }).then(() => { |
||||||
|
message.success('修改成功') |
||||||
|
subMenuList.value[index].title = inputValue |
||||||
|
subMenuList.value[index].actionType = SubMenuActionEnum.NOT |
||||||
|
}) |
||||||
|
} |
||||||
|
function handleSubMenuInputClose(index: number) { |
||||||
|
subMenuList.value[index].actionType = SubMenuActionEnum.NOT |
||||||
|
} |
||||||
|
function handleSubMenuInputBlur(index: number, item: SubMenuItem, inputValue: string) { |
||||||
|
subMenuActionIndex.value = -1 |
||||||
|
handleSubMenuInputAffirm(index, item, inputValue) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getConversationList() |
||||||
|
}) |
||||||
|
|
||||||
|
onUnmounted(() => { |
||||||
|
useMqtt().end() |
||||||
|
}) |
||||||
|
|
||||||
|
// 路由离开时的操作 |
||||||
|
onBeforeRouteLeave(() => { |
||||||
|
if (messageStatus.value !== MessageStatusEnum.END) { |
||||||
|
message.warn('请先结束对话') |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<AppContainerBox> |
<AppContainerBox> |
||||||
<template #subMenu> |
<template #subMenu> |
||||||
<AppSubMenuTitle title="角色会话"></AppSubMenuTitle> |
<!-- 标题 --> |
||||||
<!-- <div class="px-5 mb-5"> |
<AppSubMenuTitle></AppSubMenuTitle> |
||||||
<Button type="primary" class="w-full"> |
|
||||||
|
<!-- 按钮 --> |
||||||
|
<div class="px-5 mb-5"> |
||||||
|
<Button type="primary" class="w-full" @click="handleAddMessage"> |
||||||
新建会话 |
新建会话 |
||||||
</Button> |
</Button> |
||||||
</div> --> |
</div> |
||||||
<AppSubMenuList :list="subMenuList" :active-index="subMenuActive" @change="handleSubMenuChange"></AppSubMenuList> |
|
||||||
|
<!-- 会话列表 --> |
||||||
|
<AppSubMenuList |
||||||
|
v-model:input-value="subMenuInputValue" |
||||||
|
:list="subMenuList" |
||||||
|
:active-index="subMenuActiveIndex" |
||||||
|
:action-index="subMenuActionIndex" |
||||||
|
@change="handleSubMenuChange" |
||||||
|
@handle-action="handlesubMenuActionIndex" |
||||||
|
@input-affirm="handleSubMenuInputAffirm" |
||||||
|
@input-close="handleSubMenuInputClose" |
||||||
|
@input-blur="handleSubMenuInputBlur" |
||||||
|
> |
||||||
|
</AppSubMenuList> |
||||||
<AppUserInfo /> |
<AppUserInfo /> |
||||||
</template> |
</template> |
||||||
<template #content> |
<template #content> |
||||||
<AppRoleDefault></AppRoleDefault> |
<Spin :spinning="spinning" wrapper-class-name="app-content-spin"> |
||||||
|
<!-- 默认导语 --> |
||||||
|
<AppRoleDefault |
||||||
|
v-if="conversationDefaultShow" |
||||||
|
height="calc(100% - 120px)" |
||||||
|
> |
||||||
|
</AppRoleDefault> |
||||||
|
|
||||||
|
<!-- 消息列表 --> |
||||||
|
<AppMessage |
||||||
|
v-if="!conversationDefaultShow && appMessageShow" |
||||||
|
ref="appMessageRef" |
||||||
|
class="pl-27 pr-5" |
||||||
|
:list="messageList" |
||||||
|
@on-scroll-top="onScrollTop" |
||||||
|
@reload-message="reloadMessage" |
||||||
|
> |
||||||
|
</AppMessage> |
||||||
|
</Spin> |
||||||
|
|
||||||
|
<!-- 发送框 --> |
||||||
|
<AppTextarea |
||||||
|
class="pl-52 pr-32 mt-10" |
||||||
|
:btn-loading="sendBtnLoading" |
||||||
|
@send="handleSend" |
||||||
|
></AppTextarea> |
||||||
</template> |
</template> |
||||||
</AppContainerBox> |
</AppContainerBox> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<style scoped></style> |
<style lang="scss" scoped> |
||||||
|
|
||||||
|
</style> |
||||||
|
Loading…
Reference in new issue