Browse Source

feat:增加文生图功能;增加接口数据加密判断;

dxj
李朋徽 1 year ago
parent
commit
2e809218a7
  1. 8
      .env
  2. 25
      src/api/base/message.ts
  3. 39
      src/components/AppMessage/index.vue
  4. 3
      src/components/AppTextToPictureDefault/index.d.ts
  5. 3
      src/components/AppTextToPictureDefault/index.ts
  6. 104
      src/components/AppTextToPictureDefault/index.vue
  7. 26
      src/enums/menuEnum.ts
  8. 51
      src/hooks/useMqtt.ts
  9. 8
      src/layout/AppMenu/index.vue
  10. 4
      src/router/index.ts
  11. 1
      src/store/moules/messageStore/index.d.ts
  12. 34
      src/utils/axios/index.ts
  13. 53
      src/views/conversation/index.vue
  14. 347
      src/views/textToImage/index.vue
  15. 19
      src/views/textToPicture/index.vue
  16. 2
      types/vite-env.d.ts

8
.env

@ -5,4 +5,10 @@ VITE_GLOB_APP_TITLE = 青鸟语言大模型-同聪
VITE_GLOB_APP_SHORT_NAME = 同聪
# token key
VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth"
VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth"
# authenticationScheme
VITE_GLOB_APP_AUTHENTICATION_SCHEME = 'Basic'
# tokenScheme
VITE_GLOB_APP_TOKEN_SCHEME = 'crypto'

25
src/api/base/message.ts

