mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-11-08 22:28:45 +08:00
Merge remote-tracking branch 'yudao/dev' into dev
This commit is contained in:
@@ -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 './role/index.vue'
|
||||
import { Bottom, Top } from '@element-plus/icons-vue'
|
||||
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
@@ -107,8 +108,8 @@ const message = useMessage() // 消息弹窗
|
||||
const searchName = ref<string>('') // 对话搜索
|
||||
const activeConversationId = ref<string | null>(null) // 选中的对话,默认为 null
|
||||
const hoverConversationId = ref<string | null>(null) // 悬浮上去的对话
|
||||
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
|
||||
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
|
||||
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const drawer = ref<boolean>(false) // 角色仓库抽屉 TODO @fan:roleDrawer 会不会好点哈
|
||||
const loading = ref<boolean>(false) // 加载中
|
||||
const loadingTime = ref<any>() // 加载中定时器
|
||||
@@ -138,7 +139,7 @@ const searchConversation = async (e) => {
|
||||
conversationMap.value = await conversationTimeGroup(conversationList.value)
|
||||
} else {
|
||||
// 过滤
|
||||
const filterValues = conversationList.value.filter(item => {
|
||||
const filterValues = conversationList.value.filter((item) => {
|
||||
return item.title.includes(searchName.value.trim())
|
||||
})
|
||||
conversationMap.value = await conversationTimeGroup(filterValues)
|
||||
@@ -150,7 +151,7 @@ const searchConversation = async (e) => {
|
||||
*/
|
||||
const handleConversationClick = async (id: string) => {
|
||||
// 过滤出选中的对话
|
||||
const filterConversation = conversationList.value.filter(item => {
|
||||
const filterConversation = conversationList.value.filter((item) => {
|
||||
return item.id === id
|
||||
})
|
||||
// 回调 onConversationClick
|
||||
@@ -211,20 +212,20 @@ const getChatConversationList = async () => {
|
||||
const conversationTimeGroup = async (list: ChatConversationVO[]) => {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
const groupMap = {
|
||||
'置顶': [],
|
||||
'今天': [],
|
||||
'一天前': [],
|
||||
'三天前': [],
|
||||
'七天前': [],
|
||||
'三十天前': []
|
||||
置顶: [],
|
||||
今天: [],
|
||||
一天前: [],
|
||||
三天前: [],
|
||||
七天前: [],
|
||||
三十天前: []
|
||||
}
|
||||
// 当前时间的时间戳
|
||||
const now = Date.now();
|
||||
const now = Date.now()
|
||||
// 定义时间间隔常量(单位:毫秒)
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const threeDays = 3 * oneDay;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
const oneDay = 24 * 60 * 60 * 1000
|
||||
const threeDays = 3 * oneDay
|
||||
const sevenDays = 7 * oneDay
|
||||
const thirtyDays = 30 * oneDay
|
||||
for (const conversation: ChatConversationVO of list) {
|
||||
// 置顶
|
||||
if (conversation.pinned) {
|
||||
@@ -232,7 +233,7 @@ const conversationTimeGroup = async (list: ChatConversationVO[]) => {
|
||||
continue
|
||||
}
|
||||
// 计算时间差(单位:毫秒)
|
||||
const diff = now - conversation.updateTime;
|
||||
const diff = now - conversation.updateTime
|
||||
// 根据时间间隔判断
|
||||
if (diff < oneDay) {
|
||||
groupMap['今天'].push(conversation)
|
||||
@@ -271,7 +272,7 @@ const createConversation = async () => {
|
||||
*/
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 1. 二次确认
|
||||
const {value} = await ElMessageBox.prompt('修改标题', {
|
||||
const { value } = await ElMessageBox.prompt('修改标题', {
|
||||
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
|
||||
inputErrorMessage: '标题不能为空',
|
||||
inputValue: conversation.title
|
||||
@@ -285,7 +286,7 @@ const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 3. 刷新列表
|
||||
await getChatConversationList()
|
||||
// 4. 过滤当前切换的
|
||||
const filterConversationList = conversationList.value.filter(item => {
|
||||
const filterConversationList = conversationList.value.filter((item) => {
|
||||
return item.id === conversation.id
|
||||
})
|
||||
if (filterConversationList.length > 0) {
|
||||
@@ -310,8 +311,7 @@ const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
await getChatConversationList()
|
||||
// 回调
|
||||
emits('onConversationDelete', conversation)
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,16 +343,13 @@ const handleRoleRepository = async () => {
|
||||
*/
|
||||
const handleClearConversation = async () => {
|
||||
// TODO @fan:可以使用 await message.confirm( 简化,然后使用 await 改成同步的逻辑,会更简洁
|
||||
ElMessageBox.confirm(
|
||||
'确认后对话会全部清空,置顶的对话除外。',
|
||||
'确认提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
ElMessageBox.confirm('确认后对话会全部清空,置顶的对话除外。', '确认提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
await ChatConversationApi.deleteMyAllExceptPinned()
|
||||
await ChatConversationApi.deleteChatConversationMyByUnpinned()
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success'
|
||||
@@ -364,8 +361,7 @@ const handleClearConversation = async () => {
|
||||
// 回调 方法
|
||||
emits('onConversationClear')
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// ============ 组件 onMounted
|
||||
@@ -377,7 +373,7 @@ watch(activeId, async (newValue, oldValue) => {
|
||||
})
|
||||
|
||||
// 定义 public 方法
|
||||
defineExpose({createConversation})
|
||||
defineExpose({ createConversation })
|
||||
|
||||
onMounted(async () => {
|
||||
// 获取 对话列表
|
||||
@@ -394,11 +390,9 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.conversation-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -111,7 +111,7 @@ import MessageLoading from './MessageLoading.vue'
|
||||
import MessageNewChat from './MessageNewChat.vue'
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import ChatConversationUpdateForm from '@/views/ai/chat/components/ChatConversationUpdateForm.vue'
|
||||
import ChatConversationUpdateForm from './components/ChatConversationUpdateForm.vue'
|
||||
import { Download, Top } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute() // 路由
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Header from '@/views/ai/chat/components/Header.vue'
|
||||
import Header from './../components/Header.vue'
|
||||
import RoleList from './RoleList.vue'
|
||||
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
|
||||
import RoleCategoryList from './RoleCategoryList.vue'
|
||||
@@ -16,5 +16,5 @@ import ChatConversationList from './ChatConversationList.vue'
|
||||
import ChatMessageList from './ChatMessageList.vue'
|
||||
|
||||
/** AI 聊天对话 列表 */
|
||||
defineOptions({ name: 'ChatConversation' })
|
||||
defineOptions({ name: 'AiChatManager' })
|
||||
</script>
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<el-card class="dr-task" body-class="task-card" shadow="never">
|
||||
<template #header>绘画任务</template>
|
||||
<ImageTaskCard
|
||||
v-for="image in imageList"
|
||||
:key="image"
|
||||
:image-detail="image"
|
||||
@on-btn-click="handlerImageBtnClick"
|
||||
@on-mj-btn-click="handlerImageMjBtnClick"/>
|
||||
</el-card>
|
||||
<!-- 图片 detail 抽屉 -->
|
||||
<ImageDetailDrawer
|
||||
:show="isShowImageDetail"
|
||||
:id="showImageDetailId"
|
||||
@handler-drawer-close="handlerDrawerClose"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ImageApi, ImageDetailVO, ImageMjActionVO, ImageMjButtonsVO} from '@/api/ai/image';
|
||||
import ImageDetailDrawer from './ImageDetailDrawer.vue'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const imageList = ref<ImageDetailVO[]>([]) // image 列表
|
||||
const imageListInterval = ref<any>() // image 列表定时器,刷新列表
|
||||
const isShowImageDetail = ref<boolean>(false) // 是否显示 task 详情
|
||||
const showImageDetailId = ref<number>(0) // 是否显示 task 详情
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handlerDrawerClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handlerDrawerOpen = async () => {
|
||||
isShowImageDetail.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 - image 列表
|
||||
*/
|
||||
const getImageList = async () => {
|
||||
const { list } = await ImageApi.getImageList({pageNo: 1, pageSize: 20})
|
||||
imageList.value = list
|
||||
}
|
||||
|
||||
/** 图片 - btn click */
|
||||
const handlerImageBtnClick = async (type, imageDetail: ImageDetailVO) => {
|
||||
// 获取 image detail id
|
||||
showImageDetailId.value = imageDetail.id
|
||||
// 处理不用 btn
|
||||
if (type === 'more') {
|
||||
await handlerDrawerOpen()
|
||||
} else if (type === 'delete') {
|
||||
await message.confirm(`是否删除照片?`)
|
||||
await ImageApi.deleteImage(imageDetail.id)
|
||||
await getImageList()
|
||||
await message.success("删除成功!")
|
||||
} else if (type === 'download') {
|
||||
await downloadImage(imageDetail.picUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片 - mj btn click */
|
||||
const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageDetailVO) => {
|
||||
// 1、构建 params 参数
|
||||
const params = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId,
|
||||
} as ImageMjActionVO
|
||||
// 2、发送 action
|
||||
await ImageApi.midjourneyAction(params)
|
||||
// 3、刷新列表
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({getImageList})
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
await getImageList()
|
||||
// 自动刷新 image 列表
|
||||
imageListInterval.value = setInterval(async () => {
|
||||
await getImageList()
|
||||
}, 1000 * 20)
|
||||
})
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (imageListInterval.value) {
|
||||
clearInterval(imageListInterval.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
>div {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dr-task {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<el-card body-class="" class="image-card">
|
||||
<div class="image-operation">
|
||||
<div>
|
||||
<el-button type="primary" text bg v-if="imageDetail?.status === 10">生成中</el-button>
|
||||
<el-button text bg v-else-if="imageDetail?.status === 20">已完成</el-button>
|
||||
<el-button type="danger" text bg v-else-if="imageDetail?.status === 30">异常</el-button>
|
||||
</div>
|
||||
<!-- TODO @fan:1)按钮要不调整成详情、下载、再次生成、删除?;2)如果是再次生成,就把当前的参数填写到左侧的框框里? -->
|
||||
<div>
|
||||
<el-button class="btn" text :icon="Download"
|
||||
@click="handlerBtnClick('download', imageDetail)"/>
|
||||
<el-button class="btn" text :icon="Delete" @click="handlerBtnClick('delete', imageDetail)"/>
|
||||
<el-button class="btn" text :icon="More" @click="handlerBtnClick('more', imageDetail)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-wrapper" ref="cardImageRef">
|
||||
<!-- TODO @fan:要不加个点击,大图预览? -->
|
||||
<img class="image" :src="imageDetail?.picUrl"/>
|
||||
<div v-if="imageDetail?.status === 30">{{imageDetail?.errorMessage}}</div>
|
||||
</div>
|
||||
<!-- TODO @fan:style 使用 unocss 替代下 -->
|
||||
<div class="image-mj-btns">
|
||||
<el-button size="small" v-for="button in imageDetail?.buttons" :key="button"
|
||||
style="min-width: 40px;margin-left: 0; margin-right: 10px; margin-top: 5px;"
|
||||
@click="handlerMjBtnClick(button)"
|
||||
>
|
||||
{{ button.label }}{{ button.emoji }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {Delete, Download, More} from "@element-plus/icons-vue";
|
||||
import {ImageDetailVO, ImageMjButtonsVO} from "@/api/ai/image";
|
||||
import {PropType} from "vue";
|
||||
import {ElLoading} from "element-plus";
|
||||
|
||||
const cardImageRef = ref<any>() // 卡片 image ref
|
||||
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
|
||||
|
||||
const props = defineProps({
|
||||
imageDetail: {
|
||||
type: Object as PropType<ImageDetailVO>,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 按钮 - 点击事件 */
|
||||
const handlerBtnClick = async (type, imageDetail: ImageDetailVO) => {
|
||||
emits('onBtnClick', type, imageDetail)
|
||||
}
|
||||
|
||||
const handlerLoading = async (status: number) => {
|
||||
// TODO @fan:这个搞成 Loading 组件,然后通过数据驱动,这样搞可以哇?
|
||||
if (status === 10) {
|
||||
cardImageLoadingInstance.value = ElLoading.service({
|
||||
target: cardImageRef.value,
|
||||
text: '生成中...'
|
||||
})
|
||||
} else {
|
||||
if (cardImageLoadingInstance.value) {
|
||||
cardImageLoadingInstance.value.close();
|
||||
cardImageLoadingInstance.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** mj 按钮 click */
|
||||
const handlerMjBtnClick = async (button: ImageMjButtonsVO) => {
|
||||
emits('onMjBtnClick', button, props.imageDetail)
|
||||
}
|
||||
|
||||
// watch
|
||||
const { imageDetail } = toRefs(props)
|
||||
watch(imageDetail, async (newVal, oldVal) => {
|
||||
await handlerLoading(newVal.status as string)
|
||||
})
|
||||
|
||||
// emits
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick'])
|
||||
|
||||
//
|
||||
onMounted(async () => {
|
||||
await handlerLoading(props.imageDetail.status as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.image-card {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.image-operation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
//border: 1px solid red;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
overflow: hidden;
|
||||
margin-top: 20px;
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-mj-btns {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,103 +0,0 @@
|
||||
<!-- image -->
|
||||
<template>
|
||||
<div class="ai-image">
|
||||
<div class="left">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="selectModel" :options="modelOptions" />
|
||||
</div>
|
||||
<div class="modal-switch-container">
|
||||
<!-- TODO @fan:1)建议 Dall3 改成 OpenAI 绘图。因为 dall3 其实本质是模型;2)涉及到中英文的地方,中文和英文之间,有个空格哈 -->
|
||||
<Dall3 v-if="selectModel === 'DALL3绘画'"
|
||||
@on-draw-start="handlerDrawStart"
|
||||
@on-draw-complete="handlerDrawComplete" />
|
||||
<Midjourney v-if="selectModel === 'MJ绘画'" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ImageTask ref="imageTaskRef" />
|
||||
</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 ImageTask from './ImageTask.vue'
|
||||
|
||||
// ref
|
||||
const imageTaskRef = ref<any>() // image task ref
|
||||
|
||||
// 定义属性
|
||||
const selectModel = ref('DALL3绘画')
|
||||
const modelOptions = ['DALL3绘画', 'MJ绘画']
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
|
||||
/** 绘画 - start */
|
||||
const handlerDrawStart = async (type) => {
|
||||
// todo
|
||||
drawIn.value = true
|
||||
}
|
||||
|
||||
/** 绘画 - complete */
|
||||
const handlerDrawComplete = async (type) => {
|
||||
drawIn.value = false
|
||||
// todo
|
||||
await imageTaskRef.value.getImageList()
|
||||
}
|
||||
|
||||
//
|
||||
onMounted( async () => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.ai-image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
width: 350px;
|
||||
|
||||
.segmented {
|
||||
}
|
||||
|
||||
.segmented .el-segmented {
|
||||
--el-border-radius-base: 16px;
|
||||
--el-segmented-item-selected-color: #fff;
|
||||
background-color: #ececec;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.modal-switch-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 350px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -2,16 +2,16 @@
|
||||
<el-drawer
|
||||
v-model="showDrawer"
|
||||
title="图片详细"
|
||||
@close="handlerDrawerClose"
|
||||
@close="handleDrawerClose"
|
||||
custom-class="drawer-class"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<div class="item">
|
||||
<!-- <div class="header">-->
|
||||
<!-- <div>图片</div>-->
|
||||
<!-- <div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="header">-->
|
||||
<!-- <div>图片</div>-->
|
||||
<!-- <div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="body">
|
||||
<!-- TODO @fan: 要不,这里只展示图片???不用 ImageTaskCard -->
|
||||
<ImageTaskCard :image-detail="imageDetail" />
|
||||
@@ -21,57 +21,49 @@
|
||||
<div class="item">
|
||||
<div class="tip">时间</div>
|
||||
<div class="body">
|
||||
<div>提交时间:{{imageDetail.createTime}}</div>
|
||||
<!-- TODO @fan:要不加个完成时间的字段 finishTime?updateTime 不算特别合理哈 -->
|
||||
<div>生成时间:{{imageDetail.updateTime}}</div>
|
||||
<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}})
|
||||
{{ imageDetail.model }}({{ imageDetail.height }}x{{ imageDetail.width }})
|
||||
</div>
|
||||
</div>
|
||||
<!-- 提示词 -->
|
||||
<div class="item">
|
||||
<div class="tip">提示词</div>
|
||||
<div class="body">
|
||||
{{imageDetail.prompt}}
|
||||
{{ imageDetail.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地址 -->
|
||||
<div class="item">
|
||||
<div class="tip">图片地址</div>
|
||||
<div class="body">
|
||||
{{imageDetail.picUrl}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 生成地址 TODO @fan:这个字段我删除了,要不干掉? -->
|
||||
<div class="item">
|
||||
<div class="tip">生成地址</div>
|
||||
<div class="body">
|
||||
{{imageDetail.originalPicUrl}}
|
||||
{{ imageDetail.picUrl }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 风格 -->
|
||||
<div class="item">
|
||||
<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}}
|
||||
{{ imageDetail?.options?.style }}
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ImageApi, ImageDetailVO} from '@/api/ai/image';
|
||||
import ImageTaskCard from './ImageTaskCard.vue';
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
|
||||
const showDrawer = ref<boolean>(false) // 是否显示
|
||||
const imageDetail = ref<ImageDetailVO>({} as ImageDetailVO) // 图片详细信息
|
||||
const imageDetail = ref<ImageVO>({} as ImageVO) // 图片详细信息
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -86,18 +78,18 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handlerDrawerClose = async () => {
|
||||
emits('handlerDrawerClose')
|
||||
const handleDrawerClose = async () => {
|
||||
emits('handleDrawerClose')
|
||||
}
|
||||
|
||||
/** 获取 - 图片 detail */
|
||||
const getImageDetail = async (id) => {
|
||||
// 获取图片详细
|
||||
imageDetail.value = await ImageApi.getImageDetail(id)
|
||||
imageDetail.value = await ImageApi.getImageMy(id)
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handlerTaskDetail = async () => {
|
||||
const handleTaskDetail = async () => {
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
@@ -114,14 +106,11 @@ watch(id, async (newVal, oldVal) => {
|
||||
}
|
||||
})
|
||||
//
|
||||
const emits = defineEmits(['handlerDrawerClose'])
|
||||
const emits = defineEmits(['handleDrawerClose'])
|
||||
//
|
||||
onMounted(async () => {
|
||||
|
||||
})
|
||||
onMounted(async () => {})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
@@ -143,7 +132,6 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
color: #616161;
|
||||
|
||||
|
||||
.taskImage {
|
||||
border-radius: 10px;
|
||||
}
|
||||
233
src/views/ai/image/index/ImageTask.vue
Normal file
233
src/views/ai/image/index/ImageTask.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<el-card class="dr-task" body-class="task-card" shadow="never">
|
||||
<template #header>绘画任务</template>
|
||||
<div class="task-image-list" ref="imageTaskRef">
|
||||
<ImageTaskCard
|
||||
v-for="image in imageList"
|
||||
:key="image"
|
||||
:image-detail="image"
|
||||
@on-btn-click="handleImageBtnClick"
|
||||
@on-mj-btn-click="handleImageMjBtnClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-image-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:default-page-size="pageSize"
|
||||
:total="pageTotal"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 图片 detail 抽屉 -->
|
||||
<ImageDetailDrawer
|
||||
:show="isShowImageDetail"
|
||||
:id="showImageDetailId"
|
||||
@handle-drawer-close="handleDrawerClose"
|
||||
/>
|
||||
</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 { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
import { downloadImage } 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 pageTotal = ref<number>(0) // page size
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handleDrawerClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handleDrawerOpen = async () => {
|
||||
isShowImageDetail.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 - image 列表
|
||||
*/
|
||||
const getImageList = async (apply: boolean = false) => {
|
||||
imageTaskLoading.value = true
|
||||
try {
|
||||
imageTaskLoadingInstance.value = ElLoading.service({
|
||||
target: imageTaskRef.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
|
||||
}
|
||||
pageTotal.value = total
|
||||
// 需要 watch 的数据
|
||||
const newWatImages = {}
|
||||
imageList.value.forEach((item) => {
|
||||
if (item.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
newWatImages[item.id] = item
|
||||
}
|
||||
})
|
||||
inProgressImageMap.value = newWatImages
|
||||
} finally {
|
||||
if (imageTaskLoadingInstance.value) {
|
||||
imageTaskLoadingInstance.value.close()
|
||||
imageTaskLoadingInstance.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询生成中的 image 列表 */
|
||||
const refreshWatchImages = async () => {
|
||||
const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
||||
if (imageIds.length == 0) {
|
||||
return
|
||||
}
|
||||
const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
|
||||
const newWatchImages = {}
|
||||
list.forEach((image) => {
|
||||
if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
||||
newWatchImages[image.id] = image
|
||||
} else {
|
||||
const index = imageList.value.findIndex((oldImage) => image.id === oldImage.id)
|
||||
if (index >= 0) {
|
||||
// 更新 imageList
|
||||
imageList.value[index] = image
|
||||
}
|
||||
}
|
||||
})
|
||||
inProgressImageMap.value = newWatchImages
|
||||
}
|
||||
|
||||
/** 图片 - btn click */
|
||||
const handleImageBtnClick = async (type: string, imageDetail: ImageVO) => {
|
||||
// 获取 image detail id
|
||||
showImageDetailId.value = imageDetail.id
|
||||
// 处理不用 btn
|
||||
if (type === 'more') {
|
||||
await handleDrawerOpen()
|
||||
} else 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)
|
||||
await emits('onRegeneration', imageDetail)
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片 - mj btn click */
|
||||
const handleImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
|
||||
// 1、构建 params 参数
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId
|
||||
} as ImageMjActionVO
|
||||
// 2、发送 action
|
||||
await ImageApi.midjourneyAction(data)
|
||||
// 3、刷新列表
|
||||
await getImageList()
|
||||
}
|
||||
|
||||
// page change
|
||||
const handlePageChange = async (page) => {
|
||||
pageNo.value = page
|
||||
await getImageList(false)
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ getImageList })
|
||||
|
||||
// emits
|
||||
const emits = defineEmits(['onRegeneration'])
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
await getImageList()
|
||||
// 自动刷新 image 列表
|
||||
imageListInterval.value = setInterval(async () => {
|
||||
await refreshWatchImages()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (imageListInterval.value) {
|
||||
clearInterval(imageListInterval.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-image-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 140px;
|
||||
box-sizing: border-box; /* 确保内边距不会增加高度 */
|
||||
|
||||
> div {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
> div:last-of-type {
|
||||
//margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-image-pagination {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
height: 50px;
|
||||
line-height: 90px;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dr-task {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
164
src/views/ai/image/index/ImageTaskCard.vue
Normal file
164
src/views/ai/image/index/ImageTaskCard.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<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>
|
||||
<el-button text bg v-else-if="imageDetail?.status === AiImageStatusEnum.SUCCESS">
|
||||
已完成
|
||||
</el-button>
|
||||
<el-button type="danger" text bg v-else-if="imageDetail?.status === AiImageStatusEnum.FAIL">
|
||||
异常
|
||||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Download"
|
||||
@click="handleBtnClick('download', imageDetail)"
|
||||
/>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="RefreshRight"
|
||||
@click="handleBtnClick('regeneration', imageDetail)"
|
||||
/>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Delete"
|
||||
@click="handleBtnClick('delete', imageDetail)"
|
||||
/>
|
||||
<el-button class="btn" text :icon="More" @click="handleBtnClick('more', imageDetail)" />
|
||||
</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 }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO @fan:style 使用 unocss 替代下 -->
|
||||
<div class="image-mj-btns">
|
||||
<el-button
|
||||
size="small"
|
||||
v-for="button in imageDetail?.buttons"
|
||||
:key="button"
|
||||
style="min-width: 40px; margin-left: 0; margin-right: 10px; margin-top: 5px"
|
||||
@click="handleMjBtnClick(button)"
|
||||
>
|
||||
{{ button.label }}{{ button.emoji }}
|
||||
</el-button>
|
||||
</div>
|
||||
</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 { PropType } from 'vue'
|
||||
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 props = defineProps({
|
||||
imageDetail: {
|
||||
type: Object as PropType<ImageVO>,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 按钮 - 点击事件 */
|
||||
const handleBtnClick = async (type, imageDetail: ImageVO) => {
|
||||
emits('onBtnClick', type, imageDetail)
|
||||
}
|
||||
|
||||
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()
|
||||
cardImageLoadingInstance.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-card {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.image-operation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
//border: 1px solid red;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
overflow: hidden;
|
||||
margin-top: 20px;
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-mj-btns {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,40 +120,38 @@ const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
|
||||
const selectModel = ref<any>({}) // 模型
|
||||
// TODO @fan:image 改成项目里自己的哈
|
||||
// TODO @fan:这个 image,要不看看网上有没合适的图片,作为占位符,啊哈哈
|
||||
const selectModel = ref<string>('dall-e-3') // 模型
|
||||
// message
|
||||
const message = useMessage()
|
||||
const models = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'dall-e-3',
|
||||
name: 'DALL·E 3',
|
||||
image: 'https://h5.cxyhub.com/images/model_2.png',
|
||||
image: `/src/assets/ai/dall2.jpg`,
|
||||
},
|
||||
{
|
||||
key: 'dall-e-2',
|
||||
name: 'DALL·E 2',
|
||||
image: 'https://h5.cxyhub.com/images/model_1.png',
|
||||
image: `/src/assets/ai/dall3.jpg`,
|
||||
},
|
||||
]) // 模型
|
||||
selectModel.value = models.value[0]
|
||||
|
||||
const selectImageStyle = ref<any>({}) // style 样式
|
||||
// TODO @fan:image 改成项目里自己的哈
|
||||
const selectImageStyle = ref<string>('vivid') // style 样式
|
||||
|
||||
const imageStyleList = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'vivid',
|
||||
name: '清晰',
|
||||
image: 'https://h5.cxyhub.com/images/model_1.png',
|
||||
image: `/src/assets/ai/qingxi.jpg`,
|
||||
},
|
||||
{
|
||||
key: 'natural',
|
||||
name: '自然',
|
||||
image: 'https://h5.cxyhub.com/images/model_2.png',
|
||||
image: `/src/assets/ai/ziran.jpg`,
|
||||
},
|
||||
]) // style
|
||||
selectImageStyle.value = imageStyleList.value[0]
|
||||
|
||||
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // 选中 size
|
||||
const selectImageSize = ref<string>('1024x1024') // 选中 size
|
||||
const imageSizeList = ref<ImageSizeVO[]>([
|
||||
{
|
||||
key: '1024x1024',
|
||||
@@ -177,17 +175,14 @@ const imageSizeList = ref<ImageSizeVO[]>([
|
||||
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||
}
|
||||
]) // size
|
||||
selectImageSize.value = imageSizeList.value[0]
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
// TODO @fan:如果是简单注释,建议用 /** */,主要是现在项目里是这种风格哈,保持一致好点~
|
||||
// TODO @fan:handler 应该改成 handle 哈
|
||||
/** 热词 - click */
|
||||
const handlerHotWordClick = async (hotWord: string) => {
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
@@ -200,62 +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 {
|
||||
}
|
||||
142
src/views/ai/image/index/index.vue
Normal file
142
src/views/ai/image/index/index.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<!-- image -->
|
||||
<template>
|
||||
<div class="ai-image">
|
||||
<div class="left">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="selectPlatform" :options="platformOptions" />
|
||||
</div>
|
||||
<div class="modal-switch-container">
|
||||
<Dall3
|
||||
v-if="selectPlatform === AiPlatformEnum.OPENAI"
|
||||
ref="dall3Ref"
|
||||
@on-draw-start="handleDrawStart"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
<Midjourney
|
||||
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
|
||||
ref="midjourneyRef"
|
||||
/>
|
||||
<StableDiffusion
|
||||
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
|
||||
ref="stableDiffusionRef"
|
||||
@on-draw-complete="handleDrawComplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ImageTask ref="imageTaskRef" @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 { 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')
|
||||
const platformOptions = [
|
||||
{
|
||||
label: 'DALL3 绘画',
|
||||
value: AiPlatformEnum.OPENAI
|
||||
},
|
||||
{
|
||||
label: 'MJ 绘画',
|
||||
value: AiPlatformEnum.MIDJOURNEY
|
||||
},
|
||||
{
|
||||
label: 'Stable Diffusion',
|
||||
value: AiPlatformEnum.STABLE_DIFFUSION
|
||||
}
|
||||
]
|
||||
|
||||
/** 绘画 - start */
|
||||
const handleDrawStart = async (type) => {
|
||||
}
|
||||
|
||||
/** 绘画 - complete */
|
||||
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">
|
||||
.ai-image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
width: 350px;
|
||||
|
||||
.segmented {
|
||||
}
|
||||
|
||||
.segmented .el-segmented {
|
||||
--el-border-radius-base: 16px;
|
||||
--el-segmented-item-selected-color: #fff;
|
||||
background-color: #ececec;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.modal-switch-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 350px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,22 +82,31 @@
|
||||
<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
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
@@ -117,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',
|
||||
@@ -132,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',
|
||||
@@ -167,11 +176,9 @@ const imageSizeList = ref<ImageSizeVO[]>([
|
||||
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||
},
|
||||
]) // size
|
||||
selectImageSize.value = imageSizeList.value[0]
|
||||
|
||||
|
||||
// version
|
||||
const versionList = ref<any>([
|
||||
const midjourneyVersionList = ref<any>([
|
||||
{
|
||||
value: '6.0',
|
||||
label: 'v6.0',
|
||||
@@ -192,14 +199,19 @@ const versionList = ref<any>([
|
||||
value: '4.0',
|
||||
label: 'v4.0',
|
||||
},
|
||||
]) // version 列表
|
||||
])
|
||||
const nijiVersionList = ref<any>([
|
||||
{
|
||||
value: '5',
|
||||
label: 'v5',
|
||||
},
|
||||
])
|
||||
const selectVersion = ref<any>('6.0') // 选中的 version
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
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 = ''
|
||||
@@ -212,45 +224,69 @@ 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 {
|
||||
versionList.value = midjourneyVersionList.value // 默认选择 midjourney
|
||||
}
|
||||
selectVersion.value = versionList.value[0].value
|
||||
}
|
||||
|
||||
/** version - click */
|
||||
const handlerChangeVersion = async (version) => {
|
||||
const handleChangeVersion = async (version) => {
|
||||
console.log('version', version)
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handlerGenerateImage = async () => {
|
||||
// todo @范 图片生产逻辑
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
// 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">
|
||||
|
||||
437
src/views/ai/image/index/stable-diffusion/index.vue
Normal file
437
src/views/ai/image/index/stable-diffusion/index.vue
Normal file
@@ -0,0 +1,437 @@
|
||||
<!-- 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>
|
||||
251
src/views/ai/image/manager/index.vue
Normal file
251
src/views/ai/image/manager/index.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-select
|
||||
v-model="queryParams.userId"
|
||||
clearable
|
||||
placeholder="请输入用户编号"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="绘画状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择绘画状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_IMAGE_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否发布" prop="publicStatus">
|
||||
<el-select
|
||||
v-model="queryParams.publicStatus"
|
||||
placeholder="请选择是否发布"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" width="180" fixed="left" />
|
||||
<el-table-column label="图片" align="center" prop="picUrl" width="110px" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
class="h-80px w-80px"
|
||||
lazy
|
||||
:src="row.picUrl"
|
||||
:preview-src-list="[row.picUrl]"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
v-if="row.picUrl?.length > 0"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台" align="center" prop="platform" width="120">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型" align="center" prop="model" width="180" />
|
||||
<el-table-column label="绘画状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_IMAGE_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否发布" align="center" prop="publicStatus">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.publicStatus"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@change="handleUpdatePublicStatusChange(scope.row)"
|
||||
:disabled="scope.row.status !== AiImageStatusEnum.SUCCESS"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提示词" align="center" prop="prompt" width="180" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="宽度" align="center" prop="width" />
|
||||
<el-table-column label="高度" align="center" prop="height" />
|
||||
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||
<el-table-column label="任务编号" align="center" prop="taskId" />
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:image:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** AI 绘画 列表 */
|
||||
defineOptions({ name: 'AiImageManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ImageVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
platform: undefined,
|
||||
status: undefined,
|
||||
publicStatus: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ImageApi.getImagePage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ImageApi.deleteImage(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: ImageVO) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有'
|
||||
await message.confirm('确认要"' + text + '"该图片吗?')
|
||||
// 发起修改状态
|
||||
await ImageApi.updateImage({
|
||||
id: row.id,
|
||||
publicStatus: row.publicStatus
|
||||
})
|
||||
await getList()
|
||||
} catch {
|
||||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
21
src/views/ai/music/components/index.vue
Normal file
21
src/views/ai/music/components/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex h-1/1">
|
||||
<!-- 模式 -->
|
||||
<Mode class="flex-none" @generate-music="generateMusic"/>
|
||||
<!-- 音频列表 -->
|
||||
<List ref="listRef" class="flex-auto"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Mode from './mode/index.vue'
|
||||
import List from './list/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const listRef = ref<{generateMusic: (...args) => void} | null>(null)
|
||||
|
||||
function generateMusic (args: {formData: Recordable}) {
|
||||
unref(listRef)?.generateMusic(args.formData)
|
||||
}
|
||||
</script>
|
||||
9
src/views/ai/music/components/list/audioBar/index.vue
Normal file
9
src/views/ai/music/components/list/audioBar/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">播放器</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
</script>
|
||||
94
src/views/ai/music/components/list/index.vue
Normal file
94
src/views/ai/music/components/list/index.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-auto flex overflow-hidden">
|
||||
<el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
|
||||
<!-- 我的创作 -->
|
||||
<el-tab-pane label="我的创作" v-loading="loading" name="mine">
|
||||
<el-row v-if="mySongList.length" :gutter="12">
|
||||
<el-col v-for="song in mySongList" :key="song.id" :span="24">
|
||||
<songCard v-bind="song"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else description="暂无音乐"/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 试听广场 -->
|
||||
<el-tab-pane label="试听广场" v-loading="loading" name="square">
|
||||
<el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
|
||||
<el-col v-for="song in squareSongList" :key="song.id" :span="24">
|
||||
<songCard v-bind="song"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else description="暂无音乐"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- songInfo -->
|
||||
<songInfo v-bind="squareSongList[0]" class="flex-none"/>
|
||||
</div>
|
||||
<audioBar class="flex-none"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import songCard from './songCard/index.vue'
|
||||
import songInfo from './songInfo/index.vue'
|
||||
import audioBar from './audioBar/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const currentType = ref('mine')
|
||||
// loading 状态
|
||||
const loading = ref(false)
|
||||
|
||||
const mySongList = ref<Recordable[]>([])
|
||||
const squareSongList = ref<Recordable[]>([])
|
||||
|
||||
/*
|
||||
*@Description: 调接口生成音乐列表
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-06-27 17:06:44
|
||||
*/
|
||||
function generateMusic (formData: Recordable) {
|
||||
console.log(formData);
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
mySongList.value = Array.from({ length: 20 }, (_, index) => {
|
||||
return {
|
||||
id: index,
|
||||
audioUrl: '',
|
||||
videoUrl: '',
|
||||
title: '我走后',
|
||||
imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
|
||||
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
|
||||
date: '2024年04月30日 14:02:57',
|
||||
lyric: `<div class="_words_17xen_66"><div>大江东去,浪淘尽,千古风流人物。
|
||||
</div><div>故垒西边,人道是,三国周郎赤壁。
|
||||
</div><div>乱石穿空,惊涛拍岸,卷起千堆雪。
|
||||
</div><div>江山如画,一时多少豪杰。
|
||||
</div><div>
|
||||
</div><div>遥想公瑾当年,小乔初嫁了,雄姿英发。
|
||||
</div><div>羽扇纶巾,谈笑间,樯橹灰飞烟灭。
|
||||
</div><div>故国神游,多情应笑我,早生华发。
|
||||
</div><div>人生如梦,一尊还酹江月。</div></div>`
|
||||
}
|
||||
})
|
||||
loading.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
generateMusic
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.el-tabs__content{
|
||||
padding: 0 7px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
src/views/ai/music/components/list/songCard/index.vue
Normal file
29
src/views/ai/music/components/list/songCard/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1">
|
||||
<el-image :src="imageUrl" class="flex-none w-80px"/>
|
||||
<div class="ml-8px">
|
||||
<div>{{ title }}</div>
|
||||
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2">
|
||||
{{ desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
imageUrl: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
33
src/views/ai/music/components/list/songInfo/index.vue
Normal file
33
src/views/ai/music/components/list/songInfo/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<ContentWrap class="w-300px mb-[0!important] line-height-24px">
|
||||
<el-image :src="imageUrl"/>
|
||||
<div class="">{{ title }}</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1">{{ desc }}</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px">{{ date }}</div>
|
||||
<el-button size="small" round class="my-6px">信息复用</el-button>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px" v-html="lyric"></div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
imageUrl: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
},
|
||||
date: {
|
||||
type: String
|
||||
},
|
||||
lyric: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
55
src/views/ai/music/components/mode/desc.vue
Normal file
55
src/views/ai/music/components/mode/desc.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<Title title="音乐/歌词说明" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
:autosize="{ minRows: 6, maxRows: 6}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="1200"
|
||||
show-word-limit
|
||||
placeholder="一首关于糟糕分手的欢快歌曲"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="纯音乐" desc="创建一首没有歌词的歌曲">
|
||||
<template #extra>
|
||||
<el-switch v-model="formData.pure" size="small"/>
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<Title title="版本" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in [{
|
||||
value: '3',
|
||||
label: 'V3'
|
||||
}, {
|
||||
value: '2',
|
||||
label: 'V2'
|
||||
}]"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
|
||||
defineOptions({ name: 'Desc' })
|
||||
|
||||
const formData = reactive({
|
||||
desc: '',
|
||||
pure: false,
|
||||
version: '3'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
|
||||
</script>
|
||||
44
src/views/ai/music/components/mode/index.vue
Normal file
44
src/views/ai/music/components/mode/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<ContentWrap class="w-300px h-full">
|
||||
<el-radio-group v-model="generateMode" class="mb-15px">
|
||||
<el-radio-button label="desc">
|
||||
描述模式
|
||||
</el-radio-button>
|
||||
<el-radio-button label="lyric">
|
||||
歌词模式
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- 描述模式/歌词模式 切换 -->
|
||||
<component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef"/>
|
||||
|
||||
<el-button type="primary" round class="w-full" @click="generateMusic">
|
||||
创作音乐
|
||||
</el-button>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import desc from './desc.vue'
|
||||
import lyric from './lyric.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const emits = defineEmits(['generate-music'])
|
||||
|
||||
const generateMode = ref('lyric')
|
||||
|
||||
interface ModeRef {
|
||||
formData: Recordable
|
||||
}
|
||||
const modeRef = ref<ModeRef | null>(null)
|
||||
|
||||
/*
|
||||
*@Description: 根据信息生成音乐
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-06-27 16:40:16
|
||||
*/
|
||||
function generateMusic () {
|
||||
emits('generate-music', {formData: unref(modeRef)?.formData.value})
|
||||
}
|
||||
</script>
|
||||
83
src/views/ai/music/components/mode/lyric.vue
Normal file
83
src/views/ai/music/components/mode/lyric.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<Title title="歌词" desc="自己编写歌词或使用Ai生成歌词,两节/8行效果最佳">
|
||||
<el-input
|
||||
v-model="formData.lyric"
|
||||
:autosize="{ minRows: 6, maxRows: 6}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="1200"
|
||||
show-word-limit
|
||||
placeholder="请输入您自己的歌词"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐风格">
|
||||
<el-space class="flex-wrap">
|
||||
<el-tag v-for="tag in tags" :key="tag" round class="mb-8px">{{tag}}</el-tag>
|
||||
</el-space>
|
||||
|
||||
<el-button
|
||||
:type="showCustom ? 'primary': 'default'"
|
||||
round
|
||||
size="small"
|
||||
class="mb-6px"
|
||||
@click="showCustom = !showCustom"
|
||||
>自定义风格
|
||||
</el-button>
|
||||
</Title>
|
||||
|
||||
<Title v-show="showCustom" desc="描述您想要的音乐风格,Suno无法识别艺术家的名字,但可以理解流派和氛围" class="-mt-12px">
|
||||
<el-input
|
||||
v-model="formData.style"
|
||||
:autosize="{ minRows: 4, maxRows: 4}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
placeholder="输入音乐风格(英文)"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐/歌曲名称">
|
||||
<el-input v-model="formData.name" placeholder="请输入音乐/歌曲名称"/>
|
||||
</Title>
|
||||
|
||||
<Title title="版本">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in [{
|
||||
value: '3',
|
||||
label: 'V3'
|
||||
}, {
|
||||
value: '2',
|
||||
label: 'V2'
|
||||
}]"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
defineOptions({ name: 'Lyric' })
|
||||
|
||||
const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop']
|
||||
|
||||
const showCustom = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
lyric: '',
|
||||
style: '',
|
||||
name: '',
|
||||
version: ''
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
</script>
|
||||
25
src/views/ai/music/components/title/index.vue
Normal file
25
src/views/ai/music/components/title/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="mb-12px">
|
||||
<div class="flex text-[var(--el-text-color-primary)] justify-between items-center">
|
||||
<span>{{title}}</span>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px my-8px">
|
||||
{{desc}}
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
286
src/views/ai/music/manager/index.vue
Normal file
286
src/views/ai/music/manager/index.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-input
|
||||
v-model="queryParams.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音乐名称" prop="title">
|
||||
<el-input
|
||||
v-model="queryParams.title"
|
||||
placeholder="请输入音乐名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音乐状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择音乐状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生成模式" prop="generateMode">
|
||||
<el-select
|
||||
v-model="queryParams.generateMode"
|
||||
placeholder="请选择生成模式"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_GENERATE_MODE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否发布" prop="publicStatus">
|
||||
<el-select
|
||||
v-model="queryParams.publicStatus"
|
||||
placeholder="请选择是否发布"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" width="180" fixed="left" />
|
||||
<el-table-column label="音乐名称" align="center" prop="title" width="180px" fixed="left" />
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音乐状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_MUSIC_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型" align="center" prop="model" width="180" />
|
||||
<el-table-column label="内容" align="center" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.audioUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.audioUrl"
|
||||
target="_blank"
|
||||
>
|
||||
音乐
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="row.videoUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.videoUrl"
|
||||
target="_blank"
|
||||
class="!pl-5px"
|
||||
>
|
||||
视频
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="row.imageUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.imageUrl"
|
||||
target="_blank"
|
||||
class="!pl-5px"
|
||||
>
|
||||
封面
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长(秒)" align="center" prop="duration" width="100" />
|
||||
<el-table-column label="提示词" align="center" prop="prompt" width="180" />
|
||||
<el-table-column label="歌词" align="center" prop="lyric" width="180" />
|
||||
<el-table-column label="描述" align="center" prop="gptDescriptionPrompt" width="180" />
|
||||
<el-table-column label="生成模式" align="center" prop="generateMode" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_GENERATE_MODE" :value="scope.row.generateMode" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风格标签" align="center" prop="tags" width="180">
|
||||
<template #default="scope">
|
||||
<el-tag v-for="tag in scope.row.tags" :key="tag" round class="ml-2px">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否发布" align="center" prop="publicStatus">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.publicStatus"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@change="handleUpdatePublicStatusChange(scope.row)"
|
||||
:disabled="scope.row.status !== AiMusicStatusEnum.SUCCESS"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务编号" align="center" prop="taskId" width="180" />
|
||||
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:music:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { MusicApi, MusicVO } from '@/api/ai/music'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { AiMusicStatusEnum } from '@/views/ai/utils/constants'
|
||||
|
||||
/** AI 音乐 列表 */
|
||||
defineOptions({ name: 'AiMusicManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<MusicVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
title: undefined,
|
||||
status: undefined,
|
||||
generateMode: undefined,
|
||||
createTime: [],
|
||||
publicStatus: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await MusicApi.getMusicPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await MusicApi.deleteMusic(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: MusicVO) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有'
|
||||
await message.confirm('确认要"' + text + '"该音乐吗?')
|
||||
// 发起修改状态
|
||||
await MusicApi.updateMusic({
|
||||
id: row.id,
|
||||
publicStatus: row.publicStatus
|
||||
})
|
||||
await getList()
|
||||
} catch {
|
||||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
42
src/views/ai/utils/constants.ts
Normal file
42
src/views/ai/utils/constants.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Created by 芋道源码
|
||||
*
|
||||
* AI 枚举类
|
||||
*
|
||||
* 问题:为什么不放在 src/utils/constants.ts 呢?
|
||||
* 回答:主要 AI 是可选模块,考虑到独立、解耦,所以放在了 /views/ai/utils/constants.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI 平台的枚举
|
||||
*/
|
||||
export const AiPlatformEnum = {
|
||||
TONG_YI: 'TongYi', // 阿里
|
||||
YI_YAN: 'YiYan', // 百度
|
||||
DEEP_SEEK: 'DeepSeek', // DeepSeek
|
||||
ZHI_PU: 'ZhiPu', // 智谱 AI
|
||||
XING_HUO: 'XingHuo', // 讯飞
|
||||
OPENAI: 'OpenAI',
|
||||
Ollama: 'Ollama',
|
||||
STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
|
||||
MIDJOURNEY: 'Midjourney', // Midjourney
|
||||
SUNO: 'Suno' // Suno AI
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 图像生成状态的枚举
|
||||
*/
|
||||
export const AiImageStatusEnum = {
|
||||
IN_PROGRESS: 10, // 进行中
|
||||
SUCCESS: 20, // 已完成
|
||||
FAIL: 30 // 已失败
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 音乐生成状态的枚举
|
||||
*/
|
||||
export const AiMusicStatusEnum = {
|
||||
IN_PROGRESS: 10, // 进行中
|
||||
SUCCESS: 20, // 已完成
|
||||
FAIL: 30 // 已失败
|
||||
}
|
||||
13
src/views/ai/utils/utils.ts
Normal file
13
src/views/ai/utils/utils.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user