Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm
commit
d3c596dcaf
|
@ -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` })
|
||||
},
|
||||
|
||||
// 获得【我的】聊天对话列表
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface ImageVO {
|
|||
taskId: number // 任务编号
|
||||
buttons: ImageMjButtonsVO[] // mj 操作按钮
|
||||
createTime: string // 创建时间
|
||||
finishTime: string // 完成时间
|
||||
}
|
||||
|
||||
export interface ImagePageReqVO {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @fan:roleDrawer 会不会好点哈
|
||||
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;
|
||||
|
|
|
@ -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:要不加个完成时间的字段 finishTime?updateTime 不算特别合理哈 -->
|
||||
<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>
|
||||
|
|
|
@ -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 列表
|
||||
|
|
|
@ -17,21 +17,26 @@
|
|||
异常
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- TODO @fan:1)按钮要不调整成详情、下载、再次生成、删除?;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>
|
||||
|
||||
|
|
|
@ -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 @fan:image 改成项目里自己的哈
|
||||
// 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 @fan:image 改成项目里自己的哈
|
||||
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 @fan:handler 应该改成 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 {
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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 a;DPM++ 2S a;DPM++ 2M;DPM++ SDE;DPM++ 2M SDE;UniPC;Restart;另外,要不这种枚举,我们都放到 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">
|
||||
// 提示词
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue