Bladeren bron

AI生成课加入本地上传视频组件调整样式并优化加入进度条等逻辑

liyanbo 1 week geleden
bovenliggende
commit
e12d9546d4

+ 319 - 0
src/components/UploadFile/src/UploadVideo2.vue

@@ -0,0 +1,319 @@
+<template>
+  <div class="upload-box">
+    <el-upload
+      :id="uuid"
+      :accept="fileType.join(',')"
+      :action="uploadUrl"
+      :before-upload="beforeUpload"
+      :class="['upload', drag ? 'no-border' : '']"
+      :disabled="disabled"
+      :drag="drag"
+      :http-request="httpRequest"
+      :multiple="false"
+      :on-error="uploadError"
+      :on-success="uploadSuccess"
+      :show-file-list="false"
+    >
+      <template v-if="modelValue">
+        <video :src="modelValue" class="upload-video" controls></video>
+        <div class="upload-handle" @click.stop>
+          <div v-if="!disabled" class="handle-icon" @click="editVideo">
+            <Icon icon="ep:edit" />
+            <span v-if="showBtnText">{{ t('action.edit') }}</span>
+          </div>
+          <div class="handle-icon" @click="videoPreview(modelValue)">
+            <Icon icon="ep:zoom-in" />
+            <span v-if="showBtnText">{{ t('action.detail') }}</span>
+          </div>
+          <div v-if="showDelete && !disabled" class="handle-icon" @click="deleteVideo">
+            <Icon icon="ep:delete" />
+            <span v-if="showBtnText">{{ t('action.del') }}</span>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="upload-empty">
+          <slot name="empty">
+            <Icon icon="ep:plus" />
+            <!-- <span>请上传视频</span> -->
+          </slot>
+        </div>
+      </template>
+    </el-upload>
+    <!-- 上传进度条 -->
+    <div v-if="isUploading" class="uploadProgress">
+      <el-progress :percentage="uploadProgress" />
+      <div class="text-xs text-gray-500 text-right mt-1">{{ uploadProgress }}% 已上传</div>
+    </div>
+    <div class="el-upload__tip">
+      <slot name="tip"></slot>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { UploadProps } from 'element-plus'
+
+import { generateUUID } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import request from '@/config/axios'
+
+defineOptions({ name: 'UploadVideo2' })
+
+// 视频类型定义
+type FileTypes = 
+  | 'video/mp4'
+  | 'video/avi'
+  | 'video/mov'
+  | 'video/flv'
+  | 'video/webm'
+  | 'video/ogg'
+
+// 接受父组件参数
+const props = defineProps({
+  modelValue: propTypes.string.def(''),
+  drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
+  disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
+  fileSize: propTypes.number.def(500), // 视频大小限制 ==> 非必传(默认为 500M)
+  fileType: propTypes.array.def(['video/mp4', 'video/avi', 'video/mov', 'video/flv']), // 视频类型限制 ==> 非必传
+  height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
+  width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
+  borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
+  showDelete: propTypes.bool.def(true), // 是否显示删除按钮
+  showBtnText: propTypes.bool.def(true), // 是否显示按钮文字
+  directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined)
+})
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+// 生成组件唯一id
+const uuid = ref('id-' + generateUUID())
+
+// 上传进度相关状态
+const uploadProgress = ref(0)
+const isUploading = ref(false)
+
+const emit = defineEmits(['update:modelValue', 'upload-progress', 'upload-start', 'upload-complete'])
+
+const deleteVideo = () => {
+  emit('update:modelValue', '')
+}
+
+const { uploadUrl } = useUpload(props.directory)
+
+// 自定义httpRequest方法
+const httpRequest: UploadProps['httpRequest'] = async (options) => {
+  try {
+    // 触发上传开始事件
+    emit('upload-start')
+    isUploading.value = true
+    uploadProgress.value = 0
+
+    const formData = new FormData()
+    formData.append('file', options.file as File)
+
+    // 使用axios的upload方法,并传入onProgress回调
+    const res = await request.upload({
+      url: uploadUrl,
+      data: formData,
+      onProgress: (progress: number) => {
+        // 触发上传进度事件
+        uploadProgress.value = progress
+        emit('upload-progress', progress)
+      }
+    })
+
+    // 调用成功回调
+    options.onSuccess(res)
+  } catch (error) {
+    // 调用失败回调
+    options.onError(error)
+  } finally {
+    // 触发上传完成事件
+    isUploading.value = false
+    uploadProgress.value = 100
+    emit('upload-complete')
+  }
+}
+
+const editVideo = () => {
+  const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
+  dom && dom.dispatchEvent(new MouseEvent('click'))
+}
+
+const videoPreview = (videoUrl: string) => {
+  // 打开视频预览
+  window.open(videoUrl, '_blank')
+}
+
+const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  const videoSize = rawFile.size / 1024 / 1024 < props.fileSize
+  const videoType = props.fileType
+  if (!videoType.includes(rawFile.type as FileTypes))
+    message.notifyWarning('上传视频不符合所需的格式!')
+  if (!videoSize) message.notifyWarning(`上传视频大小不能超过 ${props.fileSize}M!`)
+  return videoType.includes(rawFile.type as FileTypes) && videoSize
+}
+
+// 视频上传成功提示
+const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
+  emit('update:modelValue', res.data)
+}
+
+// 视频上传错误提示
+const uploadError = () => {
+  message.notifyError('视频上传失败,请您重新上传!')
+  isUploading.value = false
+  uploadProgress.value = 0
+}
+</script>
+
+<style lang="scss" scoped>
+.is-error {
+  .upload {
+    :deep(.el-upload),
+    :deep(.el-upload-dragger) {
+      border: 1px dashed var(--el-color-danger) !important;
+
+      &:hover {
+        border-color: var(--el-color-primary) !important;
+      }
+    }
+  }
+}
+
+:deep(.disabled) {
+  .el-upload,
+  .el-upload-dragger {
+    cursor: not-allowed !important;
+    background: var(--el-disabled-bg-color);
+    border: 1px dashed var(--el-border-color-darker) !important;
+
+    &:hover {
+      border: 1px dashed var(--el-border-color-darker) !important;
+    }
+  }
+}
+
+.upload-box {
+  .no-border {
+    :deep(.el-upload) {
+      border: none !important;
+    }
+  }
+
+  :deep(.upload) {
+    .el-upload {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: v-bind(width);
+      height: v-bind(height);
+      overflow: hidden;
+      border: 1px dashed var(--el-border-color-darker);
+      border-radius: v-bind(borderradius);
+      transition: var(--el-transition-duration-fast);
+
+      &:hover {
+        border-color: var(--el-color-primary);
+
+        .upload-handle {
+          opacity: 1;
+        }
+      }
+
+      .el-upload-dragger {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        padding: 0;
+        overflow: hidden;
+        background-color: transparent;
+        border: 1px dashed var(--el-border-color-darker);
+        border-radius: v-bind(borderradius);
+
+        &:hover {
+          border: 1px dashed var(--el-color-primary);
+        }
+      }
+
+      .el-upload-dragger.is-dragover {
+        background-color: var(--el-color-primary-light-9);
+        border: 2px dashed var(--el-color-primary) !important;
+      }
+
+      .upload-video {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+
+      .upload-empty {
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        font-size: 12px;
+        line-height: 30px;
+        color: var(--el-color-info);
+
+        .el-icon {
+          font-size: 28px;
+          color: var(--el-text-color-secondary);
+        }
+      }
+
+      .upload-handle {
+        position: absolute;
+        top: 0;
+        right: 0;
+        display: flex;
+        width: 100%;
+        height: 100%;
+        cursor: pointer;
+        background: rgb(0 0 0 / 60%);
+        opacity: 0;
+        box-sizing: border-box;
+        transition: var(--el-transition-duration-fast);
+        align-items: center;
+        justify-content: center;
+
+        .handle-icon {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          padding: 0 6%;
+          color: aliceblue;
+
+          .el-icon {
+            margin-bottom: 40%;
+            font-size: 130%;
+            line-height: 130%;
+          }
+
+          span {
+            font-size: 85%;
+            line-height: 85%;
+          }
+        }
+      }
+    }
+  }
+
+  .el-upload__tip {
+    line-height: 18px;
+    text-align: center;
+  }
+
+  .uploadProgress {
+    width: 100%;
+    margin-top: 10px;
+  }
+}
+</style>

+ 1 - 0
src/types/auto-components.d.ts

@@ -175,6 +175,7 @@ declare module 'vue' {
     UploadModel: typeof import('./../components/UploadFile/src/UploadModel.vue')['default']
     UploadMusic: typeof import('./../components/UploadFile/src/UploadMusic.vue')['default']
     UploadVideo: typeof import('./../components/UploadFile/src/UploadVideo.vue')['default']
+    UploadVideo2: typeof import('./../components/UploadFile/src/UploadVideo2.vue')['default']
     UserSelectForm: typeof import('./../components/UserSelectForm/index.vue')['default']
     UserTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue')['default']
     UserTaskCustomConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue')['default']

+ 126 - 14
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -213,8 +213,8 @@
                           }}
                         </el-button>
                       </div>
