Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm

pull/471/head
puhui999 2024-07-05 18:01:40 +08:00
commit d3c596dcaf
17 changed files with 299 additions and 219 deletions

View File

@ -43,8 +43,8 @@ export const ChatConversationApi = {
},
// 删除【我的】所有对话,置顶除外
deleteMyAllExceptPinned: async () => {
return await request.delete({ url: `/ai/chat/conversation/delete-my-all-except-pinned` })
deleteChatConversationMyByUnpinned: async () => {
return await request.delete({ url: `/ai/chat/conversation/delete-by-unpinned` })
},
// 获得【我的】聊天对话列表

View File

@ -16,6 +16,7 @@ export interface ImageVO {
taskId: number // 任务编号
buttons: ImageMjButtonsVO[] // mj 操作按钮
createTime: string // 创建时间
finishTime: string // 完成时间
}
export interface ImagePageReqVO {

BIN
src/assets/ai/dall2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
src/assets/ai/dall3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
src/assets/ai/qingxi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
src/assets/ai/ziran.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -29,10 +29,29 @@ const download = {
html: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/html')
},
// 下载 MarkdownView 方法
// 下载 Markdown 方法
markdown: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/markdown')
}
}
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()
}
}

View File

@ -1,11 +1,10 @@
<!-- AI 对话 -->
<template>
<el-aside width="260px" class="conversation-container" style="height: 100%;">
<el-aside width="260px" class="conversation-container" style="height: 100%">
<!-- 左顶部对话 -->
<div style="height: 100%;">
<div style="height: 100%">
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
<Icon icon="ep:plus" class="mr-5px"/>
<Icon icon="ep:plus" class="mr-5px" />
新建对话
</el-button>
@ -18,17 +17,19 @@
@keyup="searchConversation"
>
<template #prefix>
<Icon icon="ep:search"/>
<Icon icon="ep:search" />
</template>
</el-input>
<!-- 左中间对话列表 -->
<div class="conversation-list">
<el-empty v-if="loading" description="." :v-loading="loading" />
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
<div class="conversation-item classify-title" v-if="conversationMap[conversationKey].length">
<div
class="conversation-item classify-title"
v-if="conversationMap[conversationKey].length"
>
<el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
</div>
<div
@ -40,25 +41,27 @@
@mouseout="hoverConversationId = ''"
>
<div
:class="conversation.id === activeConversationId ? 'conversation active' : 'conversation'"
:class="
conversation.id === activeConversationId ? 'conversation active' : 'conversation'
"
>
<div class="title-wrapper">
<img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg"/>
<img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" />
<span class="title">{{ conversation.title }}</span>
</div>
<div class="button-wrapper" v-show="hoverConversationId === conversation.id">
<el-button class="btn" link @click.stop="handlerTop(conversation)" >
<el-button class="btn" link @click.stop="handlerTop(conversation)">
<el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
<el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
</el-button>
<el-button class="btn" link @click.stop="updateConversationTitle(conversation)">
<el-icon title="编辑" >
<Icon icon="ep:edit"/>
<el-icon title="编辑">
<Icon icon="ep:edit" />
</el-icon>
</el-button>
<el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
<el-icon title="删除对话" >
<Icon icon="ep:delete"/>
<el-icon title="删除对话">
<Icon icon="ep:delete" />
</el-icon>
</el-button>
</div>
@ -66,20 +69,19 @@
</div>
</div>
<!-- 底部站位 -->
<div style="height: 160px; width: 100%;"></div>
<div style="height: 160px; width: 100%"></div>
</div>
</div>
<!-- 左底部工具栏 -->
<!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<div class="tool-box">
<div @click="handleRoleRepository">
<Icon icon="ep:user"/>
<Icon icon="ep:user" />
<el-text size="small">角色仓库</el-text>
</div>
<div @click="handleClearConversation">
<Icon icon="ep:delete"/>
<Icon icon="ep:delete" />
<el-text size="small">清空未置顶对话</el-text>
</div>
</div>
@ -88,17 +90,16 @@
<!-- 角色仓库抽屉 -->
<el-drawer v-model="drawer" title="角色仓库" size="754px">
<Role/>
<Role />
</el-drawer>
</el-aside>
</template>
<script setup lang="ts">
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
import {ref} from "vue";
import Role from "@/views/ai/chat/role/index.vue";
import {Bottom, Top} from "@element-plus/icons-vue";
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { ref } from 'vue'
import Role from '@/views/ai/chat/role/index.vue'
import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
const message = useMessage() //
@ -107,8 +108,8 @@ const message = useMessage() // 消息弹窗
const searchName = ref<string>('') //
const activeConversationId = ref<string | null>(null) // null
const hoverConversationId = ref<string | null>(null) //
const conversationList = ref([] as ChatConversationVO[]) //
const conversationMap = ref<any>({}) // ()
const conversationList = ref([] as ChatConversationVO[]) //
const conversationMap = ref<any>({}) // ()
const drawer = ref<boolean>(false) // TODO @fanroleDrawer
const loading = ref<boolean>(false) //
const loadingTime = ref<any>() //
@ -138,7 +139,7 @@ const searchConversation = async (e) => {
conversationMap.value = await conversationTimeGroup(conversationList.value)
} else {
//
const filterValues = conversationList.value.filter(item => {
const filterValues = conversationList.value.filter((item) => {
return item.title.includes(searchName.value.trim())
})
conversationMap.value = await conversationTimeGroup(filterValues)
@ -150,7 +151,7 @@ const searchConversation = async (e) => {
*/
const handleConversationClick = async (id: string) => {
//
const filterConversation = conversationList.value.filter(item => {
const filterConversation = conversationList.value.filter((item) => {
return item.id === id
})
// onConversationClick
@ -211,20 +212,20 @@ const getChatConversationList = async () => {
const conversationTimeGroup = async (list: ChatConversationVO[]) => {
// (30)
const groupMap = {
'置顶': [],
'今天': [],
'一天前': [],
'三天前': [],
'七天前': [],
'三十天前': []
置顶: [],
今天: [],
一天前: [],
三天前: [],
七天前: [],
三十天前: []
}
//
const now = Date.now();
const now = Date.now()
//
const oneDay = 24 * 60 * 60 * 1000;
const threeDays = 3 * oneDay;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const oneDay = 24 * 60 * 60 * 1000
const threeDays = 3 * oneDay
const sevenDays = 7 * oneDay
const thirtyDays = 30 * oneDay
for (const conversation: ChatConversationVO of list) {
//
if (conversation.pinned) {
@ -232,7 +233,7 @@ const conversationTimeGroup = async (list: ChatConversationVO[]) => {
continue
}
//
const diff = now - conversation.updateTime;
const diff = now - conversation.updateTime
//
if (diff < oneDay) {
groupMap['今天'].push(conversation)
@ -271,7 +272,7 @@ const createConversation = async () => {
*/
const updateConversationTitle = async (conversation: ChatConversationVO) => {
// 1.
const {value} = await ElMessageBox.prompt('修改标题', {
const { value } = await ElMessageBox.prompt('修改标题', {
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, //
inputErrorMessage: '标题不能为空',
inputValue: conversation.title
@ -285,7 +286,7 @@ const updateConversationTitle = async (conversation: ChatConversationVO) => {
// 3.
await getChatConversationList()
// 4.
const filterConversationList = conversationList.value.filter(item => {
const filterConversationList = conversationList.value.filter((item) => {
return item.id === conversation.id
})
if (filterConversationList.length > 0) {
@ -310,8 +311,7 @@ const deleteChatConversation = async (conversation: ChatConversationVO) => {
await getChatConversationList()
//
emits('onConversationDelete', conversation)
} catch {
}
} catch {}
}
/**
@ -343,16 +343,13 @@ const handleRoleRepository = async () => {
*/
const handleClearConversation = async () => {
// TODO @fan使 await message.confirm( 使 await
ElMessageBox.confirm(
'确认后对话会全部清空,置顶的对话除外。',
'确认提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
ElMessageBox.confirm('确认后对话会全部清空,置顶的对话除外。', '确认提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await ChatConversationApi.deleteMyAllExceptPinned()
await ChatConversationApi.deleteChatConversationMyByUnpinned()
ElMessage({
message: '操作成功!',
type: 'success'
@ -364,8 +361,7 @@ const handleClearConversation = async () => {
//
emits('onConversationClear')
})
.catch(() => {
})
.catch(() => {})
}
// ============ onMounted
@ -377,7 +373,7 @@ watch(activeId, async (newValue, oldValue) => {
})
// public
defineExpose({createConversation})
defineExpose({ createConversation })
onMounted(async () => {
//
@ -394,11 +390,9 @@ onMounted(async () => {
}
}
})
</script>
<style scoped lang="scss">
.conversation-container {
position: relative;
display: flex;

View File

@ -2,7 +2,7 @@
<el-drawer
v-model="showDrawer"
title="图片详细"
@close="handlerDrawerClose"
@close="handleDrawerClose"
custom-class="drawer-class"
>
<!-- 图片 -->
@ -22,8 +22,7 @@
<div class="tip">时间</div>
<div class="body">
<div>提交时间{{ imageDetail.createTime }}</div>
<!-- TODO @fan要不加个完成时间的字段 finishTimeupdateTime 不算特别合理哈 -->
<div>生成时间{{ imageDetail.updateTime }}</div>
<div>生成时间{{ imageDetail.finishTime }}</div>
</div>
</div>
<!-- 模型 -->
@ -79,8 +78,8 @@ const props = defineProps({
})
/** 抽屉 - close */
const handlerDrawerClose = async () => {
emits('handlerDrawerClose')
const handleDrawerClose = async () => {
emits('handleDrawerClose')
}
/** 获取 - 图片 detail */
@ -90,7 +89,7 @@ const getImageDetail = async (id) => {
}
/** 任务 - detail */
const handlerTaskDetail = async () => {
const handleTaskDetail = async () => {
showDrawer.value = true
}
@ -107,7 +106,7 @@ watch(id, async (newVal, oldVal) => {
}
})
//
const emits = defineEmits(['handlerDrawerClose'])
const emits = defineEmits(['handleDrawerClose'])
//
onMounted(async () => {})
</script>

View File

@ -6,8 +6,8 @@
v-for="image in imageList"
:key="image"
:image-detail="image"
@on-btn-click="handlerImageBtnClick"
@on-mj-btn-click="handlerImageMjBtnClick"
@on-btn-click="handleImageBtnClick"
@on-mj-btn-click="handleImageMjBtnClick"
/>
</div>
<div class="task-image-pagination">
@ -16,7 +16,7 @@
layout="prev, pager, next"
:default-page-size="pageSize"
:total="pageTotal"
@change="handlerPageChange"
@change="handlePageChange"
/>
</div>
</el-card>
@ -24,7 +24,7 @@
<ImageDetailDrawer
:show="isShowImageDetail"
:id="showImageDetailId"
@handler-drawer-close="handlerDrawerClose"
@handle-drawer-close="handleDrawerClose"
/>
</template>
<script setup lang="ts">
@ -33,6 +33,7 @@ import ImageDetailDrawer from './ImageDetailDrawer.vue'
import ImageTaskCard from './ImageTaskCard.vue'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
import { downloadImage } from '@/utils/download'
const message = useMessage() //
@ -49,12 +50,12 @@ const pageSize = ref<number>(10) // page size
const pageTotal = ref<number>(0) // page size
/** 抽屉 - close */
const handlerDrawerClose = async () => {
const handleDrawerClose = async () => {
isShowImageDetail.value = false
}
/** 任务 - detail */
const handlerDrawerOpen = async () => {
const handleDrawerOpen = async () => {
isShowImageDetail.value = true
}
@ -117,12 +118,12 @@ const refreshWatchImages = async () => {
}
/** 图片 - btn click */
const handlerImageBtnClick = async (type: string, imageDetail: ImageVO) => {
const handleImageBtnClick = async (type: string, imageDetail: ImageVO) => {
// image detail id
showImageDetailId.value = imageDetail.id
// btn
if (type === 'more') {
await handlerDrawerOpen()
await handleDrawerOpen()
} else if (type === 'delete') {
await message.confirm(`是否删除照片?`)
await ImageApi.deleteImageMy(imageDetail.id)
@ -130,11 +131,15 @@ const handlerImageBtnClick = async (type: string, imageDetail: ImageVO) => {
message.success('删除成功!')
} else if (type === 'download') {
await downloadImage(imageDetail.picUrl)
} else if (type === 'regeneration') {
// Midjourney
console.log('regeneration', imageDetail.id)
await emits('onRegeneration', imageDetail)
}
}
/** 图片 - mj btn click */
const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
const handleImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
// 1 params
const data = {
id: imageDetail.id,
@ -146,28 +151,8 @@ const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: Ima
await getImageList()
}
/** 下载 - image */
// TODO @fan download
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()
}
}
// page change
const handlerPageChange = async (page) => {
const handlePageChange = async (page) => {
pageNo.value = page
await getImageList(false)
}
@ -175,6 +160,9 @@ const handlerPageChange = async (page) => {
/** 暴露组件方法 */
defineExpose({ getImageList })
// emits
const emits = defineEmits(['onRegeneration'])
/** 组件挂在的时候 */
onMounted(async () => {
// image

View File

@ -17,21 +17,26 @@
异常
</el-button>
</div>
<!-- TODO @fan1按钮要不调整成详情下载再次生成删除2如果是再次生成就把当前的参数填写到左侧的框框里 -->
<div>
<el-button
class="btn"
text
:icon="Download"
@click="handlerBtnClick('download', imageDetail)"
@click="handleBtnClick('download', imageDetail)"
/>
<el-button
class="btn"
text
:icon="RefreshRight"
@click="handleBtnClick('regeneration', imageDetail)"
/>
<el-button
class="btn"
text
:icon="Delete"
@click="handlerBtnClick('delete', imageDetail)"
@click="handleBtnClick('delete', imageDetail)"
/>
<el-button class="btn" text :icon="More" @click="handlerBtnClick('more', imageDetail)" />
<el-button class="btn" text :icon="More" @click="handleBtnClick('more', imageDetail)" />
</div>
</div>
<div class="image-wrapper" ref="cardImageRef">
@ -48,7 +53,7 @@
v-for="button in imageDetail?.buttons"
:key="button"
style="min-width: 40px; margin-left: 0; margin-right: 10px; margin-top: 5px"
@click="handlerMjBtnClick(button)"
@click="handleMjBtnClick(button)"
>
{{ button.label }}{{ button.emoji }}
</el-button>
@ -56,10 +61,10 @@
</el-card>
</template>
<script setup lang="ts">
import { Delete, Download, More } from '@element-plus/icons-vue'
import {Delete, Download, More, RefreshRight} from '@element-plus/icons-vue'
import { ImageVO, ImageMjButtonsVO } from '@/api/ai/image'
import { PropType } from 'vue'
import { ElLoading } from 'element-plus'
import {ElLoading, LoadingOptionsResolved} from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
const cardImageRef = ref<any>() // image ref
@ -73,17 +78,17 @@ const props = defineProps({
})
/** 按钮 - 点击事件 */
const handlerBtnClick = async (type, imageDetail: ImageVO) => {
const handleBtnClick = async (type, imageDetail: ImageVO) => {
emits('onBtnClick', type, imageDetail)
}
const handlerLoading = async (status: number) => {
// TODO @fan Loading
const handleLoading = async (status: number) => {
// TODO @ Loading
if (status === AiImageStatusEnum.IN_PROGRESS) {
cardImageLoadingInstance.value = ElLoading.service({
target: cardImageRef.value,
text: '生成中...'
})
} as LoadingOptionsResolved)
} else {
if (cardImageLoadingInstance.value) {
cardImageLoadingInstance.value.close()
@ -93,7 +98,7 @@ const handlerLoading = async (status: number) => {
}
/** mj 按钮 click */
const handlerMjBtnClick = async (button: ImageMjButtonsVO) => {
const handleMjBtnClick = async (button: ImageMjButtonsVO) => {
//
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
emits('onMjBtnClick', button, props.imageDetail)
@ -102,7 +107,7 @@ const handlerMjBtnClick = async (button: ImageMjButtonsVO) => {
// watch
const { imageDetail } = toRefs(props)
watch(imageDetail, async (newVal, oldVal) => {
await handlerLoading(newVal.status as string)
await handleLoading(newVal.status as string)
})
// emits
@ -110,7 +115,7 @@ const emits = defineEmits(['onBtnClick', 'onMjBtnClick'])
//
onMounted(async () => {
await handlerLoading(props.imageDetail.status as string)
await handleLoading(props.imageDetail.status as string)
})
</script>

View File

@ -25,7 +25,7 @@
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handlerHotWordClick(hotWord)"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
@ -37,7 +37,7 @@
</div>
<el-space wrap class="model-list">
<div
:class="selectModel === model ? 'modal-item selectModel' : 'modal-item'"
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in models"
:key="model.key"
@ -45,7 +45,7 @@
<el-image
:src="model.image"
fit="contain"
@click="handlerModelClick(model)"
@click="handleModelClick(model)"
/>
<div class="model-font">{{model.name}}</div>
</div>
@ -57,14 +57,14 @@
</div>
<el-space wrap class="image-style-list">
<div
:class="selectImageStyle === imageStyle ? 'image-style-item selectImageStyle' : 'image-style-item'"
:class="selectImageStyle === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
v-for="imageStyle in imageStyleList"
:key="imageStyle.key"
>
<el-image
:src="imageStyle.image"
fit="contain"
@click="handlerStyleClick(imageStyle)"
@click="handleStyleClick(imageStyle)"
/>
<div class="style-font">{{imageStyle.name}}</div>
</div>
@ -78,8 +78,8 @@
<div class="size-item"
v-for="imageSize in imageSizeList"
:key="imageSize.key"
@click="handlerSizeClick(imageSize)">
<div :class="selectImageSize === imageSize ? 'size-wrapper selectImageSize' : 'size-wrapper'">
@click="handleSizeClick(imageSize)">
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
<div :style="imageSize.style"></div>
</div>
<div class="size-font">{{ imageSize.name }}</div>
@ -91,13 +91,13 @@
size="large"
round
:loading="drawIn"
@click="handlerGenerateImage">
@click="handleGenerateImage">
{{drawIn ? '生成中' : '生成内容'}}
</el-button>
</div>
</template>
<script setup lang="ts">
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image';
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image';
// image
interface ImageModelVO {
@ -120,42 +120,38 @@ const prompt = ref<string>('') // 提示词
const drawIn = ref<boolean>(false) //
const selectHotWord = ref<string>('') //
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) //
const selectModel = ref<any>({}) //
const selectModel = ref<string>('dall-e-3') //
// message
const message = useMessage()
// TODO @fanimage
// TODO @fan image
const models = ref<ImageModelVO[]>([
{
key: 'dall-e-3',
name: 'DALL·E 3',
image: 'https://h5.cxyhub.com/images/model_2.png',
image: `/src/assets/ai/dall2.jpg`,
},
{
key: 'dall-e-2',
name: 'DALL·E 2',
image: 'https://h5.cxyhub.com/images/model_1.png',
image: `/src/assets/ai/dall3.jpg`,
},
]) //
selectModel.value = models.value[0]
const selectImageStyle = ref<any>({}) // style
// TODO @fanimage
const selectImageStyle = ref<string>('vivid') // style
const imageStyleList = ref<ImageModelVO[]>([
{
key: 'vivid',
name: '清晰',
image: 'https://h5.cxyhub.com/images/model_1.png',
image: `/src/assets/ai/qingxi.jpg`,
},
{
key: 'natural',
name: '自然',
image: 'https://h5.cxyhub.com/images/model_2.png',
image: `/src/assets/ai/ziran.jpg`,
},
]) // style
selectImageStyle.value = imageStyleList.value[0]
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // size
const selectImageSize = ref<string>('1024x1024') // size
const imageSizeList = ref<ImageSizeVO[]>([
{
key: '1024x1024',
@ -179,17 +175,14 @@ const imageSizeList = ref<ImageSizeVO[]>([
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
}
]) // size
selectImageSize.value = imageSizeList.value[0]
// Props
const props = defineProps({})
// emits
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
// TODO @fan /** */~
// TODO @fanhandler handle
/** 热词 - click */
const handlerHotWordClick = async (hotWord: string) => {
const handleHotWordClick = async (hotWord: string) => {
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
@ -202,64 +195,66 @@ const handlerHotWordClick = async (hotWord: string) => {
}
/** 模型 - click */
const handlerModelClick = async (model: ImageModelVO) => {
if (selectModel.value === model) {
selectModel.value = {} as ImageModelVO
return
}
selectModel.value = model
const handleModelClick = async (model: ImageModelVO) => {
selectModel.value = model.key
}
/** 样式 - click */
const handlerStyleClick = async (imageStyle: ImageModelVO) => {
if (selectImageStyle.value === imageStyle) {
selectImageStyle.value = {} as ImageModelVO
return
}
selectImageStyle.value = imageStyle
const handleStyleClick = async (imageStyle: ImageModelVO) => {
selectImageStyle.value = imageStyle.key
}
/** size - click */
const handlerSizeClick = async (imageSize: ImageSizeVO) => {
if (selectImageSize.value === imageSize) {
selectImageSize.value = {} as ImageSizeVO
return
}
selectImageSize.value = imageSize
const handleSizeClick = async (imageSize: ImageSizeVO) => {
selectImageSize.value = imageSize.key
}
/** 图片生产 */
const handlerGenerateImage = async () => {
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
try {
//
drawIn.value = true
//
emits('onDrawStart', selectModel.value.key)
emits('onDrawStart', selectModel.value)
const imageSize = imageSizeList.value.find(item => item.key === selectImageSize.value) as ImageSizeVO
const form = {
platform: 'OpenAI',
prompt: prompt.value, //
model: selectModel.value.key, //
width: selectImageSize.value.width, // size
height: selectImageSize.value.height, // size
model: selectModel.value, //
width: imageSize.width, // size
height: imageSize.height, // size
options: {
style: selectImageStyle.value.key, //
style: selectImageStyle.value, //
}
} as ImageDrawReqVO
//
await ImageApi.drawImage(form)
} finally {
//
emits('onDrawComplete', selectModel.value.key)
emits('onDrawComplete', selectModel.value)
//
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
selectModel.value = imageDetail.model
//
selectImageStyle.value = imageDetail.options?.style
//
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}x${imageDetail.height}`) as ImageSizeVO
await handleSizeClick(imageSize)
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//
.prompt {
}

View File

@ -8,18 +8,23 @@
<div class="modal-switch-container">
<Dall3
v-if="selectPlatform === AiPlatformEnum.OPENAI"
@on-draw-start="handlerDrawStart"
@on-draw-complete="handlerDrawComplete"
ref="dall3Ref"
@on-draw-start="handleDrawStart"
@on-draw-complete="handleDrawComplete"
/>
<Midjourney
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
ref="midjourneyRef"
/>
<Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" />
<StableDiffusion
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
@on-draw-complete="handlerDrawComplete"
ref="stableDiffusionRef"
@on-draw-complete="handleDrawComplete"
/>
</div>
</div>
<div class="main">
<ImageTask ref="imageTaskRef" />
<ImageTask ref="imageTaskRef" @on-regeneration="handleRegeneration" />
</div>
</div>
</template>
@ -31,8 +36,13 @@ import Midjourney from './midjourney/index.vue'
import StableDiffusion from './stable-diffusion/index.vue'
import ImageTask from './ImageTask.vue'
import { AiPlatformEnum } from '@/views/ai/utils/constants'
import {ImageVO} from "@/api/ai/image";
const imageTaskRef = ref<any>() // image task ref
const dall3Ref = ref<any>() // openai ref
const midjourneyRef = ref<any>() // midjourney ref
const stableDiffusionRef = ref<any>() // stable diffusion ref
//
const selectPlatform = ref('StableDiffusion')
@ -50,20 +60,37 @@ const platformOptions = [
value: AiPlatformEnum.STABLE_DIFFUSION
}
]
const drawIn = ref<boolean>(false) //
/** 绘画 - start */
const handlerDrawStart = async (type) => {
// todo @fan
drawIn.value = true
const handleDrawStart = async (type) => {
}
/** 绘画 - complete */
const handlerDrawComplete = async (type) => {
drawIn.value = false
// todo
const handleDrawComplete = async (type) => {
await imageTaskRef.value.getImageList()
}
/** 绘画 - 重新生成 */
const handleRegeneration = async (imageDetail: ImageVO) => {
//
selectPlatform.value = imageDetail.platform
console.log('切换平台', imageDetail.platform)
// imageDetail
if (imageDetail.platform === AiPlatformEnum.MIDJOURNEY) {
await nextTick(async () => {
midjourneyRef.value.settingValues(imageDetail)
})
} else if (imageDetail.platform === AiPlatformEnum.OPENAI) {
await nextTick(async () => {
dall3Ref.value.settingValues(imageDetail)
})
} else if (imageDetail.platform === AiPlatformEnum.STABLE_DIFFUSION) {
await nextTick(async () => {
stableDiffusionRef.value.settingValues(imageDetail)
})
}
}
</script>
<style scoped lang="scss">

View File

@ -24,7 +24,7 @@
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handlerHotWordClick(hotWord)"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
@ -38,8 +38,8 @@
<div class="size-item"
v-for="imageSize in imageSizeList"
:key="imageSize.key"
@click="handlerSizeClick(imageSize)">
<div :class="selectImageSize === imageSize ? 'size-wrapper selectImageSize' : 'size-wrapper'">
@click="handleSizeClick(imageSize)">
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
<div :style="imageSize.style"></div>
</div>
<div class="size-font">{{ imageSize.key }}</div>
@ -57,7 +57,7 @@
clearable
placeholder="请选择版本"
style="width: 350px"
@change="handlerChangeVersion"
@change="handleChangeVersion"
>
<el-option
v-for="item in versionList"
@ -74,7 +74,7 @@
</div>
<el-space wrap class="model-list">
<div
:class="selectModel === model ? 'modal-item selectModel' : 'modal-item'"
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in models"
:key="model.key"
@ -82,21 +82,29 @@
<el-image
:src="model.image"
fit="contain"
@click="handlerModelClick(model)"
@click="handleModelClick(model)"
/>
<div class="model-font">{{model.name}}</div>
</div>
</el-space>
</div>
<div class="model">
<div>
<el-text tag="b">参考图</el-text>
</div>
<el-space wrap class="model-list">
<UploadImg v-model="referImage" height="80px" width="80px" />
</el-space>
</div>
<div class="btns">
<!-- <el-button size="large" round>重置内容</el-button>-->
<el-button type="primary" size="large" round @click="handlerGenerateImage"></el-button>
<el-button type="primary" size="large" round @click="handleGenerateImage"></el-button>
</div>
</template>
<script setup lang="ts">
// image
import {ImageApi, ImageMidjourneyImagineReqVO} from "@/api/ai/image";
import {ImageApi, ImageMidjourneyImagineReqVO, ImageVO} from "@/api/ai/image";
// message
const message = useMessage()
// emits
@ -118,9 +126,10 @@ interface ImageSizeVO {
//
const prompt = ref<string>('') //
const referImage = ref<any>() //
const selectHotWord = ref<string>('') //
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) //
const selectModel = ref<any>() //
const selectModel = ref<string>('midjourney') //
const models = ref<ImageModelVO[]>([
{
key: 'midjourney',
@ -133,9 +142,8 @@ const models = ref<ImageModelVO[]>([
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png',
},
]) //
selectModel.value = models.value[0] //
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // size
const selectImageSize = ref<string>('1:1') // size
const imageSizeList = ref<ImageSizeVO[]>([
{
key: '1:1',
@ -168,10 +176,8 @@ const imageSizeList = ref<ImageSizeVO[]>([
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
},
]) // size
selectImageSize.value = imageSizeList.value[0]
// version
let versionList = ref<any>([]) // version
const midjourneyVersionList = ref<any>([
{
value: '6.0',
@ -201,10 +207,11 @@ const nijiVersionList = ref<any>([
},
])
const selectVersion = ref<any>('6.0') // version
let versionList = ref<any>([]) // version
versionList.value = midjourneyVersionList.value // midjourney
/** 热词 - click */
const handlerHotWordClick = async (hotWord: string) => {
const handleHotWordClick = async (hotWord: string) => {
//
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
@ -217,17 +224,13 @@ const handlerHotWordClick = async (hotWord: string) => {
}
/** size - click */
const handlerSizeClick = async (imageSize: ImageSizeVO) => {
if (selectImageSize.value === imageSize) {
selectImageSize.value = {} as ImageSizeVO
return
}
selectImageSize.value = imageSize
const handleSizeClick = async (imageSize: ImageSizeVO) => {
selectImageSize.value = imageSize.key
}
/** 模型 - click */
const handlerModelClick = async (model: ImageModelVO) => {
selectModel.value = model
const handleModelClick = async (model: ImageModelVO) => {
selectModel.value = model.key
if (model.key === 'niji') {
versionList.value = nijiVersionList.value // niji
} else {
@ -237,33 +240,53 @@ const handlerModelClick = async (model: ImageModelVO) => {
}
/** version - click */
const handlerChangeVersion = async (version) => {
const handleChangeVersion = async (version) => {
console.log('version', version)
}
/** 图片生产 */
const handlerGenerateImage = async () => {
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
// todo @
// todo @
try {
//
emits('onDrawStart', selectModel.value.key)
emits('onDrawStart', selectModel.value)
//
const imageSize = imageSizeList.value.find(item => selectImageSize.value === item.key) as ImageSizeVO
const req = {
prompt: prompt.value,
model: selectModel.value.key,
width: selectImageSize.value.width,
height: selectImageSize.value.height,
model: selectModel.value,
width: imageSize.width,
height: imageSize.height,
version: selectVersion.value,
base64Array: [],
referImageUrl: referImage.value,
} as ImageMidjourneyImagineReqVO
await ImageApi.midjourneyImagine(req)
} finally {
//
emits('onDrawComplete', selectModel.value.key)
emits('onDrawComplete', selectModel.value)
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
//
prompt.value = imageDetail.prompt
// image size
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}:${imageDetail.height}`) as ImageSizeVO
selectImageSize.value = imageSize.key
//
const model = models.value.find(item => item.key === imageDetail.options?.model) as ImageModelVO
await handleModelClick(model)
//
selectVersion.value = versionList.value.find(item => item.value === imageDetail.options?.version).value
// image
referImage.value = imageDetail.options.referImageUrl
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">

View File

@ -120,7 +120,8 @@
</div>
</template>
<script setup lang="ts">
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image'
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image'
import {hasChinese} from '../../utils/common-utils'
// image
interface ImageModelVO {
@ -146,8 +147,8 @@ const hotWords = ref<string[]>([
// message
const message = useMessage()
// TODO @fan Euler aDPM++ 2S aDPM++ 2MDPM++ SDEDPM++ 2M SDEUniPCRestart image stableDiffusionSampler
const selectSampler = ref<any>({}) //
//
const selectSampler = ref<string>('DDIM') //
// DDIM DDPM K_DPMPP_2M K_DPMPP_2S_ANCESTRAL K_DPM_2 K_DPM_2_ANCESTRAL K_EULER K_EULER_ANCESTRAL K_HEUN K_LMS
const sampler = ref<ImageModelVO[]>([
{
@ -191,12 +192,11 @@ const sampler = ref<ImageModelVO[]>([
name: 'K_LMS'
},
])
selectSampler.value = sampler.value[0]
//
// 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art isometric
// line-art low-poly modeling-compound neon-punk origami photographic pixel-art tile-texture
const selectStylePreset = ref<any>({}) //
const selectStylePreset = ref<string>('3d-model') //
const stylePresets = ref<ImageModelVO[]>([
{
key: '3d-model',
@ -268,13 +268,11 @@ const stylePresets = ref<ImageModelVO[]>([
name: 'tile-texture'
},
])
selectStylePreset.value = stylePresets.value[0]
// (clip_guidance_preset) CLIP
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
// FAST_BLUE FAST_GREEN NONE SIMPLE SLOW SLOWER SLOWEST
const selectClipGuidancePreset = ref<any>({}) //
const selectClipGuidancePreset = ref<string>('NONE') //
const clipGuidancePresets = ref<ImageModelVO[]>([
{
key: 'NONE',
@ -305,7 +303,6 @@ const clipGuidancePresets = ref<ImageModelVO[]>([
name: 'SLOWEST'
},
])
selectClipGuidancePreset.value = clipGuidancePresets.value[0]
const steps = ref<number>(20) //
const seed = ref<number>(42) //
@ -333,6 +330,10 @@ const handleHotWordClick = async (hotWord: string) => {
const handleGenerateImage = async () => {
//
await message.confirm(`确认生成内容?`)
if (await hasChinese(prompt.value)) {
message.alert('暂不支持中文!')
return
}
try {
//
drawIn.value = true
@ -349,9 +350,9 @@ const handleGenerateImage = async () => {
seed: seed.value, //
steps: steps.value, //
scale: scale.value, //
sampler: selectSampler.value.key, //
clipGuidancePreset: selectClipGuidancePreset.value.key, // CLIP
stylePreset: selectStylePreset.value.key, //
sampler: selectSampler.value, //
clipGuidancePreset: selectClipGuidancePreset.value, // CLIP
stylePreset: selectStylePreset.value, //
}
} as ImageDrawReqVO
await ImageApi.drawImage(form)
@ -362,6 +363,22 @@ const handleGenerateImage = async () => {
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
imageWidth.value = imageDetail.width
imageHeight.value = imageDetail.height
seed.value = imageDetail.options?.seed
steps.value = imageDetail.options?.steps
scale.value = imageDetail.options?.scale
selectSampler.value = imageDetail.options?.sampler
selectClipGuidancePreset.value = imageDetail.options?.clipGuidancePreset
selectStylePreset.value = imageDetail.options?.stylePreset
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
//

View File

@ -0,0 +1,13 @@
/**
* Created by
*
* AI
*
* src/utils/common-utils.ts
* AI /views/ai/utils/common-utils.ts
*/
/** 判断字符串是否包含中文 */
export const hasChinese = async (str) => {
return /[\u4e00-\u9fa5]/.test(str)
}

1
types/env.d.ts vendored
View File

@ -19,7 +19,6 @@ interface ImportMetaEnv {
readonly VITE_UPLOAD_URL: string
readonly VITE_API_URL: string
readonly VITE_BASE_PATH: string
readonly VITE_STATIC_URL: string
readonly VITE_DROP_DEBUGGER: string
readonly VITE_DROP_CONSOLE: string
readonly VITE_SOURCEMAP: string