Browse Source

fix:图像分析上传图片增加删除;mqtt接收消息增加当前会话id拦截判断;修改会话进行中时路由拦截的方式;需修改modelType枚举类型;角色删除模型切换下拉组件;

dxj
李朋徽 1 year ago
parent
commit
28af539b44
  1. 2
      src/components/AppConversationDefault/index.vue
  2. 3
      src/components/AppModelSelect/index.vue
  3. 152
      src/components/AppTextarea/index.vue
  4. 8
      src/enums/messageEnum.ts
  5. 13
      src/hooks/useMqtt.ts
  6. 18
      src/layout/AppMenu/index.vue
  7. 1
      src/utils/is.ts
  8. 15
      src/views/conversation/index.vue
  9. 13
      src/views/repository/index.vue
  10. 22
      src/views/role/index.vue
  11. 13
      src/views/textToImage/index.vue
  12. 139
      src/views/visualAnalysis/index.vue
  13. 9
      types/mqtt.d.ts

2
src/components/AppConversationDefault/index.vue

@ -14,7 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
isHot: false,
height: '100%',
leadData: () => ({
title: '你好,我是青鸟语言大模型-同聪~',
title: '你好,青鸟AI助手-同聪~',
subTitles: [
'我可以自由的跟你对话~陪你聊天~帮你想方案~答疑解惑。',
'你可以试着问我',

3
src/components/AppModelSelect/index.vue

@ -67,4 +67,7 @@ function handelChange(index: number, item: ModelSelect) {
height: 18px;
}
}
:deep(.ant-dropdown-menu-item) {
text-align: center;
}
</style>

152
src/components/AppTextarea/index.vue

@ -1,10 +1,13 @@
<!-- 公共输入框组件 -->
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Button, Textarea, message } from 'ant-design-vue'
import { Button, Image, Textarea, Upload, message } from 'ant-design-vue'
import { CloseOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { SvgIcon } from '@/components/SvgIcon'
import { isEmpty, trim } from '@/utils/is'
import { useMessageStore } from '@/store/moules/messageStore/index'
import { MessageStatusEnum } from '@/enums/messageEnum'
import { uploadApi } from '@/api/base/file'
const props = defineProps({
placeholder: {
@ -19,11 +22,35 @@ const props = defineProps({
type: Boolean,
default: false,
},
isUpload: {
type: Boolean,
default: false,
},
defaultFileUrl: {
type: String,
default: '',
},
uploadDisabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['pressEnter', 'send', 'stopMessage'])
const emit = defineEmits([
'pressEnter',
'send',
'stopMessage',
'update:defaultFileUrl',
'onUploadSuccess',
'onUploadFailed',
'deleteFile',
])
const messageStore = useMessageStore()
const value = ref('')
const uploadLoading = ref<boolean>(false)
const fileUrl = computed({
get: () => props.defaultFileUrl,
set: value => emit('update:defaultFileUrl', value),
})
const stopShow = computed(() => {
if (!props.isStop) {
@ -45,19 +72,26 @@ function pressEnter(event: KeyboardEvent) {
return false
}
if (value.value === '') {
if (
messageStore.getMessageList.length
&& messageStore.getMessageList[messageStore.getMessageList.length - 1].messageStatus !== MessageStatusEnum.END
) {
return false
}
if (isEmpty(trim(value.value))) {
message.warning('请输入内容')
setTimeout(() => {
value.value = ''
}, 0)
return false
}
emit('pressEnter', value.value)
emit('pressEnter', value.value, fileUrl.value)
handeleSend()
}
function handeleSend() {
emit('send', value.value)
emit('send', value.value, fileUrl.value)
setTimeout(() => {
value.value = ''
}, 0)
@ -66,6 +100,37 @@ function handeleSend() {
function stopMessage() {
emit('stopMessage')
}
/**
* @description: 图片上传前的处理
*/
function handleBeforeUpload(file: File) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif'
if (!isJpgOrPng) {
message.error('只能上传JPG、JPEG、PNG、GIF格式图片!')
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error('图片尺寸不能超过10MB!')
}
return isJpgOrPng && isLt10M
}
/**
* @description: 自定义图片上传服务器
*/
async function handleCustomRequest(option: any) {
const formData = new FormData()
formData.append('file', option.file)
uploadLoading.value = true
uploadApi(formData).then((res) => {
emit('onUploadSuccess', res)
uploadLoading.value = false
}).catch(() => {
emit('onUploadFailed')
uploadLoading.value = false
})
}
</script>
<template>
@ -79,9 +144,44 @@ function stopMessage() {
<SvgIcon class="mr-2" name="stop"></SvgIcon>
停止回答
</div>
<div v-if="isUpload">
<Upload
v-if="!fileUrl"
:disabled="uploadDisabled"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:custom-request="handleCustomRequest"
:before-upload="handleBeforeUpload"
>
<div>
<LoadingOutlined v-if="uploadLoading"></LoadingOutlined>
<PlusOutlined v-else></PlusOutlined>
</div>
</Upload>
<div
v-if="fileUrl"
class="avatar-uploader"
>
<div class=" relative">
<div class="img">
<Image
:width="60"
:height="60"
:src="fileUrl"
alt="example"
fallback="data:image/png;base64,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"
></Image>
</div>
<CloseOutlined class="delete-icon" @click="$emit('deleteFile')"></CloseOutlined>
</div>
</div>
</div>
<Textarea
v-model:value="value"
class="pt-4 pr-34 pb-6 pl-4"
class="pt-4 pr-34 pb-6"
:class="[isUpload ? 'pl-20' : 'pl-4']"
:placeholder="placeholder"
:auto-size="{ minRows: 3, maxRows: 8 }"
@press-enter="pressEnter"
@ -111,5 +211,43 @@ function stopMessage() {
left: calc(50% - 40px);
z-index: 1;
}
.avatar-uploader {
width: 60px;
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
:deep(.ant-upload) {
width: 60px !important;
height: 60px !important;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
color: #666;
}
.img {
border-radius: 5px;
overflow: hidden;
}
.delete-icon {
position: absolute;
top: -6px;
right: -6px;
color: #fff;
padding: 3px;
border-radius: 5px;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.6);
font-size: 12px;
}
}
}
</style>

8
src/enums/messageEnum.ts

@ -27,17 +27,17 @@ export enum MessageStatusEnum {
}
export enum ModelTypeEnum {
// GPT4
// GPT-4
GPT4 = 1,
// GPT3.5
// GPT-3.5-Turbo
GPT3 = 2,
// 文生图 DALL·E 3
DALL_E3 = 3,
// 视觉分析 gpt-vision
VISION = 4,
// 视觉分析 GPT-4-Vision
GPT4_VISION = 4,
// 知识库 QAnything
QANYTHING = 5,

13
src/hooks/useMqtt.ts

@ -4,6 +4,7 @@ import { MqttService } from '@/utils/mqtt'
import { useUserStore } from '@/store/moules/userStore/index'
import { useMessageStore } from '@/store/moules/messageStore/index'
import { MessageStatusEnum, MessageTypeEnum, ModelTypeEnum } from '@/enums/messageEnum'
import type { MqttMessageResult } from '/#/mqtt.d'
export function useMqtt() {
const userStore = useUserStore()
@ -32,12 +33,18 @@ export function useMqtt() {
mqttService
.subscribe(topicKey)
.then(() => {
mqttService.onMessage(topicKey, (message: any) => {
mqttService.onMessage(topicKey, (message: MqttMessageResult) => {
if (!messageStore.getConversationData) {
return
}
if (message.conversationId !== messageStore.getConversationData.id) {
return
}
if (message.message_type === MessageStatusEnum.ACTICON) {
// 这里主要是数据库的数据更新,因为数据库回答是每次都是一个字,并不是通常的不断叠加,需要前端手动叠加
// 这里主要是知识库的数据更新,因为知识库回答是每次都是一个字,并不是通常的不断叠加,需要前端手动叠加
if (
messageStore.getLastMessageList.messageStatus === MessageStatusEnum.ACTICON
&& messageStore.getConversationData
&& Number(messageStore.getConversationData.modelType) === ModelTypeEnum.QANYTHING) {
messageStore.setMessageStatus(MessageStatusEnum.ACTICON)
messageStore.setMessageLastPushItem(message.message_content)

18
src/layout/AppMenu/index.vue

@ -1,16 +1,21 @@
<script setup lang="ts">
import { createVNode, ref, watch } from 'vue'
import { computed, createVNode, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Button, Modal, Popover } from 'ant-design-vue'
import { Button, Modal, Popover, message } from 'ant-design-vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import type { MenuItem } from './index.d'
import { SvgIcon } from '@/components/SvgIcon'
import { MenuTypeEnum } from '@/enums/menuEnum'
import { MessageStatusEnum } from '@/enums/messageEnum'
import { useUserStore } from '@/store/moules/userStore/index'
import { useMessageStore } from '@/store/moules/messageStore/index'
const router = useRouter()
const menuActive = ref<MenuTypeEnum>(MenuTypeEnum.TEXT_TO_TEXT)
const messageStore = useMessageStore()
const userStore = useUserStore()
const messageStatus = computed(() => messageStore.getMessageStatus)
const menuActive = ref<MenuTypeEnum>(MenuTypeEnum.TEXT_TO_TEXT)
const menu = ref<MenuItem[]>([
{
name: '会话',
@ -77,6 +82,10 @@ watch(
)
function handleToPath(item: MenuItem) {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
menuActive.value = item.key
router.push({ path: item.path })
}
@ -88,9 +97,6 @@ function showConfirm() {
onOk() {
userStore.logout()
},
onCancel() {
console.log('Cancel')
},
class: 'test',
})
}

1
src/utils/is.ts

@ -32,6 +32,7 @@ export {
isUndefined,
isWeakMap,
isWeakSet,
trim,
} from 'lodash-es'
const toString = Object.prototype.toString

15
src/views/conversation/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
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 { AppSubMenuTitle } from '@/components/AppSubMenuTitle'
@ -38,11 +37,11 @@ const appMessageRef = ref()
const modelOptions: ModelSelect[] = [
{
label: '同聪3.5',
label: 'GPT-3.5-Turbo',
value: ModelTypeEnum.GPT3,
},
{
label: '同聪4.0',
label: 'GPT-4',
value: ModelTypeEnum.GPT4,
},
]
@ -379,14 +378,6 @@ onUnmounted(() => {
messageStore.setConversationData(undefined)
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script>
<template>
@ -433,7 +424,7 @@ onBeforeRouteLeave(() => {
:is-hot="false"
:role-list="roleList"
:apply-list="applyList"
height="calc(100% - 120px)"
height="calc(100% - 148px)"
@handle-pick="handlePick"
>
</AppConversationDefault>

13
src/views/repository/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { Button, Modal, Spin, message } from 'ant-design-vue'
import DefaultImage from '@/assets/images/conversation/default_img3.png'
import { SvgIcon } from '@/components/SvgIcon'
@ -56,7 +55,7 @@ const historyMessageParams = ref({
})
const leadData = ref({
title: '你好,我是青鸟语言大模型-同聪~',
title: '你好,青鸟AI助手-同聪~',
subTitles: [
'我可以自由的跟你对话~陪你聊天~帮你想方案~答疑解惑。',
'这里是你的知识库,你可以试着问我知识库里的问题',
@ -365,14 +364,6 @@ onUnmounted(() => {
messageStore.setConversationData(undefined)
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script>
<template>
@ -421,7 +412,7 @@ onBeforeRouteLeave(() => {
<AppConversationDefault
v-if="conversationDefaultShow"
:lead-data="leadData"
height="calc(100% - 120px)"
height="calc(100% - 148px)"
@handle-pick="handlePick"
>
</AppConversationDefault>

22
src/views/role/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
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 { AppSubMenuTitle } from '@/components/AppSubMenuTitle'
@ -12,7 +11,8 @@ import type { SubMenuItem } from '@/components/AppSubMenuList/index.d'
import { AppTextarea } from '@/components/AppTextarea'
import { AppMessage } from '@/components/AppMessage'
import { AppModelSelect } from '@/components/AppModelSelect'
// import { AppModelSelect } from '@/components/AppModelSelect'
import type { ModelSelect } from '@/components/AppModelSelect/index.d'
import type { RoleData, RoleInfoAppModel } from '@/components/AppRoleDefault/index.d'
import type { MessageItem } from '@/components/AppMessage/index.d'
@ -336,9 +336,9 @@ async function handleApply(item: RoleInfoAppModel) {
/**
* @description: gpt模型切换
*/
function handleModel(index: number) {
modelIndex.value = index
}
// function handleModel(index: number) {
// modelIndex.value = index
// }
//
function handleSubMenuInputAffirm(index: number, item: SubMenuItem, inputValue: string) {
@ -370,14 +370,6 @@ onUnmounted(() => {
messageStore.setConversationData(undefined)
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script>
<template>
@ -410,13 +402,13 @@ onBeforeRouteLeave(() => {
</template>
<template #content>
<Spin :spinning="spinning" wrapper-class-name="app-content-spin">
<AppModelSelect
<!-- <AppModelSelect
v-if="conversationDefaultShow"
:active-index="modelIndex"
:options="modelOptions"
@change="handleModel"
>
</AppModelSelect>
</AppModelSelect> -->
<!-- 默认导语 -->
<AppRoleDefault
v-if="conversationDefaultShow"

13
src/views/textToImage/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { Button, Spin, message } from 'ant-design-vue'
import DefaultImage from '@/assets/images/conversation/default_img2.png'
import { AppContainerBox } from '@/components/AppContainerBox'
@ -42,7 +41,7 @@ const historyMessageParams = ref({
total: 0,
})
const leadData = ref({
title: '你好,我是青鸟语言大模型-同聪~',
title: '你好,青鸟AI助手-同聪~',
subTitles: [
'我现在可是一个超级无敌大画家~支持多种风格',
'你可以试着问我',
@ -335,14 +334,6 @@ onUnmounted(() => {
messageStore.setConversationData(undefined)
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script>
<template>
@ -381,7 +372,7 @@ onBeforeRouteLeave(() => {
:is-pick="true"
:lead-data="leadData"
:top-pick-list="topPickList"
height="calc(100% - 120px)"
height="calc(100% - 148px)"
@handle-pick="handlePick"
>
</AppConversationDefault>

139
src/views/visualAnalysis/index.vue

@ -1,8 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { Button, Image, Spin, UploadDragger, message } from 'ant-design-vue'
import { SvgIcon } from '@/components/SvgIcon'
import { Button, Spin, message } from 'ant-design-vue'
import { AppTextarea } from '@/components/AppTextarea'
import { AppContainerBox } from '@/components/AppContainerBox'
import { AppSubMenuTitle } from '@/components/AppSubMenuTitle'
@ -15,16 +13,15 @@ import type { PictureType } from '@/components/AppPicture/index.d'
import { AppMessage } from '@/components/AppMessage'
import type { MessageItem } from '@/components/AppMessage/index.d'
import type { TopPickItem } from '@/components/AppTopPicks/index.d'
import { AppModelSelect } from '@/components/AppModelSelect'
import type { ModelSelect } from '@/components/AppModelSelect/index.d'
import { useMessageStore } from '@/store/moules/messageStore/index'
import { MenuTypeEnum } from '@/enums/menuEnum'
import { MessageStatusEnum, MessageTypeEnum, ModelTypeEnum } from '@/enums/messageEnum'
import { addMessage, conversationList, conversationToTop, historyMessage, removeMessage, sendVisualAnalysis, updateMessage } from '@/api/base/message'
import { uploadApi } from '@/api/base/file'
import { useMqtt } from '@/hooks/useMqtt'
import { useMessage } from '@/hooks/useMessage'
import type { UploadFileResult } from '/#/axios'
const { createConfirm } = useMessage()
@ -39,12 +36,11 @@ const appMessageRef = ref()
const modelOptions: ModelSelect[] = [
{
label: 'DALL-E 3',
value: ModelTypeEnum.DALL_E3,
label: 'GPT-4-Vision',
value: ModelTypeEnum.GPT4_VISION,
},
]
const modelIndex = ref(0)
const fileList = ref<string[]>([])
const roleList = ref<PictureType[]>([])
const applyList = ref<PictureType[]>([])
const messageList = computed(() => messageStore.getMessageList)
@ -60,13 +56,16 @@ const historyMessageParams = ref({
const conversationDefaultShow = ref(false)
const appMessageShow = ref(true)
const spinning = ref(true)
const uploadDisabled = ref(false)
const elIndex = ref(0) //
const defaultFileUrl = ref('')
watch(
() => messageStatus.value,
(val) => {
if (val === MessageStatusEnum.END) {
sendBtnLoading.value = false
uploadDisabled.value = false
}
},
)
@ -140,14 +139,20 @@ async function handleSubMenuChange(index: number, item?: SubMenuItem) {
// item
if (item) {
getHistoryMessage()
fileList.value = []
defaultFileUrl.value = ''
}
}
/**
* @description: 发送消息
*/
async function handleSend(value: string) {
async function handleSend(value: string, fileUrl: string) {
if (!fileUrl) {
message.error('请先上传图片!')
return
}
uploadDisabled.value = true
sendBtnLoading.value = true
conversationDefaultShow.value = false
@ -164,7 +169,8 @@ async function handleSend(value: string) {
message.error('对话发送,请稍后重试!')
return
}
sendMessage(conversationData.value.id, value)
await sendMessage(conversationData.value.id, value, fileUrl)
defaultFileUrl.value = ''
spinning.value = false
}
catch (error) {
@ -175,8 +181,8 @@ async function handleSend(value: string) {
if (!conversationData.value) {
return
}
sendMessage(conversationData.value.id, value)
fileList.value = []
await sendMessage(conversationData.value.id, value, fileUrl)
defaultFileUrl.value = ''
}
}
@ -252,7 +258,7 @@ async function onScrollTop(scrollTop: number) {
/**
* @description: 发送消息hook
*/
async function sendMessage(conversationId: string, question: string): Promise<void> {
async function sendMessage(conversationId: string, question: string, fileUrl: string): Promise<void> {
if (!messageStore.getConversationData) {
return
}
@ -261,7 +267,7 @@ async function sendMessage(conversationId: string, question: string): Promise<vo
messageStore.setMessageStatus(MessageStatusEnum.LOADING)
messageStore.setMessagePushItem({
messageType: MessageTypeEnum.USER,
content: `${question}![Picture](${fileList.value[0]})`,
content: `${question}![Picture](${fileUrl})`,
time: String(new Date().getTime()),
avatar: '',
})
@ -276,24 +282,13 @@ async function sendMessage(conversationId: string, question: string): Promise<vo
sendVisualAnalysis({
conversationId,
question,
fileUrl: fileList.value[0],
fileUrl,
}).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: 点击新建会话按钮
*/
@ -306,13 +301,6 @@ function handleAddMessage() {
subMenuActiveIndex.value = -1
}
/**
* @description: 点击默认看板的精选话题
*/
function handlePick(_index: number, item: TopPickItem) {
handleSend(item.label)
}
/**
* @description: gpt模型切换
*/
@ -320,31 +308,14 @@ function handleModel(index: number) {
modelIndex.value = index
}
/**
* @description: 图片上传前的处理
*/
function handleBeforeUpload(file: File) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif'
if (!isJpgOrPng) {
message.error('只能上传JPG、JPEG、PNG、GIF格式图片!')
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error('图片尺寸不能超过10MB!')
}
return isJpgOrPng && isLt10M
function onUploadSuccess(res: UploadFileResult) {
defaultFileUrl.value = res.link
}
/**
* @description: 自定义图片上传服务器
*/
async function handleCustomRequest(option: any) {
const formData = new FormData()
formData.append('file', option.file)
uploadApi(formData).then((res) => {
fileList.value.push(res.link)
})
function onUploadFailed() {
defaultFileUrl.value = ''
}
function handleDeleteFile() {
defaultFileUrl.value = ''
}
//
@ -377,14 +348,6 @@ onUnmounted(() => {
messageStore.setConversationData(undefined)
useMqtt().end()
})
//
onBeforeRouteLeave(() => {
if (messageStatus.value !== MessageStatusEnum.END) {
message.warn('请先结束对话')
return false
}
})
</script>
<template>
@ -427,12 +390,11 @@ onBeforeRouteLeave(() => {
<!-- 默认导语 -->
<AppConversationDefault
v-if="conversationDefaultShow"
:is-pick="true"
:is-pick="false"
:is-hot="false"
:role-list="roleList"
:apply-list="applyList"
height="calc(100% - 160px)"
@handle-pick="handlePick"
height="calc(100% - 148px)"
>
</AppConversationDefault>
@ -442,49 +404,26 @@ onBeforeRouteLeave(() => {
v-if="!conversationDefaultShow && appMessageShow"
ref="appMessageRef"
class="pl-27 pr-5"
height="calc(100% - 200px)"
height="calc(100% - 120px)"
:el-index="elIndex"
:list="messageList"
@on-scroll-top="onScrollTop"
@reload-message="reloadMessage"
>
</AppMessage>
<!-- 发送框 -->
<div v-if="!fileList.length" class="upload-box absolute right-0 bottom-5 pl-44 pr-24 mt-10s w-full">
<UploadDragger
name="file"
:multiple="false"
:max-count="1"
:show-upload-list="false"
:custom-request="handleCustomRequest"
:before-upload="handleBeforeUpload"
>
<SvgIcon class="upload-icon" name="image"></SvgIcon>
<p class="ant-upload-text">
拖拽图片到这里或者点击添加
</p>
<p class="ant-upload-hint">
图片格式支持jpgjpegpnggifwebp
</p>
</UploadDragger>
</div>
<div
v-else
class="w-full"
>
<div class="pl-44 pr-24 mt-2">
<Image
class="rounded"
:width="60"
:height="60"
:src="fileList[0]"
fallback="data:image/png;base64,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"
></Image>
</div>
<AppTextarea
class="pl-44 pr-24 mt-2"
class="pl-44 pr-24 mt-10"
:is-upload="true"
:default-file-url="defaultFileUrl"
:upload-disabled="uploadDisabled"
:btn-loading="sendBtnLoading"
@on-upload-success="onUploadSuccess"
@on-upload-failed="onUploadFailed"
@delete-file="handleDeleteFile"
@send="handleSend"
></AppTextarea>
</div>

9
types/mqtt.d.ts vendored

@ -0,0 +1,9 @@
export interface MqttMessageResult {
conversationId: string
message_content: string
message_time: string
message_type: 'a' | 'e'
pid: string
useTime: string
view_type: string
}
Loading…
Cancel
Save