-                      <div v-if="section.backgroundImage.url" class="media-preview">
-                        <img :src="section.backgroundImage.url" alt="背景图预览" />
+                      <div class="upload-container">
+                        <UploadImg v-model="section.backgroundImage.url" />
                       </div>
                     </div>
 
@@ -284,8 +284,8 @@
                           }}
                         </el-button>
                       </div>
-                      <div v-if="section.backgroundVideo.url" class="media-preview">
-                        <video :src="section.backgroundVideo.url" alt="视频预览" controls ></video>
+                      <div class="upload-container">
+                        <UploadVideo2 v-model="section.backgroundVideo.url" />
                       </div>
                     </div>
                   </template>
@@ -593,29 +593,28 @@
                       
                       <!-- 视频 -->
                       <template v-else-if="dialogue.type === 'video'">
-                        <div class="media-input-group">
-                          <span class="media-label">视频提示词</span>
+                        <div class="media-input-group" style="display: flex; align-items: center; justify-content: center;">
+                          <span class="media-label" style="margin-right: 10px;">视频提示词</span>
                           <el-input
                             v-model="dialogue.videoPrompt"
                             type="textarea"
                             :autosize="{ minRows: 2, maxRows: 4 }"
                             placeholder="描述词"
                             class="media-prompt-video"
