ImageList.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <template>
  2. <el-card class="dr-task" body-class="task-card" shadow="never">
  3. <template #header>绘画任务</template>
  4. <!-- 图片列表 -->
  5. <div class="task-image-list" ref="imageListRef">
  6. <ImageCard
  7. v-for="image in imageList"
  8. :key="image.id"
  9. :image-detail="image"
  10. @on-btn-click="handleImageButtonClick"
  11. @on-mj-btn-click="handleImageMidjourneyButtonClick"
  12. />
  13. </div>
  14. <div class="task-image-pagination">
  15. <Pagination
  16. :total="pageTotal"
  17. v-model:page="queryParams.pageNo"
  18. v-model:limit="queryParams.pageSize"
  19. @pagination="getImageList"
  20. />
  21. </div>
  22. </el-card>
  23. <!-- 图片详情 -->
  24. <ImageDetail
  25. :show="isShowImageDetail"
  26. :id="showImageDetailId"
  27. @handle-drawer-close="handleDetailClose"
  28. />
  29. </template>
  30. <script setup lang="ts">
  31. import {
  32. ImageApi,
  33. ImageVO,
  34. ImageMidjourneyActionVO,
  35. ImageMidjourneyButtonsVO
  36. } from '@/api/ai/image'
  37. import ImageDetail from './ImageDetail.vue'
  38. import ImageCard from './ImageCard.vue'
  39. import { ElLoading, LoadingOptionsResolved } from 'element-plus'
  40. import { AiImageStatusEnum } from '@/views/ai/utils/constants'
  41. import download from '@/utils/download'
  42. const message = useMessage() // 消息弹窗
  43. // 图片分页相关的参数
  44. const queryParams = reactive({
  45. pageNo: 1,
  46. pageSize: 10
  47. })
  48. const pageTotal = ref<number>(0) // page size
  49. const imageList = ref<ImageVO[]>([]) // image 列表
  50. const imageListLoadingInstance = ref<any>() // image 列表是否正在加载中
  51. const imageListRef = ref<any>() // ref
  52. // 图片轮询相关的参数(正在生成中的)
  53. const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
  54. const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生成进展
  55. // 图片详情相关的参数
  56. const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
  57. const showImageDetailId = ref<number>(0) // 图片详情的图片编号
  58. /** 查看图片的详情 */
  59. const handleDetailOpen = async () => {
  60. isShowImageDetail.value = true
  61. }
  62. /** 关闭图片的详情 */
  63. const handleDetailClose = async () => {
  64. isShowImageDetail.value = false
  65. }
  66. /** 获得 image 图片列表 */
  67. const getImageList = async () => {
  68. try {
  69. // 1. 加载图片列表
  70. imageListLoadingInstance.value = ElLoading.service({
  71. target: imageListRef.value,
  72. text: '加载中...'
  73. } as LoadingOptionsResolved)
  74. const { list, total } = await ImageApi.getImagePageMy(queryParams)
  75. imageList.value = list
  76. pageTotal.value = total
  77. // 2. 计算需要轮询的图片
  78. const newWatImages = {}
  79. imageList.value.forEach((item) => {
  80. if (item.status === AiImageStatusEnum.IN_PROGRESS) {
  81. newWatImages[item.id] = item
  82. }
  83. })
  84. inProgressImageMap.value = newWatImages
  85. } finally {
  86. // 关闭正在“加载中”的 Loading
  87. if (imageListLoadingInstance.value) {
  88. imageListLoadingInstance.value.close()
  89. imageListLoadingInstance.value = null
  90. }
  91. }
  92. }
  93. /** 轮询生成中的 image 列表 */
  94. const refreshWatchImages = async () => {
  95. const imageIds = Object.keys(inProgressImageMap.value).map(Number)
  96. if (imageIds.length == 0) {
  97. return
  98. }
  99. const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
  100. const newWatchImages = {}
  101. list.forEach((image) => {
  102. if (image.status === AiImageStatusEnum.IN_PROGRESS) {
  103. newWatchImages[image.id] = image
  104. } else {
  105. const index = imageList.value.findIndex((oldImage) => image.id === oldImage.id)
  106. if (index >= 0) {
  107. // 更新 imageList
  108. imageList.value[index] = image
  109. }
  110. }
  111. })
  112. inProgressImageMap.value = newWatchImages
  113. }
  114. /** 图片的点击事件 */
  115. const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
  116. // 详情
  117. if (type === 'more') {
  118. showImageDetailId.value = imageDetail.id
  119. await handleDetailOpen()
  120. return
  121. }
  122. // 删除
  123. if (type === 'delete') {
  124. await message.confirm(`是否删除照片?`)
  125. await ImageApi.deleteImageMy(imageDetail.id)
  126. await getImageList()
  127. message.success('删除成功!')
  128. return
  129. }
  130. // 下载
  131. if (type === 'download') {
  132. await download.image(imageDetail.picUrl)
  133. return
  134. }
  135. // 重新生成
  136. if (type === 'regeneration') {
  137. await emits('onRegeneration', imageDetail)
  138. return
  139. }
  140. }
  141. /** 处理 Midjourney 按钮点击事件 */
  142. const handleImageMidjourneyButtonClick = async (
  143. button: ImageMidjourneyButtonsVO,
  144. imageDetail: ImageVO
  145. ) => {
  146. // 1. 构建 params 参数
  147. const data = {
  148. id: imageDetail.id,
  149. customId: button.customId
  150. } as ImageMidjourneyActionVO
  151. // 2. 发送 action
  152. await ImageApi.midjourneyAction(data)
  153. // 3. 刷新列表
  154. await getImageList()
  155. }
  156. defineExpose({ getImageList }) // 暴露组件方法
  157. const emits = defineEmits(['onRegeneration'])
  158. /** 组件挂在的时候 */
  159. onMounted(async () => {
  160. // 获取 image 列表
  161. await getImageList()
  162. // 自动刷新 image 列表
  163. inProgressTimer.value = setInterval(async () => {
  164. await refreshWatchImages()
  165. }, 1000 * 3)
  166. })
  167. /** 组件取消挂在的时候 */
  168. onUnmounted(async () => {
  169. if (inProgressTimer.value) {
  170. clearInterval(inProgressTimer.value)
  171. }
  172. })
  173. </script>
  174. <!-- TODO fan:这 2 个 scss 可以合并么? -->
  175. <style lang="scss">
  176. .task-card {
  177. margin: 0;
  178. padding: 0;
  179. height: 100%;
  180. position: relative;
  181. }
  182. .task-image-list {
  183. position: relative;
  184. display: flex;
  185. flex-direction: row;
  186. flex-wrap: wrap;
  187. align-content: flex-start;
  188. height: 100%;
  189. overflow: auto;
  190. padding: 20px 20px 140px;
  191. box-sizing: border-box; /* 确保内边距不会增加高度 */
  192. > div {
  193. margin-right: 20px;
  194. margin-bottom: 20px;
  195. }
  196. > div:last-of-type {
  197. //margin-bottom: 100px;
  198. }
  199. }
  200. .task-image-pagination {
  201. position: absolute;
  202. bottom: 60px;
  203. height: 50px;
  204. line-height: 90px;
  205. width: 100%;
  206. z-index: 999;
  207. background-color: #ffffff;
  208. display: flex;
  209. flex-direction: row;
  210. justify-content: center;
  211. align-items: center;
  212. }
  213. </style>
  214. <style scoped lang="scss">
  215. .dr-task {
  216. width: 100%;
  217. height: 100%;
  218. }
  219. </style>