From 6c7bc1e281d8acaa74356604adc5b445bd2c725e Mon Sep 17 00:00:00 2001 From: lipenghui <mrkezhi@163.com> Date: Tue, 23 Jan 2024 09:16:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=9B=BE=E5=83=8F=E5=88=86=E6=9E=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=96=87=E4=BB=B6=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/visualAnalysis/index.vue | 411 +++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 src/views/visualAnalysis/index.vue diff --git a/src/views/visualAnalysis/index.vue b/src/views/visualAnalysis/index.vue new file mode 100644 index 0000000..bb0377c --- /dev/null +++ b/src/views/visualAnalysis/index.vue @@ -0,0 +1,411 @@ +<script setup lang="ts"> +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' +import { onBeforeRouteLeave } from 'vue-router' +import { Button, Spin, UploadDragger, message } from 'ant-design-vue' +import type { UploadChangeParam } from 'ant-design-vue' +import DefaultImage from '@/assets/images/conversation/default_img3.png' +import { SvgIcon } from '@/components/SvgIcon' +import { AppContainerBox } from '@/components/AppContainerBox' +import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' +import { AppSubMenuList } from '@/components/AppSubMenuList' +import { AppConversationDefault } from '@/components/AppConversationDefault' +import { AppUserInfo } from '@/components/AppUserInfo' +import { SubMenuActionEnum } from '@/components/AppSubMenuList/index.d' +import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' + +import { AppMessage } from '@/components/AppMessage' +import type { MessageItem } from '@/components/AppMessage/index.d' +import { useMessageStore } from '@/store/moules/messageStore/index' +import { MenuTypeEnum } from '@/enums/menuEnum' +import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' +import { addMessage, conversationList, historyMessage, removeMessage, sendRepository, updateMessage } from '@/api/base/message' +import { uploadApi } from '@/api/base/file' +import { useMqtt } from '@/hooks/useMqtt' +import { useMessage } from '@/hooks/useMessage' + +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 leadData = ref({ + title: '你好,我是青鸟语言大模型-同聪~', + subTitles: [ + '我可以自由的跟你对话~陪你聊天~帮你想方案~答疑解惑。', + '上传图像至下方,我可以帮你分析哦', + ], + image: DefaultImage, +}) + +const conversationDefaultShow = ref(false) +const appMessageShow = ref(true) +const spinning = ref(true) + +const fileList = ref([]) + +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() + }) + }, + }) + } +} + +/** + * @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.VISUAL_ANALYSIS, 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.VISUAL_ANALYSIS) + res.forEach((item: SubMenuItem) => { + item.actionType = SubMenuActionEnum.NOT + }) + 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) + spinning.value = false + + if (!res || !res.records || !res.records.length) { + conversationDefaultShow.value = true + return + } + console.log(res) + + 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> { + conversationDefaultShow.value = false + 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 + } + + sendRepository({ + 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() { + if (messageStatus.value !== MessageStatusEnum.END) { + message.warn('请先结束对话') + return + } + conversationDefaultShow.value = true + subMenuActiveIndex.value = -1 +} + +function handleUploadChange(info: UploadChangeParam) { + const status = info.file.status + if (status !== 'uploading') { + console.log(info.file, info.fileList) + } + if (status === 'done') { + message.success(`${info.file.name} file uploaded successfully.`) + } + else if (status === 'error') { + message.error(`${info.file.name} file upload failed.`) + } +} + +function handleUploadDrop(e: DragEvent) { + console.log('drop', e) +} + +async function handleCustomRequest(option: any) { + console.log(option) + const { file } = option + // uploadApi(file, handleUploadDrop()=>{ + // console.log(111); + + // }) +} + +// 下面是子菜单操作项 +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> + +<template> + <AppContainerBox> + <template #subMenu> + <!-- 标题 --> + <AppSubMenuTitle title="图像分析"></AppSubMenuTitle> + + <!-- 按钮 --> + <div class="px-5 mb-5"> + <Button type="primary" class="w-full" @click="handleAddMessage"> + 新建会话 + </Button> + </div> + + <!-- 会话列表 --> + <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 /> + </template> + <template #content> + <Spin :spinning="spinning" wrapper-class-name="app-content-spin"> + <!-- 默认导语 --> + <AppConversationDefault + v-if="conversationDefaultShow" + height="calc(100% - 160px)" + :lead-data="leadData" + > + </AppConversationDefault> + + <!-- 消息列表 --> + <AppMessage + v-if="!conversationDefaultShow && appMessageShow" + ref="appMessageRef" + class="pl-27 pr-5" + :list="messageList" + @on-scroll-top="onScrollTop" + @reload-message="reloadMessage" + > + </AppMessage> + </Spin> + + <!-- 发送框 --> + <div class="upload-box absolute right-0 bottom-5 pl-52 pr-32 mt-10s w-full"> + <UploadDragger + v-model:fileList="fileList" + name="file" + :multiple="true" + :max-count="1" + :custom-request="handleCustomRequest" + @change="handleUploadChange" + @drop="handleUploadDrop" + > + <SvgIcon class="upload-icon" name="image"></SvgIcon> + <p class="ant-upload-text"> + 拖拽图片到这里,或者点击添加 + </p> + <p class="ant-upload-hint"> + 图片格式支持jpg、jpeg、png、gif、webp + </p> + </UploadDragger> + </div> + </template> + </AppContainerBox> +</template> + +<style lang="scss" scoped> +.upload-box { + .upload-icon { + width: 50px; + height: 50px; + } +} +</style>