Procházet zdrojové kódy

图生图批量操作&裁图功能

lushixing před 1 měsícem
rodič
revize
74f225886a

+ 1 - 1
src/router/modules/faceSwapVideo.js

@@ -6,7 +6,7 @@ const faceSwapVideoRouter = {
   path: "/faceSwapVideo",
   component: Layout,
   redirect: "/face-swap-video/faceSwapVideoIndex",
-  meta: { title: "视频换脸管理", icon: "sidebar-icon4" },
+  meta: { title: "视频换脸管理", icon: "sidebar-icon5" },
   children: [
     {
       path: "faceSwapVideoIndex",

+ 5 - 1
src/utils/request.js

@@ -55,7 +55,11 @@ service.interceptors.response.use(
           location.reload()
         })
       } else {
-        return Promise.reject('error');
+        if (res.code === 500) {
+          return response.data
+        } else {
+          return Promise.reject('error');
+        }
       }
     } else {
       return response.data

+ 449 - 165
src/views/crop-list/components/ImagesGroupModal.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dialog title="结果预定与审核" width="80%" custom-class="reservation-approval-dialog" :visible.sync="dialogVisible">
+  <el-dialog title="结果预定与审核" width="80%" custom-class="reservation-approval-dialog" :visible.sync="dialogVisible" :close-on-click-modal="false">
     <div class="reservation-approval-block">
       <div class="reservation-approval-header">
         <div class="crop-type-block" v-if="pages == 'crop' && statusPages == 4">
@@ -17,7 +17,11 @@
           <span class="label" style="white-space: nowrap;">颜色代码:</span>
           <el-input v-model="form.colorCode" size="small"></el-input>
         </div>
-        <div class="reservation-approval__status">
+        <div class="crop-colorCode-block" style="display: flex;align-items: center;" v-if="cropPreview && statusPages == 4">
+          <span class="label" style="white-space: nowrap;">SKU:</span>
+          <el-input v-model="form.sku" size="small"></el-input>
+        </div>
+        <div class="reservation-approval__status" v-if="!cropPreview">
           <ul v-if="pages == 'review'">
             <li :class="{'active': statusPages == 2}">待审核图</li>
             <li :class="{'active': statusPages == 3}">待复核</li>
@@ -29,10 +33,10 @@
             <li :class="{'active': statusPages >= 6}">已完成</li>
           </ul>
         </div>
-        <div class="reservation-approval__save" v-if="pages == 'review' || (pages == 'crop' && statusPages != 4)">
+        <div class="reservation-approval__save" v-if="!cropPreview && (pages == 'review' || (pages == 'crop' && statusPages >= 4))">
           <el-button
             size="small"
-            :disabled="(pages == 'review' && statusPages == 4) || (pages == 'crop' && statusPages >= 6)"
+            :disabled="(pages == 'review' && statusPages == 4) || (pages == 'crop' && statusPages >= 6) || !imagesList.length"
             :loading="saveLoading"
             @click="saveHandle"
           >
@@ -43,29 +47,81 @@
           <el-button
             type="primary"
             size="small"
-            :disabled="(pages == 'review' && statusPages == 4) || (pages == 'crop' && statusPages >= 6)"
+            :disabled="(pages == 'review' && statusPages == 4) || (pages == 'crop' && statusPages >= 6) || !imagesList.length"
             :loading="reviewLoading"
             @click="handleConfirm"
           >
             {{ pages == 'crop' && statusPages == 4 ? '确认裁图' : '审核通过' }}
           </el-button>
         </div>
+        <div class="reservation-approval__submit" v-if="statusPages == 8">
+          <el-button
+            type="primary"
+            size="small"
+            :loading="submitLoading"
+            @click="uploadGalleryList"
+          >
+            上传图库
+          </el-button>
+        </div>
       </div>
       <div class="reservation-approval-body" :class="{'full': !form.regenerateImage}">
         <el-row>
           <el-col :span="24">
             <div class="images-list-wapper" v-if="statusPages >= 5">
-              <div class="images-list__one" v-for="(item, count) in cropImages" :key="count">
-                <h2>{{ item.title }}</h2>
+              <div class="images-list__one">
+                <h2>主图1:1</h2>
+                <draggable
+                  tag="div"
+                  class="images-list-block"
+                  v-model="imagesList"
+                  @start="onDragStart"
+                  @end="onDragEnd"
+                  :options="{ animation: 150 }">
+                  <div class="images-items" v-for="(acc, index) in imagesList" v-if="acc.position === 'main' && acc.width === acc.height" :key="index">
+                    <images-item :images-list="cropPreview ? imagesList : originalImagesList" :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                  </div>
+                </draggable>
+              </div>
+              <div class="images-list__one">
+                <h2>主图3:4</h2>
+                <draggable
+                  tag="div"
+                  class="images-list-block"
+                  v-model="imagesList"
+                  @start="onDragStart"
+                  @end="onDragEnd"
+                  :options="{ animation: 150 }">
+                  <div class="images-items" v-for="(acc, index) in imagesList" v-if="acc.position === 'main' && acc.width !== acc.height" :key="index">
+                    <images-item :images-list="cropPreview ? imagesList : originalImagesList" :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                  </div>
+                </draggable>
+              </div>
+              <div class="images-list__one">
+                <h2>竖图2:3</h2>
                 <draggable
                   tag="div"
                   class="images-list-block"
-                  v-model="item.images"
+                  v-model="imagesList"
                   @start="onDragStart"
                   @end="onDragEnd"
                   :options="{ animation: 150 }">
-                  <div class="images-items" v-for="(acc, index) in item.images" :key="acc.id">
-                    <images-item :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                  <div class="images-items" v-for="(acc, index) in imagesList" v-if="acc.position === 'list'" :key="index">
+                    <images-item :images-list="cropPreview ? imagesList : originalImagesList" :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                  </div>
+                </draggable>
+              </div>
+              <div class="images-list__one">
+                <h2>颜色图1:1</h2>
+                <draggable
+                  tag="div"
+                  class="images-list-block"
+                  v-model="imagesList"
+                  @start="onDragStart"
+                  @end="onDragEnd"
+                  :options="{ animation: 150 }">
+                  <div class="images-items" v-for="(acc, index) in imagesList" v-if="acc.position === 'color'" :key="index">
+                    <images-item :images-list="cropPreview ? imagesList : originalImagesList" :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
                   </div>
                 </draggable>
               </div>
@@ -77,9 +133,34 @@
                 v-model="imagesList"
                 @start="onDragStart"
                 @end="onDragEnd"
+                :move="checkMove"
                 :options="{ animation: 150 }">
-                <div class="images-items" v-for="(acc, index) in imagesList" :key="acc.id">
-                  <images-item :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                <div class="images-items" v-for="(acc, index) in imagesList" :key="index">
+                  <images-item :images-list="cropPreview ? imagesList : originalImagesList" :crop-preview="cropPreview" :pages="pages" :item="acc" :index="index" :status-pages="statusPages" @delImage="delImage" @needRegenerate="needRegenerate"></images-item>
+                </div>
+                <div class="images-items fixed-item" v-if="cropPreview">
+                  <div class="images-items__img">
+                    <el-upload
+                      ref="uploadRefCrop"
+                      class="upload-demo"
+                      drag
+                      :action="action" 
+                      :show-file-list="false" 
+                      :headers="{
+                        'Authorization': 'Bearer ' + token
+                      }"
+                      multiple 
+                      :on-success="handleVideoSuccess" 
+                      :before-upload="beforeUploadVideo">
+                      <div v-loading="uploading">
+                        <div class="el-upload__info">
+                          <i class="el-icon-upload"></i>
+                          <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+                          <div class="el-upload__tip" slot="tip">图片要求:支持JPG、PNG和WEBP,最大为20M</div>
+                        </div>
+                      </div>
+                    </el-upload>
+                  </div>
                 </div>
               </draggable>
             </div>
