From a3d67c7ce5065172ddb55bf9bde2c0dc0f128af9 Mon Sep 17 00:00:00 2001
From: username <1532322479@qq.com>
Date: Fri, 25 Apr 2025 12:59:29 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9B=BE=E5=BA=93=E6=8E=A5?=
 =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87=E7=94=9F?=
 =?UTF-8?q?=E6=88=90=E5=B9=BB=E7=81=AF=E7=89=87=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 bs-admin/pom.xml                              |  11 ++
 .../controller/CtGalleryCataController.java   |   1 -
 .../controller/CtGalleryImagesController.java | 137 ++++++++++++++++--
 .../com/bs/ct/controller/CtTagController.java |   5 +
 .../ct/controller/CtTaskBranchController.java |   2 -
 .../controller/CtTaskFeedbackController.java  |  73 +++++++++-
 .../com/bs/ct/domain/CtGalleryImages.java     |  29 ++++
 .../com/bs/ct/utils/OperateImageUtils.java    |  88 ++++++++++-
 bs-ui/src/views/system/tag/index.vue          |  82 ++++++++---
 .../other-task/components/AuditDialog.vue     |   2 +-
 .../send/other-task/components/EditDialog.vue |   8 +-
 11 files changed, 391 insertions(+), 47 deletions(-)

diff --git a/bs-admin/pom.xml b/bs-admin/pom.xml
index 4a4d24b..a2085b9 100644
--- a/bs-admin/pom.xml
+++ b/bs-admin/pom.xml
@@ -16,6 +16,17 @@
     </description>
 
     <dependencies>
+        <!--JAVA使用javacv实现图片合成短视频,相关JAR包-->
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv</artifactId>
+            <version>1.5.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv-platform</artifactId>
+            <version>1.5.2</version>
+        </dependency>
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
diff --git a/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryCataController.java b/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryCataController.java
index 07e7564..23b4c60 100644
--- a/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryCataController.java
+++ b/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryCataController.java
@@ -54,7 +54,6 @@ public class CtGalleryCataController extends BaseController {
         condition(queryWrapper,ctGalleryCata);
         List<CtGalleryCata> list = ctGalleryCataService.list(queryWrapper);
         List<CtGalleryCata> ctGalleryCatas = buildTree(list);
-
         return getDataTable(ctGalleryCatas);
     }
 
diff --git a/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryImagesController.java b/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryImagesController.java
index 766faa7..f541b56 100644
--- a/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryImagesController.java
+++ b/bs-admin/src/main/java/com/bs/ct/controller/CtGalleryImagesController.java
@@ -1,12 +1,10 @@
 package com.bs.ct.controller;
 
 import java.awt.image.BufferedImage;
+import java.io.File;
 import java.io.IOException;
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Random;
+import java.util.*;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
 