+                            style="flex: 1; margin-right: 10px;"
                           />
-                          <div v-if="dialogue.videoUrl" class="media-preview">
-                            <video :src="dialogue.videoUrl" alt="视频预览" controls></video>
-                          </div>
                           <el-button
                             type="primary"
                             size="small"
                             :loading="dialogue.generatingVideo"
                             :disabled="
-                                  !dialogue.videoPrompt
+                                  !dialogue.videoPrompt || dialogue.generatingVideo
                                 "
                             @click="generateDialogueVideo(sectionIndex, dialogueIndex)"
                             class="generate-btn"
+                            style="margin-right: 10px;"
                           >
-                            {{
+                            {{ 
                               dialogue.generatingVideo
                                 ? '生成中...'
                                 : dialogue.videoUrl
@@ -623,11 +622,13 @@
                                   : '生成'
                             }}
                           </el-button>
+                          <div class="upload-container" style="margin: 0 10px;">
+                            <UploadVideo2 v-model="dialogue.videoUrl" />
+                          </div>
                           <button
                             class="remove-btn"
                             @click="removeDialogue(sectionIndex, dialogueIndex)"
-                          >×</button
-                          >
+                          >×</button>
                         </div>
                       </template>
                     </div>
@@ -831,6 +832,9 @@
 <script setup>
 import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
 import { Dialog } from '@/components/Dialog'
+import { ElMessage } from 'element-plus'
+import UploadImg from '@/components/UploadFile/src/UploadImg.vue'
+import UploadVideo2 from '@/components/UploadFile/src/UploadVideo2.vue'
 import { ChatMessageApi } from '@/api/ai/chat/message'
 import { ChatConversationApi } from '@/api/ai/chat/conversation'
 import { ChatRoleApi } from '@/api/ai/model/chatRole'
@@ -1939,7 +1943,6 @@ const validateScript = () => {
       } else if (dialogue.type === 'video') {
         // 检查视频类型的对话
         if (
-          !dialogue.videoPrompt.trim() ||
           !dialogue.videoUrl
         ) {
           errorMessages.value.push(
@@ -1955,6 +1958,44 @@ const validateScript = () => {
   validationMessage.value = errorMessages.value.length > 0 ? '校验失败' : '校验通过'
 }
 
+// 图片上传前验证
+const beforeImageUpload = (file) => {
+  const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/webp'
+  const isLt2M = file.size / 1024 / 1024 < 2
+
+  if (!isJPG) {
+    ElMessage.error('只能上传 JPG、PNG、GIF、WebP 格式的图片!')
+    return false
+  }
+  if (!isLt2M) {
+    ElMessage.error('图片大小不能超过 2MB!')
+    return false
+  }
+  return true
+}
+
+// 处理图片上传成功
+const handleImageUpload = (response, file, sectionIndex) => {
+  // 这里假设上传成功后返回的response包含图片的URL
+  // 实际项目中需要根据后端API的返回格式进行调整
+  const imageUrl = response.url || URL.createObjectURL(file)
+  scriptData.sections[sectionIndex].backgroundImage.url = imageUrl
+  scriptData.sections[sectionIndex].backgroundImage.generating = false
+  ElMessage.success('图片上传成功!')
+}
+
+// 编辑图片
+const editImage = (sectionIndex) => {
+  // 这里可以添加编辑图片的逻辑,比如打开图片编辑器
+  ElMessage.info('编辑图片功能待实现')
+}
+
+// 删除图片
+const deleteImage = (sectionIndex) => {
+  scriptData.sections[sectionIndex].backgroundImage.url = ''
+  ElMessage.success('图片删除成功!')
+}
+
 // 步骤4:保存脚本
 const saveScript = async () => {
   validateScript()
@@ -2465,7 +2506,78 @@ onUnmounted(() => {
 .add-dialogue-btn.poem:hover {
   background-color: #73767a;
   border-color: #73767a;
+}
+
+/* 上传相关样式 */
+.upload-container {
+  margin-top: 10px;
+  display: inline-block;
+}
+
+.media-upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  margin-top: 10px;
+  background-color: #fafafa;
+}
+
+.upload-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.upload-text {
+  margin-bottom: 16px;
+  color: #999;
+}
+
+.upload-btn {
+  margin-left: 10px;
+}
+
+.upload-btn-full {
+  width: 100%;
+}
+
+.media-preview {
+  position: relative;
+  margin-top: 10px;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.media-preview img,
+.media-preview video {
+  width: 100%;
+  max-height: 300px;
+  object-fit: cover;
+}
+
+.media-preview .preview-actions {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.6);
+  padding: 10px;
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 0;
+}
+
+.media-preview .preview-actions button {
   color: white;
+  margin-left: 10px;
+}
+
+.media-preview .preview-actions button:hover {
+  color: #409eff;
 }
 
 .add-dialogue-btn.video {