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>