@@ -105,8 +186,7 @@
                 :show-file-list="false" 
                 :headers="{
                   'Authorization': 'Bearer ' + token
-                }"
-                multiple 
+                }" 
                 :on-success="handleVideoSuccess" 
                 :before-upload="beforeUploadVideo">
                 <div v-loading="uploading">
@@ -135,9 +215,10 @@
                 </div>
               </el-upload>
             </el-form-item>
+            <span class="tips">重生成之后,需要保存!</span>
           </el-form>
           <span class="regenerate-footer">
-            <el-button size="small" @click="resetForm">取消</el-button>
+            <el-button size="small" @click="colseRegenerateImage">取消</el-button>
             <el-button type="primary" size="small" :loading="regenerateLoading" @click="handleRegenerate">重生成</el-button>
           </span>
         </div>
@@ -171,6 +252,7 @@ export default {
     return {
       action: api.fileUrl,
       uploading: false,
+      cropPreview: false,
       pages: '',
       statusPages: '',
       paramsId: '',
@@ -197,43 +279,30 @@ export default {
       ],
       imagesList: [],
       originalOrders: [],
+      originalImagesList: [],
       dialogVisible: false,
       saveLoading: false,
+      submitLoading: false,
       reviewLoading: false,
       regenerateLoading: false,
-      comfirmLoading: false
+      isCropDone: false
     };
   },
   computed: {
     token() {
       return getToken();
-    },
-    cropImages() {
-      if (this.statusPages >= 5) {
-        return [
-          {
-            title: '主图1:1',
-            images: this.imagesList.filter(acc => acc.position == 'main' && acc.width == acc.height)
-          },
-          {
-            title: '主图3:4',
-            images: this.imagesList.filter(acc => acc.position == 'main' && acc.width != acc.height)
-          },
-          {
-            title: '竖图2:3',
-            images: this.imagesList.filter(acc => acc.position == 'list')
-          }
-        ]
-      } else {
-        return []
-      }
     }
   },
   watch: {
     dialogVisible(val) {
       if (!val) {
-        this.comfirmLoading = false;
         this.regenerateLoading = false;
+        if (this.reviewLoading) {
+          // 说明裁图还没有执行完毕
+          this.isCropDone = true;
+        } else {
+          this.isCropDone = false;
+        }
         this.resetForm();
       }
     }
@@ -242,8 +311,26 @@ export default {
     show(row, type) {
       this.dialogVisible = true;
       this.$nextTick(() => {
+        // if (localStorage.getItem('cropInfo')) {
+        //   try {
+        //     const cropInfo = JSON.parse(localStorage.getItem('cropInfo'));
+        //     this.form.sku = cropInfo.sku || null;
+        //     this.form.cropType = cropInfo.cropType || null;
+        //     this.form.colorCode = cropInfo.colorCode || '';
+        //   } catch (e) {}
+        // }
+        this.cropPreview = false;
+        this.saveLoading = false;
         this.pages = type;
         this.paramsId = row.id;
+        if (row.type == 1) {
+          this.originalImagesList = [...row.referenceImagesList, ...row.originalImagesList];
+        } else {
+          this.originalImagesList = row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).map(acc => {
+            return acc.imageUrl
+          });
+        }
+        // this.originalImagesList = [...row.referenceImagesList, ...row.originalImagesList];
         this.taskId = row.aiGeneratedImageVOList && row.aiGeneratedImageVOList[0].taskId;
         this.statusPages = row.status * 1;
         this.form.sku = row.sku;
@@ -261,6 +348,35 @@ export default {
         console.log(this.imagesList, 444)
       })
     },
+    init() {
+      this.dialogVisible = true;
+      this.$nextTick(() => {
+        this.saveLoading = false;
+        this.reviewLoading = false;
+        this.taskId = '';
+        this.cropPreview = true;
+        this.pages = 'crop';
+        this.statusPages = 4;
+        this.originalImagesList = []
+      })
+    },
+    uploadGalleryList() {
+      this.submitLoading = true;
+      const params = {
+        taskId: this.taskId
+      }
+      request({
+        url: '/imageTask/uploadGallery',
+        method: 'post',
+        data: params
+      }).then(res => {
+        if (res.code == 200) {
+          this.$message.success(res.msg || '操作成功!');
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    },
     saveHandle() {
       this.saveLoading = true;
       request({
@@ -282,87 +398,177 @@ export default {
         this.saveLoading = false;
       })
     },
+    async handleCrop() {
+      // 参数校验
+      if (!this.validateCropParams()) return;
+
+      try {
+        this.reviewLoading = true;
+
+        // 如果是裁图预览模式,先直传图片
+        if (this.cropPreview && !this.taskId) {
+          const res = await this.directSubmitImages();
+          if (res.code === 200) {
+            this.isCropDone = false;
+            this.dialogVisible = false
+            this.$emit('update-success');
+            this.$message.success('裁图已提交');
+          }
+        } else {
+          // 执行裁图
+          const res = await this.cropImagesHandle();
+          if (res.code === 200 && this.taskId && !this.isCropDone) {
+            this.isCropDone = false;
+            this.afterCropSuccess(res);
+          }
+          if (res.code === 500) {
+            this.isCropDone = false;
+            this.$emit('update-success');
+          }
+        }
+      } catch (err) {
+        console.error(err);
+      } finally {
+        this.reviewLoading = false;
+      }
+    },
+
+    // ======================
+    // 参数校验
+    // ======================
+    validateCropParams() {
+      if (this.cropPreview && !this.form.sku) {
+        this.$message.error('请填写SKU!');
+        return false;
+      }
+
+      if (!this.form.cropType || !this.form.colorCode) {
+        this.$message.error('请选择裁切类型或填写颜色代码!');
+        return false;
+      }
+
+      return true;
+    },
+
+    // ======================
+    // 直传图片(directSubmit)
+    // ======================
+    directSubmitImages() {
+      return request({
+        url: '/imageTask/directSubmit',
+        method: 'post',
+        data: {
+          sessionId: `${Date.now() + 10}_${genCidHex16()}`,
+          applicationId: 6,
+          sizes: this.sizeList.map(acc => ({
+            position: acc.positionCode,
+            height: acc.height,
+            width: acc.width,
+            sort: acc.sort
+          })),
+          tasks: [
+            {
+              sku: this.form.sku,
+              type: this.form.cropType,
+              colorCode: this.form.colorCode,
+              images: this.imagesList.map((acc, index) => ({
+                imageUrl: acc.imageUrl,
+                imageOrder: index
+              }))
+            }
+          ]
+        }
+      });
+    },
+
+    // ======================
+    // 裁图接口(crop)
+    // ======================
+    cropImagesHandle() {
+      return request({
+        url: '/imageTask/crop',
+        method: 'post',
+        data: {
+          sessionId: `${Date.now() + 10}_${genCidHex16()}`,
+          applicationId: 6,
+          sku: this.form.sku,
+          type: this.form.cropType,
+          taskId: this.taskId,
+          colorCode: this.form.colorCode,
+          sizes: this.sizeList.map(acc => ({
+            position: acc.positionCode,
+            height: acc.height,
+            width: acc.width,
+            sort: acc.sort
+          }))
+        }
+      });
+    },
+
+    // ======================
+    // 裁图成功后的统一处理
+    // ======================
+    afterCropSuccess(res) {
+      this.statusPages += 1;
+
+      this.$emit('update-success');
+      this.$emit('update-status', this.paramsId, this.statusPages);
+      
+      this.imagesList = res.data.map(acc => ({
+        id: acc.id,
+        imageUrl: acc.imageUrl,
+        imageOrder: acc.imageOrder,
+        position: acc.position,
+        width: acc.width,
+        height: acc.height
+      }));
+
+      this.$message.success(res.msg || '操作成功!');
+    },
     handleConfirm() {
       if (this.pages == 'crop' && this.statusPages == 4) {
-        // 裁图
-        if (!this.form.cropType || !this.form.colorCode) {
-          this.$message.error('请选择裁切类型或填写颜色代码!');
-          return;
-        }
+        this.handleCrop();
+      } else {
         this.reviewLoading = true;
         request({
-          url: '/imageTask/crop',
+          url: `/imageTask/audit/${this.paramsId}`,
           method: 'post',
           data: {
-            sessionId: `${Date.now() + 10}_${genCidHex16()}`,
-            applicationId: 6,
-            sku: this.form.sku,
-            type: this.form.cropType,
-            taskId: this.taskId,
-            colorCode: this.form.colorCode,
-            sizes: this.sizeList.map(acc => {
+            id: this.paramsId,
+            aiGeneratedImageUpdateParams: this.imagesList.map(acc => {
               return {
-                position: acc.positionCode,
-                height: acc.height,
-                width: acc.width,
-                sort: acc.sort
+                id: acc.id,
+                imageUrl: acc.imageUrl,
+                imageOrder: acc.imageOrder,
               }
             })
           }
         }).then(res => {
           if (res.code == 200) {
             this.statusPages += 1; 
-            this.$emit('update-success');
+            if (this.statusPages >= 6) {
+              this.form.regenerateImage = null;
+            }
             this.$emit('update-status', this.paramsId, this.statusPages);
-            // 更新imagesList
-            this.imagesList = res.data.map(acc => {
-              return {
-                id: acc.id,
-                imageUrl: acc.imageUrl,
-                imageOrder: acc.imageOrder,
-                width: acc.width,
-                height: acc.height
-              }
-            })
             this.$message.success(res.msg || '操作成功!');
           }
         }).finally(() => {
           this.reviewLoading = false;
         })
-        return;
       }
-      this.reviewLoading = true;
-      request({
-        url: `/imageTask/audit/${this.paramsId}`,
-        method: 'post',
-        data: {
-          id: this.paramsId,
-          aiGeneratedImageUpdateParams: this.imagesList.map(acc => {
-            return {
-              id: acc.id,
-              imageUrl: acc.imageUrl,
-              imageOrder: acc.imageOrder,
-            }
-          })
-        }
-      }).then(res => {
-        if (res.code == 200) {
-          this.statusPages += 1; 
-          if (this.statusPages >= 6) {
-            this.form.regenerateImage = null;
-          }
-          this.$emit('update-status', this.paramsId, this.statusPages);
-          this.$message.success(res.msg || '操作成功!');
-        }
-      }).finally(() => {
-        this.reviewLoading = false;
-      })
+      
     },
     // 拖动开始:记录原始 imageOrder 顺序
     onDragStart() {
       this.originalOrders = this.imagesList.map(item => item.imageOrder)
     },
-
+    checkMove(evt) {
+      // 禁止 fixed-item 被拖动
+      if (evt.dragged.classList.contains('fixed-item')) {
+        return false
+      }
+      return true
+    },
     // 拖动结束:按当前位置重新赋值 imageOrder
     onDragEnd() {
       this.imagesList = this.imagesList.map((item, index) => {
@@ -379,30 +585,50 @@ export default {
         cancelButtonText: "取消",
         type: "warning"
       }).then(() => {
-        request({
-          url: '/image/delete',
-          method: 'post',
-          data: [row.id]
-        }).then(res => {
-          if (res.code == 200) {
-            this.imagesList.splice(itemIndex, 1); 
-            this.$emit('update-images', this.paramsId, row.id);
-            if (row.id == this.form.imageId) {
-              this.form.regenerateImage = null;
+        if (this.cropPreview) {
+          this.imagesList.splice(itemIndex, 1);
+          this.$notify({
+            title: "成功",
+            message: "删除成功",
+            type: "success",
+            duration: 3000
+          });
+        } else {
+          request({
+            url: '/image/delete',
+            method: 'post',
+            data: [row.id]
+          }).then(res => {
+            if (res.code == 200) {
+              this.imagesList.forEach((acc, itemIndex) => {
+                if (acc.id == row.id) {
+                  this.imagesList.splice(itemIndex, 1);
+                }
+              })
+              this.$emit('update-images', this.paramsId, row.id);
+              if (row.id == this.form.imageId) {
+                this.form.regenerateImage = null;
 
+              }
+              this.$notify({
+                title: "成功",
+                message: "删除成功",
+                type: "success",
+                duration: 3000
+              });
             }
-            this.$notify({
-              title: "成功",
-              message: "删除成功",
-              type: "success",
-              duration: 3000
-            });
-          }
-        }).finally(() => {
-          
-        })
+          }).finally(() => {
+            
+          })
+        }
+        
       });
     },
+    colseRegenerateImage() {
+      this.form.regenerateImage = null;
+      this.form.prompt = '';
+      this.form.imageUrl = '';
+    },
     async handleRegenerate() {
       if (this.pages == 'review' && !this.form.prompt) {
         this.$message.error('请填写内容!');
@@ -412,69 +638,78 @@ export default {
         this.$message.error('请上传裁图图片!');
         return;
       }
-      this.regenerateLoading = true;
-      try {
-        const payload = {
-          sessionId: `${Date.now() + 10}_${genCidHex16()}`,
-          applicationId: 2,
-          images: this.pages == 'review' ? [this.form.regenerateImage] : [this.form.imageUrl]
-        }
-        if (this.pages == 'review') {
-          payload.prompt = this.form.prompt;
-        }
-        const result = await fetchStreamText(`/app/ai/send/imageMessage`, payload, {
-          onChunk: (chunk) => {
-            console.log('实时分片:', chunk)
-            // this.replyText += chunk // Vue 可实时更新聊天内容
-          }
-        })
+      if (this.pages == 'review') {
+        this.regenerateLoading = true;
         try {
-          this.regenerateLoading = false;
-          if (result && typeof result == 'string') {
-            console.log('完整文本:', result)
-            const data = JSON.parse(result)
-            if (result.code == 200) {
-              this.imagesList.forEach(acc => {
-                if (acc.id == this.form.imageId) {
-                  acc.imageUrl = data.image
-                }
-              })
-            } else {
-              if (result.code == 401) {
-                this.$router.push(`/login`);
-              } else {
-                this.$message.error(result.msg || result.errorContent || '请求出错,请联系管理员!');
-              }
+          const payload = {
+            sessionId: `${Date.now() + 10}_${genCidHex16()}`,
+            applicationId: 2,
+            images: this.pages == 'review' ? [this.form.regenerateImage] : [this.form.imageUrl]
+          }
+          if (this.pages == 'review') {
+            payload.prompt = this.form.prompt;
+          }
+          const result = await fetchStreamText(`/app/ai/send/imageMessage`, payload, {
+            onChunk: (chunk) => {
+              console.log('实时分片:', chunk)
+              // this.replyText += chunk // Vue 可实时更新聊天内容
             }
-          } else {
-            console.log('JSON 对象12', result)
-            if (result.code == 200) {
-              this.imagesList.forEach(acc => {
-                if (acc.id == this.form.imageId) {
-                  acc.imageUrl = result.image
+          })
+          try {
+            this.regenerateLoading = false;
+            if (result && typeof result == 'string') {
+              console.log('完整文本:', result)
+              const data = JSON.parse(result)
+              if (result.code == 200) {
+                this.imagesList.forEach(acc => {
+                  if (acc.id == this.form.imageId) {
+                    acc.imageUrl = data.image
+                  }
+                })
+              } else {
+                if (result.code == 401) {
+                  this.$router.push(`/login`);
+                } else {
+                  this.$message.error(result.msg || result.errorContent || '请求出错,请联系管理员!');
                 }
-              })
+              }
             } else {
-              if (result.code == 401) {
-                this.$router.push(`/login`);
+              console.log('JSON 对象12', result)
+              if (result.code == 200) {
+                this.imagesList.forEach(acc => {
+                  if (acc.id == this.form.imageId) {
+                    acc.imageUrl = result.image
+                  }
+                })
               } else {
-                this.$message.error(result.msg || result.errorContent || '请求出错,请联系管理员!');
+                if (result.code == 401) {
+                  this.$router.push(`/login`);
+                } else {
+                  this.$message.error(result.msg || result.errorContent || '请求出错,请联系管理员!');
+                }
+                
               }
-              
             }
+          } catch (e) {
+            console.error('报错:', e)
           }
         } catch (e) {
-          console.error('报错:', e)
+          console.error('请求错误:', e)
+        } finally {
+          this.regenerateLoading = false;
         }
-      } catch (e) {
-        console.error('请求错误:', e)
-      } finally {
-        this.regenerateLoading = false;
+      } else {
+        this.imagesList.forEach(acc => {
+          if (acc.id == this.form.imageId) {
+            acc.imageUrl = this.form.imageUrl;
+          }
+        })
       }
     },
-    needRegenerate(index) {
-      this.form.imageId = this.imagesList[index].id;
-      this.form.regenerateImage = this.imagesList[index].imageUrl;
+    needRegenerate(item) {
+      this.form.imageUrl = '';
+      this.form.imageId = this.imagesList.filter(acc => acc.id == item.id)[0].id;
+      this.form.regenerateImage = this.imagesList.filter(acc => acc.id == item.id)[0].imageUrl;
     },
     replaceFile() {
       if (this.$refs.uploadRef) {
@@ -505,7 +740,14 @@ export default {
     handleVideoSuccess(res) {
       this.uploading = false
       if (res.code == 200) {
-        this.form.imageUrl = res.data.url
+        if (this.cropPreview) {
+          this.imagesList.push({
+            imageUrl: res.data.url
+          })
+        } else {
+          this.form.imageUrl = res.data.url;
+        }
+        
       } else {
         this.$message.error('上传失败,请重新上传!');
       }
@@ -527,6 +769,15 @@ export default {
       });
     },
     resetForm() {
+      this.cropPreview = false;
+      this.pages = '';
+      this.statusPages = '';
+      this.paramsId = '';
+      this.taskId = '';
+      this.imagesList = [];
+      this.originalOrders = [];
+      const data = { sku: this.form.sku, cropType: this.form.cropType, colorCode: this.form.colorCode };
+      localStorage.setItem('cropInfo', JSON.stringify(data));
       this.form = {
         imageId: '',
         regenerateImage: null,
@@ -585,6 +836,7 @@ export default {
         border-radius: 200px;
         font-size: 12px;
         line-height: 32px;
+        white-space: nowrap;
 
         &.active {
           background: #ae8877;
@@ -604,6 +856,11 @@ export default {
         width: 75%;
       }
 
+      .tips {
+        font-size: 12px;
+        color: red;
+      }
+
       &.full .el-row {
         width: 100%;
       }
@@ -661,6 +918,25 @@ export default {
             display: block;
             z-index: 10;
           }
+
+          .upload-demo {
+            width: 100%;
+            height: 100%;
+            display: flex;
+            align-items: center;
+          }
+
+          .el-upload-dragger {
+            width: 100%;
+            height: 100%;
+            border: none;
+            padding: 0 20px;
+
+            .el-icon-upload {
+              margin: 0;
+              margin-bottom: 30px;
+            }
+          }
         }
         .btns-group {
           position: absolute;
@@ -801,6 +1077,14 @@ export default {
       width: 25%;
     }
   }
+  @media screen and (max-width: 1540px) {
+    .reservation-approval-block .reservation-approval-header {
+      gap: 10px;
+    }
+    .reservation-approval-dialog {
+      width: 98% !important;
+    }
+  }
 </style>
 
 

+ 127 - 11
src/views/crop-list/components/ImagesItem.vue

@@ -1,22 +1,40 @@
 <template>
   <div>
     <div class="images-items__img">
-      <span class="size">序号:{{ item.imageOrder }}</span>
+      <span class="size">序号:{{ item.imageOrder || index }}</span>
       <span class="size" style="top: 30px;" v-if="statusPages > 4">{{ item.width }} x {{ item.height }}</span>
-      <el-image 
-        style="max-height: 100%;"
-        :src="item.imageUrl" 
-        :preview-src-list="[item.imageUrl]">
-      </el-image>
-      <div class="btns-group" v-if="(pages == 'review' && [2,3].includes(statusPages*1)) || (pages == 'crop' && [5].includes(statusPages*1))">
+      <img class="items-img" :src="item.imageUrl" @click="imagePreview" alt="">
+      <div class="btns-group" v-if="(pages == 'review' && [2,3].includes(statusPages*1)) || (pages == 'crop' && [4,5].includes(statusPages*1))">
         <el-button size="small" @click="delImage(item, index)">删除</el-button>
-        <el-button type="primary" size="small" @click="needRegenerate(index)">重生成</el-button>
+        <el-button size="small" @click="handleDownload(item.imageUrl)">下载</el-button>
+        <el-button type="primary" size="small" v-if="!cropPreview" @click="needRegenerate(item)">重生成</el-button>
       </div>
     </div>
+    
+    <el-dialog
+      title=""
+      custom-class="preview-images"
+      :visible.sync="dialogVisible"
+      append-to-body
+      width="100%">
+      <div class="images-dialog">
+        <div class="images-dialog__left">
+          <el-carousel trigger="click" :autoplay="false" arrow="always">
+            <el-carousel-item v-for="(acc, i) in imagesList" :key="i">
+              <img :src="acc" alt="">
+            </el-carousel-item>
+          </el-carousel>
+        </div>
+        <div class="images-dialog__right">
+          <p><img :src="item.imageUrl" alt=""></img></p>
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import downloadUtil from "@/utils/downloadUtil";
 export default {
   name: "ImagesItem",
   props: {
@@ -36,20 +54,118 @@ export default {
     pages: {
       type: String,
       default: 'review'
+    },
+    cropPreview: {
+      type: Boolean,
+      default: false
+    },
+    imagesList: {
+      type: Array,
+      default: () => {
+        return []
+      }
     }
   },
   data() {
-    return {};
+    return {
+      dialogVisible: false
+    };
   },
   methods: {
     delImage(item, index) {
       this.$emit('delImage', item, index);
     },
-    needRegenerate(index) {
-      this.$emit('needRegenerate', index);
+    needRegenerate(item) {
+      this.$emit('needRegenerate', item);
+    },
+    async handleDownload(image) {
+      const match = image.match(/\/([^\/?#]+)$/);
+      if (match) {
+        const fileName = match[1]
+        try {
+          const res = await downloadUtil.fileDownload(image, fileName);
+          if (res) {
+            this.$message.success("下载成功");
+          }
+        } catch (error) {
+          this.$message.error(error.message);
+        } finally {
+          
+        }
+      }
+    },
+    imagePreview() {
+      this.dialogVisible = true;
     }
   }
 };
 </script>
+<style lang="scss">
+  .preview-images {
+    margin-top: 5vh !important;
+    margin-bottom: 5vh;
+    max-height: 90vh;
+    box-sizing: border-box;
+    background: transparent;
+    .el-dialog__header {
+      padding: 0;
+
+      .el-dialog__headerbtn {
+        position: fixed;
+        top: 20px;
+        right: 20px;
+        .el-icon-close {
+          color: #fff;
+          font-size: 30px;
+          font-weight: normal;
+        }
+      }
+    }
+    .el-dialog__body {
+      padding: 0;
+    }
+    .images-dialog {
+      overflow: hidden;
+      width: 80%;
+      margin: 0 auto;
+    }
+    .images-dialog__left,.images-dialog__right {
+      float: left;
+      width: 50%;
+      display: flex;
+      justify-content: center;
+    }
+    .el-carousel {
+      width: 80%;
+      aspect-ratio: 3/4;
+      .el-carousel__container {
+        height: 100%;
+      }
+      .el-carousel__item {
+        display: flex;
+        align-items: center;
+      }
+      img {
+        display: block;
+        width: 100%;
+      }
+    }
+    .images-dialog__right {
+      p {
+        width: 80%;
+        aspect-ratio: 3/4;
+        background: #fff;
+        display: flex;
+        align-items: center;
+        overflow: hidden;
+        margin: 0;
+      }
+      img {
+        display: block;
+        width: 100%;
+      }
+    }
+  }
+</style>
 
 

+ 112 - 49
src/views/crop-list/index.vue

@@ -19,14 +19,14 @@
           </el-form-item>
           <el-button type="primary" @click="getList()">搜索</el-button >
           <el-button type="primary" @click="showCropSizeModal()">裁图尺寸</el-button >
-          <el-button type="primary" @click="productsSync()" icon="el-icon-upload" :loading="uploadLoading">上传图库</el-button>
+          <el-button type="primary" @click="aiCropPreview()" :loading="uploadLoading">AI裁图</el-button>
         </el-form>
       </div>
     </div>
     <div class="table-container">
       <el-table  style="width: 100%" v-loading="listLoading" :key="tableKey" :data="list" row-key="id"
         stripe border fit highlight-current-row @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" :selectable="selectable"></el-table-column>
+        <!-- <el-table-column type="selection" width="55" :selectable="selectable"></el-table-column> -->
         <el-table-column label="SKU名称" align="center">
           <template slot-scope="scope">
             <el-popover
@@ -35,11 +35,22 @@
               trigger="manual"
               v-model="scope.row._popoverVisible"
             >
-              <el-input
-                v-model="editValue"
-                type="textarea"
-                :rows="3"
-              />
+              <div class="input-block">
+                <span style="display: block;">SKU:</span>
+                <el-input
+                  v-model="editValue"
+                  type="textarea"
+                  :rows="2"
+                />
+              </div>
+              <div class="input-block" style="margin-top: 10px;" v-if="scope.row.status >= 5">
+                <span style="display: block;">颜色代码:</span>
+                <el-input
+                  v-model="editValueColor"
+                  type="textarea"
+                  :rows="2"
+                />
+              </div>
 
               <div class="popover-footer">
                 <el-button size="mini" @click="cancelEdit(scope.row)">取消</el-button>
@@ -47,15 +58,22 @@
               </div>
 
               <span slot="reference" class="click-text" @click="openEdit(scope.row)">
-                {{ scope.row.sku }}
+                {{ scope.row.sku }} 
+                <template v-if="scope.row.colorCode">
+                  <br>
+                  {{ scope.row.colorCode }}
+                </template>
               </span>
             </el-popover>
           </template>
         </el-table-column>
         <el-table-column label="原图" min-width="100" align="center">
           <template slot-scope="scope">
-            <p style="margin: 0;cursor: pointer;" v-if="scope.row.referenceImagesList && scope.row.referenceImagesList.length" @click="openGallery(scope.row, 'default')">
-              <img style="max-width: 100px;"  :src="scope.row.referenceImagesList[0]"  alt="">
+            <p v-if="scope.row.type == 1 && scope.row.referenceImagesList && scope.row.referenceImagesList.length" @click="openGallery(scope.row, 'default')">
+              <img style="max-width: 100px;"  :src="scope.row.referenceImagesList[0]"  alt=""></img>
+            </p>
+            <p style="margin: 0;cursor: pointer;" v-if="scope.row.type == 2 && scope.row.aiGeneratedImageVOList && scope.row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).length" @click="openGallery(scope.row, 'default')">
+              <img style="max-width: 100px;"  :src="scope.row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1)[0].imageUrl"  alt="">
             </p>
           </template>
         </el-table-column>
@@ -73,22 +91,34 @@
             <el-tag v-if="scope.row.status == 1">AI生成中</el-tag>
             <el-tag type="warning" v-if="scope.row.status == 2">待审核</el-tag>
             <el-tag type="warning" v-if="scope.row.status == 3">待复核</el-tag>
-            <el-tag type="success" v-if="scope.row.status == 4">审核完成</el-tag>
+            <el-tag type="success" v-if="scope.row.status == 4">待裁图</el-tag>
             <el-tag type="warning" v-if="scope.row.status == 5">裁图待复核</el-tag>
             <el-tag type="success" v-if="scope.row.status == 6">已完成</el-tag>
-            <el-tag type="danger" v-if="scope.row.status == 7">失败</el-tag>
+            <el-tag type="danger" v-if="scope.row.status == 7">生图失败</el-tag>
             <el-tag v-if="scope.row.status == 8">待推送</el-tag>
+            <el-tag v-if="scope.row.status == 9">裁图中</el-tag>
           </template>
         </el-table-column>
         <el-table-column label="处理人" min-width="100" align="center" prop="creator" />
         <el-table-column label="处理时间" min-width="100" align="center" prop="updateTime" />
-        <el-table-column label="操作" align="center" width="160" fixed="right">
+        <el-table-column label="操作" align="center" width="250" fixed="right">
           <template slot-scope="scope">
             <!-- scope.row.status == 2 || scope.row.status == 3 -->
-            <el-button type="text" v-if="scope.row.status == 2 || scope.row.status == 3" size="mini" @click="showImagesGroupModal(scope.row, 'review')">审核</el-button>
-            <el-button type="text" v-if="scope.row.status * 1 >= 4 && scope.row.status != 7" size="mini" @click="showImagesGroupModal(scope.row, 'crop')">裁图</el-button>
-            <el-button v-if="scope.row.status * 1 > 1 && scope.row.status != 7" type="text" @click="handleDownload(scope.row)" size="mini">下载</el-button>
-            <el-button type="text" @click="handleRegenerate(scope.row)" size="mini">重生成</el-button>
+            <el-button size="mini" v-if="scope.row.status == 2 || scope.row.status == 3" @click="showImagesGroupModal(scope.row, 'review')">审核</el-button>
+            <el-button type="success" size="mini" v-if="scope.row.status * 1 >= 4 && scope.row.status != 7 && scope.row.status != 9 && scope.row.status != 6" @click="showImagesGroupModal(scope.row, 'crop')">裁图</el-button>
+            <el-button type="primary" v-if="scope.row.status * 1 > 1 && scope.row.status != 7" size="mini" @click="handleDownload(scope.row)">下载</el-button>
+            <!-- 自动生成 1 手动生成 2 -->
+            <el-button type="info" size="mini" v-if="scope.row.type != 2" @click="handleRegenerate(scope.row)">重生成</el-button>
+
+            <el-dropdown v-if="scope.row.status == 8" @command="uploadGalleryList($event, scope.row)">
+              <el-button type="warning" size="mini">
+                上传图库<i class="el-icon-arrow-down el-icon--right"></i>
+              </el-button>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item command="1">覆盖模式</el-dropdown-item>
+                <el-dropdown-item command="2">追加模式</el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
             <el-popover
               placement="left"
               width="320"
@@ -101,7 +131,7 @@
                   :key="index"
                   class="log-item"
                 >
-                  <div class="log-user">{{ log.creator }}</div>
+                  <div class="log-user">{{ log.userName }}</div>
                   <div class="log-action">{{ log.msg }}</div>
                   <div class="log-time">{{ log.updateTime }}</div>
                 </div>
@@ -114,8 +144,8 @@
               <!-- 触发按钮 -->
               <el-button
                 slot="reference"
-                type="text"
-                size="small"
+                size="mini"
+                type="danger"
               >
                 操作日志
               </el-button>
@@ -127,7 +157,7 @@
     <!-- 分页 -->
     <swPage v-if="total > 0" key="2" :listQuery="listQuery" :total="total" pos="btmRight" @retPage="retPage" />
     <crop-size-modal ref="CropSizeModal" :list="sizeList" @success="getSizeTemplate()"></crop-size-modal>
-    <images-group-modal ref="ImagesGroupModal" :sizeList="sizeList" @update-status="updateStatus" @update-images="updateImages" @update-success="getList()"></images-group-modal>
+    <images-group-modal ref="ImagesGroupModal" :sizeList="sizeList" @update-status="updateStatus" @update-images="updateImages" @update-success="handleFilter()"></images-group-modal>
   </div>
 </template>
 
@@ -167,6 +197,7 @@ export default {
       total: 0,
       skuLoading: false,
       editValue: '',
+      editValueColor: '',
       listLoading: false,
       uploadLoading: false,
       listQuery: {
@@ -191,11 +222,12 @@ export default {
         {label: 'AI生成中', value: '1'},
         {label: '待审核', value: '2'},
         {label: '待复核', value: '3'},
-        {label: '审核完成', value: '4'},
+        {label: '待裁图', value: '4'},
         {label: '裁图待复核', value: '5'},
         {label: '已完成', value: '6'},
-        {label: '失败', value: '7'},
-        {label: '待推送', value: '8'}
+        {label: '生图失败', value: '7'},
+        {label: '待推送', value: '8'},
+        {label: '裁图中', value: '9'}
       ]
     }
   },
@@ -206,6 +238,9 @@ export default {
     document.removeEventListener('click', this.handleClickOutside)
   },
   methods: {
+    aiCropPreview() {
+      this.$refs.ImagesGroupModal.init();
+    },
     selectable(row, index) {
       // 返回 false → 该行不可选
       // 返回 true  → 该行可选
@@ -277,30 +312,38 @@ export default {
     openEdit(row) {
       // 先关闭所有 popover
       this.list.forEach(item => {
-        item._popoverVisible = false
+        item._popoverVisible = false;
       })
-      row._popoverVisible = true
-      this.editValue = row.sku
+      row._popoverVisible = true;
+      this.editValue = row.sku;
+      this.editValueColor = row.colorCode;
     },
     cancelEdit(row) {
       row._popoverVisible = false
     },
     confirmEdit(row) {
       // 👉 这里可直接调用保存接口
+      const params = {
+        sessionId: `${Date.now() + 10}_${genCidHex16()}`,
+        applicationId: 5,
+        id: row.id,
+        sku: this.editValue
+      }
+      if (row.status >= 5) {
+        params['colorCode'] = this.editValueColor;
+      }
       this.skuLoading = true;
       request({
         url: '/imageTask/update',
         method: 'post',
-        data: {
-          sessionId: `${Date.now() + 10}_${genCidHex16()}`,
-          applicationId: 5,
-          id: row.id,
-          sku: this.editValue
-        }
+        data: params
       }).then(res => {
         if (res.code == 200) {
           row._popoverVisible = false;
           row.sku = this.editValue;
+          if (row.status >= 5) {
+            row.colorCode = this.editValueColor;
+          }
           this.$message.success("操作成功");
         }
       }).finally(() => {
@@ -338,30 +381,45 @@ export default {
       this.listQuery.page = 1;
       this.getList();
     },
-    productsSync() {
-      const params = this.listQuery ? { productCode: this.listQuery.productCode } : {}
-      if (!this.listQuery.productCode) {
-        this.$message.error("产品款号不能为空!");
-        return;
+    // 上传图库
+    uploadGalleryList(command, row) {
+      this.listLoading = true;
+      const params = {
+        taskId: row.id,
+        uploadType: command || '2'
       }
-      this.productsSyncLoading = true;
-      pullGalleryProduct(params).then(res => {
-        if (200 == res.code) {
-          this.$message.success(res.msg || "操作成功");
+      request({
+        url: '/imageTask/uploadGallery',
+        method: 'post',
+        data: params
+      }).then(res => {
+        if (res.code == 200) {
+          this.$message.success(res.msg || '操作成功!');
         }
-        this.productsSyncLoading = false;
       }).finally(() => {
-        this.productsSyncLoading = false;
+        this.listLoading = false
       })
     },
     openGallery(row, type) {
       let images = [];
       if (type == 'default') {
-        images = row.referenceImagesList.slice(0, 10);
+        if (row.type == 1) {
+          images = [...row.referenceImagesList, ...row.originalImagesList].slice(0, 10);
+        } else {
+          images = row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).slice(0, 10).map(acc => {
+            return acc.imageUrl
+          });
+        }
       } else {
-        row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).slice(0, 10).forEach(acc => {
-          images.push(acc.imageUrl)
-        })
+        if (row.type == 1) {
+          row.aiGeneratedImageVOList.filter(acc => acc.imageType == 1).slice(0, 10).forEach(acc => {
+            images.push(acc.imageUrl)
+          })
+        } else {
+          row.aiGeneratedImageVOList.filter(acc => acc.imageType == 2).slice(0, 10).forEach(acc => {
+            images.push(acc.imageUrl)
+          })
+        }
       }
 
       Fancybox.show(
@@ -445,7 +503,12 @@ export default {
 
 .table-container {
   .el-button+.el-button {
-    margin-left: 0;
+    margin-left: 7px;
+    margin-bottom: 10px;
+  }
+  .el-button+span,
+  .el-button+.el-dropdown {
+    margin-left: 7px;
   }
 }
 .button {

+ 41 - 10
src/views/design-agent/feature-mod-index.vue

@@ -300,6 +300,17 @@
                         >
                           <i class="el-icon-info" style="color: #ccc;cursor: pointer;" />
                         </el-tooltip>
+
+                        <div class="clothing-type__block" style="margin-left: 20px;" v-if="ruleForm.isBatch">
+                          <el-select v-model="ruleForm.clothingType" size="small" placeholder="衣服类型">
+                            <el-option
+                              v-for="item in clothingTypes"
+                              :key="item.value"
+                              :label="item.label"
+                              :value="item.value">
+                            </el-option>
+                          </el-select>
+                        </div>
                       </div>
                     </template>
                     <el-input
@@ -319,7 +330,7 @@
                       </div>
                     </template>
                     <div>
-                      <el-input-number size="small" v-model="ruleForm.batchCount" :controls="false"></el-input-number> 张
+                      <el-input-number size="small" :min="1" :step="1" step-strictly v-model="ruleForm.batchCount" :controls="false"></el-input-number> 张
                     </div>
                   </el-form-item>
                   <template v-if="pagesData.applicationId == 2 && !ruleForm.isBatch">
@@ -545,10 +556,17 @@ export default {
           content: []
         }],
         tabIndex: 1,
-        batchCount: null
+        batchCount: 1,
+        clothingType: ''
       },
       swiperList: {},
       ratioList: ['3:2', '16:9', '1:1', '2:3', '3:4', '9:16'],
+      clothingTypes: [
+        {label: '所有上装', value: 1},
+        {label: '所有下装', value: 2},
+        {label: '连衣裙', value: 3},
+        {label: '整套衣服', value: 4}
+      ],
       rules: {
         imageUrl: [
           { validator: (rule, value, callback) => this.checkImage(rule, value, callback), trigger: "change" }
@@ -864,19 +882,32 @@ export default {
       this.$refs['ruleForm'].validate(async (valid) => {
         if (valid) {
           if (this.pagesData.applicationId == 2 && this.ruleForm.isBatch) {
-            if (!this.ruleForm.screenDescription) {
-              this.$message.error('画面描述为必填项!');
-              return;
-            }
-            if (this.ruleForm.screenDescription.trim() && this.ruleForm.screenDescription.trim().length < 3) {
-              this.$message.error('画面描述至少需要 3 个字符!');
-              return;
+            let keyword_1 = '整套服装';
+            let keyword_2 = '那套服装';
+            let defaultDescription = `任务:进行精确的虚拟服装替换。
+            参考图像分析:
+            请仔细查看前几张图像([图片1], [图片2]...[倒数第二张])。识别这些图像中模特所穿戴的{{s%}},包括其款式、材质、颜色、图案、纹理以及在直播间灯光下的所有细节。
+            目标图像定义:
+            请查看最后一张图像([最后一张图片])。这张图片定义了目标人物的身份、面部特征、发型、当前的姿势以及所处的背景环境。
+            执行操作:
+            保持最后一张图像中的人物(脸、发型、身体形态)和背景完全不变。将前几张参考图像中的{{ss%}},自然、真实地“穿”在最后一张图像的人物身上。
+            细节要求:
+            1. 新的服装必须完美贴合目标人物的当前姿势。
+            2. 必须根据最后一张图像中的环境光线,在衣服上生成极其真实的褶皱、阴影和高光。
+            3. 确保服装的材质感(如棉麻、丝绸、牛仔等)得到完美还原。`;
+            if (this.ruleForm.clothingType) {
+              const keywords = this.clothingTypes.filter(acc => acc.value == this.ruleForm.clothingType);
+              if (keywords.length) {
+                keyword_1 = keywords[0].label;
+                keyword_2 = keywords[0].label;
+              }
             }
+            defaultDescription = defaultDescription.replace(/{{s%}}/g, keyword_1).replace(/{{ss%}}/g, keyword_2);
             this.comfirmLoading = true;
             let payload = {
               sessionId: `${Date.now() + 10}_${genCidHex16()}`,
               applicationId: 5,
-              prompt: this.ruleForm.screenDescription,
+              prompt: this.ruleForm.screenDescription || defaultDescription,
               poseImages: this.ruleForm.imageList,
               count: this.ruleForm.batchCount,
               clothesItems: this.ruleForm.editableTabs.map(acc => {