康鲁杰
2025-04-18 044d520a74256e435e55f215060bf06bc7442d7f
easegen-front/src/views/myCourse/index.vue
@@ -147,7 +147,22 @@
  </ContentWrap>
  <!-- è§†é¢‘播放弹框 -->
  <videoDialog ref="videoRef" />
  <el-dialog
    v-model="videoPlayDialogVisible"
    title="视频预览"
    width="60%"
    @close="handleVideoPlayClose"
  >
    <div class="video-play-container">
      <video
        ref="currentPlayVideo"
        v-if="currentPlayUrl"
        :src="currentPlayUrl"
        controls
        class="play-video"
      ></video>
    </div>
  </el-dialog>
  <!-- å­—幕生成弹框 -->
  <el-dialog
@@ -276,24 +291,11 @@
  <!-- ç‰‡å¤´ç‰‡å°¾è®¾ç½®å¼¹æ¡† -->
  <el-dialog
    v-model="headerFooterDialogVisible"
    title="片头片尾设置"
    title="片头片尾"
    width="70%"
    @close="pauseAllVideos('headerFooter')"
  >
    <el-tabs v-model="activeTab">
      <el-tab-pane label="设置片头片尾" name="setting">
        <el-form :model="headerFooterForm" label-width="120px">
          <el-form-item label="片头视频">
            <UploadFile v-model="headerFooterForm.titles" :fileType="['mp4']" :limit="1" @on-success="handleFileSuccess('audition', $event)"/>
          </el-form-item>
          <el-form-item label="片尾视频">
            <UploadFile v-model="headerFooterForm.trailer" :fileType="['mp4']" :limit="1" @on-success="handleFileSuccess1('audition', $event)"/>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="applyHeaderFooter" :loading="applyingHeaderFooter">保存设置</el-button>
          </el-form-item>
        </el-form>
      </el-tab-pane>
      <el-tab-pane label="合成视频" name="merge">
        <el-form :model="formData1" label-width="120px">
          <el-form-item label="视频格式">
