Browse Source

feat:基本完成会话功能;

dxj
李朋徽 1 year ago
parent
commit
3bead79c63
  1. 5
      src/api/base/message.ts
  2. 22
      src/assets/svg/again.svg
  3. 16
      src/assets/svg/copy.svg
  4. 20
      src/assets/svg/stop.svg
  5. 135
      src/components/AppMessage/index.vue
  6. 11
      src/design/public.scss
  7. 2
      src/enums/messageEnum.ts
  8. 63
      src/hooks/useMqtt.ts
  9. 6
      src/store/moules/messageStore/index.ts
  10. 38
      src/utils/copyTextToClipboard.ts
  11. 12
      src/utils/mqtt.ts
  12. 113
      src/views/conversation/index.vue

5
src/api/base/message.ts

@ -12,14 +12,14 @@ export async function conversationList() {
/** /**
* @description * @description
*/ */
export async function historyMessage(data: { export async function historyMessage(params: {
conversationId: string conversationId: string
current: number current: number
size: number size: number
}) { }) {
return defHttp.get({ return defHttp.get({
url: `/open-chat/chat/chatMessageLog/page`, url: `/open-chat/chat/chatMessageLog/page`,
data, params,
}) })
} }
@ -34,6 +34,7 @@ export async function sendMessage(data: {
return defHttp.post({ return defHttp.post({
url: `/open-chat/chat/session`, url: `/open-chat/chat/session`,
data, data,
timeout: 30 * 1000,
}) })
} }

22
src/assets/svg/again.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 6</title>
<defs>
<rect id="path-1" x="0" y="0" width="22" height="22"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="回答结束" transform="translate(-639.000000, -692.000000)">
<g id="编组-6" transform="translate(639.000000, 692.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<g id="tizhibianbiezhongxinceshi" mask="url(#mask-2)" fill="#4670E3" fill-rule="nonzero">
<g transform="translate(0.000000, 0.004246)" id="路径">
<path d="M3.18539434,11.6316828 L6.37084384,8.24859031 L4.50456772,8.24859031 C4.52177325,8.19915198 4.53895121,8.14979637 4.55756296,8.10107495 C5.6647278,5.12189882 8.39897328,3.01358719 11.5972718,3.01358719 C15.7752653,3.01358719 19.162604,6.61218997 19.162604,11.050169 C19.162604,15.4887822 15.7752653,19.0874952 11.5972718,19.0874952 C8.52354796,19.0874952 5.87811497,17.1409266 4.69506935,14.3451659 C4.68359899,14.3186407 4.67499624,14.2899924 4.66498724,14.2627503 L2.63545158,16.4047836 C2.64832815,16.4298199 2.65833714,16.4585509 2.67267509,16.483532 C2.67554268,16.4892948 2.67841027,16.4950024 2.68125028,16.5007376 C4.47161804,19.7856979 7.81602567,21.9957537 11.6473719,21.9957537 C17.3651225,21.9957537 22,17.0715531 22,10.9971186 C22,4.92351133 17.3651225,6.8598109e-16 11.6473719,6.8598109e-16 C6.86784656,6.8598109e-16 2.84453738,3.44038912 1.65286142,8.11679155 C1.64139106,8.15980538 1.63852347,8.20488716 1.62848692,8.24856275 L-6.61481765e-16,8.24856275 L3.18539434,11.6316828 Z"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

16
src/assets/svg/copy.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="24px" viewBox="0 0 22 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>fuzhi</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="回答结束" transform="translate(-849.000000, -691.000000)">
<g id="fuzhi" transform="translate(849.000000, 692.000000)">
<rect id="矩形" x="0" y="0" width="22" height="22"></rect>
<g id="编组-5" transform="translate(1.000000, 0.000000)" fill="#4670E3" fill-rule="nonzero" stroke="#4670E3" stroke-width="0.4">
<path d="M16.8235294,19.4117647 L0,19.4117647 L0,0 L16.8235294,0 L16.8235294,19.4117647 Z M1.52941176,18.0252101 L15.2941176,18.0252101 L15.2941176,1.38655462 L1.52941176,1.38655462 L1.52941176,18.0252101 Z" id="形状"></path>
<path d="M3.4,7.6 L13.4,7.6 L13.4,9 L3.4,9 L3.4,7.6 Z M3.4,10.6 L13.4,10.6 L13.4,12 L3.4,12 L3.4,10.6 Z M3.4,13.6 L13.4,13.6 L13.4,15 L3.4,15 L3.4,13.6 Z M3.4,4.6 L8.4,4.6 L8.4,6 L3.4,6 L3.4,4.6 Z" id="形状"></path>
<polygon id="路径" points="19.4117647 22 6.47058824 22 6.47058824 20.6134454 18.1792717 20.6134454 18.1792717 3.97478992 16.977591 3.97478992 16.977591 2.58823529 19.4117647 2.58823529"></polygon>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

20
src/assets/svg/stop.svg

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 6</title>
<defs>
<rect id="path-1" x="0" y="0" width="22" height="22"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="回答中" transform="translate(-641.000000, -668.000000)">
<g id="编组-6" transform="translate(641.000000, 668.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<g id="tingzhi" mask="url(#mask-2)" fill="#4670E3" fill-rule="nonzero">
<path d="M8.27047258,8.27376029 L13.7620168,8.27376029 L13.7620168,13.7653042 L8.27047258,13.7653042 L8.27047258,8.27376029 Z M20.5028874,16.5385339 L18.4572872,14.4970525 C20.1853307,10.8423285 19.0340567,6.47431111 15.7291045,4.14612872 C12.4241523,1.81794634 7.92352899,2.20446963 5.06394361,5.06207587 C2.20435824,7.91968212 1.8147193,12.4200365 4.1406133,15.7265995 C6.46650729,19.0331625 10.8337269,20.1874601 14.4896465,18.4619472 L16.5352467,20.5048015 C11.758152,23.2868757 5.65835199,22.09116 2.28515453,17.7114216 C-1.08804293,13.3316833 -0.686533687,7.12877414 3.22314327,3.22045025 C7.13282022,-0.687873643 13.3358683,-1.08723612 17.7144393,2.2874767 C22.0930102,5.66218951 23.2866148,11.7624026 20.5028874,16.5385339 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

135
src/components/AppMessage/index.vue

@ -1,22 +1,104 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { nextTick, onMounted, ref, watch } from 'vue'
import type { MessageItem } from './index.d' import type { MessageItem } from './index.d'
import { SvgIcon } from '@/components/SvgIcon'
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
import { copyText } from '@/utils/copyTextToClipboard'
defineProps<{ const props = defineProps<{
list: MessageItem[] list: MessageItem[]
}>() }>()
const emit = defineEmits(['onScrollTop', 'reloadMessage'])
defineExpose({ getScrollTopDistance, seamlessScrollToTop })
const messageRef = ref<Element | null>(null) const messageRef = ref<Element | null>(null)
const elHeight = ref(0) //
const defaultScrollTop = ref(0) //
const isStop = ref(false) //
onMounted(async () => { watch(
() => props.list[props.list.length - 1],
() => {
isStop.value && scrollToBottom()
},
{ immediate: true },
)
/**
* @description: 滚动到底部
*/
async function scrollToBottom() {
if (!messageRef.value) {
return
}
await nextTick()
defaultScrollTop.value = messageRef.value.scrollTop
messageRef.value.scrollTo(0, messageRef.value.scrollHeight)
}
/**
* @description: 监听滚动
*/
function onScroll() {
if (!messageRef.value) {
return
}
// 20px
if (messageRef.value.scrollTop - defaultScrollTop.value > 20) {
isStop.value = true
}
else {
isStop.value = false
}
emit('onScrollTop', messageRef.value.scrollTop)
}
/**
* @description: 上滑加载无缝滚动
*/
async function seamlessScrollToTop() {
if (!messageRef.value) {
return
}
await getScrollTopDistance()
messageRef.value.scrollTo(0, elHeight.value)
}
/**
* @description: 获取前10个元素的总高度用于无缝滚动顶部的距离
* @param size 获取的元素个数默认为10
*/
async function getScrollTopDistance(size = 10) {
if (!messageRef.value || messageRef.value.children.length < size) {
return
}
await nextTick()
elHeight.value = 0
Array.from(messageRef.value.children).slice(0, size).forEach((item) => {
elHeight.value = elHeight.value + item.clientHeight
})
// +130
elHeight.value += 130
}
/**
* @description: 重新回答
*/
function reloadMessage() {
emit('reloadMessage')
}
onMounted(async () => {
scrollToBottom()
}) })
</script> </script>
<template> <template>
<div ref="messageRef" class="app-message"> <div ref="messageRef" class="app-message" @scroll="onScroll">
<div class="content-wrap">
<div v-for="(item, index) in list" :key="item.time + index" class="item"> <div v-for="(item, index) in list" :key="item.time + index" class="item">
<div v-if="item.messageType === MessageTypeEnum.USER" class="user"> <div v-if="item.messageType === MessageTypeEnum.USER" class="user">
<div class="content"> <div class="content">
@ -34,36 +116,34 @@ onMounted(async () => {
</div> </div>
<div <div
v-if="item.messageStatus === MessageStatusEnum.ACTICON" v-if="item.messageStatus === MessageStatusEnum.ACTICON"
class="loading" class="stop"
> >
正在回答中... <SvgIcon class="icon" name="stop"></SvgIcon>
停止回答
</div> </div>
<div v-if="item.messageStatus === MessageStatusEnum.END" class="btns"> <div v-if="item.messageStatus === MessageStatusEnum.END" class="btns">
<div class="copy" @click="copyText(item.content)"> <div class="copy" @click="copyText(item.content)">
<SvgIcon class="icon" name="copy"></SvgIcon>
复制 复制
</div> </div>
<div v-if="index === 0" class="reload" @click="reloadMessage"> <div v-if="index === list.length - 1" class="reload" @click="reloadMessage">
<SvgIcon class="icon" name="again"></SvgIcon>
重新回答 重新回答
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@include app('message') { @include app('message') {
height: calc(100% - 120px); height: calc(100% - 120px);
overflow: auto; overflow: auto;
.content-wrap {
box-sizing: border-box; box-sizing: border-box;
transform: rotate(180deg);
.item { .item {
padding: 0 15px; padding: 0 15px;
margin-top: 15px; margin-top: 15px;
transform: rotate(180deg);
.user, .user,
.ai { .ai {
display: flex; display: flex;
@ -71,6 +151,7 @@ onMounted(async () => {
.user { .user {
justify-content: flex-end; justify-content: flex-end;
.content { .content {
max-width: calc(100% - 110px);
color: #ffffff; color: #ffffff;
padding: 10px 15px; padding: 10px 15px;
background: linear-gradient(131deg, #009bfc 0%, #00eadb 100%); background: linear-gradient(131deg, #009bfc 0%, #00eadb 100%);
@ -92,24 +173,28 @@ onMounted(async () => {
height: 40px; height: 40px;
margin-right: 15px; margin-right: 15px;
} }
.icon {
margin-right: 5px;
}
.content { .content {
width: calc(100% - 55px); width: calc(100% - 110px);
padding: 10px 15px; padding: 10px 15px;
background: #ffffff; background: #ffffff;
border-radius: 0rpx 15px 15px 15px; border-radius: 0px 15px 15px 15px;
border: 1px solid #e7edef; border: 1px solid #e7edef;
} }
.error { .error {
color: red; color: red;
height: 30px; height: 30px;
padding-left: 60px; padding-left: 60px;
margin: 10px auto 0 auto; margin-top: 10px;
} }
.loading { .stop {
height: 30px; height: 30px;
color: #009dfb; color: #4670e3;
padding-left: 60px; padding-left: 60px;
margin: 20rpx auto 0 auto; margin-top: 10px;
cursor: pointer;
} }
.btns { .btns {
width: 100%; width: 100%;
@ -117,19 +202,15 @@ onMounted(async () => {
padding-left: 60px; padding-left: 60px;
margin: 10px auto 0 auto; margin: 10px auto 0 auto;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
.copy { .copy {
color: #009dfb; color: #4670e3;
padding: 5px 15px;
background: #c1effb;
border-radius: 15px;
text-align: center;
margin-right: 20px; margin-right: 20px;
cursor: pointer;
} }
.reload { .reload {
color: #009dfb; color: #4670e3;
} cursor: pointer;
} }
} }
} }

11
src/design/public.scss

@ -12,19 +12,14 @@
height: 8px; height: 8px;
} }
// ::-webkit-scrollbar-track {
// background: transparent;
// }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: rgba($color: #000000, $alpha: 0.1); background-color: rgba($color: #eeeeee, $alpha: 0.6);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: rgba($color: #bdc1c94d, $alpha: 0.2); background-color: rgba($color: #cccccc, $alpha: 0.4);
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0 0 6px rgba($color: #000000, $alpha: 0.2); // box-shadow: inset 0 0 6px rgba($color: #000000, $alpha: 0.2);
cursor: pointer;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {

2
src/enums/messageEnum.ts

@ -2,7 +2,7 @@
* @description: ai/user * @description: ai/user
*/ */
export enum MessageTypeEnum { export enum MessageTypeEnum {
AI = 'ai', AI = 'bot',
USER = 'user', USER = 'user',
} }

63
src/hooks/useMqtt.ts

@ -3,6 +3,7 @@ import { MqttService } from '@/utils/mqtt'
import { useUserStore } from '@/store/moules/userStore/index' import { useUserStore } from '@/store/moules/userStore/index'
import { useMessageStore } from '@/store/moules/messageStore/index' import { useMessageStore } from '@/store/moules/messageStore/index'
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
import { sendMessage as sendMessageApi } from '@/api/base/message'
export function useMqtt() { export function useMqtt() {
const userStore = useUserStore() const userStore = useUserStore()
@ -30,11 +31,9 @@ export function useMqtt() {
.subscribe(topicKey) .subscribe(topicKey)
.then(() => { .then(() => {
mqttService.onMessage(topicKey, (messageData: any) => { mqttService.onMessage(topicKey, (messageData: any) => {
console.log(messageData)
if (messageData.message_type === MessageStatusEnum.ACTICON) { if (messageData.message_type === MessageStatusEnum.ACTICON) {
messageStore.setMessageStatus(MessageStatusEnum.ACTICON) messageStore.setMessageStatus(MessageStatusEnum.ACTICON)
messageStore.setMessageFirstItem({ messageStore.setMessageLastItem({
messageType: MessageTypeEnum.AI, messageType: MessageTypeEnum.AI,
content: messageData.message_content, content: messageData.message_content,
time: String(new Date().getTime()), time: String(new Date().getTime()),
@ -44,7 +43,7 @@ export function useMqtt() {
} }
if (messageData.message_type === MessageStatusEnum.END) { if (messageData.message_type === MessageStatusEnum.END) {
messageStore.setMessageStatus(MessageStatusEnum.END) messageStore.setMessageStatus(MessageStatusEnum.END)
messageStore.setMessageFirstItem({ messageStore.setMessageLastItem({
messageType: MessageTypeEnum.AI, messageType: MessageTypeEnum.AI,
content: messageData.message_content, content: messageData.message_content,
time: String(new Date().getTime()), time: String(new Date().getTime()),
@ -54,9 +53,6 @@ export function useMqtt() {
} }
}) })
}) })
.catch(() => {
// uni.hideLoading()
})
} }
/** /**
@ -79,8 +75,61 @@ export function useMqtt() {
}) })
} }
/**
* @description:
*/
const unsubscribe = (topicKey: string) => {
mqttService.unsubscribe(topicKey)
}
/**
* @description:
*/
const end = () => {
console.log('123123123123')
mqttService.end()
}
/**
* @description: hook
*/
const sendMessage = async (roleId: number, 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
}
try {
await sendMessageApi({
roleId,
conversationId,
question,
})
}
catch (error: any) {
messageStore.setMessageStatus(MessageStatusEnum.END)
}
}
return { return {
connect, connect,
subscribe, subscribe,
unsubscribe,
end,
sendMessage,
} }
} }

6
src/store/moules/messageStore/index.ts

@ -39,10 +39,14 @@ export const useMessageStore = defineStore('useMessageStore', {
this.messageList.push(item) this.messageList.push(item)
}, },
setMessageFirstItem(item: MessageItem) { setMessageUnshiftItem(item: MessageItem) {
this.messageList.unshift(item) this.messageList.unshift(item)
}, },
setMessageLastItem(item: MessageItem) {
this.messageList[this.messageList.length - 1] = item
},
setTopicKey(key: string) { setTopicKey(key: string) {
this.topicKey = key this.topicKey = key
}, },

38
src/utils/copyTextToClipboard.ts

@ -0,0 +1,38 @@
import { message } from 'ant-design-vue'
export function copyText(text: string, prompt: string | null = '已成功复制到剪切板!') {
// 浏览器禁用了非安全域的 navigator.clipboard 对象
// 在线上环境会报错 TypeError: Cannot read properties of undefined (reading 'writeText')
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(
() => {
prompt && message.success(prompt)
},
(error: Error) => {
message.error(`复制失败!${error.message}`)
},
)
}
else {
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
try {
// 尝试执行复制操作
const success = document.execCommand('copy')
if (success) {
message.success('复制成功')
}
else {
message.error('复制失败')
}
}
catch (error) {
message.error(`复制失败:${error}`)
}
document.body.removeChild(textarea)
}
}

12
src/utils/mqtt.ts

@ -1,4 +1,5 @@
import mqtt from 'mqtt' import mqtt from 'mqtt'
import { message } from 'ant-design-vue'
export interface Options { export interface Options {
host: string host: string
@ -24,13 +25,11 @@ export class MqttService {
const { protocol, host, port } = this.options const { protocol, host, port } = this.options
const connectUrl = `${protocol}://${host}:${port}/mqtt` const connectUrl = `${protocol}://${host}:${port}/mqtt`
this.client = mqtt.connect(connectUrl, this.options) this.client = mqtt.connect(connectUrl, this.options)
console.log(this.client)
this.client.on('connect', () => { this.client.on('connect', () => {
console.log('连接成功')
resolve(true) resolve(true)
}) })
this.client.on('error', (error: any) => { this.client.on('error', (error: any) => {
console.log('连接失败!', error) message.error('连接失败!')
reject(error) reject(error)
}) })
}) })
@ -42,13 +41,12 @@ export class MqttService {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.subscribe(topic, (err: any) => { this.client?.subscribe(topic, (err: any) => {
if (err) { if (err) {
console.log('订阅失败!', err) message.error('订阅失败!请稍后重试')
reject(err) reject(err)
} }
else { else {
console.log('订阅成功!', topic)
resolve(true) resolve(true)
} }
}) })
@ -76,7 +74,7 @@ export class MqttService {
this.client.unsubscribe(topic, (err: any) => { this.client.unsubscribe(topic, (err: any) => {
if (err) { if (err) {
console.log('取消订阅失败!', err) message.error('取消订阅失败!!请联系管理员')
} }
else { else {
console.log('取消订阅成功!', topic) console.log('取消订阅成功!', topic)

113
src/views/conversation/index.vue

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { Button } from 'ant-design-vue' import { onBeforeRouteLeave } from 'vue-router'
import { Button, 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'
@ -11,7 +12,7 @@ import { AppMessage } from '@/components/AppMessage'
import type { MessageItem } from '@/components/AppMessage/index.d' import type { MessageItem } from '@/components/AppMessage/index.d'
import { useMessageStore } from '@/store/moules/messageStore/index' import { useMessageStore } from '@/store/moules/messageStore/index'
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
import { conversationList, historyMessage, sendMessage } from '@/api/base/message' import { conversationList, historyMessage } from '@/api/base/message'
import { useMqtt } from '@/hooks/useMqtt' import { useMqtt } from '@/hooks/useMqtt'
const messageStore = useMessageStore() const messageStore = useMessageStore()
@ -19,10 +20,12 @@ const messageStore = useMessageStore()
const sendBtnLoading = ref(false) const sendBtnLoading = ref(false)
const subMenuActive = ref(0) const subMenuActive = ref(0)
const subMenuList = ref<SubMenuItem[]>([]) const subMenuList = ref<SubMenuItem[]>([])
const appMessage = ref()
const messageList = computed(() => messageStore.getMessageList) const messageList = computed(() => messageStore.getMessageList)
const messageStatus = computed(() => messageStore.getMessageStatus)
const conversationData = computed(() => messageStore.getConversationData) const conversationData = computed(() => messageStore.getConversationData)
const historyMessageParams = ref({ const historyMessageParams = ref({
conversationId: conversationData.value ? conversationData.value.id : '', conversationId: '',
current: 1, current: 1,
size: 10, size: 10,
total: 0, total: 0,
@ -30,40 +33,33 @@ const historyMessageParams = ref({
const conversationDefaultShow = ref(true) const conversationDefaultShow = ref(true)
watch(
() => messageStatus.value,
(val) => {
if (val === MessageStatusEnum.END) {
sendBtnLoading.value = false
}
},
)
function handleSubMenuChange(index: number) { function handleSubMenuChange(index: number) {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return
}
subMenuActive.value = index subMenuActive.value = index
} }
/**
* @description: 发送消息
*/
function handleSend(value: string) { function handleSend(value: string) {
console.log('handleSend', value)
sendBtnLoading.value = true sendBtnLoading.value = true
conversationDefaultShow.value = false conversationDefaultShow.value = false
messageStore.setMessageStatus(MessageStatusEnum.LOADING)
messageStore.setMessageFirstItem({
messageType: MessageTypeEnum.USER,
content: value,
time: String(new Date().getTime()),
avatar: '',
})
messageStore.setMessageFirstItem({
messageType: MessageTypeEnum.AI,
content: '正在思考中...',
time: String(new Date().getTime()),
avatar: '',
messageStatus: MessageStatusEnum.LOADING,
})
if (!messageStore.getConversationData) { if (!messageStore.getConversationData) {
return return
} }
sendMessage({ useMqtt().sendMessage(messageStore.getConversationData.roleId, messageStore.getConversationData.id, value)
roleId: messageStore.getConversationData.roleId,
conversationId: messageStore.getConversationData.id,
question: value,
}).then(() => {
}).catch(() => {
messageStore.setMessageStatus(MessageStatusEnum.END)
})
} }
/** /**
@ -86,6 +82,10 @@ async function getConversationList() {
* @description: 获取历史对话记录 * @description: 获取历史对话记录
*/ */
async function getHistoryMessage() { async function getHistoryMessage() {
if (!conversationData.value) {
return
}
historyMessageParams.value.conversationId = conversationData.value.id
const res = await historyMessage(historyMessageParams.value) const res = await historyMessage(historyMessageParams.value)
if (!res || !res.records || !res.records.length) { if (!res || !res.records || !res.records.length) {
return return
@ -99,14 +99,59 @@ async function getHistoryMessage() {
messageStatus: MessageStatusEnum.END, messageStatus: MessageStatusEnum.END,
} }
historyMessageParams.value.total = res.total historyMessageParams.value.total = res.total
messageStore.setMessagePushItem(itemData) messageStore.setMessageUnshiftItem(itemData)
}) })
conversationDefaultShow.value = false 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()
//
appMessage.value.seamlessScrollToTop()
}
}
/**
* @description: 重新回答
*/
function reloadMessage() {
if (!messageStore.getConversationData) {
return
}
const question = messageList.value[messageList.value.length - 2]?.content
useMqtt().sendMessage(messageStore.getConversationData.roleId, messageStore.getConversationData.id, question)
}
onMounted(() => { onMounted(() => {
getConversationList() getConversationList()
}) })
onUnmounted(() => {
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script> </script>
<template> <template>
@ -139,7 +184,15 @@ onMounted(() => {
</AppConversationDefault> </AppConversationDefault>
<!-- 消息列表 --> <!-- 消息列表 -->
<AppMessage v-else :list="messageList"></AppMessage> <AppMessage
v-else
ref="appMessage"
class="pl-27 pr-5"
:list="messageList"
@on-scroll-top="onScrollTop"
@reload-message="reloadMessage"
>
</AppMessage>
<!-- 发送框 --> <!-- 发送框 -->
<AppTextarea <AppTextarea

Loading…
Cancel
Save