diff --git a/src/api/ai/image/index.ts b/src/api/ai/image/index.ts index 496fac53..43ddc7ec 100644 --- a/src/api/ai/image/index.ts +++ b/src/api/ai/image/index.ts @@ -12,16 +12,11 @@ export interface ImageVO { publicStatus: boolean // 公开状态 picUrl: string // 任务地址 errorMessage: string // 错误信息 - options: object // 配置 Map + options: any // 配置 Map taskId: number // 任务编号 - buttons: ImageMjButtonsVO[] // mj 操作按钮 - createTime: string // 创建时间 - finishTime: string // 完成时间 -} - -export interface ImagePageReqVO { - pageNo: number // 分页编号 - pageSize: number // 分页大小 + buttons: ImageMidjourneyButtonsVO[] // mj 操作按钮 + createTime: Date // 创建时间 + finishTime: Date // 完成时间 } export interface ImageDrawReqVO { @@ -43,22 +38,22 @@ export interface ImageMidjourneyImagineReqVO { version: string // 版本 } -export interface ImageMjActionVO { +export interface ImageMidjourneyActionVO { id: number // 图片编号 customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识 } -export interface ImageMjButtonsVO { +export interface ImageMidjourneyButtonsVO { customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识 emoji: string // 图标 emoji label: string // Make Variations 文本 style: number // 样式: 2(Primary)、3(Green) } -// AI API 密钥 API +// AI 图片 API export const ImageApi = { // 获取【我的】绘图分页 - getImagePageMy: async (params: ImagePageReqVO) => { + getImagePageMy: async (params: PageParam) => { return await request.get({ url: `/ai/image/my-page`, params }) }, // 获取【我的】绘图记录 @@ -85,7 +80,7 @@ export const ImageApi = { return await request.post({ url: `/ai/image/midjourney/imagine`, data }) }, // 【Midjourney】Action 操作(二次生成图片) - midjourneyAction: async (data: ImageMjActionVO) => { + midjourneyAction: async (data: ImageMidjourneyActionVO) => { return await request.post({ url: `/ai/image/midjourney/action`, data }) }, diff --git a/src/assets/ai/clear.svg b/src/assets/ai/clear.svg deleted file mode 100644 index e75a4e8a..00000000 --- a/src/assets/ai/clear.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/utils/download.ts b/src/utils/download.ts index 4e8b8c60..1d07484b 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -32,26 +32,25 @@ const download = { // 下载 Markdown 方法 markdown: (data: Blob, fileName: string) => { download0(data, fileName, 'text/markdown') + }, + // 下载图片(允许跨域) + image: (url: string) => { + const image = new Image() + image.setAttribute('crossOrigin', 'anonymous') + image.src = url + image.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + const ctx = canvas.getContext('2d') as CanvasDrawImage + ctx.drawImage(image, 0, 0, image.width, image.height) + const url = canvas.toDataURL('image/png') + const a = document.createElement('a') + a.href = url + a.download = 'image.png' + a.click() + } } } export default download - -/** 图片下载(通过浏览器图片下载) */ -export const downloadImage = async (imageUrl) => { - const image = new Image() - image.setAttribute('crossOrigin', 'anonymous') - image.src = imageUrl - image.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = image.width - canvas.height = image.height - const ctx = canvas.getContext('2d') as CanvasDrawImage - ctx.drawImage(image, 0, 0, image.width, image.height) - const url = canvas.toDataURL('image/png') - const a = document.createElement('a') - a.href = url - a.download = 'image.png' - a.click() - } -} diff --git a/src/views/ai/chat/index/components/message/MessageList.vue b/src/views/ai/chat/index/components/message/MessageList.vue index b48fc295..2cc84079 100644 --- a/src/views/ai/chat/index/components/message/MessageList.vue +++ b/src/views/ai/chat/index/components/message/MessageList.vue @@ -1,5 +1,5 @@ - diff --git a/src/views/ai/image/index/ImageTaskCard.vue b/src/views/ai/image/index/components/ImageCard.vue similarity index 55% rename from src/views/ai/image/index/ImageTaskCard.vue rename to src/views/ai/image/index/components/ImageCard.vue index 0fbcc3ec..4ba78cac 100644 --- a/src/views/ai/image/index/ImageTaskCard.vue +++ b/src/views/ai/image/index/components/ImageCard.vue @@ -2,58 +2,53 @@
- + 生成中 - + 已完成 - + 异常
+
- - + +
- - -
- {{ imageDetail?.errorMessage }} + +
+ {{ detail?.errorMessage }}
- +
{{ button.label }}{{ button.emoji }} @@ -61,34 +56,53 @@ diff --git a/src/views/ai/image/index/components/ImageDetail.vue b/src/views/ai/image/index/components/ImageDetail.vue new file mode 100644 index 00000000..ad15aa8d --- /dev/null +++ b/src/views/ai/image/index/components/ImageDetail.vue @@ -0,0 +1,224 @@ + + + + diff --git a/src/views/ai/image/index/ImageTask.vue b/src/views/ai/image/index/components/ImageList.vue similarity index 53% rename from src/views/ai/image/index/ImageTask.vue rename to src/views/ai/image/index/components/ImageList.vue index b7746b50..aed1580f 100644 --- a/src/views/ai/image/index/ImageTask.vue +++ b/src/views/ai/image/index/components/ImageList.vue @@ -1,85 +1,87 @@ + - diff --git a/src/views/ai/image/index/index.vue b/src/views/ai/image/index/index.vue index 843e80cc..a2207be8 100644 --- a/src/views/ai/image/index/index.vue +++ b/src/views/ai/image/index/index.vue @@ -12,10 +12,7 @@ @on-draw-start="handleDrawStart" @on-draw-complete="handleDrawComplete" /> - +
- +
diff --git a/src/views/ai/image/index/stable-diffusion/index.vue b/src/views/ai/image/index/stable-diffusion/index.vue deleted file mode 100644 index b700a132..00000000 --- a/src/views/ai/image/index/stable-diffusion/index.vue +++ /dev/null @@ -1,437 +0,0 @@ - - - - diff --git a/src/views/ai/model/chatRole/ChatRoleForm.vue b/src/views/ai/model/chatRole/ChatRoleForm.vue index c33d289d..7815fd85 100644 --- a/src/views/ai/model/chatRole/ChatRoleForm.vue +++ b/src/views/ai/model/chatRole/ChatRoleForm.vue @@ -13,7 +13,7 @@ - + - + @@ -32,7 +32,7 @@ - + - + - + { - return type === 'my-create' || type === 'my-update' -} +const isUser = computed(() => { + return formType.value === 'my-create' || formType.value === 'my-update' +}) // TODO @fan:直接使用 formRules;只要隐藏掉的字段,它是不会校验的哈; const getFormRules = async (type: string) => { diff --git a/src/views/ai/utils/constants.ts b/src/views/ai/utils/constants.ts index 442f83b5..aa2a71f5 100644 --- a/src/views/ai/utils/constants.ts +++ b/src/views/ai/utils/constants.ts @@ -48,3 +48,308 @@ export enum AiWriteTypeEnum { WRITING = 1, // 撰写 REPLY // 回复 } + +// ========== 【图片 UI】相关的枚举 ========== +export const ImageHotWords = [ + '中国旗袍', + '古装美女', + '卡通头像', + '机甲战士', + '童话小屋', + '中国长城' +] // 图片热词 + +export const ImageHotEnglishWords = [ + 'Chinese Cheongsam', + 'Ancient Beauty', + 'Cartoon Avatar', + 'Mech Warrior', + 'Fairy Tale Cottage', + 'The Great Wall of China' +] // 图片热词(英文) + +export interface ImageModelVO { + key: string + name: string + image?: string +} + +export const StableDiffusionSamplers: ImageModelVO[] = [ + { + key: 'DDIM', + name: 'DDIM' + }, + { + key: 'DDPM', + name: 'DDPM' + }, + { + key: 'K_DPMPP_2M', + name: 'K_DPMPP_2M' + }, + { + key: 'K_DPMPP_2S_ANCESTRAL', + name: 'K_DPMPP_2S_ANCESTRAL' + }, + { + key: 'K_DPM_2', + name: 'K_DPM_2' + }, + { + key: 'K_DPM_2_ANCESTRAL', + name: 'K_DPM_2_ANCESTRAL' + }, + { + key: 'K_EULER', + name: 'K_EULER' + }, + { + key: 'K_EULER_ANCESTRAL', + name: 'K_EULER_ANCESTRAL' + }, + { + key: 'K_HEUN', + name: 'K_HEUN' + }, + { + key: 'K_LMS', + name: 'K_LMS' + } +] + +export const StableDiffusionStylePresets: ImageModelVO[] = [ + { + key: '3d-model', + name: '3d-model' + }, + { + key: 'analog-film', + name: 'analog-film' + }, + { + key: 'anime', + name: 'anime' + }, + { + key: 'cinematic', + name: 'cinematic' + }, + { + key: 'comic-book', + name: 'comic-book' + }, + { + key: 'digital-art', + name: 'digital-art' + }, + { + key: 'enhance', + name: 'enhance' + }, + { + key: 'fantasy-art', + name: 'fantasy-art' + }, + { + key: 'isometric', + name: 'isometric' + }, + { + key: 'line-art', + name: 'line-art' + }, + { + key: 'low-poly', + name: 'low-poly' + }, + { + key: 'modeling-compound', + name: 'modeling-compound' + }, + // neon-punk origami photographic pixel-art tile-texture + { + key: 'neon-punk', + name: 'neon-punk' + }, + { + key: 'origami', + name: 'origami' + }, + { + key: 'photographic', + name: 'photographic' + }, + { + key: 'pixel-art', + name: 'pixel-art' + }, + { + key: 'tile-texture', + name: 'tile-texture' + } +] + +export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [ + { + key: 'NONE', + name: 'NONE' + }, + { + key: 'FAST_BLUE', + name: 'FAST_BLUE' + }, + { + key: 'FAST_GREEN', + name: 'FAST_GREEN' + }, + { + key: 'SIMPLE', + name: 'SIMPLE' + }, + { + key: 'SLOW', + name: 'SLOW' + }, + { + key: 'SLOWER', + name: 'SLOWER' + }, + { + key: 'SLOWEST', + name: 'SLOWEST' + } +] + +export const Dall3Models: ImageModelVO[] = [ + { + key: 'dall-e-3', + name: 'DALL·E 3', + image: `/src/assets/ai/dall2.jpg` + }, + { + key: 'dall-e-2', + name: 'DALL·E 2', + image: `/src/assets/ai/dall3.jpg` + } +] + +export const Dall3StyleList: ImageModelVO[] = [ + { + key: 'vivid', + name: '清晰', + image: `/src/assets/ai/qingxi.jpg` + }, + { + key: 'natural', + name: '自然', + image: `/src/assets/ai/ziran.jpg` + } +] + +export interface ImageSizeVO { + key: string + name: string + style: string + width: string + height: string +} + +export const Dall3SizeList: ImageSizeVO[] = [ + { + key: '1024x1024', + name: '1:1', + width: '1024', + height: '1024', + style: 'width: 30px; height: 30px;background-color: #dcdcdc;' + }, + { + key: '1024x1792', + name: '3:5', + width: '1024', + height: '1792', + style: 'width: 30px; height: 50px;background-color: #dcdcdc;' + }, + { + key: '1792x1024', + name: '5:3', + width: '1792', + height: '1024', + style: 'width: 50px; height: 30px;background-color: #dcdcdc;' + } +] + +export const MidjourneyModels: ImageModelVO[] = [ + { + key: 'midjourney', + name: 'MJ', + image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png' + }, + { + key: 'niji', + name: 'NIJI', + image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png' + } +] + +export const MidjourneySizeList: ImageSizeVO[] = [ + { + key: '1:1', + width: '1', + height: '1', + style: 'width: 30px; height: 30px;background-color: #dcdcdc;' + }, + { + key: '3:4', + width: '3', + height: '4', + style: 'width: 30px; height: 40px;background-color: #dcdcdc;' + }, + { + key: '4:3', + width: '4', + height: '3', + style: 'width: 40px; height: 30px;background-color: #dcdcdc;' + }, + { + key: '9:16', + width: '9', + height: '16', + style: 'width: 30px; height: 50px;background-color: #dcdcdc;' + }, + { + key: '16:9', + width: '16', + height: '9', + style: 'width: 50px; height: 30px;background-color: #dcdcdc;' + } +] + +export const MidjourneyVersions = [ + { + value: '6.0', + label: 'v6.0' + }, + { + value: '5.2', + label: 'v5.2' + }, + { + value: '5.1', + label: 'v5.1' + }, + { + value: '5.0', + label: 'v5.0' + }, + { + value: '4.0', + label: 'v4.0' + } +] + +export const NijiVersionList = [ + { + value: '5', + label: 'v5' + } +] diff --git a/src/views/ai/utils/utils.ts b/src/views/ai/utils/utils.ts index d79e7609..be20852e 100644 --- a/src/views/ai/utils/utils.ts +++ b/src/views/ai/utils/utils.ts @@ -8,7 +8,7 @@ */ /** 判断字符串是否包含中文 */ -export const hasChinese = async (str) => { +export const hasChinese = (str: string) => { return /[\u4e00-\u9fa5]/.test(str) } diff --git a/src/views/bpm/category/index.vue b/src/views/bpm/category/index.vue index 46fa6cf1..085b3715 100644 --- a/src/views/bpm/category/index.vue +++ b/src/views/bpm/category/index.vue @@ -126,7 +126,6 @@ diff --git a/src/views/mall/promotion/kefu/components/KeFuChatBox.vue b/src/views/mall/promotion/kefu/components/KeFuChatBox.vue index 872de7ab..245791d0 100644 --- a/src/views/mall/promotion/kefu/components/KeFuChatBox.vue +++ b/src/views/mall/promotion/kefu/components/KeFuChatBox.vue @@ -4,9 +4,16 @@
{{ keFuConversation.userNickname }}
- +
+ 加载更多 +
+
-
+
+ + + +
+
+ 有新消息 + +
@@ -86,6 +105,8 @@ import EmojiSelectPopover from './tools/EmojiSelectPopover.vue' import PictureSelectUpload from './tools/PictureSelectUpload.vue' import TextMessageItem from './message/TextMessageItem.vue' import ImageMessageItem from './message/ImageMessageItem.vue' +import ProductMessageItem from './message/ProductMessageItem.vue' +import OrderMessageItem from './message/OrderMessageItem.vue' import { Emoji } from './tools/emoji' import { KeFuMessageContentTypeEnum } from './tools/constants' import { isEmpty } from '@/utils/is' @@ -101,23 +122,47 @@ const messageTool = useMessage() const message = ref('') // 消息 const messageList = ref([]) // 消息列表 const keFuConversation = ref({} as KeFuConversationRespVO) // 用户会话 -// 获得消息 TODO puhui999: 先不考虑下拉加载历史消息 +const showNewMessageTip = ref(false) // 显示有新消息提示 +const queryParams = reactive({ + pageNo: 1, + conversationId: 0 +}) +const total = ref(0) // 消息总条数 +// 获得消息 const getMessageList = async (conversation: KeFuConversationRespVO) => { keFuConversation.value = conversation - const { list } = await KeFuMessageApi.getKeFuMessagePage({ - pageNo: 1, - conversationId: conversation.id - }) - messageList.value = list.reverse() - // TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动 + queryParams.conversationId = conversation.id + const messageTotal = messageList.value.length + if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) { + return + } + const res = await KeFuMessageApi.getKeFuMessagePage(queryParams) + total.value = res.total + for (const item of res.list) { + if (messageList.value.some((val) => val.id === item.id)) { + continue + } + messageList.value.push(item) + } await scrollToBottom() } +const getMessageList0 = computed(() => { + messageList.value.sort((a: any, b: any) => a.createTime - b.createTime) + return messageList.value +}) + // 刷新消息列表 -const refreshMessageList = () => { +const refreshMessageList = async () => { if (!keFuConversation.value) { return } - getMessageList(keFuConversation.value) + + queryParams.pageNo = 1 + await getMessageList(keFuConversation.value) + if (loadHistory.value) { + // 有下角显示有新消息提示 + showNewMessageTip.value = true + } } defineExpose({ getMessageList, refreshMessageList }) // 是否显示聊天区域 @@ -140,7 +185,7 @@ const handleSendPicture = async (picUrl: string) => { const handleSendMessage = async () => { // 1. 校验消息是否为空 if (isEmpty(unref(message.value))) { - messageTool.warning('请输入消息后再发送哦!') + messageTool.notifyWarning('请输入消息后再发送哦!') return } // 2. 组织发送消息 @@ -167,12 +212,41 @@ const innerRef = ref() const scrollbarRef = ref>() // 滚动到底部 const scrollToBottom = async () => { - // 1. 滚动到最新消息 + // 1. 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动 + if (loadHistory.value) { + return + } + // 2.1 滚动到最新消息,关闭新消息提示 await nextTick() scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight) - // 2. 消息已读 + showNewMessageTip.value = false + // 2.2 消息已读 await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id) } +// 查看新消息 +const handleToNewMessage = async () => { + loadHistory.value = false + await scrollToBottom() +} + +const loadingMore = ref(false) // 滚动到顶部加载更多 +const loadHistory = ref(false) // 加载历史消息 +const handleScroll = async ({ scrollTop }) => { + const messageTotal = messageList.value.length + if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) { + return + } + // 距顶 20 加载下一页数据 + loadingMore.value = scrollTop < 20 +} +const handleOldMessage = async () => { + loadHistory.value = true + // 加载消息列表 + queryParams.pageNo += 1 + await getMessageList(keFuConversation.value) + loadingMore.value = false + // TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置 +} /** * 是否显示时间 * @param {*} item - 数据 @@ -196,6 +270,32 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => { } &-content { + position: relative; + + .loadingMore { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 50px; + background-color: #eee; + color: #666; + text-align: center; + line-height: 50px; + transform: translateY(-100%); + transition: transform 0.3s ease-in-out; + } + + .newMessageTip { + position: absolute; + bottom: 35px; + right: 35px; + background-color: #fff; + padding: 10px; + border-radius: 30px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */ + } + .ss-row-left { justify-content: flex-start; diff --git a/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue b/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue index 947a7ad6..1301fce3 100644 --- a/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue +++ b/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue @@ -1,5 +1,6 @@