@@ -305,49 +307,81 @@
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="片头视频">
                <div class="video-container">
                  <video
                    v-if="formData1.value.titles"
                    :src="formData1.value.titles"
                    controls
                    class="preview-video"
                    @error="handleVideoError('titles')"
                  ></video>
                  <div v-else class="no-video">暂无片头视频</div>
                <div class="video-select-container">
                  <div class="video-grid">
                    <div
                      v-for="item in titlesList"
                      :key="item.id"
                      class="video-card"
                      :class="{ 'is-selected': formData1.value.titles === item.url }"
                      @click="handleTitlesSelect(item)"
                    >
                      <div class="video-thumbnail">
                        <video
                          :src="item.url"
                          class="thumbnail-video"
                          controls
                        ></video>
                      </div>
                      <div class="video-info">
                        <span class="video-name">{{ item.name }}</span>
                        <el-icon v-if="formData1.value.titles === item.url" class="selected-icon"><Check /></el-icon>
                      </div>
                    </div>
                  </div>
                </div>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="片尾视频">
                <div class="video-container">
                  <video
                    v-if="formData1.value.trailer"
                    :src="formData1.value.trailer"
                    controls
                    class="preview-video"
                    @error="handleVideoError('trailer')"
                  ></video>
                  <div v-else class="no-video">暂无片尾视频</div>
                <div class="video-select-container">
                  <div class="video-grid">
                    <div
                      v-for="item in trailerList"
                      :key="item.id"
                      class="video-card"
                      :class="{ 'is-selected': formData1.value.trailer === item.url }"
                      @click="handleTrailerSelect(item)"
                    >
                      <div class="video-thumbnail">
                        <video
                          :src="item.url"
                          class="thumbnail-video"
                          controls
                        ></video>
                      </div>
                      <div class="video-info">
                        <span class="video-name">{{ item.name }}</span>
                        <el-icon v-if="formData1.value.trailer === item.url" class="selected-icon"><Check /></el-icon>
                      </div>
                    </div>
                  </div>
                </div>
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item>
            <el-button
              type="primary"
              @click="hecheng"
              :loading="applyingHeaderFooter"
              :disabled="!formData1.value?.titles || !formData1.value?.trailer"
            >
              å¼€å§‹åˆæˆ
            </el-button>
            <span v-if="!formData1.value?.titles || !formData1.value?.trailer" class="ml-10px text-red-500">
              è¯·å…ˆè®¾ç½®ç‰‡å¤´å’Œç‰‡å°¾è§†é¢‘
            </span>
            <div style="display: flex; width: 100%;">
              <div style="flex: 1;">
                <span v-if="!formData1.value?.titles || !formData1.value?.trailer" class="text-red-500">
                  è¯·å…ˆé€‰æ‹©ç‰‡å¤´å’Œç‰‡å°¾è§†é¢‘
                </span>
              </div>
              <div style="margin-right: 165px;">
                <el-button
                  type="primary"
                  @click="hecheng"
                  :loading="applyingHeaderFooter"
                  :disabled="!formData1.value?.titles || !formData1.value?.trailer"
                >
                  å¼€å§‹åˆæˆ
                </el-button>
              </div>
            </div>
          </el-form-item>
        </el-form>
      </el-tab-pane>
      <el-tab-pane label="预览与下载" name="preview">
      <el-tab-pane label="预览下载" name="preview">
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="preview-section">
@@ -436,9 +470,9 @@
import axios from 'axios'
import { config } from '@/config/axios/config'
import {createVideo, createVideoMeger, videoMeger} from "@/api/pptTemplate";
import { ArrowDown } from '@element-plus/icons-vue'
import { ArrowDown, Check, VideoPlay, VideoPause } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { listFile } from '@/api/system/file'
const router = useRouter()
const message = useMessage()
const { t } = useI18n()
@@ -492,7 +526,7 @@
// ç‰‡å¤´ç‰‡å°¾å¼¹æ¡†ç›¸å…³
const headerFooterDialogVisible = ref(false)
const activeTab = ref('setting')
const activeTab = ref('merge')
const headerFooterForm = reactive({
  id: null as number | null,
  titles: '',
@@ -500,16 +534,13 @@
})
const applyingHeaderFooter = ref(false)
const handleFileSuccess = (fileType,response) => {
  if (fileType === 'audition') {
    headerFooterForm.titles = response.data
  }
};
const handleFileSuccess1 = (fileType,response) => {
  if (fileType === 'audition') {
    headerFooterForm.trailer = response.data
  }
};
const titlesList = ref([])
const trailerList = ref([])
// è§†é¢‘播放相关
const currentPlayUrl = ref('')
const videoRefs = ref<HTMLVideoElement[]>([])
// èŽ·å–è§†é¢‘åˆ—è¡¨
const getList = async () => {
  loading.value = true
@@ -1005,6 +1036,31 @@
  polling.value = false
}
// èŽ·å–ç‰‡å¤´ç‰‡å°¾åˆ—è¡¨
const getTitlesTrailerList = async () => {
  try {
    // èŽ·å–ç‰‡å¤´åˆ—è¡¨
    const titlesRes = await listFile({ type: 1 })
    titlesList.value = titlesRes.list || []
    // èŽ·å–ç‰‡å°¾åˆ—è¡¨
    const trailerRes = await listFile({ type: 2 })
    trailerList.value = trailerRes.list || []
  } catch (error) {
    console.error('获取片头片尾列表失败:', error)
  }
}
// å¤„理片头选择
const handleTitlesSelect = (item) => {
  formData1.value.titles = item.url
}
// å¤„理片尾选择
const handleTrailerSelect = (item) => {
  formData1.value.trailer = item.url
}
// å¤„理片头片尾按钮点击
const handleHeaderFooter = async (row) => {
  headerFooterForm.id = row.id
@@ -1013,7 +1069,8 @@
  headerFooterForm.trailer = details.trailer || ''
  formData1.value = details
  headerFooterDialogVisible.value = true
  activeTab.value = 'setting'
  activeTab.value = 'merge'
  await getTitlesTrailerList()
}
// åº”用片头片尾设置
@@ -1041,6 +1098,16 @@
const hecheng = async () => {
  try {
    applyingHeaderFooter.value = true
    // 1. å…ˆä¿å­˜é…ç½®
    const saveParams = {
      id: headerFooterForm.id,
      titles: formData1.value.titles,
      trailer: formData1.value.trailer
    }
    await pptTemplateApi.createVideo(saveParams)
    // 2. å†è°ƒç”¨åˆæˆæŽ¥å£
    let obj = {}
    if (formData1.isvideo == '2') {
      obj = {
@@ -1108,6 +1175,14 @@
onMounted(() => {
  getList()
})
// æ’­æ”¾é¢„览
const playPreview = (type: 'titles' | 'trailer') => {
  const video = type === 'titles' ? formData1.value.titles : formData1.value.trailer
  if (video) {
    window.open(video, '_blank')
  }
}
</script>
<style scoped>
@@ -1188,6 +1263,7 @@
  padding: 20px;
  border-radius: 4px;
  height: 100%;
  min-height: 500px;
}
.preview-section h4 {
@@ -1197,7 +1273,7 @@
.video-container {
  width: 100%;
  height: 200px;
  height: 400px;
  background: #000;
  border-radius: 4px;
  overflow: hidden;
@@ -1223,61 +1299,109 @@
  background: #f5f7fa;
}
.button-group {
  display: flex;
  justify-content: center;
  gap: 10px;
}
.ml-10px {
  margin-left: 10px;
}
.text-red-500 {
  color: #f56c6c;
}
.subtitle-dialog :deep(.el-dialog__body) {
  padding: 20px;
  min-height: 500px;
}
.preview-section {
  background: #f5f7fa;
  padding: 20px;
  border-radius: 4px;
  height: 100%;
  min-height: 400px;
}
.video-container {
  width: 100%;
  height: 300px;
  background: #000;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 15px;
  position: relative;
}
.preview-video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}
.no-video {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #909399;
  font-size: 14px;
  background: #f5f7fa;
}
.video-container1{
  height: 490px !important;
  height: 700px !important;
}
.video-select-container {
  display: flex;
  flex-direction: column;
  gap: 15px;
}
.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 20px;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
  max-height: 400px;
  overflow-y: auto;
}
.video-card {
  position: relative;
  background: #fff;
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.video-card.is-selected {
  box-shadow: 0 4px 16px 0 rgba(64, 158, 255, 0.2);
}
.video-thumbnail {
  position: relative;
  width: 100%;
  padding-top: 56.25%; /* 16:9 æ¯”例 */
  background: #000;
  overflow: hidden;
}
.thumbnail-video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.video-info {
  padding: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #fff;
}
.video-name {
  flex: 1;
  font-size: 14px;
  color: #303133;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-right: 8px;
}
.selected-icon {
  color: var(--el-color-primary);
  font-size: 18px;
  flex-shrink: 0;
}
/* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
.video-grid::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
.video-grid::-webkit-scrollbar-thumb {
  background: #c0c4cc;
  border-radius: 3px;
}
.video-grid::-webkit-scrollbar-track {
  background: #f5f7fa;
  border-radius: 3px;
}
.video-play-container {
  width: 100%;
  background: #000;
  border-radius: 4px;
  overflow: hidden;
  aspect-ratio: 16/9;
}
.play-video {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
</style>
media_task_queue.json
ÎļþÒÑɾ³ý
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/controller/admin/titlestrailer/vo/TitlesTrailerRespVO.java
@@ -1,13 +1,16 @@
package cn.iocoder.yudao.module.digitalcourse.controller.admin.titlestrailer.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType;
import com.fhs.core.trans.vo.VO;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ExcelIgnoreUnannotated
public class TitlesTrailerRespVO {
public class TitlesTrailerRespVO implements VO {
    private Long id;
@@ -19,4 +22,10 @@
    private LocalDateTime createTime;
    @Trans(type = TransType.SIMPLE, targetClassName = "cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO",
            fields = "nickname", ref = "creatorName")
    private String creator;
    private String creatorName;
}
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/dal/mysql/courses/CoursesMapper.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.digitalcourse.controller.admin.courses.vo.AppCoursesPageReqVO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.courses.CoursesDO;
import org.apache.ibatis.annotations.Mapper;
@@ -16,6 +17,7 @@
public interface CoursesMapper extends BaseMapperX<CoursesDO> {
    default PageResult<CoursesDO> selectPage(AppCoursesPageReqVO reqVO) {
        Long loginUserId = WebFrameworkUtils.getLoginUserId();
        return selectPage(reqVO, new LambdaQueryWrapperX<CoursesDO>()
                .likeIfPresent(CoursesDO::getName, reqVO.getName())
                .eqIfPresent(CoursesDO::getAspect, reqVO.getAspect())
@@ -29,7 +31,8 @@
                .eqIfPresent(CoursesDO::getSubtitlesStyle, reqVO.getSubtitlesStyle())
                .eqIfPresent(CoursesDO::getCreator, reqVO.getCreator())
                .betweenIfPresent(CoursesDO::getCreateTime, reqVO.getCreateTime())
                .apply(loginUserId != 1, "creator = {0}", loginUserId)
                .orderByDesc(CoursesDO::getId));
    }
}
}