ImageList.vue 6.3 KB

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