16 changed files with 618 additions and 109 deletions
@ -0,0 +1,3 @@
|
||||
export interface Props { |
||||
height?: string |
||||
} |
@ -0,0 +1,3 @@
|
||||
import AppTextToPictureDefault from './index.vue' |
||||
|
||||
export { AppTextToPictureDefault } |
@ -0,0 +1,104 @@
|
||||
<!-- 会话默认对话组件 --> |
||||
<script setup lang="ts"> |
||||
import { ref } from 'vue' |
||||
import type { Props } from './index.d' |
||||
import DefaultImage from '@/assets/images/conversation/default_img.png' |
||||
import { AppContentDefaultBox } from '@/components/AppContentDefaultBox' |
||||
import { AppDefaultLead } from '@/components/AppDefaultLead' |
||||
import { AppModelSelect } from '@/components/AppModelSelect' |
||||
import { AppTopPicks } from '@/components/AppTopPicks' |
||||
import type { TopPickItem } from '@/components/AppTopPicks/index.d' |
||||
|
||||
withDefaults(defineProps<Props>(), { |
||||
height: '100%', |
||||
}) |
||||
|
||||
const leadData = { |
||||
title: '你好,我是青鸟语言大模型-同聪~', |
||||
subTitles: [ |
||||
'我现在可是一个超级无敌大画家~支持多种风格', |
||||
'你可以试着问我', |
||||
], |
||||
image: DefaultImage, |
||||
} |
||||
|
||||
const topPickList = ref<TopPickItem[]>([ |
||||
{ |
||||
id: '1', |
||||
label: '啦啦啦啦啦啦', |
||||
}, |
||||
{ |
||||
id: '2', |
||||
label: '啦啦啦啦啦啦', |
||||
}, |
||||
{ |
||||
id: '3', |
||||
label: '啦啦啦啦啦啦啦啦啦啦啦啦', |
||||
}, |
||||
{ |
||||
id: '4', |
||||
label: '啦啦啦啦啦啦', |
||||
}, |
||||
{ |
||||
id: '5', |
||||
label: '啦啦啦啦啦啦', |
||||
}, |
||||
]) |
||||
</script> |
||||
|
||||
<template> |
||||
<AppContentDefaultBox class="content-default-style"> |
||||
<AppModelSelect></AppModelSelect> |
||||
<AppDefaultLead |
||||
class="mt-20" |
||||
:title="leadData.title" |
||||
:sub-titles="leadData.subTitles" |
||||
:image="leadData.image" |
||||
></AppDefaultLead> |
||||
<AppTopPicks class="pl-20 mt-10" :list="topPickList"></AppTopPicks> |
||||
</AppContentDefaultBox> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.content-default-style { |
||||
height: v-bind(height); |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.category-box { |
||||
width: calc(50% - 10px); |
||||
padding: 15px 20px; |
||||
background-color: #edf3ff; |
||||
border-radius: 8px; |
||||
.top { |
||||
position: relative; |
||||
.title { |
||||
font-size: 18px; |
||||
font-weight: bold; |
||||
margin-bottom: 0; |
||||
} |
||||
.sub-title { |
||||
color: #6d7278; |
||||
margin-left: 30px; |
||||
margin-bottom: 0; |
||||
} |
||||
.icon { |
||||
width: 45px; |
||||
height: 18px; |
||||
cursor: pointer; |
||||
position: absolute; |
||||
right: 0; |
||||
top: 6px; |
||||
} |
||||
} |
||||
.picture-box { |
||||
margin-top: 20px; |
||||
.picture-item-role { |
||||
margin-left: -10px; |
||||
} |
||||
.picture-item + .picture-item { |
||||
margin-left: 15px; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -1,22 +1,28 @@
|
||||
export enum MenuTypeEnum { |
||||
// 会话
|
||||
CONVERSATION = 'conversation', |
||||
// 文生文
|
||||
TEXT_TO_TEXT = 1, |
||||
|
||||
// 文生图
|
||||
TEXT_TO_PICTURE = 'textToPicture', |
||||
// 文生图
|
||||
TEXT_TO_IMAGE, |
||||
|
||||
// 角色
|
||||
ROLE = 'role', |
||||
// 角色
|
||||
ROLE, |
||||
|
||||
// 任务
|
||||
// 视觉分析
|
||||
VISUAL_ANALYSIS, |
||||
|
||||
// 知识库
|
||||
REPOSITORY, |
||||
|
||||
// 任务
|
||||
TASK = 'task', |
||||
|
||||
// 渠道
|
||||
// 渠道
|
||||
CHANNEL = 'channel', |
||||
|
||||
// 小程序
|
||||
// 小程序
|
||||
APPLET = 'applet', |
||||
|
||||
// 我的
|
||||
// 我的
|
||||
USER = 'user', |
||||
} |
||||
|
@ -0,0 +1,347 @@
|
||||
<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' |
||||
import { AppSubMenuList } from '@/components/AppSubMenuList' |
||||
import { AppTextToPictureDefault } from '@/components/AppTextToPictureDefault' |
||||
import { AppUserInfo } from '@/components/AppUserInfo' |
||||
import { SubMenuActionEnum } from '@/components/AppSubMenuList/index.d' |
||||
import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' |
||||
|
||||
import { AppTextarea } from '@/components/AppTextarea' |
||||
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, sendTextToImage, updateMessage } from '@/api/base/message' |
||||
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 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() |
||||
}) |
||||
}, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @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.TEXT_TO_IMAGE, 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.TEXT_TO_IMAGE) |
||||
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 |
||||
} |
||||
|
||||
sendTextToImage({ |
||||
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> |
||||
|
||||
<template> |
||||
<AppContainerBox> |
||||
<template #subMenu> |
||||
<!-- 标题 --> |
||||
<AppSubMenuTitle></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"> |
||||
<!-- 默认导语 --> |
||||
<AppTextToPictureDefault |
||||
v-if="conversationDefaultShow" |
||||
height="calc(100% - 120px)" |
||||
> |
||||
</AppTextToPictureDefault> |
||||
|
||||
<!-- 消息列表 --> |
||||
<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> |
||||
</AppContainerBox> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
|
||||
</style> |
@ -1,19 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { AppContainerBox } from '@/components/AppContainerBox' |
||||
import { AppUserInfo } from '@/components/AppUserInfo' |
||||
</script> |
||||
|
||||
<template> |
||||
<AppContainerBox> |
||||
<template #subMenu> |
||||
我是子菜单 |
||||
|
||||
<AppUserInfo /> |
||||
</template> |
||||
<template #content> |
||||
我是文生图区域 |
||||
</template> |
||||
</AppContainerBox> |
||||
</template> |
||||
|
||||
<style scoped></style> |
Loading…
Reference in new issue