Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev
# Conflicts: # src/views/ai/utils/constants.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="messageContainer" class="h-100% overflow-y relative">
|
||||
<div ref="messageContainer" class="h-100% overflow-y-auto relative">
|
||||
<div class="chat-list" v-for="(item, index) in list" :key="index">
|
||||
<!-- 靠左 message:system、assistant 类型 -->
|
||||
<div class="left-message message-item" v-if="item.type !== 'user'">
|
||||
@@ -101,13 +101,12 @@ const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义
|
||||
|
||||
/** 滚动到底部 */
|
||||
const scrollToBottom = async (isIgnore?: boolean) => {
|
||||
// 注意要使用 nextTick 以免获取不到dom
|
||||
await nextTick(() => {
|
||||
if (isIgnore || !isScrolling.value) {
|
||||
messageContainer.value.scrollTop =
|
||||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
})
|
||||
// 注意要使用 nextTick 以免获取不到 dom
|
||||
await nextTick()
|
||||
if (isIgnore || !isScrolling.value) {
|
||||
messageContainer.value.scrollTop =
|
||||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="['edit', role]">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
<Icon icon="ep:edit" color="#787878" />编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="['delete', role]" style="color: red">
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>删除</span>
|
||||
<Icon icon="ep:delete" color="red" />删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -43,9 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||
import { PropType, ref } from 'vue'
|
||||
import { Delete, EditPen, More } from '@element-plus/icons-vue'
|
||||
import {ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {PropType, ref} from 'vue'
|
||||
import {More} from '@element-plus/icons-vue'
|
||||
|
||||
const tabsRef = ref<any>() // tabs ref
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@
|
||||
@click="handlerAddRole"
|
||||
class="ml-20px"
|
||||
>
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<Icon icon="ep:user" style="margin-right: 5px;" />
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -67,15 +64,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {ref} from 'vue'
|
||||
import RoleHeader from './RoleHeader.vue'
|
||||
import RoleList from './RoleList.vue'
|
||||
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
|
||||
import RoleCategoryList from './RoleCategoryList.vue'
|
||||
import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import { Search, User } from '@element-plus/icons-vue'
|
||||
import { TabsPaneContext } from 'element-plus'
|
||||
import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import {Search} from '@element-plus/icons-vue'
|
||||
import {TabsPaneContext} from 'element-plus'
|
||||
|
||||
const router = useRouter() // 路由对象
|
||||
|
||||
@@ -222,15 +219,14 @@ onMounted(async () => {
|
||||
// 获取 role 数据
|
||||
await getActiveTabsRole()
|
||||
})
|
||||
// TODO @fan:css 是不是可以融合到 scss 里面呀?
|
||||
</script>
|
||||
<style lang="css">
|
||||
<!-- 覆盖 element ui css -->
|
||||
<style lang="scss">
|
||||
.el-tabs__content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,14 @@
|
||||
<Icon icon="ep:setting" class="ml-10px" />
|
||||
</el-button>
|
||||
<el-button size="small" class="btn" @click="handlerMessageClear">
|
||||
<img src="@/assets/ai/clear.svg" class="h-14px" />
|
||||
<Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" />
|
||||
</el-button>
|
||||
<el-button size="small" class="btn">
|
||||
<Icon icon="ep:download" color="#787878" />
|
||||
</el-button>
|
||||
<el-button size="small" class="btn" @click="handleGoTopMessage" >
|
||||
<Icon icon="ep:top" color="#787878" />
|
||||
</el-button>
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<el-button size="small" :icon="Download" class="btn" />
|
||||
<el-button size="small" :icon="Top" class="btn" @click="handleGoTopMessage" />
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
@@ -180,11 +183,6 @@ const handleConversationClick = async (conversation: ChatConversationVO) => {
|
||||
// 更新选中的对话 id
|
||||
activeConversationId.value = conversation.id
|
||||
activeConversation.value = conversation
|
||||
// 处理进行中的对话
|
||||
// TODO @fan:这里,和上面的 “对话进行中,不允许切换” 是不是重叠了?
|
||||
if (conversationInProgress.value) {
|
||||
await stopStream()
|
||||
}
|
||||
// 刷新 message 列表
|
||||
await getMessageList()
|
||||
// 滚动底部
|
||||
@@ -203,7 +201,11 @@ const handlerConversationDelete = async (delConversation: ChatConversationVO) =>
|
||||
}
|
||||
/** 清空选中的对话 */
|
||||
const handleConversationClear = async () => {
|
||||
// TODO @fan:需要加一个 对话进行中,不允许切换
|
||||
// 对话进行中,不允许切换
|
||||
if (conversationInProgress.value) {
|
||||
message.alert('对话中,不允许切换!')
|
||||
return false
|
||||
}
|
||||
activeConversationId.value = null
|
||||
activeConversation.value = null
|
||||
activeMessageList.value = []
|
||||
@@ -363,7 +365,7 @@ const handlePromptInput = (event) => {
|
||||
isComposing.value = false
|
||||
}, 400)
|
||||
}
|
||||
// TODO @fan:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
|
||||
// TODO @芋艿:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
|
||||
const onCompositionstart = () => {
|
||||
isComposing.value = true
|
||||
}
|
||||
@@ -394,7 +396,6 @@ const doSendMessage = async (content: string) => {
|
||||
} as ChatMessageVO)
|
||||
}
|
||||
|
||||
// TODO @fan:= = 不知道哪里被改动了。点击【发送】后,不会跳转到消息最底部了。。
|
||||
/** 真正执行【发送】消息操作 */
|
||||
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||
// 创建 AbortController 实例,以便中止请求
|
||||
@@ -421,9 +422,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||
createTime: new Date()
|
||||
} as ChatMessageVO)
|
||||
// 1.2 滚动到最下面
|
||||
nextTick(async () => {
|
||||
await scrollToBottom() // 底部
|
||||
})
|
||||
await nextTick()
|
||||
await scrollToBottom() // 底部
|
||||
// 1.3 开始滚动
|
||||
textRoll()
|
||||
|
||||
@@ -573,7 +573,6 @@ onMounted(async () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-layout {
|
||||
// TODO @范 这里height不能 100% 先这样临时处理 TODO @fan:这个目前要搞处理么?
|
||||
position: absolute;
|
||||
flex: 1;
|
||||
top: 0;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="showDrawer"
|
||||
title="图片详细"
|
||||
@close="handleDrawerClose"
|
||||
custom-class="drawer-class"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<div class="item">
|
||||
<!-- <div class="header">-->
|
||||
<!-- <div>图片</div>-->
|
||||
<!-- <div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="body">
|
||||
<!-- TODO @fan: 要不,这里只展示图片???不用 ImageTaskCard -->
|
||||
<ImageTaskCard :image-detail="imageDetail" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="item">
|
||||
<div class="tip">时间</div>
|
||||
<div class="body">
|
||||
<div>提交时间:{{ imageDetail.createTime }}</div>
|
||||
<div>生成时间:{{ imageDetail.finishTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型 -->
|
||||
<div class="item">
|
||||
<div class="tip">模型</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.model }}({{ imageDetail.height }}x{{ imageDetail.width }})
|
||||
</div>
|
||||
</div>
|
||||
<!-- 提示词 -->
|
||||
<div class="item">
|
||||
<div class="tip">提示词</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地址 -->
|
||||
<div class="item">
|
||||
<div class="tip">图片地址</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.picUrl }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 风格 -->
|
||||
<div class="item" v-if="imageDetail?.options?.style">
|
||||
<div class="tip">风格</div>
|
||||
<div class="body">
|
||||
<!-- TODO @fan:貌似需要把 imageStyleList 搞到 api/image/index.ts 枚举起来? -->
|
||||
<!-- TODO @fan:这里的展示,可能需要按照平台做区分 -->
|
||||
{{ imageDetail?.options?.style }}
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
|
||||
const showDrawer = ref<boolean>(false) // 是否显示
|
||||
const imageDetail = ref<ImageVO>({} as ImageVO) // 图片详细信息
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handleDrawerClose = async () => {
|
||||
emits('handleDrawerClose')
|
||||
}
|
||||
|
||||
/** 获取 - 图片 detail */
|
||||
const getImageDetail = async (id) => {
|
||||
// 获取图片详细
|
||||
imageDetail.value = await ImageApi.getImageMy(id)
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handleTaskDetail = async () => {
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
// watch show
|
||||
const { show } = toRefs(props)
|
||||
watch(show, async (newValue, oldValue) => {
|
||||
showDrawer.value = newValue as boolean
|
||||
})
|
||||
// watch id
|
||||
const { id } = toRefs(props)
|
||||
watch(id, async (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
await getImageDetail(newVal)
|
||||
}
|
||||
})
|
||||
//
|
||||
const emits = defineEmits(['handleDrawerClose'])
|
||||
//
|
||||
onMounted(async () => {})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 10px;
|
||||
color: #616161;
|
||||
|
||||
.taskImage {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,58 +2,53 @@
|
||||
<el-card body-class="" class="image-card">
|
||||
<div class="image-operation">
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
text
|
||||
bg
|
||||
v-if="imageDetail?.status === AiImageStatusEnum.IN_PROGRESS"
|
||||
>
|
||||
<el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
|
||||
生成中
|
||||
</el-button>
|
||||
<el-button text bg v-else-if="imageDetail?.status === AiImageStatusEnum.SUCCESS">
|
||||
<el-button text bg v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
|
||||
已完成
|
||||
</el-button>
|
||||
<el-button type="danger" text bg v-else-if="imageDetail?.status === AiImageStatusEnum.FAIL">
|
||||
<el-button type="danger" text bg v-else-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||
异常
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 操作区 -->
|
||||
<div>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Download"
|
||||
@click="handleBtnClick('download', imageDetail)"
|
||||
@click="handleButtonClick('download', detail)"
|
||||
/>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="RefreshRight"
|
||||
@click="handleBtnClick('regeneration', imageDetail)"
|
||||
@click="handleButtonClick('regeneration', detail)"
|
||||
/>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Delete"
|
||||
@click="handleBtnClick('delete', imageDetail)"
|
||||
/>
|
||||
<el-button class="btn" text :icon="More" @click="handleBtnClick('more', imageDetail)" />
|
||||
<el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" />
|
||||
<el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-wrapper" ref="cardImageRef">
|
||||
<!-- TODO @fan:要不加个点击,大图预览? -->
|
||||
<img class="image" :src="imageDetail?.picUrl" />
|
||||
<div v-if="imageDetail?.status === AiImageStatusEnum.FAIL">
|
||||
{{ imageDetail?.errorMessage }}
|
||||
<el-image
|
||||
class="image"
|
||||
:src="detail?.picUrl"
|
||||
:preview-src-list="[detail.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
|
||||
{{ detail?.errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO @fan:style 使用 unocss 替代下 -->
|
||||
<!-- Midjourney 专属操作 -->
|
||||
<div class="image-mj-btns">
|
||||
<el-button
|
||||
size="small"
|
||||
v-for="button in imageDetail?.buttons"
|
||||
v-for="button in detail?.buttons"
|
||||
:key="button"
|
||||
style="min-width: 40px; margin-left: 0; margin-right: 10px; margin-top: 5px"
|
||||
@click="handleMjBtnClick(button)"
|
||||
class="min-w-40px ml-0 mr-10px mt-5px"
|
||||
@click="handleMidjourneyBtnClick(button)"
|
||||
>
|
||||
{{ button.label }}{{ button.emoji }}
|
||||
</el-button>
|
||||
@@ -61,34 +56,53 @@
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {Delete, Download, More, RefreshRight} from '@element-plus/icons-vue'
|
||||
import { ImageVO, ImageMjButtonsVO } from '@/api/ai/image'
|
||||
import { Delete, Download, More, RefreshRight } from '@element-plus/icons-vue'
|
||||
import { ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
|
||||
import { PropType } from 'vue'
|
||||
import {ElLoading, LoadingOptionsResolved} from 'element-plus'
|
||||
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
const cardImageRef = ref<any>() // 卡片 image ref
|
||||
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
|
||||
const message = useMessage()
|
||||
const message = useMessage() // 消息
|
||||
|
||||
const props = defineProps({
|
||||
imageDetail: {
|
||||
detail: {
|
||||
type: Object as PropType<ImageVO>,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 按钮 - 点击事件 */
|
||||
const handleBtnClick = async (type, imageDetail: ImageVO) => {
|
||||
emits('onBtnClick', type, imageDetail)
|
||||
const cardImageRef = ref<any>() // 卡片 image ref
|
||||
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
|
||||
|
||||
/** 处理点击事件 */
|
||||
const handleButtonClick = async (type, detail: ImageVO) => {
|
||||
emits('onBtnClick', type, detail)
|
||||
}
|
||||
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
const handleMidjourneyBtnClick = async (button: ImageMidjourneyButtonsVO) => {
|
||||
// 确认窗体
|
||||
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
|
||||
emits('onMjBtnClick', button, props.detail)
|
||||
}
|
||||
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
|
||||
|
||||
/** 监听详情 */
|
||||
const { detail } = toRefs(props)
|
||||
watch(detail, async (newVal, oldVal) => {
|
||||
await handleLoading(newVal.status as string)
|
||||
})
|
||||
|
||||
/** 处理加载状态 */
|
||||
const handleLoading = async (status: number) => {
|
||||
// TODO @芋艿:这个搞成 Loading 组件,然后通过数据驱动,这样搞可以哇?
|
||||
// 情况一:如果是生成中,则设置加载中的 loading
|
||||
if (status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
cardImageLoadingInstance.value = ElLoading.service({
|
||||
target: cardImageRef.value,
|
||||
text: '生成中...'
|
||||
} as LoadingOptionsResolved)
|
||||
// 情况二:如果已经生成结束,则移除 loading
|
||||
} else {
|
||||
if (cardImageLoadingInstance.value) {
|
||||
cardImageLoadingInstance.value.close()
|
||||
@@ -97,25 +111,9 @@ const handleLoading = async (status: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
/** mj 按钮 click */
|
||||
const handleMjBtnClick = async (button: ImageMjButtonsVO) => {
|
||||
// 确认窗体
|
||||
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
|
||||
emits('onMjBtnClick', button, props.imageDetail)
|
||||
}
|
||||
|
||||
// watch
|
||||
const { imageDetail } = toRefs(props)
|
||||
watch(imageDetail, async (newVal, oldVal) => {
|
||||
await handleLoading(newVal.status as string)
|
||||
})
|
||||
|
||||
// emits
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick'])
|
||||
|
||||
//
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await handleLoading(props.imageDetail.status as string)
|
||||
await handleLoading(props.detail.status as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
224
src/views/ai/image/index/components/ImageDetail.vue
Normal file
224
src/views/ai/image/index/components/ImageDetail.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="showDrawer"
|
||||
title="图片详细"
|
||||
@close="handleDrawerClose"
|
||||
custom-class="drawer-class"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<div class="item">
|
||||
<div class="body">
|
||||
<el-image
|
||||
class="image"
|
||||
:src="detail?.picUrl"
|
||||
:preview-src-list="[detail.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="item">
|
||||
<div class="tip">时间</div>
|
||||
<div class="body">
|
||||
<div>提交时间:{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
|
||||
<div>生成时间:{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型 -->
|
||||
<div class="item">
|
||||
<div class="tip">模型</div>
|
||||
<div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
|
||||
</div>
|
||||
<!-- 提示词 -->
|
||||
<div class="item">
|
||||
<div class="tip">提示词</div>
|
||||
<div class="body">
|
||||
{{ detail.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地址 -->
|
||||
<div class="item">
|
||||
<div class="tip">图片地址</div>
|
||||
<div class="body">
|
||||
{{ detail.picUrl }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- StableDiffusion 专属区域 -->
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.sampler"
|
||||
>
|
||||
<div class="tip">采样方法</div>
|
||||
<div class="body">
|
||||
{{
|
||||
StableDiffusionSamplers.find(
|
||||
(item: ImageModelVO) => item.key === detail?.options?.sampler
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="
|
||||
detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.clipGuidancePreset
|
||||
"
|
||||
>
|
||||
<div class="tip">CLIP</div>
|
||||
<div class="body">
|
||||
{{
|
||||
StableDiffusionClipGuidancePresets.find(
|
||||
(item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.stylePreset"
|
||||
>
|
||||
<div class="tip">风格</div>
|
||||
<div class="body">
|
||||
{{
|
||||
StableDiffusionStylePresets.find(
|
||||
(item: ImageModelVO) => item.key === detail?.options?.stylePreset
|
||||
)?.name
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.steps"
|
||||
>
|
||||
<div class="tip">迭代步数</div>
|
||||
<div class="body">
|
||||
{{ detail?.options?.steps }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.scale"
|
||||
>
|
||||
<div class="tip">引导系数</div>
|
||||
<div class="body">
|
||||
{{ detail?.options?.scale }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.seed"
|
||||
>
|
||||
<div class="tip">随机因子</div>
|
||||
<div class="body">
|
||||
{{ detail?.options?.seed }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dall3 专属区域 -->
|
||||
<div class="item" v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style">
|
||||
<div class="tip">风格选择</div>
|
||||
<div class="body">
|
||||
{{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Midjourney 专属区域 -->
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version"
|
||||
>
|
||||
<div class="tip">模型版本</div>
|
||||
<div class="body">
|
||||
{{ detail?.options?.version }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.referImageUrl"
|
||||
>
|
||||
<div class="tip">参考图</div>
|
||||
<div class="body">
|
||||
<el-image :src="detail.options.referImageUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
Dall3StyleList,
|
||||
ImageModelVO,
|
||||
StableDiffusionClipGuidancePresets,
|
||||
StableDiffusionSamplers,
|
||||
StableDiffusionStylePresets
|
||||
} from '@/views/ai/utils/constants'
|
||||
import { formatTime } from '@/utils'
|
||||
|
||||
const showDrawer = ref<boolean>(false) // 是否显示
|
||||
const detail = ref<ImageVO>({} as ImageVO) // 图片详细信息
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 关闭抽屉 */
|
||||
const handleDrawerClose = async () => {
|
||||
emits('handleDrawerClose')
|
||||
}
|
||||
|
||||
/** 监听 drawer 是否打开 */
|
||||
const { show } = toRefs(props)
|
||||
watch(show, async (newValue, oldValue) => {
|
||||
showDrawer.value = newValue as boolean
|
||||
})
|
||||
|
||||
/** 获取图片详情 */
|
||||
const getImageDetail = async (id: number) => {
|
||||
detail.value = await ImageApi.getImageMy(id)
|
||||
}
|
||||
|
||||
/** 监听 id 变化,加载最新图片详情 */
|
||||
const { id } = toRefs(props)
|
||||
watch(id, async (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
await getImageDetail(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['handleDrawerClose'])
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 10px;
|
||||
color: #616161;
|
||||
|
||||
.taskImage {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +1,87 @@
|
||||
<template>
|
||||
<el-card class="dr-task" body-class="task-card" shadow="never">
|
||||
<template #header>绘画任务</template>
|
||||
<div class="task-image-list" ref="imageTaskRef">
|
||||
<ImageTaskCard
|
||||
<!-- 图片列表 -->
|
||||
<div class="task-image-list" ref="imageListRef">
|
||||
<ImageCard
|
||||
v-for="image in imageList"
|
||||
:key="image"
|
||||
:image-detail="image"
|
||||
@on-btn-click="handleImageBtnClick"
|
||||
@on-mj-btn-click="handleImageMjBtnClick"
|
||||
:key="image.id"
|
||||
:detail="image"
|
||||
@on-btn-click="handleImageButtonClick"
|
||||
@on-mj-btn-click="handleImageMidjourneyButtonClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-image-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:default-page-size="pageSize"
|
||||
<Pagination
|
||||
:total="pageTotal"
|
||||
@change="handlePageChange"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getImageList"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 图片 detail 抽屉 -->
|
||||
<ImageDetailDrawer
|
||||
|
||||
<!-- 图片详情 -->
|
||||
<ImageDetail
|
||||
:show="isShowImageDetail"
|
||||
:id="showImageDetailId"
|
||||
@handle-drawer-close="handleDrawerClose"
|
||||
@handle-drawer-close="handleDetailClose"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO, ImageMjActionVO, ImageMjButtonsVO } from '@/api/ai/image'
|
||||
import ImageDetailDrawer from './ImageDetailDrawer.vue'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
import {
|
||||
ImageApi,
|
||||
ImageVO,
|
||||
ImageMidjourneyActionVO,
|
||||
ImageMidjourneyButtonsVO
|
||||
} from '@/api/ai/image'
|
||||
import ImageDetail from './ImageDetail.vue'
|
||||
import ImageCard from './ImageCard.vue'
|
||||
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
import { downloadImage } from '@/utils/download'
|
||||
import download from '@/utils/download'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const imageList = ref<ImageVO[]>([]) // image 列表
|
||||
const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||
const imageListInterval = ref<any>() // image 列表定时器,刷新列表
|
||||
const isShowImageDetail = ref<boolean>(false) // 是否显示 task 详情
|
||||
const showImageDetailId = ref<number>(0) // 是否显示 task 详情
|
||||
const imageTaskRef = ref<any>() // ref
|
||||
const imageTaskLoadingInstance = ref<any>() // loading
|
||||
const imageTaskLoading = ref<boolean>(false) // loading
|
||||
const pageNo = ref<number>(1) // page no
|
||||
const pageSize = ref<number>(10) // page size
|
||||
// 图片分页相关的参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
const pageTotal = ref<number>(0) // page size
|
||||
const imageList = ref<ImageVO[]>([]) // image 列表
|
||||
const imageListLoadingInstance = ref<any>() // image 列表是否正在加载中
|
||||
const imageListRef = ref<any>() // ref
|
||||
// 图片轮询相关的参数(正在生成中的)
|
||||
const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||
const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生成进展
|
||||
// 图片详情相关的参数
|
||||
const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
|
||||
const showImageDetailId = ref<number>(0) // 图片详情的图片编号
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handleDrawerClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handleDrawerOpen = async () => {
|
||||
/** 查看图片的详情 */
|
||||
const handleDetailOpen = async () => {
|
||||
isShowImageDetail.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 - image 列表
|
||||
*/
|
||||
const getImageList = async (apply: boolean = false) => {
|
||||
imageTaskLoading.value = true
|
||||
/** 关闭图片的详情 */
|
||||
const handleDetailClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 获得 image 图片列表 */
|
||||
const getImageList = async () => {
|
||||
try {
|
||||
imageTaskLoadingInstance.value = ElLoading.service({
|
||||
target: imageTaskRef.value,
|
||||
// 1. 加载图片列表
|
||||
imageListLoadingInstance.value = ElLoading.service({
|
||||
target: imageListRef.value,
|
||||
text: '加载中...'
|
||||
} as LoadingOptionsResolved)
|
||||
const { list, total } = await ImageApi.getImagePageMy({
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
if (apply) {
|
||||
imageList.value = [...imageList.value, ...list]
|
||||
} else {
|
||||
imageList.value = list
|
||||
}
|
||||
const { list, total } = await ImageApi.getImagePageMy(queryParams)
|
||||
imageList.value = list
|
||||
pageTotal.value = total
|
||||
// 需要 watch 的数据
|
||||
|
||||
// 2. 计算需要轮询的图片
|
||||
const newWatImages = {}
|
||||
imageList.value.forEach((item) => {
|
||||
if (item.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
@@ -88,9 +90,10 @@ const getImageList = async (apply: boolean = false) => {
|
||||
})
|
||||
inProgressImageMap.value = newWatImages
|
||||
} finally {
|
||||
if (imageTaskLoadingInstance.value) {
|
||||
imageTaskLoadingInstance.value.close()
|
||||
imageTaskLoadingInstance.value = null
|
||||
// 关闭正在“加载中”的 Loading
|
||||
if (imageListLoadingInstance.value) {
|
||||
imageListLoadingInstance.value.close()
|
||||
imageListLoadingInstance.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,50 +120,52 @@ const refreshWatchImages = async () => {
|
||||
inProgressImageMap.value = newWatchImages
|
||||
}
|
||||
|
||||
/** 图片 - btn click */
|
||||
const handleImageBtnClick = async (type: string, imageDetail: ImageVO) => {
|
||||
// 获取 image detail id
|
||||
showImageDetailId.value = imageDetail.id
|
||||
// 处理不用 btn
|
||||
/** 图片的点击事件 */
|
||||
const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
|
||||
// 详情
|
||||
if (type === 'more') {
|
||||
await handleDrawerOpen()
|
||||
} else if (type === 'delete') {
|
||||
showImageDetailId.value = imageDetail.id
|
||||
await handleDetailOpen()
|
||||
return
|
||||
}
|
||||
// 删除
|
||||
if (type === 'delete') {
|
||||
await message.confirm(`是否删除照片?`)
|
||||
await ImageApi.deleteImageMy(imageDetail.id)
|
||||
await getImageList()
|
||||
message.success('删除成功!')
|
||||
} else if (type === 'download') {
|
||||
await downloadImage(imageDetail.picUrl)
|
||||
} else if (type === 'regeneration') {
|
||||
// Midjourney 平台
|
||||
console.log('regeneration', imageDetail.id)
|
||||
return
|
||||
}
|
||||
// 下载
|
||||
if (type === 'download') {
|
||||
await download.image(imageDetail.picUrl)
|
||||
return
|
||||
}
|
||||
// 重新生成
|
||||
if (type === 'regeneration') {
|
||||
await emits('onRegeneration', imageDetail)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片 - mj btn click */
|
||||
const handleImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
|
||||
// 1、构建 params 参数
|
||||
/** 处理 Midjourney 按钮点击事件 */
|
||||
const handleImageMidjourneyButtonClick = async (
|
||||
button: ImageMidjourneyButtonsVO,
|
||||
imageDetail: ImageVO
|
||||
) => {
|
||||
// 1. 构建 params 参数
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId
|
||||
} as ImageMjActionVO
|
||||
// 2、发送 action
|
||||
} as ImageMidjourneyActionVO
|
||||
// 2. 发送 action
|
||||
await ImageApi.midjourneyAction(data)
|
||||
// 3、刷新列表
|
||||
// 3. 刷新列表
|
||||
await getImageList()
|
||||
}
|
||||
|
||||
// page change
|
||||
const handlePageChange = async (page) => {
|
||||
pageNo.value = page
|
||||
await getImageList(false)
|
||||
}
|
||||
defineExpose({ getImageList }) // 暴露组件方法
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ getImageList })
|
||||
|
||||
// emits
|
||||
const emits = defineEmits(['onRegeneration'])
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
@@ -168,19 +173,20 @@ onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
await getImageList()
|
||||
// 自动刷新 image 列表
|
||||
imageListInterval.value = setInterval(async () => {
|
||||
inProgressTimer.value = setInterval(async () => {
|
||||
await refreshWatchImages()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (imageListInterval.value) {
|
||||
clearInterval(imageListInterval.value)
|
||||
if (inProgressTimer.value) {
|
||||
clearInterval(inProgressTimer.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- TODO fan:这 2 个 scss 可以合并么? -->
|
||||
<style lang="scss">
|
||||
.task-card {
|
||||
margin: 0;
|
||||
@@ -197,8 +203,7 @@ onUnmounted(async () => {
|
||||
align-content: flex-start;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 140px;
|
||||
padding: 20px 20px 140px;
|
||||
box-sizing: border-box; /* 确保内边距不会增加高度 */
|
||||
|
||||
> div {
|
||||
@@ -224,7 +229,6 @@ onUnmounted(async () => {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dr-task {
|
||||
width: 100%;
|
||||
@@ -3,12 +3,11 @@
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
||||
<!-- TODO @fan:style 看看能不能哟 unocss 替代 -->
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px;"
|
||||
class="w-100% mt-15px"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
@@ -20,12 +19,13 @@
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button round
|
||||
class="btn"
|
||||
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
<el-button
|
||||
round
|
||||
class="btn"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
@@ -38,16 +38,11 @@
|
||||
<el-space wrap class="model-list">
|
||||
<div
|
||||
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
|
||||
v-for="model in models"
|
||||
v-for="model in Dall3Models"
|
||||
:key="model.key"
|
||||
|
||||
>
|
||||
<el-image
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@click="handleModelClick(model)"
|
||||
/>
|
||||
<div class="model-font">{{model.name}}</div>
|
||||
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
|
||||
<div class="model-font">{{ model.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
@@ -57,16 +52,12 @@
|
||||
</div>
|
||||
<el-space wrap class="image-style-list">
|
||||
<div
|
||||
:class="selectImageStyle === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
|
||||
v-for="imageStyle in imageStyleList"
|
||||
:class="style === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
|
||||
v-for="imageStyle in Dall3StyleList"
|
||||
:key="imageStyle.key"
|
||||
>
|
||||
<el-image
|
||||
:src="imageStyle.image"
|
||||
fit="contain"
|
||||
@click="handleStyleClick(imageStyle)"
|
||||
/>
|
||||
<div class="style-font">{{imageStyle.name}}</div>
|
||||
<el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" />
|
||||
<div class="style-font">{{ imageStyle.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
@@ -75,11 +66,15 @@
|
||||
<el-text tag="b">画面比例</el-text>
|
||||
</div>
|
||||
<el-space wrap class="size-list">
|
||||
<div class="size-item"
|
||||
v-for="imageSize in imageSizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)">
|
||||
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
|
||||
<div
|
||||
class="size-item"
|
||||
v-for="imageSize in Dall3SizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
|
||||
>
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="size-font">{{ imageSize.name }}</div>
|
||||
@@ -87,126 +82,60 @@
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button type="primary"
|
||||
size="large"
|
||||
round
|
||||
:loading="drawIn"
|
||||
@click="handleGenerateImage">
|
||||
{{drawIn ? '生成中' : '生成内容'}}
|
||||
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image';
|
||||
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||
import {
|
||||
Dall3Models,
|
||||
Dall3StyleList,
|
||||
ImageHotWords,
|
||||
Dall3SizeList,
|
||||
ImageModelVO,
|
||||
AiPlatformEnum
|
||||
} from '@/views/ai/utils/constants'
|
||||
|
||||
// image 模型
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
|
||||
// image 大小
|
||||
interface ImageSizeVO {
|
||||
key: string
|
||||
name: string,
|
||||
style: string,
|
||||
width: string,
|
||||
height: string,
|
||||
}
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
|
||||
const selectModel = ref<string>('dall-e-3') // 模型
|
||||
// message
|
||||
const message = useMessage()
|
||||
const models = ref<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`,
|
||||
},
|
||||
]) // 模型
|
||||
const selectSize = ref<string>('1024x1024') // 选中 size
|
||||
const style = ref<string>('vivid') // style 样式
|
||||
|
||||
const selectImageStyle = ref<string>('vivid') // style 样式
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
const imageStyleList = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'vivid',
|
||||
name: '清晰',
|
||||
image: `/src/assets/ai/qingxi.jpg`,
|
||||
},
|
||||
{
|
||||
key: 'natural',
|
||||
name: '自然',
|
||||
image: `/src/assets/ai/ziran.jpg`,
|
||||
},
|
||||
]) // style
|
||||
|
||||
const selectImageSize = ref<string>('1024x1024') // 选中 size
|
||||
const imageSizeList = ref<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;',
|
||||
}
|
||||
]) // size
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
/** 热词 - click */
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 取消选中
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord
|
||||
// 替换提示词
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** 模型 - click */
|
||||
/** 选择 model 模型 */
|
||||
const handleModelClick = async (model: ImageModelVO) => {
|
||||
selectModel.value = model.key
|
||||
}
|
||||
|
||||
/** 样式 - click */
|
||||
/** 选择 style 样式 */
|
||||
const handleStyleClick = async (imageStyle: ImageModelVO) => {
|
||||
selectImageStyle.value = imageStyle.key
|
||||
style.value = imageStyle.key
|
||||
}
|
||||
|
||||
/** size - click */
|
||||
/** 选择 size 大小 */
|
||||
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
selectImageSize.value = imageSize.key
|
||||
selectSize.value = imageSize.key
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
@@ -217,42 +146,41 @@ const handleGenerateImage = async () => {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', selectModel.value)
|
||||
const imageSize = imageSizeList.value.find(item => item.key === selectImageSize.value) as ImageSizeVO
|
||||
emits('onDrawStart', AiPlatformEnum.OPENAI)
|
||||
const imageSize = Dall3SizeList.find((item) => item.key === selectSize.value) as ImageSizeVO
|
||||
const form = {
|
||||
platform: 'OpenAI',
|
||||
platform: AiPlatformEnum.OPENAI,
|
||||
prompt: prompt.value, // 提示词
|
||||
model: selectModel.value, // 模型
|
||||
width: imageSize.width, // size 不能为空
|
||||
height: imageSize.height, // size 不能为空
|
||||
options: {
|
||||
style: selectImageStyle.value, // 图像生成的风格
|
||||
style: style.value // 图像生成的风格
|
||||
}
|
||||
} as ImageDrawReqVO
|
||||
// 发送请求
|
||||
await ImageApi.drawImage(form)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', selectModel.value)
|
||||
emits('onDrawComplete', AiPlatformEnum.OPENAI)
|
||||
// 加载结束
|
||||
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
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
prompt.value = detail.prompt
|
||||
selectModel.value = detail.model
|
||||
style.value = detail.options?.style
|
||||
const imageSize = Dall3SizeList.find(
|
||||
(item) => item.key === `${detail.width}x${detail.height}`
|
||||
) as ImageSizeVO
|
||||
await handleSizeClick(imageSize)
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
// 提示词
|
||||
@@ -309,7 +237,6 @@ defineExpose({ settingValues })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 样式 style
|
||||
.image-style {
|
||||
margin-top: 30px;
|
||||
@@ -7,7 +7,7 @@
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px;"
|
||||
class="w-100% mt-15px"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
@@ -19,12 +19,13 @@
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button round
|
||||
class="btn"
|
||||
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
<el-button
|
||||
round
|
||||
class="btn"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
@@ -35,17 +36,36 @@
|
||||
<el-text tag="b">尺寸</el-text>
|
||||
</div>
|
||||
<el-space wrap class="size-list">
|
||||
<div class="size-item"
|
||||
v-for="imageSize in imageSizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)">
|
||||
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
|
||||
<div
|
||||
class="size-item"
|
||||
v-for="imageSize in MidjourneySizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
:class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'"
|
||||
>
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="size-font">{{ imageSize.key }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="model">
|
||||
<div>
|
||||
<el-text tag="b">模型</el-text>
|
||||
</div>
|
||||
<el-space wrap class="model-list">
|
||||
<div
|
||||
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
|
||||
v-for="model in MidjourneyModels"
|
||||
:key="model.key"
|
||||
>
|
||||
<el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
|
||||
<div class="model-font">{{ model.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="version">
|
||||
<div>
|
||||
<el-text tag="b">版本</el-text>
|
||||
@@ -53,11 +73,9 @@
|
||||
<el-space wrap class="version-list">
|
||||
<el-select
|
||||
v-model="selectVersion"
|
||||
class="version-select"
|
||||
class="version-select !w-350px"
|
||||
clearable
|
||||
placeholder="请选择版本"
|
||||
style="width: 350px"
|
||||
@change="handleChangeVersion"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in versionList"
|
||||
@@ -68,228 +86,130 @@
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="model">
|
||||
<div>
|
||||
<el-text tag="b">模型</el-text>
|
||||
</div>
|
||||
<el-space wrap class="model-list">
|
||||
<div
|
||||
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
|
||||
v-for="model in models"
|
||||
:key="model.key"
|
||||
|
||||
>
|
||||
<el-image
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@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" />
|
||||
<UploadImg v-model="referImageUrl" height="120px" width="120px" />
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<!-- <el-button size="large" round>重置内容</el-button>-->
|
||||
<el-button type="primary" size="large" round @click="handleGenerateImage">生成内容</el-button>
|
||||
<el-button type="primary" size="large" round @click="handleGenerateImage">
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageMidjourneyImagineReqVO, ImageVO } from '@/api/ai/image'
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
ImageHotWords,
|
||||
ImageSizeVO,
|
||||
ImageModelVO,
|
||||
MidjourneyModels,
|
||||
MidjourneySizeList,
|
||||
MidjourneyVersions,
|
||||
NijiVersionList
|
||||
} from '@/views/ai/utils/constants'
|
||||
|
||||
// image 模型
|
||||
import {ImageApi, ImageMidjourneyImagineReqVO, ImageVO} from "@/api/ai/image";
|
||||
// message
|
||||
const message = useMessage()
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
|
||||
// image 大小
|
||||
interface ImageSizeVO {
|
||||
key: string
|
||||
style: string,
|
||||
width: string,
|
||||
height: string,
|
||||
}
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const referImage = ref<any>() // 参考图
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
|
||||
const selectModel = ref<string>('midjourney') // 选中的热词
|
||||
const models = ref<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',
|
||||
},
|
||||
]) // 模型
|
||||
|
||||
const selectImageSize = ref<string>('1:1') // 选中 size
|
||||
const imageSizeList = ref<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;',
|
||||
},
|
||||
]) // size
|
||||
|
||||
// version
|
||||
const midjourneyVersionList = ref<any>([
|
||||
{
|
||||
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',
|
||||
},
|
||||
])
|
||||
const nijiVersionList = ref<any>([
|
||||
{
|
||||
value: '5',
|
||||
label: 'v5',
|
||||
},
|
||||
])
|
||||
// 表单
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const referImageUrl = ref<any>() // 参考图
|
||||
const selectModel = ref<string>('midjourney') // 选中的模型
|
||||
const selectSize = ref<string>('1:1') // 选中 size
|
||||
const selectVersion = ref<any>('6.0') // 选中的 version
|
||||
let versionList = ref<any>([]) // version 列表
|
||||
versionList.value = midjourneyVersionList.value // 默认选择 midjourney
|
||||
const versionList = ref<any>(MidjourneyVersions) // version 列表
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
/** 热词 - click */
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 取消
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
selectHotWord.value = hotWord
|
||||
// 设置提示次
|
||||
prompt.value = hotWord
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord // 选中
|
||||
prompt.value = hotWord // 设置提示次
|
||||
}
|
||||
|
||||
/** size - click */
|
||||
/** 点击 size 尺寸 */
|
||||
const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
selectImageSize.value = imageSize.key
|
||||
selectSize.value = imageSize.key
|
||||
}
|
||||
|
||||
/** 模型 - click */
|
||||
/** 点击 model 模型 */
|
||||
const handleModelClick = async (model: ImageModelVO) => {
|
||||
selectModel.value = model.key
|
||||
if (model.key === 'niji') {
|
||||
versionList.value = nijiVersionList.value // 默认选择 niji
|
||||
versionList.value = NijiVersionList // 默认选择 niji
|
||||
} else {
|
||||
versionList.value = midjourneyVersionList.value // 默认选择 midjourney
|
||||
versionList.value = MidjourneyVersions // 默认选择 midjourney
|
||||
}
|
||||
selectVersion.value = versionList.value[0].value
|
||||
}
|
||||
|
||||
/** version - click */
|
||||
const handleChangeVersion = async (version) => {
|
||||
console.log('version', version)
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
/** 图片生成 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
// todo @芋艿 图片生产逻辑
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', selectModel.value)
|
||||
emits('onDrawStart', AiPlatformEnum.MIDJOURNEY)
|
||||
// 发送请求
|
||||
const imageSize = imageSizeList.value.find(item => selectImageSize.value === item.key) as ImageSizeVO
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => selectSize.value === item.key
|
||||
) as ImageSizeVO
|
||||
const req = {
|
||||
prompt: prompt.value,
|
||||
model: selectModel.value,
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
version: selectVersion.value,
|
||||
referImageUrl: referImage.value,
|
||||
referImageUrl: referImageUrl.value
|
||||
} as ImageMidjourneyImagineReqVO
|
||||
await ImageApi.midjourneyImagine(req)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', selectModel.value)
|
||||
emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY)
|
||||
// 加载结束
|
||||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (imageDetail: ImageVO) => {
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
// 提示词
|
||||
prompt.value = imageDetail.prompt
|
||||
prompt.value = detail.prompt
|
||||
// image size
|
||||
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}:${imageDetail.height}`) as ImageSizeVO
|
||||
selectImageSize.value = imageSize.key
|
||||
const imageSize = MidjourneySizeList.find(
|
||||
(item) => item.key === `${detail.width}:${detail.height}`
|
||||
) as ImageSizeVO
|
||||
selectSize.value = imageSize.key
|
||||
// 选中模型
|
||||
const model = models.value.find(item => item.key === imageDetail.options?.model) as ImageModelVO
|
||||
const model = MidjourneyModels.find((item) => item.key === detail.options?.model) as ImageModelVO
|
||||
await handleModelClick(model)
|
||||
// 版本
|
||||
selectVersion.value = versionList.value.find(item => item.value === imageDetail.options?.version).value
|
||||
selectVersion.value = versionList.value.find(
|
||||
(item) => item.value === detail.options?.version
|
||||
).value
|
||||
// image
|
||||
referImage.value = imageDetail.options.referImageUrl
|
||||
referImageUrl.value = detail.options.referImageUrl
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
@@ -354,7 +274,6 @@ defineExpose({ settingValues })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 尺寸
|
||||
.image-size {
|
||||
width: 100%;
|
||||
272
src/views/ai/image/index/components/stableDiffusion/index.vue
Normal file
272
src/views/ai/image/index/components/stableDiffusion/index.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<!-- dall3 -->
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
class="w-100% mt-15px"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="hot-words">
|
||||
<div>
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button
|
||||
round
|
||||
class="btn"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in ImageHotEnglishWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">采样方法</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px">
|
||||
<el-option
|
||||
v-for="item in StableDiffusionSamplers"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">CLIP</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px">
|
||||
<el-option
|
||||
v-for="item in StableDiffusionClipGuidancePresets"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">风格</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px">
|
||||
<el-option
|
||||
v-for="item in StableDiffusionStylePresets"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">图片尺寸</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input v-model="width" class="w-170px" placeholder="图片宽度" />
|
||||
<el-input v-model="height" class="w-170px" placeholder="图片高度" />
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">迭代步数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="steps"
|
||||
type="number"
|
||||
size="large"
|
||||
class="!w-350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">引导系数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="scale"
|
||||
type="number"
|
||||
size="large"
|
||||
class="!w-350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">随机因子</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="seed"
|
||||
type="number"
|
||||
size="large"
|
||||
class="!w-350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||
import { hasChinese } from '@/views/ai/utils/utils'
|
||||
import {
|
||||
AiPlatformEnum,
|
||||
ImageHotEnglishWords,
|
||||
StableDiffusionClipGuidancePresets,
|
||||
StableDiffusionSamplers,
|
||||
StableDiffusionStylePresets
|
||||
} from '@/views/ai/utils/constants'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
// 表单
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const width = ref<number>(512) // 图片宽度
|
||||
const height = ref<number>(512) // 图片高度
|
||||
const sampler = ref<string>('DDIM') // 采样方法
|
||||
const steps = ref<number>(20) // 迭代步数
|
||||
const seed = ref<number>(42) // 控制生成图像的随机性
|
||||
const scale = ref<number>(7.5) // 引导系数
|
||||
const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
||||
const stylePreset = ref<string>('3d-model') // 风格
|
||||
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||
|
||||
/** 选择热词 */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 情况一:取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:选中
|
||||
selectHotWord.value = hotWord // 选中
|
||||
prompt.value = hotWord // 替换提示词
|
||||
}
|
||||
|
||||
/** 图片生成 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
if (hasChinese(prompt.value)) {
|
||||
message.alert('暂不支持中文!')
|
||||
return
|
||||
}
|
||||
await message.confirm(`确认生成内容?`)
|
||||
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
|
||||
// 发送请求
|
||||
const form = {
|
||||
platform: AiPlatformEnum.STABLE_DIFFUSION,
|
||||
model: 'stable-diffusion-v1-6',
|
||||
prompt: prompt.value, // 提示词
|
||||
width: width.value, // 图片宽度
|
||||
height: height.value, // 图片高度
|
||||
options: {
|
||||
seed: seed.value, // 随机种子
|
||||
steps: steps.value, // 图片生成步数
|
||||
scale: scale.value, // 引导系数
|
||||
sampler: sampler.value, // 采样算法
|
||||
clipGuidancePreset: clipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: stylePreset.value // 风格
|
||||
}
|
||||
} as ImageDrawReqVO
|
||||
await ImageApi.drawImage(form)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
|
||||
// 加载结束
|
||||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充值 */
|
||||
const settingValues = async (detail: ImageVO) => {
|
||||
prompt.value = detail.prompt
|
||||
width.value = detail.width
|
||||
height.value = detail.height
|
||||
seed.value = detail.options?.seed
|
||||
steps.value = detail.options?.steps
|
||||
scale.value = detail.options?.scale
|
||||
sampler.value = detail.options?.sampler
|
||||
clipGuidancePreset.value = detail.options?.clipGuidancePreset
|
||||
stylePreset.value = detail.options?.stylePreset
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ settingValues })
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
|
||||
// 热词
|
||||
.hot-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
|
||||
.word-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模型
|
||||
.group-item {
|
||||
margin-top: 30px;
|
||||
|
||||
.group-item-body {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,10 +12,7 @@
|
||||
@on-draw-start="handleDrawStart"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
<Midjourney
|
||||
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
|
||||
ref="midjourneyRef"
|
||||
/>
|
||||
<Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" ref="midjourneyRef" />
|
||||
<StableDiffusion
|
||||
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
|
||||
ref="stableDiffusionRef"
|
||||
@@ -24,28 +21,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ImageTask ref="imageTaskRef" @on-regeneration="handleRegeneration" />
|
||||
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO @fan:在整个挪到 /views/ai/image/index 目录。因为我想在 /views/ai/image/manager 做管理的功能,进行下区分!
|
||||
import Dall3 from './dall3/index.vue'
|
||||
import Midjourney from './midjourney/index.vue'
|
||||
import StableDiffusion from './stable-diffusion/index.vue'
|
||||
import ImageTask from './ImageTask.vue'
|
||||
import ImageList from './components/ImageList.vue'
|
||||
import { AiPlatformEnum } from '@/views/ai/utils/constants'
|
||||
import {ImageVO} from "@/api/ai/image";
|
||||
import { ImageVO } from '@/api/ai/image'
|
||||
import Dall3 from './components/dall3/index.vue'
|
||||
import Midjourney from './components/midjourney/index.vue'
|
||||
import StableDiffusion from './components/stableDiffusion/index.vue'
|
||||
|
||||
|
||||
const imageTaskRef = ref<any>() // image task ref
|
||||
const dall3Ref = ref<any>() // openai ref
|
||||
const imageListRef = ref<any>() // image 列表 ref
|
||||
const dall3Ref = ref<any>() // dall3(openai) ref
|
||||
const midjourneyRef = ref<any>() // midjourney ref
|
||||
const stableDiffusionRef = ref<any>() // stable diffusion ref
|
||||
|
||||
// 定义属性
|
||||
const selectPlatform = ref('StableDiffusion')
|
||||
const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
|
||||
const platformOptions = [
|
||||
{
|
||||
label: 'DALL3 绘画',
|
||||
@@ -61,35 +56,27 @@ const platformOptions = [
|
||||
}
|
||||
]
|
||||
|
||||
/** 绘画 - start */
|
||||
const handleDrawStart = async (type) => {
|
||||
/** 绘画 start */
|
||||
const handleDrawStart = async (platform: string) => {}
|
||||
|
||||
/** 绘画 complete */
|
||||
const handleDrawComplete = async (platform: string) => {
|
||||
await imageListRef.value.getImageList()
|
||||
}
|
||||
|
||||
/** 绘画 - complete */
|
||||
const handleDrawComplete = async (type) => {
|
||||
await imageTaskRef.value.getImageList()
|
||||
}
|
||||
|
||||
/** 绘画 - 重新生成 */
|
||||
const handleRegeneration = async (imageDetail: ImageVO) => {
|
||||
/** 重新生成:将画图详情填充到对应平台 */
|
||||
const handleRegeneration = async (image: 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)
|
||||
})
|
||||
selectPlatform.value = image.platform
|
||||
// 根据不同平台填充 image
|
||||
await nextTick()
|
||||
if (image.platform === AiPlatformEnum.MIDJOURNEY) {
|
||||
midjourneyRef.value.settingValues(image)
|
||||
} else if (image.platform === AiPlatformEnum.OPENAI) {
|
||||
dall3Ref.value.settingValues(image)
|
||||
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
|
||||
stableDiffusionRef.value.settingValues(image)
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
<!-- dall3 -->
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
||||
<!-- TODO @fan:style 看看能不能哟 unocss 替代 -->
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="hot-words">
|
||||
<div>
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button
|
||||
round
|
||||
class="btn"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">采样方法</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="selectSampler" placeholder="Select" size="large" style="width: 350px">
|
||||
<el-option v-for="item in sampler" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">CLIP</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select
|
||||
v-model="selectClipGuidancePreset"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in clipGuidancePresets"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">风格</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="selectStylePreset" placeholder="Select" size="large" style="width: 350px">
|
||||
<el-option
|
||||
v-for="item in stylePresets"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">图片尺寸</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input v-model="imageWidth" style="width: 170px" placeholder="图片宽度" />
|
||||
<el-input v-model="imageHeight" style="width: 170px" placeholder="图片高度" />
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">迭代步数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="steps"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">引导系数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="scale"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">随机因子</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="seed"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||
import { hasChinese } from '@/views/ai/utils/utils'
|
||||
|
||||
// image 模型
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const imageWidth = ref<number>(512) // 图片宽度
|
||||
const imageHeight = ref<number>(512) // 图片高度
|
||||
|
||||
const hotWords = ref<string[]>([
|
||||
'中国旗袍',
|
||||
'古装美女',
|
||||
'卡通头像',
|
||||
'机甲战士',
|
||||
'童话小屋',
|
||||
'中国长城'
|
||||
]) // 热词
|
||||
// message
|
||||
const message = useMessage()
|
||||
|
||||
// 采样方法
|
||||
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[]>([
|
||||
{
|
||||
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'
|
||||
}
|
||||
])
|
||||
|
||||
// 风格
|
||||
// 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<string>('3d-model') // 模型
|
||||
const stylePresets = ref<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'
|
||||
}
|
||||
])
|
||||
|
||||
// 文本提示相匹配的图像(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<string>('NONE') // 模型
|
||||
const clipGuidancePresets = ref<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'
|
||||
}
|
||||
])
|
||||
|
||||
const steps = ref<number>(20) // 迭代步数
|
||||
const seed = ref<number>(42) // 控制生成图像的随机性
|
||||
const scale = ref<number>(7.5) // 引导系数
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
/** 热词 - click */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
selectHotWord.value = hotWord
|
||||
// 替换提示词
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
if (await hasChinese(prompt.value)) {
|
||||
message.alert('暂不支持中文!')
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', 'StableDiffusion')
|
||||
// 发送请求
|
||||
const form = {
|
||||
platform: 'StableDiffusion',
|
||||
model: 'stable-diffusion-v1-6',
|
||||
prompt: prompt.value, // 提示词
|
||||
width: imageWidth.value, // 图片宽度
|
||||
height: imageHeight.value, // 图片高度
|
||||
options: {
|
||||
seed: seed.value, // 随机种子
|
||||
steps: steps.value, // 图片生成步数
|
||||
scale: scale.value, // 引导系数
|
||||
sampler: selectSampler.value, // 采样算法
|
||||
clipGuidancePreset: selectClipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: selectStylePreset.value // 风格
|
||||
}
|
||||
} as ImageDrawReqVO
|
||||
await ImageApi.drawImage(form)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', 'StableDiffusion')
|
||||
// 加载结束
|
||||
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">
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
|
||||
// 热词
|
||||
.hot-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
|
||||
.word-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模型
|
||||
.group-item {
|
||||
margin-top: 30px;
|
||||
|
||||
.group-item-body {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
<el-form-item label="角色头像" prop="avatar">
|
||||
<UploadImg v-model="formData.avatar" height="60px" width="60px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser(formType)">
|
||||
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser">
|
||||
<el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
|
||||
<el-option
|
||||
v-for="chatModel in chatModelList"
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类别" prop="category" v-if="!isUser(formType)">
|
||||
<el-form-item label="角色类别" prop="category" v-if="!isUser">
|
||||
<el-input v-model="formData.category" placeholder="请输入角色类别" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述" prop="description">
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-form-item label="角色设定" prop="systemMessage">
|
||||
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否公开" prop="publicStatus" v-if="!isUser(formType)">
|
||||
<el-form-item label="是否公开" prop="publicStatus" v-if="!isUser">
|
||||
<el-radio-group v-model="formData.publicStatus">
|
||||
<el-radio
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
@@ -43,10 +43,10 @@
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色排序" prop="sort" v-if="!isUser(formType)">
|
||||
<el-form-item label="角色排序" prop="sort" v-if="!isUser">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入角色排序" class="!w-1/1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开启状态" prop="status" v-if="!isUser(formType)">
|
||||
<el-form-item label="开启状态" prop="status" v-if="!isUser">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
@@ -97,10 +97,9 @@ const formRef = ref() // 表单 Ref
|
||||
const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
|
||||
|
||||
/** 是否【我】自己创建,私有角色 */
|
||||
// TODO @fan:建议改成计算函数 computed
|
||||
const isUser = (type: string) => {
|
||||
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) => {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
/** 判断字符串是否包含中文 */
|
||||
export const hasChinese = async (str) => {
|
||||
export const hasChinese = (str: string) => {
|
||||
return /[\u4e00-\u9fa5]/.test(str)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user