@ -1,9 +1,11 @@
import { defHttp } from '@/utils/axios/index'
import type { MenuTypeEnum } from '@/enums/menuEnum'
/**
* @description
*/
export async function addMessage(data: {
type: MenuTypeEnum
title: string
}) {
return defHttp.post({
@ -34,9 +36,9 @@ export async function removeMessage(ids: string) {
/**
* @description
*/
export async function conversationList() {
export async function conversationList(type: number) {
return defHttp.get({
url: `/open-chat/chat/conversation/list`,
url: `/open-chat/chat/conversation/list?type=${type}`,
})
}
@ -55,10 +57,9 @@ export async function historyMessage(params: {
}
/**
* @description
* @description
*/
export async function sendMessage(data: {
roleId: number
export async function sendTextToText(data: {
conversationId: string
question: string
}) {
@ -69,6 +70,20 @@ export async function sendMessage(data: {
})
}
/**
* @description
*/
export async function sendTextToImage(data: {
conversationId: string
question: string
}) {
return defHttp.post({
url: `/open-gpts/gpts/getDallEImages`,
data,
timeout: 30 * 1000,
})
}
/**
* @description chat信息
*/

39
src/components/AppMessage/index.vue

@ -1,11 +1,14 @@
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { Image } from 'ant-design-vue'
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import type { MessageItem } from './index.d'
import { SvgIcon } from '@/components/SvgIcon'
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
import { MenuTypeEnum } from '@/enums/menuEnum'
import { copyText } from '@/utils/copyTextToClipboard'
import { useMessageStore } from '@/store/moules/messageStore/index'
const props = defineProps({
list: {
@ -18,10 +21,13 @@ const emit = defineEmits(['onScrollTop', 'reloadMessage'])
defineExpose({ getScrollTopDistance, seamlessScrollToTop })
const messageStore = useMessageStore()
const messageRef = ref<Element | null>(null)
const elHeight = ref(0) //
const defaultScrollTop = ref(0) //
const isAutoScroll = ref(true) //
const conversationData = computed(() => messageStore.getConversationData)
watch(
() => props.list[props.list.length - 1],
@ -39,8 +45,12 @@ async function scrollToBottom() {
return
}
await nextTick()
defaultScrollTop.value = messageRef.value.scrollTop
messageRef.value.scrollTo(0, messageRef.value.scrollHeight)
setTimeout(() => {
if (messageRef.value) {
defaultScrollTop.value = messageRef.value.scrollTop
messageRef.value.scrollTo(0, messageRef.value.scrollHeight)
}
}, 200)
}
/**
@ -114,7 +124,22 @@ onMounted(async () => {
<div v-if="item.messageType === MessageTypeEnum.AI" class="ai">
<img class="icon-ai" src="@/assets/images/conversation/logo.png" alt="">
<div class="content">
<MdPreview editor-id="preview-only-ai" :model-value="item.content" />
<MdPreview
v-if="conversationData?.type === MenuTypeEnum.TEXT_TO_TEXT"
editor-id="preview-only-ai"
:model-value="item.content"
/>
<div v-if="conversationData?.type === MenuTypeEnum.TEXT_TO_IMAGE">
<Image
:width="200"
:src="item.content"
fallback="data:image/png;base64,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"
></Image>
<p v-if="!item.content.includes('http')">
{{ item.content }}
</p>
</div>
</div>
<div v-if="item.messageStatus === MessageStatusEnum.ERROR" class="error">
哎呀出问题了...
@ -151,7 +176,11 @@ onMounted(async () => {
}
:deep(.md-editor-preview-wrapper) {
padding: 0;
.text-to-image {
width: 30%;
}
}
.item {
padding: 0 15px;
margin-top: 20px;
@ -187,7 +216,7 @@ onMounted(async () => {
margin-right: 5px;
}
.content {
width: calc(100% - 110px);
max-width: calc(100% - 110px);
padding: 10px 15px;
background: #ffffff;
box-shadow: 0px 2px 10px 0px rgba(128, 135, 152, 0.4);

3
src/components/AppTextToPictureDefault/index.d.ts vendored

@ -0,0 +1,3 @@
export interface Props {
height?: string
}

3
src/components/AppTextToPictureDefault/index.ts

@ -0,0 +1,3 @@
import AppTextToPictureDefault from './index.vue'
export { AppTextToPictureDefault }

104
src/components/AppTextToPictureDefault/index.vue

@ -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>

26
src/enums/menuEnum.ts

@ -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',
}

51
src/hooks/useMqtt.ts

@ -1,14 +1,17 @@
import { ref } from 'vue'
import type { Options } from '@/utils/mqtt'
import { MqttService } from '@/utils/mqtt'
import { useUserStore } from '@/store/moules/userStore/index'
import { useMessageStore } from '@/store/moules/messageStore/index'
import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum'
import { sendMessage as sendMessageApi } from '@/api/base/message'
import { MenuTypeEnum } from '@/enums/menuEnum'
export function useMqtt() {
const userStore = useUserStore()
const messageStore = useMessageStore()
const messageData = ref('')
const options: Options = {
host: import.meta.env.VITE_GLOB_MQTT_HOST,
port: import.meta.env.VITE_GLOB_MQTT_PORT,
@ -30,22 +33,22 @@ export function useMqtt() {
mqttService
.subscribe(topicKey)
.then(() => {
mqttService.onMessage(topicKey, (messageData: any) => {
if (messageData.message_type === MessageStatusEnum.ACTICON) {
mqttService.onMessage(topicKey, (message: any) => {
if (message.message_type === MessageStatusEnum.ACTICON) {
messageStore.setMessageStatus(MessageStatusEnum.ACTICON)
messageStore.setMessageLastItem({
messageType: MessageTypeEnum.AI,
content: messageData.message_content,
content: message.message_content,
time: String(new Date().getTime()),
avatar: '',
messageStatus: MessageStatusEnum.ACTICON,
})
}
if (messageData.message_type === MessageStatusEnum.END) {
if (message.message_type === MessageStatusEnum.END) {
messageStore.setMessageStatus(MessageStatusEnum.END)
messageStore.setMessageLastItem({
messageType: MessageTypeEnum.AI,
content: messageData.message_content,
content: message.message_content,
time: String(new Date().getTime()),
avatar: '',
messageStatus: MessageStatusEnum.END,
@ -91,45 +94,11 @@ export function useMqtt() {
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 {
messageData,
connect,
subscribe,
unsubscribe,
end,
sendMessage,
}
}

8
src/layout/AppMenu/index.vue

@ -6,19 +6,19 @@ import { SvgIcon } from '@/components/SvgIcon'
import { MenuTypeEnum } from '@/enums/menuEnum'
const router = useRouter()
const menuActive = ref<MenuTypeEnum>(MenuTypeEnum.CONVERSATION)
const menuActive = ref<MenuTypeEnum>(MenuTypeEnum.TEXT_TO_TEXT)
const menu = ref<MenuItem[]>([
{
name: '会话',
icon: 'wei_xin',
path: '/conversation',
key: MenuTypeEnum.CONVERSATION,
key: MenuTypeEnum.TEXT_TO_TEXT,
},
{
name: '文生图',
icon: 'wen_sheng_tu',
path: '/textToPicture',
key: MenuTypeEnum.TEXT_TO_PICTURE,
path: '/textToImage',
key: MenuTypeEnum.TEXT_TO_IMAGE,
},
{
name: '角色',

4
src/router/index.ts

@ -41,8 +41,8 @@ export const constantRoutes: Array<RouteRecordRaw> = [
},
{
name: 'TextToPicture',
path: '/textToPicture',
component: () => import('@/views/textToPicture/index.vue'),
path: '/textToImage',
component: () => import('@/views/textToImage/index.vue'),
meta: {
title: '文生图',
},

1
src/store/moules/messageStore/index.d.ts vendored

@ -9,6 +9,7 @@ export interface ConversationData {
id: string
isDeleted: number
roleId: number
type: number
status: number
title: string
updateTime: string

34
src/utils/axios/index.ts

@ -2,7 +2,7 @@
// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
import type { AxiosInstance, AxiosResponse } from 'axios'
import { clone } from 'lodash-es'
import { clone, cloneDeep } from 'lodash-es'
import axios from 'axios'
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform'
import { VAxios } from './Axios'
@ -22,7 +22,7 @@ const { createMessage, createErrorModal, createSuccessModal } = useMessage()
// 请求白名单,无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 不需要解密接口白名单
const notDecryptWhiteList = ['/hulk-auth/oauth/token']
const notDecryptWhiteList = ['/hulk-auth/oauth/token', '/open-chat/chat/session', '/open-gpts/gpts/getDallEImages']
/**
* @description: 便
@ -33,9 +33,23 @@ const transform: AxiosTransform = {
*/
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { isTransformResponse, isReturnNativeResponse } = options
const newRes = cloneDeep(res)
let newData: Result
if (strHasArr(res.config.url!, notDecryptWhiteList)) {
newData = newRes.data
}
if (import.meta.env.VITE_GLOB_APP_TOKEN_SCHEME) {
newData = JSON.parse(crypto.decryptAES(newRes.data as unknown as string, crypto.aesKey))
}
else {
newData = newRes.data
}
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer')
return res.data
return newData
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse)
@ -44,17 +58,11 @@ const transform: AxiosTransform = {
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
if (strHasArr(res.config.url!, notDecryptWhiteList)) {
return res.data
}
else {
return JSON.parse(crypto.decryptAES(res.data as unknown as string, crypto.aesKey))
}
return newData
}
// 错误的时候返回
const data = JSON.parse(crypto.decryptAES(res.data as unknown as string, crypto.aesKey))
const data = newData
if (!data) {
// return '[HTTP] Request has no return value';
throw new Error(HttpErrorMsgEnum.API_REQUEST_FAILED)
@ -267,8 +275,8 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// authentication schemes,e.g: Bearer
tokenScheme: 'crypto',
authenticationScheme: 'Basic',
tokenScheme: import.meta.env.VITE_GLOB_APP_TOKEN_SCHEME,
authenticationScheme: import.meta.env.VITE_GLOB_APP_AUTHENTICATION_SCHEME,
timeout: 10 * 1000,
// 基础接口地址
// baseURL: globSetting.apiUrl,

53
src/views/conversation/index.vue

@ -14,8 +14,9 @@ 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, updateMessage } from '@/api/base/message'
import { addMessage, conversationList, historyMessage, removeMessage, sendTextToText, updateMessage } from '@/api/base/message'
import { useMqtt } from '@/hooks/useMqtt'
import { useMessage } from '@/hooks/useMessage'
@ -28,7 +29,7 @@ const subMenuActiveIndex = ref(0) // 当前会话索引
const subMenuActionIndex = ref(-1) //
const subMenuList = ref<SubMenuItem[]>([])
const subMenuInputValue = ref<string>('')
const appMessage = ref()
const appMessageRef = ref()
const messageList = computed(() => messageStore.getMessageList)
const messageStatus = computed(() => messageStore.getMessageStatus)
const conversationData = computed(() => messageStore.getConversationData)
@ -116,17 +117,17 @@ async function handleSend(value: string) {
if (subMenuActiveIndex.value === -1) {
try {
await addMessage({ title: '新的对话' })
await addMessage({ type: MenuTypeEnum.TEXT_TO_TEXT, title: '新的对话' })
await getConversationList()
await nextTick()
useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, value)
sendMessage(conversationData.value.id, value)
}
catch (error) {
message.error('新建对话失败,请稍后重试!')
}
}
else {
useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, value)
sendMessage(conversationData.value.id, value)
}
}
@ -134,7 +135,7 @@ async function handleSend(value: string) {
* @description: 获取会话列表
*/
async function getConversationList() {
const res = await conversationList()
const res = await conversationList(MenuTypeEnum.TEXT_TO_TEXT)
res.forEach((item: SubMenuItem) => {
item.actionType = SubMenuActionEnum.NOT
})
@ -159,6 +160,8 @@ async function getHistoryMessage() {
if (!res || !res.records || !res.records.length) {
return
}
console.log(res)
res.records.forEach((item: any) => {
const itemData: MessageItem = {
messageType: item.roleType === MessageTypeEnum.USER ? MessageTypeEnum.USER : MessageTypeEnum.AI,
@ -191,8 +194,40 @@ async function onScrollTop(scrollTop: number) {
await getHistoryMessage()
//
appMessage.value.seamlessScrollToTop()
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
}
sendTextToText({
conversationId,
question,
}).catch(() => {
messageStore.getMessageList.splice(-2)
messageStore.setMessageStatus(MessageStatusEnum.END)
})
}
/**
@ -203,7 +238,7 @@ function reloadMessage() {
return
}
const question = messageList.value[messageList.value.length - 2]?.content
useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, question)
sendMessage(conversationData.value.id, question)
}
/**
@ -287,7 +322,7 @@ onBeforeRouteLeave(() => {
<!-- 消息列表 -->
<AppMessage
v-if="!conversationDefaultShow && appMessageShow"
ref="appMessage"
ref="appMessageRef"
class="pl-27 pr-5"
:list="messageList"
@on-scroll-top="onScrollTop"

347
src/views/textToImage/index.vue

@ -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>

19
src/views/textToPicture/index.vue

@ -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>

2
types/vite-env.d.ts vendored

@ -11,6 +11,8 @@ interface ImportMetaEnv {
VITE_GLOB_MQTT_PORT: number
VITE_GLOB_MQTT_PROTOCOL: mqtt.MqttProtocol
VITE_GLOB_APP_AUTHORIZATION: string
VITE_GLOB_APP_AUTHENTICATION_SCHEME: string
VITE_GLOB_APP_TOKEN_SCHEME: string
VITE_GLOB_APP_TOKEN_KEY: string
VITE_GLOB_MQTT_USERNAME: string
VITE_GLOB_MQTT_PASSWORD: string

Loading…
Cancel
Save