Prechádzať zdrojové kódy

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

puhui999 1 rok pred
rodič
commit
d3c596dcaf

+ 2 - 2
src/api/ai/chat/conversation/index.ts

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

+ 1 - 0
src/api/ai/image/index.ts

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

BIN
src/assets/ai/dall2.jpg


BIN
src/assets/ai/dall3.jpg


BIN
src/assets/ai/qingxi.jpg


BIN
src/assets/ai/ziran.jpg


+ 20 - 1
src/utils/download.ts

@@ -29,10 +29,29 @@ const download = {
   html: (data: Blob, fileName: string) => {
     download0(data, fileName, 'text/html')
   },
-  // 下载 MarkdownView 方法
+  // 下载 Markdown 方法
   markdown: (data: Blob, fileName: string) => {
     download0(data, fileName, 'text/markdown')
   }
 }
 
 export default download
+
+/** 图片下载(通过浏览器图片下载)  */
+export const downloadImage = async (imageUrl) => {
+  const image = new Image()
+  image.setAttribute('crossOrigin', 'anonymous')
+  image.src = imageUrl
+  image.onload = () => {
+    const canvas = document.createElement('canvas')
+    canvas.width = image.width
+    canvas.height = image.height
+    const ctx = canvas.getContext('2d') as CanvasDrawImage
+    ctx.drawImage(image, 0, 0, image.width, image.height)
+    const url = canvas.toDataURL('image/png')
+    const a = document.createElement('a')
+    a.href = url
+    a.download = 'image.png'
+    a.click()
+  }
+}

+ 52 - 58
src/views/ai/chat/Conversation.vue

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

+ 6 - 7
src/views/ai/image/ImageDetailDrawer.vue

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

+ 18 - 30
src/views/ai/image/ImageTask.vue

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

+ 19 - 14
src/views/ai/image/ImageTaskCard.vue

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

+ 46 - 51
src/views/ai/image/dall3/index.vue

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

+ 39 - 12
src/views/ai/image/index.vue

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

+ 54 - 31
src/views/ai/image/midjourney/index.vue

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

+ 29 - 12
src/views/ai/image/stable-diffusion/index.vue

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

+ 13 - 0
src/views/ai/utils/common-utils.ts

@@ -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)
+}

+ 0 - 1
types/env.d.ts

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