8 files modified
3 files added
1005 ■■■■■ changed files
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java 228 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/productPoint/AiProductPointController.java 41 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/entity/AiProductPoint.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/service/impl/AiProductPointServiceImpl.java 4 ●●●● patch | view | raw | blame | history
src/main/resources/static/febs/videoNative.html 243 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html 34 ●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/fileUpload/videoNative.html 243 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/fileUpload/videoPlayer.html 146 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/productPoint/add.html 22 ●●●●● patch | view | raw | blame | history
src/main/resources/templates/febs/views/modules/ai/productPoint/info.html 25 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
@@ -8,6 +8,7 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
@@ -28,11 +29,11 @@
@RequestMapping(value = "/fileUpload")
public class FileUploadController extends BaseController {
    // @Value("${file.upload.dir}")
    private String uploadDir = "d:/upload/files";
    // 基础上传目录
    public static String baseUploadDir = "/home/javaweb/webresource/ai/file";
    // @Value("${file.chunk.dir}")
    private String chunkDir = "d:/upload/chunks";
    // 分片存储目录
    public static String baseChunkDir = "/home/javaweb/webresource/ai/file/chunks";
    /**
     * 上传文件分片
@@ -44,6 +45,9 @@
                                    @RequestParam("chunks") int chunks,
                                    @RequestParam("fileMd5") String fileMd5) {
        try {
            String companyId = getCurrentUserCompanyId();
            // 构建公司专属分片目录
            String chunkDir = baseChunkDir + "/" + companyId;
            // 确保分片目录存在
            Path chunkPath = Paths.get(chunkDir, fileMd5);
            if (!Files.exists(chunkPath)) {
@@ -69,6 +73,10 @@
                                    @RequestParam("fileMd5") String fileMd5,
                                    @RequestParam("chunks") int chunks) {
        try {
            String companyId = getCurrentUserCompanyId();
            // 构建公司专属上传目录
            String uploadDir = baseUploadDir + "/" + companyId;
            // 确保上传目录存在
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
@@ -76,9 +84,11 @@
            }
            // 生成唯一文件名
            String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
            String uniqueFileName = fileName+ "_" + UUID.randomUUID().toString() ;
            Path targetFilePath = uploadPath.resolve(uniqueFileName);
            // 构建公司专属分片目录
            String chunkDir = baseChunkDir + "/" + companyId;
            // 确保分片目录存在
            Path chunkPath = Paths.get(chunkDir, fileMd5);
            if (!Files.exists(chunkPath)) {
@@ -116,31 +126,205 @@
     * 播放视频文件
     */
    @GetMapping("/play/{fileName}")
    public void playVideo(@PathVariable("fileName") String fileName, HttpServletResponse response) {
    public void playVideo(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
        log.info("开始播放视频文件: {}", fileName);
        // 配置参数
        final int BUFFER_SIZE = 1024 * 1024; // 1MB缓冲区,提高I/O性能
        final long MAX_INITIAL_SEGMENT = 1024 * 1024 * 15; // 最大初始片段大小(15MB),约30秒中等码率视频
        try {
            String companyId = getCurrentUserCompanyId();
            String uploadDir = baseUploadDir + "/" + companyId;
            Path filePath = Paths.get(uploadDir, fileName);
            // 检查文件是否存在
            if (!Files.exists(filePath)) {
                log.warn("视频文件不存在: {}", filePath);
                response.setStatus(HttpStatus.NOT_FOUND.value());
                return;
            }
            // 检查文件是否为常规文件
            if (!Files.isRegularFile(filePath)) {
                log.warn("路径不是常规文件: {}", filePath);
                response.setStatus(HttpStatus.BAD_REQUEST.value());
                return;
            }
            // 设置响应头
            response.setContentType("video/mp4");
            response.setContentLengthLong(Files.size(filePath));
            response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
            // 获取文件大小
            long fileSize = Files.size(filePath);
            log.debug("视频文件大小: {} bytes", fileSize);
            // 检查是否有Range请求头
            String rangeHeader = request.getHeader("Range");
            if (rangeHeader != null) {
                log.debug("收到Range请求: {}", rangeHeader);
                // 解析Range头,格式为"bytes=start-end"
                String[] ranges = rangeHeader.replace("bytes=", "").split("-");
                long start = 0, end = fileSize - 1;
                try {
                    if (!ranges[0].isEmpty()) {
                        start = Long.parseLong(ranges[0]);
                    }
                    if (ranges.length > 1 && !ranges[1].isEmpty()) {
                        end = Long.parseLong(ranges[1]);
                    } else {
                        // 如果没有指定结束位置,返回从开始位置到文件末尾的内容
                        // 但限制最大长度,避免一次性传输过大的内容
                        end = Math.min(start + BUFFER_SIZE * 10, fileSize - 1);
                    }
                } catch (NumberFormatException e) {
                    log.error("无效的Range头格式: {}", rangeHeader, e);
                    response.setStatus(HttpStatus.BAD_REQUEST.value());
                    return;
                }
                // 确保范围有效
                if (start > end || start >= fileSize) {
                    log.warn("无效的Range范围: {}-{},文件大小: {}", start, end, fileSize);
                    response.setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
                    response.setHeader("Content-Range", "bytes */" + fileSize);
                    return;
                }
                // 限制结束位置不超过文件大小
                if (end >= fileSize) {
                    end = fileSize - 1;
                }
                // 计算内容长度
                long contentLength = end - start + 1;
                log.debug("处理Range请求: {}-{},长度: {}", start, end, contentLength);
                // 设置响应状态和头部信息
                response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
                response.setContentType(getContentTypeByExtension(fileName));
                response.setContentLengthLong(contentLength);
                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
                response.setHeader("Accept-Ranges", "bytes");
                response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
                response.setHeader("Cache-Control", "public, max-age=3600"); // 添加缓存控制
                // 使用NIO提高读取性能
                try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
                     OutputStream outputStream = response.getOutputStream()) {
                    randomAccessFile.seek(start);
                    byte[] buffer = new byte[BUFFER_SIZE];
                    long remaining = contentLength;
                    int bytesRead;
                    while (remaining > 0 && (bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                        remaining -= bytesRead;
                        // 定期刷新输出流,避免缓冲区溢出
                        if (remaining % (BUFFER_SIZE * 4) == 0) {
                            outputStream.flush();
                        }
                    }
                    // 确保所有数据都被写入
                    outputStream.flush();
                    log.debug("Range请求处理完成: {}-{}", start, end);
                }
            } else {
                log.debug("收到完整文件请求,返回初始片段");
                // 没有Range头,返回文件大小的10分之一和15MB中较小的那个
                long oneTenthSize = fileSize / 10;
                // 计算10分之一和15MB的较小值
                long maxInitialSize = Math.min(oneTenthSize, MAX_INITIAL_SEGMENT);
                // 确保至少返回1字节,且不超过文件大小
                long actualEnd = Math.min(Math.max(maxInitialSize, 1), fileSize - 1);
                log.debug("返回初始片段: {} bytes (10分之一大小: {} bytes, 15MB限制: {} bytes, 文件总大小: {} bytes)",
                          actualEnd + 1, oneTenthSize, MAX_INITIAL_SEGMENT, fileSize);
            // 流式传输文件
            try (InputStream inputStream = Files.newInputStream(filePath);
                 OutputStream outputStream = response.getOutputStream()) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                // 设置响应状态和头部信息
                response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); // 使用206状态码,因为只返回部分内容
                response.setContentType(getContentTypeByExtension(fileName));
                response.setContentLengthLong(actualEnd + 1);
                response.setHeader("Content-Range", "bytes 0-" + actualEnd + "/" + fileSize);
                response.setHeader("Accept-Ranges", "bytes");
                response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
                response.setHeader("Cache-Control", "public, max-age=3600"); // 添加缓存控制
                // 流式传输文件的初始片段
                try (InputStream inputStream = Files.newInputStream(filePath);
                     OutputStream outputStream = response.getOutputStream()) {
                    byte[] buffer = new byte[BUFFER_SIZE];
                    long written = 0;
                    int bytesRead;
                    while (written <= actualEnd && (bytesRead = inputStream.read(buffer)) != -1) {
                        long toWrite = Math.min(bytesRead, actualEnd + 1 - written);
                        outputStream.write(buffer, 0, (int)toWrite);
                        written += toWrite;
                        // 定期刷新输出流
                        if (written % (BUFFER_SIZE * 4) == 0) {
                            outputStream.flush();
                        }
                        // 如果已经写完初始片段,则停止
                        if (written > actualEnd) {
                            break;
                        }
                    }
                    // 确保所有数据都被写入
                    outputStream.flush();
                    log.debug("初始片段传输完成,大小: {} bytes", written);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            log.error("播放视频文件时发生错误: {}", fileName, e);
            try {
                // 确保响应状态被设置
                if (!response.isCommitted()) {
                    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                }
            } catch (Exception ex) {
                log.error("设置响应状态时发生错误", ex);
            }
        } finally {
            log.info("视频播放请求处理完成: {}", fileName);
        }
    }
    /**
     * 根据文件扩展名获取MIME类型
     */
    private String getContentTypeByExtension(String fileName) {
        String extension = "";
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex > 0) {
            extension = fileName.substring(lastDotIndex).toLowerCase();
        }
        switch (extension) {
            case ".mp4":
                return "video/mp4";
            case ".avi":
                return "video/x-msvideo";
            case ".mov":
                return "video/quicktime";
            case ".wmv":
                return "video/x-ms-wmv";
            case ".flv":
                return "video/x-flv";
            case ".webm":
                return "video/webm";
            case ".mkv":
                return "video/x-matroska";
            case ".m3u8":
                return "application/vnd.apple.mpegurl";
            case ".ts":
                return "video/MP2T";
            default:
                return "application/octet-stream";
        }
    }
@@ -150,6 +334,9 @@
    @GetMapping("/list")
    public FebsResponse getFileList() {
        try {
            String companyId = getCurrentUserCompanyId();
            String uploadDir = baseUploadDir + "/" + companyId;
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                return new FebsResponse().data(new ArrayList<>());
@@ -183,6 +370,9 @@
    @PostMapping("/delete")
    public FebsResponse deleteFile(@RequestParam("fileName") String fileName) {
        try {
            String companyId = getCurrentUserCompanyId();
            String uploadDir = baseUploadDir + "/" + companyId;
            Path filePath = Paths.get(uploadDir, fileName);
            if (Files.exists(filePath)) {
                Files.delete(filePath);
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java
@@ -24,4 +24,18 @@
        return FebsUtil.view("modules/ai/fileUpload/index");
    }
    @GetMapping("videoPlayer")
    @RequiresPermissions("videoPlayer:index")
    public String videoPlayer() {
        return FebsUtil.view("modules/ai/fileUpload/videoPlayer");
    }
    @GetMapping("videoNative")
    @RequiresPermissions("videoNative:index")
    public String videoNative() {
        return FebsUtil.view("modules/ai/fileUpload/videoNative");
    }
}
src/main/java/cc/mrbird/febs/ai/controller/productPoint/AiProductPointController.java
@@ -1,5 +1,7 @@
package cc.mrbird.febs.ai.controller.productPoint;
import cc.mrbird.febs.ai.controller.fileUpload.FileUploadController;
import cc.mrbird.febs.ai.entity.AiProductCategory;
import cc.mrbird.febs.ai.entity.AiProductPoint;
import cc.mrbird.febs.ai.service.AiProductPointService;
import cc.mrbird.febs.common.annotation.ControllerEndpoint;
@@ -13,6 +15,13 @@
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
@@ -67,4 +76,36 @@
        String companyId = getCurrentUserCompanyId();
        return new FebsResponse().success().data(service.pointTree(companyId));
    }
    @GetMapping("fileList/parent")
    @ControllerEndpoint(exceptionMessage = "获取文件列表失败")
    public List<FileUploadController.FileInfo> parent(){
        List<FileUploadController.FileInfo> list = new ArrayList<>();
        try {
            String companyId = getCurrentUserCompanyId();
            String uploadDir = FileUploadController.baseUploadDir + "/" + companyId;
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                return list;
            }
            Files.list(uploadPath).forEach(path -> {
                if (Files.isRegularFile(path)) {
                    try {
                        FileUploadController.FileInfo fileInfo = new FileUploadController.FileInfo();
                        fileInfo.setFileName(path.getFileName().toString());
                        list.add(fileInfo);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            return list;
        }
    }
}
src/main/java/cc/mrbird/febs/ai/entity/AiProductPoint.java
@@ -54,6 +54,11 @@
     */
    private String description;
    /**
     * 视屏名称
     */
    private String videoName;
    @TableField(exist = false)
    private String productCategoryName;
}
src/main/java/cc/mrbird/febs/ai/service/impl/AiProductPointServiceImpl.java
@@ -81,11 +81,14 @@
        entity.setCompanyId(dto.getCompanyId());
        entity.setIsNormal(dto.getIsNormal() );
        entity.setFinderUserName(dto.getFinderUserName());
        entity.setFeedId(dto.getFeedId());
        entity.setFeedImg(dto.getFeedImg());
        entity.setTitle(dto.getTitle());
        entity.setVideoName(dto.getVideoName());
        entity.setDescription(dto.getDescription());
        entity.setCreatedTime(new Date());
        this.save(entity);
        return new FebsResponse().success().message("操作成功");
    }
@@ -104,6 +107,7 @@
                            .set(AiProductPoint::getTitle, dto.getTitle())
                            .set(AiProductPoint::getFeedImg, dto.getFeedImg())
                            .set(AiProductPoint::getDescription, dto.getDescription())
                            .set(AiProductPoint::getVideoName, dto.getVideoName())
                            .set(AiProductPoint::getUpdatedTime, new Date())
                            .eq(AiProductPoint::getId, id)
            );
src/main/resources/static/febs/videoNative.html
New file
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>视频播放器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background-color: #f0f2f5;
            color: #333;
            line-height: 1.6;
        }
        .container {
            max-width: 900px;
            margin: 30px auto;
            padding: 30px;
            background: #f8f8f8;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            margin-bottom: 40px;
        }
        .header h2 {
            color: #333;
            margin-bottom: 10px;
            font-size: 24px;
        }
        .header p {
            color: #666;
            font-size: 14px;
        }
        .play-container {
            margin-top: 40px;
            padding: 30px;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .play-container h3 {
            margin-bottom: 20px;
            color: #333;
            font-size: 20px;
        }
        video {
            width: 100%;
            max-width: 800px;
            height: auto;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .control-panel {
            margin-top: 20px;
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .input-group {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        input[type="text"] {
            width: 400px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #1E9FFF;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #009688;
        }
        .message {
            margin-top: 10px;
            padding: 10px;
            border-radius: 4px;
            font-size: 14px;
        }
        .message.success {
            background-color: #f0f9ff;
            color: #1E9FFF;
            border-left: 4px solid #1E9FFF;
        }
        .message.error {
            background-color: #fff2f0;
            color: #ff4d4f;
            border-left: 4px solid #ff4d4f;
        }
        @media (max-width: 768px) {
            .container {
                margin: 20px;
                padding: 20px;
            }
            input[type="text"] {
                width: 100%;
                max-width: 300px;
            }
            .control-panel {
                flex-direction: column;
                align-items: flex-start;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2>视频播放器</h2>
            <p>支持视频文件播放和跳跃播放</p>
        </div>
        <!-- 播放区域 -->
        <div class="play-container">
            <h3>视频播放</h3>
            <video id="videoPlayer" controls width="100%" height="auto">
                <source id="videoSource" src="" type="video/mp4">
                您的浏览器不支持视频播放
            </video>
            <div class="control-panel">
                <div class="input-group">
                    <input type="text" id="videoUrl" placeholder="请输入视频播放地址">
                    <button id="loadVideo">加载视频</button>
                </div>
            </div>
            <div id="message" class="message" style="display: none;"></div>
        </div>
    </div>
    <script>
        // 初始化
        function init() {
            // 绑定加载视频按钮事件
            document.getElementById('loadVideo').addEventListener('click', function() {
                var videoUrl = document.getElementById('videoUrl').value.trim();
                if (!videoUrl) {
                    showMessage('请输入视频播放地址', 'error');
                    return;
                }
                loadVideo(videoUrl);
            });
        }
        // 加载视频
        function loadVideo(videoUrl) {
            var videoPlayer = document.getElementById('videoPlayer');
            var videoSource = document.getElementById('videoSource');
            // 根据文件扩展名设置正确的MIME类型
            var extension = videoUrl.split('.').pop().toLowerCase();
            var mimeType = 'video/mp4';
            switch(extension) {
                case 'avi':
                    mimeType = 'video/x-msvideo';
                    break;
                case 'mov':
                    mimeType = 'video/quicktime';
                    break;
                case 'wmv':
                    mimeType = 'video/x-ms-wmv';
                    break;
                case 'flv':
                    mimeType = 'video/x-flv';
                    break;
                case 'webm':
                    mimeType = 'video/webm';
                    break;
                case 'mkv':
                    mimeType = 'video/x-matroska';
                    break;
            }
            videoSource.src = videoUrl;
            videoSource.type = mimeType;
            videoPlayer.load();
            showMessage('视频加载中...', 'success');
            // 视频加载完成事件
            videoPlayer.addEventListener('loadedmetadata', function() {
                showMessage('视频加载完成', 'success');
            });
            // 视频加载错误事件
            videoPlayer.addEventListener('error', function() {
                showMessage('视频加载失败,请检查地址是否正确', 'error');
            });
        }
        // 显示消息
        function showMessage(text, type) {
            var messageDiv = document.getElementById('message');
            messageDiv.textContent = text;
            messageDiv.className = 'message ' + type;
            messageDiv.style.display = 'block';
            // 3秒后自动隐藏
            setTimeout(function() {
                messageDiv.style.display = 'none';
            }, 3000);
        }
        // 页面加载完成后初始化
        window.onload = init;
    </script>
</body>
</html>
src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html
@@ -539,15 +539,12 @@
                    },
                    success: function(response) {
                        if (response.code === 200 || response.success) {
                            layer.msg('文件上传成功,页面将刷新', {icon: 1, time: 1000});
                            layer.msg('文件上传成功', {icon: 1});
                            $('#uploadStatus').text('上传成功');
                            $('#uploadProgress').text('100%');
                            $('#progressFill').css('width', '100%').text('100%');
                            // 刷新页面
                            setTimeout(function() {
                                location.reload();
                            }, 1000);
                            $('#playContainer').show();
                            refreshFileList();
                        } else {
                            layer.msg('文件合并失败: ' + response.message, {icon: 2});
                            $('#uploadStatus').text('上传失败');
@@ -632,7 +629,32 @@
                                    var videoPlayer = document.getElementById('videoPlayer');
                                    var videoSource = document.getElementById('videoSource');
                                    // 根据文件扩展名设置正确的MIME类型
                                    var extension = fileName.split('.').pop().toLowerCase();
                                    var mimeType = 'video/mp4';
                                    switch(extension) {
                                        case 'avi':
                                            mimeType = 'video/x-msvideo';
                                            break;
                                        case 'mov':
                                            mimeType = 'video/quicktime';
                                            break;
                                        case 'wmv':
                                            mimeType = 'video/x-ms-wmv';
                                            break;
                                        case 'flv':
                                            mimeType = 'video/x-flv';
                                            break;
                                        case 'webm':
                                            mimeType = 'video/webm';
                                            break;
                                        case 'mkv':
                                            mimeType = 'video/x-matroska';
                                            break;
                                    }
                                    videoSource.src = '/fileUpload/play/' + encodeURIComponent(fileName);
                                    videoSource.type = mimeType;
                                    videoPlayer.load();
                                    videoPlayer.play();
                                });
src/main/resources/templates/febs/views/modules/ai/fileUpload/videoNative.html
New file
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>视频播放器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background-color: #f0f2f5;
            color: #333;
            line-height: 1.6;
        }
        .container {
            max-width: 900px;
            margin: 30px auto;
            padding: 30px;
            background: #f8f8f8;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            margin-bottom: 40px;
        }
        .header h2 {
            color: #333;
            margin-bottom: 10px;
            font-size: 24px;
        }
        .header p {
            color: #666;
            font-size: 14px;
        }
        .play-container {
            margin-top: 40px;
            padding: 30px;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .play-container h3 {
            margin-bottom: 20px;
            color: #333;
            font-size: 20px;
        }
        video {
            width: 100%;
            max-width: 800px;
            height: auto;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .control-panel {
            margin-top: 20px;
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .input-group {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        input[type="text"] {
            width: 400px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #1E9FFF;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #009688;
        }
        .message {
            margin-top: 10px;
            padding: 10px;
            border-radius: 4px;
            font-size: 14px;
        }
        .message.success {
            background-color: #f0f9ff;
            color: #1E9FFF;
            border-left: 4px solid #1E9FFF;
        }
        .message.error {
            background-color: #fff2f0;
            color: #ff4d4f;
            border-left: 4px solid #ff4d4f;
        }
        @media (max-width: 768px) {
            .container {
                margin: 20px;
                padding: 20px;
            }
            input[type="text"] {
                width: 100%;
                max-width: 300px;
            }
            .control-panel {
                flex-direction: column;
                align-items: flex-start;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2>视频播放器</h2>
            <p>支持视频文件播放和跳跃播放</p>
        </div>
        <!-- 播放区域 -->
        <div class="play-container">
            <h3>视频播放</h3>
            <video id="videoPlayer" controls width="100%" height="auto">
                <source id="videoSource" src="" type="video/mp4">
                您的浏览器不支持视频播放
            </video>
            <div class="control-panel">
                <div class="input-group">
                    <input type="text" id="videoUrl" placeholder="请输入视频播放地址">
                    <button id="loadVideo">加载视频</button>
                </div>
            </div>
            <div id="message" class="message" style="display: none;"></div>
        </div>
    </div>
    <script>
        // 初始化
        function init() {
            // 绑定加载视频按钮事件
            document.getElementById('loadVideo').addEventListener('click', function() {
                var videoUrl = document.getElementById('videoUrl').value.trim();
                if (!videoUrl) {
                    showMessage('请输入视频播放地址', 'error');
                    return;
                }
                loadVideo(videoUrl);
            });
        }
        // 加载视频
        function loadVideo(videoUrl) {
            var videoPlayer = document.getElementById('videoPlayer');
            var videoSource = document.getElementById('videoSource');
            // 根据文件扩展名设置正确的MIME类型
            var extension = videoUrl.split('.').pop().toLowerCase();
            var mimeType = 'video/mp4';
            switch(extension) {
                case 'avi':
                    mimeType = 'video/x-msvideo';
                    break;
                case 'mov':
                    mimeType = 'video/quicktime';
                    break;
                case 'wmv':
                    mimeType = 'video/x-ms-wmv';
                    break;
                case 'flv':
                    mimeType = 'video/x-flv';
                    break;
                case 'webm':
                    mimeType = 'video/webm';
                    break;
                case 'mkv':
                    mimeType = 'video/x-matroska';
                    break;
            }
            videoSource.src = videoUrl;
            videoSource.type = mimeType;
            videoPlayer.load();
            showMessage('视频加载中...', 'success');
            // 视频加载完成事件
            videoPlayer.addEventListener('loadedmetadata', function() {
                showMessage('视频加载完成', 'success');
            });
            // 视频加载错误事件
            videoPlayer.addEventListener('error', function() {
                showMessage('视频加载失败,请检查地址是否正确', 'error');
            });
        }
        // 显示消息
        function showMessage(text, type) {
            var messageDiv = document.getElementById('message');
            messageDiv.textContent = text;
            messageDiv.className = 'message ' + type;
            messageDiv.style.display = 'block';
            // 3秒后自动隐藏
            setTimeout(function() {
                messageDiv.style.display = 'none';
            }, 3000);
        }
        // 页面加载完成后初始化
        window.onload = init;
    </script>
</body>
</html>
src/main/resources/templates/febs/views/modules/ai/fileUpload/videoPlayer.html
New file
@@ -0,0 +1,146 @@
<div class="layui-fluid layui-anim febs-anim" id="febs-video-player" lay-title="视频播放器">
    <div class="layui-row febs-container">
        <div class="layui-col-md12">
            <div class="layui-fluid">
                <div class="upload-container">
                    <div class="upload-header">
                        <h2>视频播放器</h2>
                        <p>支持视频文件播放和跳跃播放</p>
                    </div>
                    <!-- 播放区域 -->
                    <div class="play-container" id="playContainer">
                        <h3 style="margin-bottom: 20px; color: #333;">视频播放</h3>
                        <video id="videoPlayer" controls width="100%" height="auto" style="max-width: 800px;">
                            <source id="videoSource" src="" type="video/mp4">
                            您的浏览器不支持视频播放
                        </video>
                        <div class="layui-row layui-col-space10" style="margin-top: 20px;">
                            <div class="layui-col-xs12">
                                <div class="layui-input-inline" style="width: 400px;">
                                    <input type="text" id="videoUrl" placeholder="请输入视频播放地址" class="layui-input">
                                </div>
                                <button class="layui-btn layui-btn-normal" id="loadVideo">加载视频</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<style>
    .upload-container {
        max-width: 900px;
        margin: 30px auto;
        padding: 30px;
        background: #f8f8f8;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .upload-header {
        text-align: center;
        margin-bottom: 40px;
    }
    .upload-header h2 {
        color: #333;
        margin-bottom: 10px;
        font-size: 24px;
    }
    .upload-header p {
        color: #666;
        font-size: 14px;
    }
    .play-container {
        margin-top: 40px;
        padding: 30px;
        background: #fff;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .play-container h3 {
        margin-bottom: 20px;
        color: #333;
        font-size: 20px;
    }
    video {
        width: 100%;
        max-width: 800px;
        height: auto;
        border-radius: 4px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    @media (max-width: 768px) {
        .upload-container {
            margin: 20px;
            padding: 20px;
        }
    }
</style>
<script data-th-inline="javascript">
    layui.use(['layer', 'jquery'], function() {
        var layer = layui.layer,
        $ = layui.jquery;
        // 初始化
        init();
        function init() {
            // 绑定加载视频按钮事件
            $('#loadVideo').click(function() {
                var videoUrl = $('#videoUrl').val().trim();
                if (!videoUrl) {
                    layer.msg('请输入视频播放地址', {icon: 2});
                    return;
                }
                loadVideo(videoUrl);
            });
        }
        // 加载视频
        function loadVideo(videoUrl) {
            var videoPlayer = document.getElementById('videoPlayer');
            var videoSource = document.getElementById('videoSource');
            // 根据文件扩展名设置正确的MIME类型
            var extension = videoUrl.split('.').pop().toLowerCase();
            var mimeType = 'video/mp4';
            switch(extension) {
                case 'avi':
                    mimeType = 'video/x-msvideo';
                    break;
                case 'mov':
                    mimeType = 'video/quicktime';
                    break;
                case 'wmv':
                    mimeType = 'video/x-ms-wmv';
                    break;
                case 'flv':
                    mimeType = 'video/x-flv';
                    break;
                case 'webm':
                    mimeType = 'video/webm';
                    break;
                case 'mkv':
                    mimeType = 'video/x-matroska';
                    break;
            }
            videoSource.src = videoUrl;
            videoSource.type = mimeType;
            videoPlayer.load();
            layer.msg('视频加载中...', {icon: 16, time: 0}, function() {
                layer.closeAll();
                layer.msg('视频加载完成', {icon: 1});
            });
        }
    });
</script>
src/main/resources/templates/febs/views/modules/ai/productPoint/add.html
@@ -20,6 +20,16 @@
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label">视屏选择:</label>
                                        <div class="layui-input-block">
                                            <select name="videoName" class="video-add-productCategory">
                                                <option value="">请选择</option>
                                            </select>
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">类型:</label>
                                        <div class="layui-input-block">
                                            <select name="isNormal" class="point-type" lay-filter="point-type-select">
@@ -192,6 +202,18 @@
            }
        });
        //(下拉框)
        $.get(ctx + 'admin/productPoint/fileList/parent', function (data) {
            for (var k in data)
            {
                $(".video-add-productCategory").append("<option value='" + data[k].fileName + "'>" + data[k].fileName + "</option>");
            }
            layui.use('form', function () {
                var form = layui.form;
                form.render();
            });
        });
        form.on('submit(productPoint-add-form-submit)', function (data) {
            data.field.description = editor.txt.html();
            $.ajax({
src/main/resources/templates/febs/views/modules/ai/productPoint/info.html
@@ -19,6 +19,17 @@
                                        </select>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label">视屏选择:</label>
                                        <div class="layui-input-block">
                                            <select name="videoName" class="video-add-productCategory"  id="video-add-productCategory-select">
                                                <option value="">请选择</option>
                                            </select>
                                        </div>
                                    </div>
                                </div>
                                <div class="layui-row layui-col-space10 layui-form-item">
                                    <div class="layui-col-lg6">
                                        <label class="layui-form-label febs-form-item-require">类型:</label>
@@ -200,6 +211,19 @@
            }
        });
        //(下拉框)
        $.get(ctx + 'admin/productPoint/fileList/parent', function (data) {
            for (var k in data)
            {
                $(".video-add-productCategory").append("<option value='" + data[k].fileName + "'>" + data[k].fileName + "</option>");
            }
            layui.use('form', function () {
                var form = layui.form;
                $("#video-add-productCategory-select").val(aiProductPoint.videoName)
                form.render();
            });
        });
        setTimeout(() => {
            initProductPointInfo();
        }, 500);
@@ -210,6 +234,7 @@
                "isNormal": aiProductPoint.isNormal,
                "finderUserName": aiProductPoint.finderUserName,
                "productCategoryId": aiProductPoint.productCategoryId,
                "videoName": aiProductPoint.videoName,
                "feedId": aiProductPoint.feedId,
            });