@@ -80,18 +78,115 @@ public class CtGalleryImagesController extends BaseController {
     private ICtTaskInfoService ctTaskInfoService;
     @Resource
     private ICtImagesFeedbackRefService ctImagesFeedbackRefService;
+    @Resource
+    private ICtGalleryCataService ctGalleryCataService;
+    @Resource
+    private ICtTaskFeedbackService ctTaskFeedbackService;
     /**
      * 分页查询图库图片列表
      */
     @ApiOperation("分页查询图库图片列表")
     @GetMapping("/pageList")
     public TableDataInfo pageList(CtGalleryImages ctGalleryImages) {
-        startPage();
+        Integer pageNum = ctGalleryImages.getPageNum() != null ? ctGalleryImages.getPageNum() : 1;
+        Integer pageSize = ctGalleryImages.getPageSize() != null ? ctGalleryImages.getPageSize() : 10;
         LambdaQueryWrapper<CtGalleryImages> queryWrapper = new LambdaQueryWrapper();
-        condition(queryWrapper,ctGalleryImages);
+        if (Validator.isEmpty(ctGalleryImages.getImageTitle())) {
+            condition(queryWrapper,ctGalleryImages);
+        }
         List<CtGalleryImages> list = ctGalleryImagesService.list(queryWrapper);
+        setAll(list);
         setFile(list);
-        return getDataTable(list);
+        if (Validator.isNotEmpty(ctGalleryImages.getImageTitle())) {
+            list = filterByImageTitle(list,ctGalleryImages.getImageTitle());
+        }
+        Integer start = (pageNum - 1) * pageSize;
+        List<CtGalleryImages> result = list.stream()
+                .skip(start)  // 跳过前4条数据
+                .limit(pageSize) // 最多取6条数据
+                .collect(Collectors.toList());
+        TableDataInfo data = getDataTable(result);
+        data.setTotal(list.size());
+        return data;
+    }
+
+
+    // 过滤方法
+    public static List<CtGalleryImages> filterByImageTitle(List<CtGalleryImages> list, String filterText) {
+        return list.stream()
+                .filter(image -> {
+                    boolean matchImageName = image.getImageName() != null && image.getImageName().contains(filterText);
+                    boolean matchImageTitle = image.getImageTitle() != null && image.getImageTitle().contains(filterText);
+                    boolean matchKeyWords = image.getKeyWords() != null && image.getKeyWords().contains(filterText);
+                    boolean matchCataName = image.getCataName() != null && image.getCataName().contains(filterText);
+                    boolean matchTagNames = image.getTagNames() != null && image.getTagNames().stream().anyMatch(tag -> tag.contains(filterText));
+                    boolean matchTaskTitles = image.getTaskTitles() != null && image.getTaskTitles().stream().anyMatch(task -> task.contains(filterText));
+                    return matchImageName || matchImageTitle || matchKeyWords || matchCataName || matchTagNames || matchTaskTitles;
+                })
+                .collect(Collectors.toList());
+    }
+
+    private void setAll(List<CtGalleryImages> list) {
+        List<CtGalleryCata> ctGalleryCatas = ctGalleryCataService.list();
+        List<CtGalleryImagesTag> imagesTags = ctGalleryImagesTagService.list();
+        HashMap<Long, String> longStringHashMap = ctGalleryCatas.stream()
+                .collect(Collectors.toMap(
+                        CtGalleryCata::getId,
+                        CtGalleryCata::getCataName,
+                        (existing, replacement) -> existing,
+                        HashMap::new
+                ));
+        Map<Long, List<String>> imageStringHashMap = imagesTags.stream()
+                .collect(Collectors.groupingBy(
+                        CtGalleryImagesTag::getImageId,
+                        Collectors.mapping(
+                                CtGalleryImagesTag::getTagName,
+                                Collectors.toList()
+                        )
+                ));
+        //查询相关任务名称
+        List<CtImagesFeedbackRef> imagesFeedbackRefs = ctImagesFeedbackRefService.list();
+        Map<Long, List<Long>> refMap = imagesFeedbackRefs.stream()
+                .collect(Collectors.groupingBy(
+                        CtImagesFeedbackRef::getImageId,  // 分组键:imageId
+                        Collectors.mapping(
+                                CtImagesFeedbackRef::getFeedbackId,  // 提取值:feedbackId
+                                Collectors.toList()  // 收集为列表
+                        )
+                ));
+        List<CtTaskFeedback> ctTaskFeedbacks = ctTaskFeedbackService.list();
+        HashMap<Long, Long> feedbackIdsMap = ctTaskFeedbacks.stream()
+                .collect(Collectors.toMap(
+                        CtTaskFeedback::getId,          // 键:id
+                        CtTaskFeedback::getTaskId,      // 值:taskId
+                        (existing, replacement) -> existing,  // 处理键冲突:保留现有值
+                        () -> new HashMap<>()  // 指定 Map 实现类(可选)
+                ));
+        List<CtTaskInfo> ctTaskInfos = ctTaskInfoService.list();
+        HashMap<Long, String> infoMap = ctTaskInfos.stream()
+                .collect(Collectors.toMap(
+                        CtTaskInfo::getId,
+                        CtTaskInfo::getTaskTitle,
+                        (existing, replacement) -> existing,
+                        HashMap::new
+                ));
+        for (CtGalleryImages ctGalleryImage : list) {
+            Long cataId = ctGalleryImage.getCataId();
+            String cataName =  longStringHashMap.get(cataId);
+            ctGalleryImage.setCataName(cataName);
+            List<String> tagNames = imageStringHashMap.get(ctGalleryImage.getId());
+            ctGalleryImage.setTagNames(tagNames);
+            List<Long> feedList = refMap.get(ctGalleryImage.getId());
+            List<String>  taskTitles = new ArrayList<>();
+            if (null != feedList && feedList.size() > 0) {
+                for (Long feedId : feedList) {
+                    Long taskInfoId = feedbackIdsMap.get(feedId);
+                    String taskTitleName =  infoMap.get(taskInfoId);
+                    taskTitles.add(taskTitleName);
+                }
+                ctGalleryImage.setTaskTitles(taskTitles);
+            }
+        }
     }
 
     private void setFile(List<CtGalleryImages> list) {
@@ -141,12 +236,14 @@ public class CtGalleryImagesController extends BaseController {
         List<CtGalleryImages> list = ctGalleryImagesService.list(new LambdaQueryWrapper<CtGalleryImages>()
                 .in(CtGalleryImages::getId, imagesIds));
         BufferedImage[] imgs = new BufferedImage[list.size()];
+        Map<Integer, File> imgMap = new HashMap<>();
         int index = 0;
         for (CtGalleryImages galleryImages : list) {
             String attachUrl = galleryImages.getImagePath().replace("/profile", "");
             String filePath = profile + attachUrl;
             try {
                 imgs[index++] = OperateImageUtils.getBufferedImage(filePath);
+                imgMap.put(index++, new File(filePath));
             } catch (IOException e) {
                 e.printStackTrace();
                 return AjaxResult.error("读取图片失败: " + filePath);
@@ -190,6 +287,12 @@ public class CtGalleryImagesController extends BaseController {
                         return AjaxResult.error("垂直合并需要至少2张图片");
                     }
                     break;
+                case "video":
+                    int width = imgs[0].getWidth();
+                    int height = imgs[0].getHeight();
+                    String mp4SavePath = profile + UUIDUtils.generatorUUID() + "output.mp4";
+                    OperateImageUtils.createMp4(mp4SavePath, imgMap, width, height);
+                    break;
                 default:
                     return AjaxResult.error("不支持的图片类型: " + imageType);
             }
@@ -330,12 +433,6 @@ public class CtGalleryImagesController extends BaseController {
         CtTaskInfo ctTaskInfo = new CtTaskInfo();
         Long newId = System.currentTimeMillis() + new Random().nextInt(1000);
         ctTaskInfo.setId(newId);
-        List<CtTaskBranch> branchList = ctTaskInfo.getBranchList();
-        for (CtTaskBranch sdTaskOtherBranch : branchList) {
-            sdTaskOtherBranch.setTaskId(ctTaskInfo.getId());
-            sdTaskOtherBranch.setType("机构");
-            ctTaskBranchService.saveOrUpdate(sdTaskOtherBranch);
-        }
         //设置发起单位与部门
         LoginUser loginUser = getLoginUser();
         Long deptId = loginUser.getDeptId();
@@ -348,6 +445,20 @@ public class CtGalleryImagesController extends BaseController {
                 ctTaskInfo.setBranchCode(String.valueOf(parentDept.getDeptId()));
                 ctTaskInfo.setBranchName(parentDept.getDeptName());
             }
+            CtTaskBranch branch = new CtTaskBranch();
+            branch.setBranchCode(String.valueOf(deptId));
+            branch.setBranchName(sysDept.getDeptName());
+            branch.setTaskId(ctTaskInfo.getId());
+            branch.setType("机构");
+            ctTaskBranchService.saveOrUpdate(branch);
+            CtTaskBranch taskBranch = new CtTaskBranch();
+            taskBranch.setBranchCode(String.valueOf(deptId));
+            taskBranch.setUserName(loginUser.getUsername());
+            taskBranch.setUserId(String.valueOf(loginUser.getUserId()));
+            taskBranch.setTaskStatus("0");
+            taskBranch.setTaskId(ctTaskInfo.getId());
+            taskBranch.setType("人员");
+            ctTaskBranchService.saveOrUpdate(taskBranch);
         }
         if ("1".equals(ctTaskInfo.getStatus())) {
             ctTaskInfo.setTaskDate(new Date());
diff --git a/bs-admin/src/main/java/com/bs/ct/controller/CtTagController.java b/bs-admin/src/main/java/com/bs/ct/controller/CtTagController.java
index 5d72a48..ad813c9 100644
--- a/bs-admin/src/main/java/com/bs/ct/controller/CtTagController.java
+++ b/bs-admin/src/main/java/com/bs/ct/controller/CtTagController.java
@@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletResponse;
 
 import com.bs.cm.domain.CmAttach;
 import com.bs.cm.service.ICmAttachService;
+import com.bs.common.exception.ServiceException;
 import com.bs.ct.domain.CtTaskInfo;
 import com.bs.ct.domain.CtTaskTemplateTag;
 import io.swagger.annotations.Api;
@@ -111,6 +112,10 @@ public class CtTagController extends BaseController {
     @Log(title = "标签信息新增", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody CtTag ctTag) {
+        List<CtTag> ctTags = ctTagService.list(new LambdaQueryWrapper<CtTag>().eq(CtTag::getTagName, ctTag.getTagName()));
+        if (null != ctTags && ctTags.size() > 0) {
+            throw new ServiceException("该标签名称已经存在");
+        }
         updateFile(ctTag);
         return toAjax(ctTagService.save(ctTag));
     }
diff --git a/bs-admin/src/main/java/com/bs/ct/controller/CtTaskBranchController.java b/bs-admin/src/main/java/com/bs/ct/controller/CtTaskBranchController.java
index 4cfdf79..dcf3fa8 100644
--- a/bs-admin/src/main/java/com/bs/ct/controller/CtTaskBranchController.java
+++ b/bs-admin/src/main/java/com/bs/ct/controller/CtTaskBranchController.java
@@ -285,8 +285,6 @@ public class CtTaskBranchController extends BaseController {
                 taskOtherBranch.setTaskDate(byId.getTaskDate());
                 taskOtherBranch.setSdTaskOther(byId);
             }
-
-
             List<CtTaskFeedback> feedbackListByCreateTime = ctTaskFeedbackService.list(new LambdaQueryWrapper<CtTaskFeedback>()
                     .eq(CtTaskFeedback::getTaskBranchId, taskOtherBranch.getId())
                     .ne(CtTaskFeedback::getStatus, 0)
diff --git a/bs-admin/src/main/java/com/bs/ct/controller/CtTaskFeedbackController.java b/bs-admin/src/main/java/com/bs/ct/controller/CtTaskFeedbackController.java
index 55a4dcb..7ddd10e 100644
--- a/bs-admin/src/main/java/com/bs/ct/controller/CtTaskFeedbackController.java
+++ b/bs-admin/src/main/java/com/bs/ct/controller/CtTaskFeedbackController.java
@@ -6,9 +6,12 @@ import javax.servlet.http.HttpServletResponse;
 
 import com.bs.cm.domain.CmAttach;
 import com.bs.cm.service.ICmAttachService;
+import com.bs.common.core.domain.entity.SysDept;
 import com.bs.common.core.domain.entity.SysUser;
+import com.bs.common.core.domain.model.LoginUser;
 import com.bs.ct.domain.*;
 import com.bs.ct.service.*;
+import com.bs.system.service.ISysDeptService;
 import com.bs.system.service.ISysUserService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -57,6 +60,10 @@ public class CtTaskFeedbackController extends BaseController {
     private ICtImagesFeedbackRefService ctImagesFeedbackRefService;
     @Resource
     private ICtGalleryImagesService ctGalleryImagesService;
+    @Resource
+    private ICtGalleryImagesTagService ctGalleryImagesTagService;
+    @Autowired
+    private ISysDeptService deptService;
     /**
      * 分页查询任务反馈列表
      */
@@ -76,7 +83,7 @@ public class CtTaskFeedbackController extends BaseController {
                     .eq(CtImagesFeedbackRef::getFeedbackId, ctFeedback.getId()));
             if (null != ctImagesFeedbackRefs && ctImagesFeedbackRefs.size() > 0) {
                 List<Long> imageIds = ctImagesFeedbackRefs.stream()
-                        .map(CtImagesFeedbackRef::getImageId) // 提取 imageId
+                        .map(CtImagesFeedbackRef::getImageId)
                         .collect(Collectors.toList());
                 List<CtGalleryImages> galleryImages = ctGalleryImagesService.list(new LambdaQueryWrapper<CtGalleryImages>()
                         .in(CtGalleryImages::getId, imageIds));
@@ -89,6 +96,70 @@ public class CtTaskFeedbackController extends BaseController {
         }
     }
 
+    //
+
+    /**
+     * 通过图片新增任务反馈
+     */
+    @ApiOperation("通过图片新增任务反馈")
+    @Log(title = "通过图片新增任务反馈", businessType = BusinessType.INSERT)
+    @PostMapping("/addByImage")
+    public AjaxResult addByImage(@RequestBody CtTaskFeedback ctTaskFeedback) {
+        List<Long> imageIds = ctTaskFeedback.getImageIds();
+        Long taskId = ctTaskFeedback.getTaskId();
+        List<CtTaskTag> ctTaskTags = ctTaskTagService.list(new LambdaQueryWrapper<CtTaskTag>()
+                .eq(CtTaskTag::getTaskId, taskId));
+        List<String> tagNamesByTag = ctTaskTags.stream()
+                .map(CtTaskTag::getTagName)
+                .collect(Collectors.toList());
+        LoginUser loginUser = getLoginUser();
+        SysDept sysDept = deptService.selectDeptById(loginUser.getDeptId());
+        List<Long> notTags = new ArrayList<>();
+        Map<String, List<Long>> tagImageIdMap = new HashMap<>();
+        for (Long imageId : imageIds) {
+            List<CtGalleryImagesTag> ctGalleryImagesTags = ctGalleryImagesTagService.list(new LambdaQueryWrapper<CtGalleryImagesTag>()
+                   .eq(CtGalleryImagesTag::getImageId, imageId));
+            List<String> tagNames = ctGalleryImagesTags.stream()
+                    .map(CtGalleryImagesTag::getTagName)
+                    .collect(Collectors.toList());
+            boolean hasMatch = false;
+            for (String tagName : tagNames) {
+                if (tagNamesByTag.contains(tagName)) {
+                    tagImageIdMap.computeIfAbsent(tagName, k -> new ArrayList<>()).add(imageId);
+                    hasMatch = true;
+                }
+            }
+            if (!hasMatch) {
+                notTags.add(imageId);
+            }
+        }
+        // 为每个标签组创建反馈任务
+        for (Map.Entry<String, List<Long>> entry : tagImageIdMap.entrySet()) {
+            String tagName = entry.getKey();
+            List<Long> groupImageIds = entry.getValue();
+            Optional<CtTaskTag> firstMatch = ctTaskTags.stream()
+                    .filter(tag -> tagName.equals(tag.getTagName()))
+                    .findFirst();
+            if (firstMatch.isPresent()) {
+                CtTaskTag tag = firstMatch.get();
+                CtTaskFeedback feedback = new CtTaskFeedback();
+                feedback.setTaskId(taskId);
+                feedback.setTaskTagId(tag.getId());
+                feedback.setImageIds(groupImageIds);
+                feedback.setFeedbackTime(new Date());
+                feedback.setUserId(String.valueOf(loginUser.getUserId()));
+                feedback.setUserName(loginUser.getUsername());
+                feedback.setBranchCode(String.valueOf(loginUser.getDeptId()));
+                feedback.setUserDept(sysDept.getDeptName());
+                boolean save = ctTaskFeedbackService.save(feedback);
+                if (save) {
+                    saveRef(feedback);
+                }
+            }
+        }
+        return success(notTags);
+    }
+
     /**
      * 查询任务反馈列表
      */
diff --git a/bs-admin/src/main/java/com/bs/ct/domain/CtGalleryImages.java b/bs-admin/src/main/java/com/bs/ct/domain/CtGalleryImages.java
index 75e2b69..e424231 100644
--- a/bs-admin/src/main/java/com/bs/ct/domain/CtGalleryImages.java
+++ b/bs-admin/src/main/java/com/bs/ct/domain/CtGalleryImages.java
@@ -121,6 +121,35 @@ public class CtGalleryImages extends BaseEntity{
     @TableField(exist = false)
     private Long feedbackId;
 
+    /** 生成图片类型:
+     * 九宫格 nineGrid
+     * 四宫格 fourGrid
+     * 水平合并 horizontalMerge
+     * 垂直合并 verticalMerge
+     * 视频 video
+     * */
+
     @TableField(exist = false)
     private String imageType;
+
+    /** 目录名称 */
+
+    @TableField(exist = false)
+    private String cataName;
+
+    /** 标签名称 */
+
+    @TableField(exist = false)
+    private List<String> tagNames;
+
+    /** 标签名称 */
+
+    @TableField(exist = false)
+    private List<String> taskTitles;
+
+    @TableField(exist = false)
+    private Integer pageNum;
+
+    @TableField(exist = false)
+    private Integer pageSize;
 }
diff --git a/bs-admin/src/main/java/com/bs/ct/utils/OperateImageUtils.java b/bs-admin/src/main/java/com/bs/ct/utils/OperateImageUtils.java
index a87d336..b7a796b 100644
--- a/bs-admin/src/main/java/com/bs/ct/utils/OperateImageUtils.java
+++ b/bs-admin/src/main/java/com/bs/ct/utils/OperateImageUtils.java
@@ -1,9 +1,16 @@
 package com.bs.ct.utils;
 
+
+import org.bytedeco.ffmpeg.global.avcodec;
+import org.bytedeco.ffmpeg.global.avutil;
+import org.bytedeco.javacv.FFmpegFrameRecorder;
+import org.bytedeco.javacv.FrameRecorder;
+import org.bytedeco.javacv.Java2DFrameConverter;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
-
+import java.util.HashMap;
+import java.util.Map;
 import javax.imageio.ImageIO;
 
 /**
@@ -112,9 +119,11 @@ public class OperateImageUtils {
         return DestImage;
     }
 
-    /**合并任数量的图片成一张图片
-     * @param isHorizontal true代表水平合并,fasle代表垂直合并
-     * @param imgs 欲合并的图片数组
+    /**
+     * 合并任数量的图片成一张图片
+     *
+     * @param isHorizontal true代表水平合并,false代表垂直合并
+     * @param imgs         欲合并的图片数组
      * @return
      * @throws IOException
      */
@@ -130,11 +139,9 @@ public class OperateImageUtils {
             if (img.getWidth() > allwMax) {
                 allwMax = img.getWidth();
             }
-            ;
             if (img.getHeight() > allhMax) {
                 allhMax = img.getHeight();
             }
-            ;
         }
         // 创建新图片
         if (isHorizontal) {
@@ -165,6 +172,7 @@ public class OperateImageUtils {
 
     /**
      * 合并图片成指定行数和列数的网格
+     *
      * @param rows 行数
      * @param cols 列数
      * @param imgs 欲合并的图片数组
@@ -205,12 +213,73 @@ public class OperateImageUtils {
         return destImage;
     }
 
+    public static void createMp4(String mp4SavePath, Map<Integer, File> imgMap, int width, int height) throws FrameRecorder.Exception {
+        // 调整宽度和高度为偶数
+        width = width % 2 == 0 ? width : width + 1;
+        height = height % 2 == 0 ? height : height + 1;
+
+        // 视频宽高最好是按照常见的视频的宽高  16:9  或者 9:16
+        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
+        // 设置视频编码层模式
+        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
+        // 设置视频为25帧每秒
+        recorder.setFrameRate(25);
+        // 设置视频图像数据格式
+        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
+
+        recorder.setFormat("mp4");
+        try {
+            recorder.start();
+            Java2DFrameConverter converter = new Java2DFrameConverter();
+            // 录制一个22秒的视频,22秒为自定义的一个视频时间长度,图片少则在22秒内,多则到22秒停止
+            for (int i = 0; i < 22; i++) {
+                if (imgMap.containsKey(i)) {
+                    BufferedImage read = ImageIO.read(imgMap.get(i));
+                    // 调整图片尺寸为偶数
+                    read = resizeToEven(read);
+                    // 转换图像颜色模式为 TYPE_3BYTE_BGR
+                    BufferedImage bgrImage = new BufferedImage(read.getWidth(), read.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+                    java.awt.Graphics g = bgrImage.getGraphics();
+                    g.drawImage(read, 0, 0, null);
+                    g.dispose();
+
+                    // 一秒是25帧 所以要记录25次
+                    for (int j = 0; j < 25; j++) {
+                        recorder.record(converter.getFrame(bgrImage));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            // 最后一定要结束并释放资源
+            recorder.stop();
+            recorder.release();
+        }
+    }
+
+    public static BufferedImage resizeToEven(BufferedImage image) {
+        int width = image.getWidth();
+        int height = image.getHeight();
+        width = width % 2 == 0 ? width : width + 1;
+        height = height % 2 == 0 ? height : height + 1;
+        java.awt.Image tmp = image.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH);
+        BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        java.awt.Graphics2D g2d = resized.createGraphics();
+        g2d.drawImage(tmp, 0, 0, null);
+        g2d.dispose();
+        return resized;
+    }
+
+
     public static void main(String[] args) {
         try {
             // 读取待合并的文件
             BufferedImage[] imgs = new BufferedImage[9];
+            Map<Integer, File> imgMap = new HashMap<>();
             for (int i = 0; i < 9; i++) {
                 imgs[i] = getBufferedImage("D:\\edge下载\\下载\\1_2019年-2024年数据\\" + (i + 1) + ".jpg");
+                imgMap.put(i, new File("D:\\edge下载\\下载\\1_2019年-2024年数据\\" + (i + 1) + ".jpg"));
             }
 
             // 合并成九宫格
@@ -245,6 +314,13 @@ public class OperateImageUtils {
             saveImage(multiVerticalMergedImg, "D:\\edge下载\\下载\\1_2019年-2024年数据\\", "multiVerticalMerge.jpg", "jpg");
             System.out.println("多张图片垂直合并完毕!");
 
+            // 测试图片转视频
+            int width = imgs[0].getWidth();
+            int height = imgs[0].getHeight();
+            String mp4SavePath = "D:\\edge下载\\下载\\1_2019年-2024年数据\\output.mp4";
+            createMp4(mp4SavePath, imgMap, width, height);
+            System.out.println("MP4视频生成完毕!");
+
         } catch (IOException e) {
             e.printStackTrace();
         }
diff --git a/bs-ui/src/views/system/tag/index.vue b/bs-ui/src/views/system/tag/index.vue
index a6b7dc6..5dcaf7e 100644
--- a/bs-ui/src/views/system/tag/index.vue
+++ b/bs-ui/src/views/system/tag/index.vue
@@ -90,6 +90,11 @@
           <dict-tag :options="dict.type.sys_yes_no" :value="scope.row.isPhoto"/>
         </template>
       </el-table-column>
+      <el-table-column label="标签类型" align="center" prop="tagType" sortable='custom'>
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.tag_type" :value="scope.row.tagType"/>
+        </template>
+      </el-table-column>
       <el-table-column label="拍照数量要求" align="center" prop="photoNum" sortable='custom'/>
       <el-table-column label="照片存放目录" align="center" prop="saveDir" sortable='custom'>
         <template slot-scope="scope">
@@ -131,38 +136,64 @@
     <!-- 添加或修改标签信息对话框 -->
     <el-dialog :title="title" :visible.sync="open" width="70%" append-to-body :close-on-click-modal="false">
       <el-form ref="form" :model="form" :rules="rules" label-width="120px">
-        <el-row :gutter="20">
+        <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="标签名称" prop="tagName">
-              <el-input v-model="form.tagName" placeholder="请输入标签名称"/>
+              <el-input
+                v-model="form.tagName"
+                placeholder="请输入标签名称"
+                clearable
+              />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="是否需要拍照" prop="isPhoto">
-              <el-radio-group v-model="form.isPhoto">
+              <el-radio-group v-model="form.isPhoto" class="radio-group">
                 <el-radio
                   v-for="dict in dict.type.sys_yes_no"
                   :key="dict.value"
                   :label="dict.value"
-                >{{dict.label}}</el-radio>
+                  style="margin-right: 16px;"
+                >{{ dict.label }}</el-radio>
               </el-radio-group>
-<!--              <el-input v-model="form.isPhoto" placeholder="请输入是否需要拍照"/>-->
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <!-- 标签类型 + 拍照数量要求 -->
+        <el-row :gutter="24">
+          <el-col :span="12">
+            <el-form-item label="标签类型">
+              <el-select
+                v-model="form.tagType"
+                placeholder="请选择"
+                clearable
+                filterable
+                style="width: 100%;"
+              >
+                <el-option
+                  v-for="dict in dict.type.tag_type"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="拍照数量要求" prop="photoNum">
               <el-input-number
                 v-model="form.photoNum"
-                style="width: 100%;"
                 placeholder="请输入拍照数量要求"
                 :min="0"
                 :step="1"
+                style="width: 100%;"
               />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+        </el-row>
+        <!-- 照片存放目录(单独一行, span=24) -->
+        <el-row :gutter="24">
+          <el-col :span="24">
             <el-form-item label="照片存放目录" prop="saveDir">
               <treeselect
                 v-model="form.saveDir"
@@ -170,31 +201,45 @@
                 :normalizer="normalizer"
                 placeholder="选择照片存放目录"
                 :allow-empty="true"
+                style="width: 100%;"
               />
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <!-- 标签说明(文本域,单独一行) -->
+        <el-row :gutter="24">
           <el-col :span="24">
             <el-form-item label="标签说明" prop="tagDesc">
-              <el-input v-model="form.tagDesc" type="textarea" placeholder="请输入内容"/>
+              <el-input
+                v-model="form.tagDesc"
+                type="textarea"
+                placeholder="请输入内容"
+                :rows="3"
+              />
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <!-- 备注(文本域,单独一行) -->
+        <el-row :gutter="24">
           <el-col :span="24">
             <el-form-item label="备注" prop="remarks">
-              <el-input v-model="form.remarks" type="textarea" placeholder="请输入内容"/>
+              <el-input
+                v-model="form.remarks"
+                type="textarea"
+                placeholder="请输入内容"
+                :rows="3"
+              />
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="20">
+        <!-- 附件(单独一行) -->
+        <el-row :gutter="24">
           <el-col :span="24">
             <el-form-item label="附件" prop="files">
               <FileUpload
                 v-model="form.files"
                 accept=".xls,.xlsx,.doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.ppt,.pptx"
-              ></FileUpload>
+              />
             </el-form-item>
           </el-col>
         </el-row>
@@ -237,7 +282,7 @@ export default {
     Treeselect
   },
   name: "Tag",
-  dicts: ['sys_yes_no'],
+  dicts: ['sys_yes_no','tag_type'],
   data() {
     return {
       // 遮罩层
@@ -285,7 +330,6 @@ export default {
   },
   created() {
     this.getList();
-
     listCata().then(res => {
       const list = res.data || [];
       this.cates = list;
@@ -377,14 +421,14 @@ export default {
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
-          const api = this.form.id != null ? updateTag : addTag;
+          const api = this.form.id != null? updateTag : addTag;
           api(this.form)
             .then(response => {
-              this.$modal.msgSuccess(this.form.id != null ? "修改成功" : "新增成功");
+              this.$modal.msgSuccess(this.form.id != null? "修改成功" : "新增成功");
               this.open = false;
               this.getList();
             })
-            .catch(this.handleApiError);
+            .catch();
         }
       });
     },
diff --git a/bs-ui/src/views/task-distribut/send/other-task/components/AuditDialog.vue b/bs-ui/src/views/task-distribut/send/other-task/components/AuditDialog.vue
index 962a58d..cba6176 100644
--- a/bs-ui/src/views/task-distribut/send/other-task/components/AuditDialog.vue
+++ b/bs-ui/src/views/task-distribut/send/other-task/components/AuditDialog.vue
@@ -3,7 +3,7 @@
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     :visible.sync="dialogVisible"
-    :title="`我的任务${isAudit ? '审核':'反馈'}`"
+    :title="`任务${isAudit ? '审核':'反馈'}`"
     width="80%"
   >
     <div style="height: 70vh;width:100%;overflow-y: scroll;overflow-x: hidden;"  v-loading="loading">
diff --git a/bs-ui/src/views/task-distribut/send/other-task/components/EditDialog.vue b/bs-ui/src/views/task-distribut/send/other-task/components/EditDialog.vue
index 8f2a916..feb902a 100644
--- a/bs-ui/src/views/task-distribut/send/other-task/components/EditDialog.vue
+++ b/bs-ui/src/views/task-distribut/send/other-task/components/EditDialog.vue
@@ -37,7 +37,7 @@
                 :value="dict.value"
               />
             </el-select>
-            
+
           </el-form-item>
           <el-row :gutter="10">
             <el-col :span="12">
@@ -73,7 +73,7 @@
             >
             </el-input>
           </el-form-item>
-          
+
           <el-button
             v-if="editAble"
             type="primary"
@@ -134,7 +134,7 @@
                 >
                   <el-link type="warning" slot="reference">移除</el-link>
                 </el-popconfirm>
-                
+
               </template>
             </el-table-column>
           </el-table>
@@ -294,7 +294,7 @@ export default {
       startDate: null,
       endDate: null,
     },
-    title: "其他任务",
+    title: "任务",
     updateApi: updateInfo,
     addApi: addInfo,
   },