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

This commit is contained in:
puhui999
2024-07-05 18:01:40 +08:00
17 changed files with 299 additions and 219 deletions

View File

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

View File

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

View File

@ -6,8 +6,8 @@
v-for="image in imageList"
:key="image"
:image-detail="image"
@on-btn-click="handlerImageBtnClick"
@on-mj-btn-click="handlerImageMjBtnClick"
@on-btn-click="handleImageBtnClick"
@on-mj-btn-click="handleImageMjBtnClick"
/>
</div>
<div class="task-image-pagination">
@ -16,7 +16,7 @@
layout="prev, pager, next"
:default-page-size="pageSize"
:total="pageTotal"
@change="handlerPageChange"
@change="handlePageChange"
/>
</div>
</el-card>
@ -24,7 +24,7 @@
<ImageDetailDrawer
:show="isShowImageDetail"
:id="showImageDetailId"
@handler-drawer-close="handlerDrawerClose"
@handle-drawer-close="handleDrawerClose"
/>
</template>
<script setup lang="ts">
@ -33,6 +33,7 @@ import ImageDetailDrawer from './ImageDetailDrawer.vue'
import ImageTaskCard from './ImageTaskCard.vue'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
import { downloadImage } from '@/utils/download'
const message = useMessage() // 消息弹窗
@ -49,12 +50,12 @@ const pageSize = ref<number>(10) // page size
const pageTotal = ref<number>(0) // page size
/** 抽屉 - close */
const handlerDrawerClose = async () => {
const handleDrawerClose = async () => {
isShowImageDetail.value = false
}
/** 任务 - detail */
const handlerDrawerOpen = async () => {
const handleDrawerOpen = async () => {
isShowImageDetail.value = true
}
@ -117,12 +118,12 @@ const refreshWatchImages = async () => {
}
/** 图片 - btn click */
const handlerImageBtnClick = async (type: string, imageDetail: ImageVO) => {
const handleImageBtnClick = async (type: string, imageDetail: ImageVO) => {
// 获取 image detail id
showImageDetailId.value = imageDetail.id
// 处理不用 btn
if (type === 'more') {
await handlerDrawerOpen()
await handleDrawerOpen()
} else if (type === 'delete') {
await message.confirm(`是否删除照片?`)
await ImageApi.deleteImageMy(imageDetail.id)
@ -130,11 +131,15 @@ const handlerImageBtnClick = async (type: string, imageDetail: ImageVO) => {
message.success('删除成功!')
} else if (type === 'download') {
await downloadImage(imageDetail.picUrl)
} else if (type === 'regeneration') {
// Midjourney 平台
console.log('regeneration', imageDetail.id)
await emits('onRegeneration', imageDetail)
}
}
/** 图片 - mj btn click */
const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
const handleImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
// 1、构建 params 参数
const data = {
id: imageDetail.id,
@ -146,28 +151,8 @@ const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: Ima
await getImageList()
}
/** 下载 - image */
// TODO @fan貌似可以考虑抽到 download 里面,作为一个方法
const downloadImage = async (imageUrl) => {
const image = new Image()
image.setAttribute('crossOrigin', 'anonymous')
image.src = imageUrl
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d') as CanvasDrawImage
ctx.drawImage(image, 0, 0, image.width, image.height)
const url = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = url
a.download = 'image.png'
a.click()
}
}
// page change
const handlerPageChange = async (page) => {
const handlePageChange = async (page) => {
pageNo.value = page
await getImageList(false)
}
@ -175,6 +160,9 @@ const handlerPageChange = async (page) => {
/** 暴露组件方法 */
defineExpose({ getImageList })
// emits
const emits = defineEmits(['onRegeneration'])
/** 组件挂在的时候 */
onMounted(async () => {
// 获取 image 列表

View File

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

View File

@ -25,7 +25,7 @@
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
v-for="hotWord in hotWords"
:key="hotWord"
@click="handlerHotWordClick(hotWord)"
@click="handleHotWordClick(hotWord)"
>
{{ hotWord }}
</el-button>
@ -37,7 +37,7 @@
</div>
<el-space wrap class="model-list">
<div
:class="selectModel === model ? 'modal-item selectModel' : 'modal-item'"
:class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'"
v-for="model in models"
:key="model.key"
@ -45,7 +45,7 @@
<el-image
:src="model.image"
fit="contain"
@click="handlerModelClick(model)"
@click="handleModelClick(model)"
/>
<div class="model-font">{{model.name}}</div>
</div>
@ -57,14 +57,14 @@
</div>
<el-space wrap class="image-style-list">
<div
:class="selectImageStyle === imageStyle ? 'image-style-item selectImageStyle' : 'image-style-item'"
:class="selectImageStyle === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'"
v-for="imageStyle in imageStyleList"
:key="imageStyle.key"
>
<el-image
:src="imageStyle.image"
fit="contain"
@click="handlerStyleClick(imageStyle)"
@click="handleStyleClick(imageStyle)"
/>
<div class="style-font">{{imageStyle.name}}</div>
</div>
@ -78,8 +78,8 @@
<div class="size-item"
v-for="imageSize in imageSizeList"
:key="imageSize.key"
@click="handlerSizeClick(imageSize)">
<div :class="selectImageSize === imageSize ? 'size-wrapper selectImageSize' : 'size-wrapper'">
@click="handleSizeClick(imageSize)">
<div :class="selectImageSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'">
<div :style="imageSize.style"></div>
</div>
<div class="size-font">{{ imageSize.name }}</div>
@ -91,13 +91,13 @@
size="large"
round
:loading="drawIn"
@click="handlerGenerateImage">
@click="handleGenerateImage">
{{drawIn ? '生成中' : '生成内容'}}
</el-button>
</div>
</template>
<script setup lang="ts">
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image';
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image';
// image 模型
interface ImageModelVO {
@ -120,42 +120,38 @@ const prompt = ref<string>('') // 提示词
const drawIn = ref<boolean>(false) // 生成中
const selectHotWord = ref<string>('') // 选中的热词
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
const selectModel = ref<any>({}) // 模型
const selectModel = ref<string>('dall-e-3') // 模型
// message
const message = useMessage()
// TODO @fanimage 改成项目里自己的哈
// TODO @fan这个 image要不看看网上有没合适的图片作为占位符啊哈哈
const models = ref<ImageModelVO[]>([
{
key: 'dall-e-3',
name: 'DALL·E 3',
image: 'https://h5.cxyhub.com/images/model_2.png',
image: `/src/assets/ai/dall2.jpg`,
},
{
key: 'dall-e-2',
name: 'DALL·E 2',
image: 'https://h5.cxyhub.com/images/model_1.png',
image: `/src/assets/ai/dall3.jpg`,
},
]) // 模型
selectModel.value = models.value[0]
const selectImageStyle = ref<any>({}) // style 样式
// TODO @fanimage 改成项目里自己的哈
const selectImageStyle = ref<string>('vivid') // style 样式
const imageStyleList = ref<ImageModelVO[]>([
{
key: 'vivid',
name: '清晰',
image: 'https://h5.cxyhub.com/images/model_1.png',
image: `/src/assets/ai/qingxi.jpg`,
},
{
key: 'natural',
name: '自然',
image: 'https://h5.cxyhub.com/images/model_2.png',
image: `/src/assets/ai/ziran.jpg`,
},
]) // style
selectImageStyle.value = imageStyleList.value[0]
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // 选中 size
const selectImageSize = ref<string>('1024x1024') // 选中 size
const imageSizeList = ref<ImageSizeVO[]>([
{
key: '1024x1024',
@ -179,17 +175,14 @@ const imageSizeList = ref<ImageSizeVO[]>([
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
}
]) // size
selectImageSize.value = imageSizeList.value[0]
// 定义 Props
const props = defineProps({})
// 定义 emits
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
// TODO @fan如果是简单注释建议用 /** */,主要是现在项目里是这种风格哈,保持一致好点~
// TODO @fanhandler 应该改成 handle 哈
/** 热词 - click */
const handlerHotWordClick = async (hotWord: string) => {
const handleHotWordClick = async (hotWord: string) => {
// 取消选中
if (selectHotWord.value == hotWord) {
selectHotWord.value = ''
@ -202,64 +195,66 @@ const handlerHotWordClick = async (hotWord: string) => {
}
/** 模型 - click */
const handlerModelClick = async (model: ImageModelVO) => {
if (selectModel.value === model) {
selectModel.value = {} as ImageModelVO
return
}
selectModel.value = model
const handleModelClick = async (model: ImageModelVO) => {
selectModel.value = model.key
}
/** 样式 - click */
const handlerStyleClick = async (imageStyle: ImageModelVO) => {
if (selectImageStyle.value === imageStyle) {
selectImageStyle.value = {} as ImageModelVO
return
}
selectImageStyle.value = imageStyle
const handleStyleClick = async (imageStyle: ImageModelVO) => {
selectImageStyle.value = imageStyle.key
}
/** size - click */
const handlerSizeClick = async (imageSize: ImageSizeVO) => {
if (selectImageSize.value === imageSize) {
selectImageSize.value = {} as ImageSizeVO
return
}
selectImageSize.value = imageSize
const handleSizeClick = async (imageSize: ImageSizeVO) => {
selectImageSize.value = imageSize.key
}
/** 图片生产 */
const handlerGenerateImage = async () => {
const handleGenerateImage = async () => {
// 二次确认
await message.confirm(`确认生成内容?`)
try {
// 加载中
drawIn.value = true
// 回调
emits('onDrawStart', selectModel.value.key)
emits('onDrawStart', selectModel.value)
const imageSize = imageSizeList.value.find(item => item.key === selectImageSize.value) as ImageSizeVO
const form = {
platform: 'OpenAI',
prompt: prompt.value, // 提示词
model: selectModel.value.key, // 模型
width: selectImageSize.value.width, // size 不能为空
height: selectImageSize.value.height, // size 不能为空
model: selectModel.value, // 模型
width: imageSize.width, // size 不能为空
height: imageSize.height, // size 不能为空
options: {
style: selectImageStyle.value.key, // 图像生成的风格
style: selectImageStyle.value, // 图像生成的风格
}
} as ImageDrawReqVO
// 发送请求
await ImageApi.drawImage(form)
} finally {
// 回调
emits('onDrawComplete', selectModel.value.key)
emits('onDrawComplete', selectModel.value)
// 加载结束
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
selectModel.value = imageDetail.model
//
selectImageStyle.value = imageDetail.options?.style
//
const imageSize = imageSizeList.value.find(item => item.key === `${imageDetail.width}x${imageDetail.height}`) as ImageSizeVO
await handleSizeClick(imageSize)
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
// 提示词
.prompt {
}

View File

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

View File

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

View File

@ -120,7 +120,8 @@
</div>
</template>
<script setup lang="ts">
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image'
import {ImageApi, ImageDrawReqVO, ImageVO} from '@/api/ai/image'
import {hasChinese} from '../../utils/common-utils'
// image 模型
interface ImageModelVO {
@ -146,8 +147,8 @@ const hotWords = ref<string[]>([
// message
const message = useMessage()
// 采样方法 TODO @fan有 Euler aDPM++ 2S aDPM++ 2MDPM++ SDEDPM++ 2M SDEUniPCRestart另外要不这种枚举我们都放到 image 里?写成 stableDiffusionSampler
const selectSampler = ref<any>({}) // 模型
// 采样方法
const selectSampler = ref<string>('DDIM') // 模型
// DDIM DDPM K_DPMPP_2M K_DPMPP_2S_ANCESTRAL K_DPM_2 K_DPM_2_ANCESTRAL K_EULER K_EULER_ANCESTRAL K_HEUN K_LMS
const sampler = ref<ImageModelVO[]>([
{
@ -191,12 +192,11 @@ const sampler = ref<ImageModelVO[]>([
name: 'K_LMS'
},
])
selectSampler.value = sampler.value[0]
// 风格
// 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art isometric
// line-art low-poly modeling-compound neon-punk origami photographic pixel-art tile-texture
const selectStylePreset = ref<any>({}) // 模型
const selectStylePreset = ref<string>('3d-model') // 模型
const stylePresets = ref<ImageModelVO[]>([
{
key: '3d-model',
@ -268,13 +268,11 @@ const stylePresets = ref<ImageModelVO[]>([
name: 'tile-texture'
},
])
selectStylePreset.value = stylePresets.value[0]
// 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
// FAST_BLUE FAST_GREEN NONE SIMPLE SLOW SLOWER SLOWEST
const selectClipGuidancePreset = ref<any>({}) // 模型
const selectClipGuidancePreset = ref<string>('NONE') // 模型
const clipGuidancePresets = ref<ImageModelVO[]>([
{
key: 'NONE',
@ -305,7 +303,6 @@ const clipGuidancePresets = ref<ImageModelVO[]>([
name: 'SLOWEST'
},
])
selectClipGuidancePreset.value = clipGuidancePresets.value[0]
const steps = ref<number>(20) // 迭代步数
const seed = ref<number>(42) // 控制生成图像的随机性
@ -333,6 +330,10 @@ const handleHotWordClick = async (hotWord: string) => {
const handleGenerateImage = async () => {
// 二次确认
await message.confirm(`确认生成内容?`)
if (await hasChinese(prompt.value)) {
message.alert('暂不支持中文!')
return
}
try {
// 加载中
drawIn.value = true
@ -349,9 +350,9 @@ const handleGenerateImage = async () => {
seed: seed.value, // 随机种子
steps: steps.value, // 图片生成步数
scale: scale.value, // 引导系数
sampler: selectSampler.value.key, // 采样算法
clipGuidancePreset: selectClipGuidancePreset.value.key, // 文本提示相匹配的图像 CLIP
stylePreset: selectStylePreset.value.key, // 风格
sampler: selectSampler.value, // 采样算法
clipGuidancePreset: selectClipGuidancePreset.value, // 文本提示相匹配的图像 CLIP
stylePreset: selectStylePreset.value, // 风格
}
} as ImageDrawReqVO
await ImageApi.drawImage(form)
@ -362,6 +363,22 @@ const handleGenerateImage = async () => {
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (imageDetail: ImageVO) => {
prompt.value = imageDetail.prompt
imageWidth.value = imageDetail.width
imageHeight.value = imageDetail.height
seed.value = imageDetail.options?.seed
steps.value = imageDetail.options?.steps
scale.value = imageDetail.options?.scale
selectSampler.value = imageDetail.options?.sampler
selectClipGuidancePreset.value = imageDetail.options?.clipGuidancePreset
selectStylePreset.value = imageDetail.options?.stylePreset
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</script>
<style scoped lang="scss">
// 提示词

View File

@ -0,0 +1,13 @@
/**
* Created by 芋道源码
*
* AI 枚举类
*
* 问题:为什么不放在 src/utils/common-utils.ts 呢?
* 回答:主要 AI 是可选模块,考虑到独立、解耦,所以放在了 /views/ai/utils/common-utils.ts
*/
/** 判断字符串是否包含中文 */
export const hasChinese = async (str) => {
return /[\u4e00-\u9fa5]/.test(str)
}