Pārlūkot izejas kodu

1、加入图生图
2、加入图生视频、文生视频
3、去除水印

liyanbo 7 mēneši atpakaļ
vecāks
revīzija
9b14e8dcae

+ 71 - 0
src/api/ai/video/index.ts

@@ -0,0 +1,71 @@
+import request from '@/config/axios'
+
+// AI 视频 VO
+export interface VideoVO {
+  id: number // 编号
+  platform: string // 平台
+  model: string // 模型
+  prompt: string // 提示词
+  promptVideo: string // 提示词
+  width: number // 图片宽度
+  height: number // 图片高度
+  status: number // 状态
+  publicStatus: boolean // 公开状态
+  videoUrl: string // 任务地址
+  errorMessage: string // 错误信息
+  options: any // 配置 Map<string, string>
+  taskId: number // 任务编号
+  createTime: Date // 创建时间
+  finishTime: Date // 完成时间
+  tenantId: Number // 租户id
+}
+
+export interface VideoDrawReqVO {
+  prompt: string // 提示词
+  modelId: number // 模型
+  style: string // 图像生成的风格
+  width: string // 图片宽度
+  height: string // 图片高度
+  options: object // 绘制参数,Map<String, String>
+}
+
+// AI 图片 API
+export const VideoApi = {
+  // 获取【我的】视频分页
+  getVideoPageMy: async (params: any) => {
+    return await request.get({ url: `/ai/video/my-page`, params })
+  },
+  // 获取【我的】视频记录
+  getVideoMy: async (id: number) => {
+    return await request.get({ url: `/ai/video/get-my?id=${id}` })
+  },
+  // 获取【我的】视频记录列表
+  getVideoListMyByIds: async (ids: number[]) => {
+    return await request.get({ url: `/ai/video/my-list-by-ids`, params: { ids: ids.join(',') } })
+  },
+  // 生成图片
+  drawVideo: async (data: VideoDrawReqVO) => {
+    return await request.post({ url: `/ai/video/draw`, data })
+  },
+  // 删除【我的】视频记录
+  deleteVideoMy: async (id: number) => {
+    return await request.delete({ url: `/ai/video/delete-my?id=${id}` })
+  },
+
+  // ================ 视频管理 ================
+
+  // 查询视频分页
+  getVideoPage: async (params: any) => {
+    return await request.get({ url: `/ai/video/page`, params })
+  },
+
+  // 更新视频发布状态
+  updateVideo: async (data: any) => {
+    return await request.put({ url: '/ai/video/update', data })
+  },
+
+  // 删除视频
+  deleteVideo: async (id: number) => {
+    return await request.delete({ url: `/ai/video/delete?id=` + id })
+  }
+}

+ 0 - 1
src/components/UploadFile/src/useUpload.ts

@@ -2,7 +2,6 @@ import * as FileApi from '@/api/infra/file'
 // import CryptoJS from 'crypto-js'
 import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
 import axios from 'axios'
-import { service } from '@/config/axios/service'
 
 /**
  * 获得上传 URL

+ 4 - 4
src/layout/components/Setting/src/components/InterfaceDisplay.vue

@@ -228,9 +228,9 @@ watch(
       <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
     </div>
 
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('watermark.watermark') }}</span>
-      <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
-    </div>
+<!--    <div class="flex items-center justify-between">-->
+<!--      <span class="text-14px">{{ t('watermark.watermark') }}</span>-->
+<!--      <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />-->
+<!--    </div>-->
   </div>
 </template>

+ 1 - 0
src/utils/dict.ts

@@ -220,6 +220,7 @@ export enum DICT_TYPE {
   AI_PLATFORM = 'ai_platform', // AI 平台
   AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
   AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
+  AI_VIDEO_STATUS = 'ai_video_status', // AI 视频状态
   AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
   AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
   AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型

+ 1 - 1
src/views/Profile/Index.vue

@@ -27,7 +27,7 @@
   </div>
 </template>
 <script lang="ts" setup>
-import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
+import { BasicInfo, ProfileUser, ResetPwd } from './components'
 
 const { t } = useI18n()
 defineOptions({ name: 'Profile' })

+ 199 - 0
src/views/ai/image/index/components/imageEdit/index.vue

@@ -0,0 +1,199 @@
+<!-- dall3 -->
+<template>
+  <div class="prompt">
+
+    <el-text tag="b">上传图片</el-text>
+    <el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
+    <UploadImg v-model="promptImage" />
+    <el-input
+      v-model="prompt"
+      maxlength="1024"
+      :rows="5"
+      class="w-100% mt-15px"
+      input-style="border-radius: 7px;"
+      placeholder="例如:童话里的小屋应该是什么样子?"
+      show-word-limit
+      type="textarea"
+    />
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">平台</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select
+        v-model="otherPlatform"
+        placeholder="Select"
+        size="large"
+        class="!w-350px"
+        @change="handlerPlatformChange"
+      >
+        <el-option
+          v-for="item in AiImageOtherPlatformEnum"
+          :key="item.key"
+          :label="item.name"
+          :value="item.key"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">模型</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
+        <el-option
+          v-for="item in platformModels"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">图片尺寸</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-button type="primary">自动匹配</el-button>
+    </el-space>
+  </div>
+  <div class="btns">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0 && promptImage === ''"
+      @click="handleGenerateImage"
+    >
+      {{ drawIn ? '生成中' : '生成内容' }}
+    </el-button>
+  </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
+import { AiPlatformEnum, ImageHotWords, AiImageOtherPlatformEnum, AiModelTypeEnum } from '@/views/ai/utils/constants'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 消息弹窗
+
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
+// 定义属性
+const drawIn = ref<boolean>(false) // 生成中
+const selectHotWord = ref<string>('') // 选中的热词
+// 表单
+const prompt = ref<string>('') // 提示词
+const promptImage = ref<string>('') // 参考图片
+const width = ref<number>(1024) // 图片宽度
+const height = ref<number>(1024) // 图片高度
+const otherPlatform = ref<string>(AiPlatformEnum.DOU_BAO) // 平台
+const platformModels = ref<ModelVO[]>([]) // 模型列表
+const modelId = ref<number>() // 选中的模型
+
+/** 图片生成 */
+const handleGenerateImage = async () => {
+  // 二次确认
+  await message.confirm(`确认生成内容?`)
+  try {
+    // 加载中
+    drawIn.value = true
+    // 回调
+    emits('onDrawStart', otherPlatform.value)
+    // 发送请求
+    const form = {
+      platform: otherPlatform.value,
+      modelId: modelId.value, // 模型
+      prompt: prompt.value, // 提示词
+      promptImage: promptImage.value, // 参考图片
+      width: width.value, // 图片宽度
+      height: height.value, // 图片高度
+      options: {}
+    } as unknown as ImageDrawReqVO
+    await ImageApi.drawImage(form)
+  } finally {
+    // 回调
+    emits('onDrawComplete', otherPlatform.value)
+    // 加载结束
+    drawIn.value = false
+  }
+}
+
+/** 填充值 */
+const settingValues = async (detail: ImageVO) => {
+  prompt.value = detail.prompt
+  width.value = detail.width
+  height.value = detail.height
+}
+
+/** 平台切换 */
+const handlerPlatformChange = async (platform: string) => {
+
+  platformModels.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE_EDIT)
+  // 根据选择的平台筛选模型
+  // platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform && item.type === AiModelTypeEnum.IMAGE_EDIT)
+
+  // 切换平台,默认选择一个模型
+  if (platformModels.value.length > 0) {
+    modelId.value = platformModels.value[0].id // 使用 model 属性作为值
+  } else {
+    modelId.value = undefined
+  }
+}
+
+/** 监听 models 变化 */
+watch(
+  () => props.models,
+  () => {
+    handlerPlatformChange(otherPlatform.value)
+  },
+  { immediate: true, deep: true }
+)
+/** 暴露组件方法 */
+defineExpose({ settingValues })
+</script>
+<style scoped lang="scss">
+.hot-words {
+  display: flex;
+  flex-direction: column;
+  margin-top: 30px;
+
+  .word-list {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: start;
+    margin-top: 15px;
+
+    .btn {
+      margin: 0;
+    }
+  }
+}
+
+// 模型
+.group-item {
+  margin-top: 30px;
+
+  .group-item-body {
+    margin-top: 15px;
+    width: 100%;
+  }
+}
+
+.btns {
+  display: flex;
+  justify-content: center;
+  margin-top: 50px;
+}
+</style>

+ 12 - 2
src/views/ai/image/index/index.vue

@@ -12,6 +12,12 @@
           :models="models"
           @on-draw-complete="handleDrawComplete"
         />
+        <ImageEdit
+          v-if="selectPlatform === 'imageEdit'"
+          ref="imageEditRef"
+          :models="models"
+          @on-draw-complete="handleDrawComplete"
+        />
         <Dall3
           v-if="selectPlatform === AiPlatformEnum.OPENAI"
           ref="dall3Ref"
@@ -46,6 +52,7 @@ import Dall3 from './components/dall3/index.vue'
 import Midjourney from './components/midjourney/index.vue'
 import StableDiffusion from './components/stableDiffusion/index.vue'
 import Common from './components/common/index.vue'
+import ImageEdit from './components/imageEdit/index.vue'
 import { ModelApi, ModelVO } from '@/api/ai/model/model'
 import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
@@ -59,9 +66,13 @@ const commonRef = ref<any>() // stable diffusion ref
 const selectPlatform = ref('common') // 选中的平台
 const platformOptions = [
   {
-    label: '通用',
+    label: '文生图',
     value: 'common'
   },
+  {
+    label: '图片编辑',
+    value: 'imageEdit'
+  },
   // {
   //   label: 'DALL3 绘画',
   //   value: AiPlatformEnum.OPENAI
@@ -102,7 +113,6 @@ const handleRegeneration = async (image: ImageVO) => {
   //   // 新增:通用平台(包括豆包)使用 commonRef 填充数据
   //   commonRef.value.settingValues(image)
   }
-  // TODO @fan:貌似 other 重新设置不行?
 }
 
 /** 组件挂载的时候 */

+ 13 - 0
src/views/ai/image/manager/index.vue

@@ -133,6 +133,19 @@
         :formatter="dateFormatter"
         width="180px"
       />
+      <el-table-column label="参考图" align="center" prop="promptImage" width="110px" fixed="left">
+        <template #default="{ row }">
+          <el-image
+            class="h-80px w-80px"
+            lazy
+            :src="row.promptImage"
+            :preview-src-list="[row.promptImage]"
+            preview-teleported
+            fit="cover"
+            v-if="row.promptImage?.length > 0"
+          />
+        </template>
+      </el-table-column>
       <el-table-column label="宽度" align="center" prop="width" />
       <el-table-column label="高度" align="center" prop="height" />
       <el-table-column label="错误信息" align="center" prop="errorMessage" />

+ 27 - 4
src/views/ai/utils/constants.ts

@@ -28,6 +28,7 @@ export const AiPlatformEnum = {
 export const AiModelTypeEnum = {
   CHAT: 1, // 聊天
   IMAGE: 2, // 图像
+  IMAGE_EDIT: 7, // 图像编辑
   VOICE: 3, // 音频
   VIDEO: 4, // 视频
   EMBEDDING: 5, // 向量
@@ -35,10 +36,10 @@ export const AiModelTypeEnum = {
 }
 
 export const AiImageOtherPlatformEnum: ImageModelVO[] = [
-  {
-    key: AiPlatformEnum.TONG_YI,
-    name: '通义万相'
-  },
+  // {
+  //   key: AiPlatformEnum.TONG_YI,
+  //   name: '通义万相'
+  // },
   // {
   //   key: AiPlatformEnum.YI_YAN,
   //   name: '百度千帆'
@@ -57,6 +58,13 @@ export const AiImageOtherPlatformEnum: ImageModelVO[] = [
   }
 ]
 
+export const AiVideoOtherPlatformEnum: VideoModelVO[] = [
+  {
+    key: AiPlatformEnum.DOU_BAO,  // 新增:豆包平台选项
+    name: '豆包绘画'
+  }
+]
+
 export const AiMusicOtherPlatformEnum: ImageModelVO[] = [
   {
     key: AiPlatformEnum.SUNO,
@@ -73,6 +81,15 @@ export const AiImageStatusEnum = {
   FAIL: 30 // 已失败
 }
 
+/**
+ * AI 图像生成状态的枚举
+ */
+export const AiVideoStatusEnum = {
+  IN_PROGRESS: 10, // 进行中
+  SUCCESS: 20, // 已完成
+  FAIL: 30 // 已失败
+}
+
 /**
  * AI 音乐生成状态的枚举
  */
@@ -122,6 +139,12 @@ export interface ImageModelVO {
   image?: string
 }
 
+export interface VideoModelVO {
+  key: string
+  name: string
+  video?: string
+}
+
 export const StableDiffusionSamplers: ImageModelVO[] = [
   {
     key: 'DDIM',

+ 157 - 0
src/views/ai/video/index/components/VideoCard.vue

@@ -0,0 +1,157 @@
+<template>
+  <el-card body-class="" class="video-card">
+    <div class="video-operation">
+      <div>
+        <el-button type="primary" text bg v-if="detail?.status === AiVideoStatusEnum.IN_PROGRESS">
+          生成中
+        </el-button>
+        <el-button text bg v-else-if="detail?.status === AiVideoStatusEnum.SUCCESS">
+          已完成
+        </el-button>
+        <el-button type="danger" text bg v-else-if="detail?.status === AiVideoStatusEnum.FAIL">
+          异常
+        </el-button>
+      </div>
+      <!-- 操作区 -->
+      <div>
+        <el-button
+          class="btn"
+          text
+          :icon="Download"
+          @click="handleButtonClick('download', detail)"
+        />
+        <el-button
+          class="btn"
+          text
+          :icon="RefreshRight"
+          @click="handleButtonClick('regeneration', detail)"
+        />
+        <el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" />
+        <el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" />
+      </div>
+    </div>
+    <div class="video-wrapper" ref="cardVideoRef">
+      <video
+        class="video"
+        :src="detail?.videoUrl"
+        controls
+      ></video>
+      <div v-if="detail?.status === AiVideoStatusEnum.FAIL">
+        {{ detail?.errorMessage }}
+      </div>
+    </div>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { Delete, Download, More, RefreshRight } from '@element-plus/icons-vue'
+import { VideoVO } from '@/api/ai/video'
+import { PropType, ref, toRefs, watch, onMounted } from 'vue'
+import { ElLoading, LoadingOptionsResolved } from 'element-plus'
+import { AiVideoStatusEnum } from '@/views/ai/utils/constants'
+import { useMessage } from '@/hooks/web/useMessage'
+
+const message = useMessage() // 消息
+
+const props = defineProps({
+  detail: {
+    type: Object as PropType<VideoVO>,
+    require: true
+  }
+})
+
+const cardVideoRef = ref<any>() // 卡片 video ref
+const cardVideoLoadingInstance = ref<any>() // 卡片 video ref
+
+/** 处理点击事件  */
+const handleButtonClick = async (type, detail: VideoVO) => {
+  if (type === 'regeneration') {
+    // 显示重新生成加载状态
+    const loadingInstance = ElLoading.service({
+      target: cardVideoRef.value,
+      text: '重新加载中...'
+    });
+    try {
+      emits('onBtnClick', type, detail);
+      // 等待父组件处理完成后,detail更新会自动触发watch重新加载图片
+    } finally {
+      loadingInstance.close();
+    }
+  } else {
+    emits('onBtnClick', type, detail);
+  }
+}
+
+const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
+
+/** 监听详情 */
+const { detail } = toRefs(props)
+watch(detail, async (newVal, oldVal) => {
+  await handleLoading(newVal.status as string)
+})
+
+/** 处理加载状态 */
+const handleLoading = async (status: number) => {
+  // 情况一:如果是生成中,则设置加载中的 loading
+  if (status === AiVideoStatusEnum.IN_PROGRESS) {
+    cardVideoLoadingInstance.value = ElLoading.service({
+      target: cardVideoRef.value,
+      text: '生成中...'
+    } as LoadingOptionsResolved)
+    // 情况二:如果已经生成结束,则移除 loading
+  } else {
+    if (cardVideoLoadingInstance.value) {
+      cardVideoLoadingInstance.value.close()
+      cardVideoLoadingInstance.value = null
+    }
+  }
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await handleLoading(props.detail.status as string)
+})
+</script>
+
+<style scoped lang="scss">
+.video-card {
+  width: 320px;
+  height: auto;
+  border-radius: 10px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+
+  .video-operation {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+
+    .btn {
+      //border: 1px solid red;
+      padding: 10px;
+      margin: 0;
+    }
+  }
+
+  .video-wrapper {
+    overflow: hidden;
+    margin-top: 20px;
+    height: 280px;
+    flex: 1;
+
+    .video {
+      width: 100%;
+      border-radius: 10px;
+    }
+  }
+
+  .video-mj-btns {
+    margin-top: 5px;
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+  }
+}
+</style>

+ 124 - 0
src/views/ai/video/index/components/VideoDetail.vue

@@ -0,0 +1,124 @@
+<template>
+  <el-drawer
+    v-model="showDrawer"
+    title="图片详细"
+    @close="handleDrawerClose"
+    custom-class="drawer-class"
+  >
+    <!-- 图片 -->
+    <div class="item">
+      <div class="body">
+        <video
+          class="video"
+          :src="detail?.videoUrl"
+          controls
+        ></video>
+      </div>
+    </div>
+    <!-- 时间 -->
+    <div class="item">
+      <div class="tip">时间</div>
+      <div class="body">
+        <div>提交时间:{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
+        <div>生成时间:{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
+      </div>
+    </div>
+    <!-- 模型 -->
+    <div class="item">
+      <div class="tip">模型</div>
+      <div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
+    </div>
+    <!-- 提示词 -->
+    <div class="item">
+      <div class="tip">提示词</div>
+      <div class="body">
+        {{ detail.prompt }}
+      </div>
+    </div>
+    <!-- 地址 -->
+    <div class="item">
+      <div class="tip">图片地址</div>
+      <div class="body">
+        {{ detail.videoUrl }}
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { VideoApi, VideoVO } from '@/api/ai/video'
+import { formatTime } from '@/utils'
+
+const showDrawer = ref<boolean>(false) // 是否显示
+const detail = ref<VideoVO>({} as VideoVO) // 图片详细信息
+
+const props = defineProps({
+  show: {
+    type: Boolean,
+    require: true,
+    default: false
+  },
+  id: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 关闭抽屉  */
+const handleDrawerClose = async () => {
+  emits('handleDrawerClose')
+}
+
+/** 监听 drawer 是否打开 */
+const { show } = toRefs(props)
+watch(show, async (newValue, oldValue) => {
+  showDrawer.value = newValue as boolean
+})
+
+/**  获取图片详情  */
+const getVideoDetail = async (id: number) => {
+  detail.value = await VideoApi.getVideoMy(id)
+}
+
+/** 监听 id 变化,加载最新图片详情 */
+const { id } = toRefs(props)
+watch(id, async (newVal, oldVal) => {
+  if (newVal) {
+    await getVideoDetail(newVal)
+  }
+})
+
+const emits = defineEmits(['handleDrawerClose'])
+</script>
+<style scoped lang="scss">
+.item {
+  margin-bottom: 20px;
+  width: 100%;
+  overflow: hidden;
+  word-wrap: break-word;
+
+  .header {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+  }
+
+  .tip {
+    font-weight: bold;
+    font-size: 16px;
+  }
+
+  .body {
+    margin-top: 10px;
+    color: #616161;
+
+    .video{
+      width: 100%;
+    }
+
+    .taskVideo {
+      border-radius: 10px;
+    }
+  }
+}
+</style>

+ 227 - 0
src/views/ai/video/index/components/VideoList.vue

@@ -0,0 +1,227 @@
+<template>
+  <el-card class="dr-task" body-class="task-card" shadow="never">
+    <template #header>
+      视频任务
+      <!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
+<!--      <el-button @click="handleViewPublic">视频作品</el-button>-->
+    </template>
+    <!-- 图片列表 -->
+    <div class="task-video-list" ref="videoListRef">
+      <VideoCard
+        v-for="video in videoList"
+        :key="video.id"
+        :detail="video"
+        @on-btn-click="handleVideoButtonClick"
+      />
+    </div>
+    <div class="task-video-pagination">
+      <Pagination
+        :total="pageTotal"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getVideoList"
+      />
+    </div>
+  </el-card>
+
+  <!-- 图片详情 -->
+  <VideoDetail
+    :show="isShowVideoDetail"
+    :id="showVideoDetailId"
+    @handle-drawer-close="handleDetailClose"
+  />
+</template>
+<script setup lang="ts">
+import {
+  VideoApi,
+  VideoVO
+} from '@/api/ai/video'
+import VideoDetail from './VideoDetail.vue'
+import VideoCard from './VideoCard.vue'
+import { ElLoading, LoadingOptionsResolved } from 'element-plus'
+import { AiVideoStatusEnum } from '@/views/ai/utils/constants'
+import download from '@/utils/download'
+
+const message = useMessage() // 消息弹窗
+const router = useRouter() // 路由
+
+// 图片分页相关的参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const pageTotal = ref<number>(0) // page size
+const videoList = ref<VideoVO[]>([]) // video 列表
+const videoListLoadingInstance = ref<any>() // video 列表是否正在加载中
+const videoListRef = ref<any>() // ref
+// 图片轮询相关的参数(正在生成中的)
+const inProgressVideoMap = ref<{}>({}) // 监听的 video 映射,一般是生成中(需要轮询),key 为 video 编号,value 为 video
+const inProgressTimer = ref<any>() // 生成中的 video 定时器,轮询生成进展
+// 图片详情相关的参数
+const isShowVideoDetail = ref<boolean>(false) // 图片详情是否展示
+const showVideoDetailId = ref<number>(0) // 图片详情的图片编号
+
+/** 处理查看视频作品 */
+const handleViewPublic = () => {
+  router.push({
+    name: 'AiVideoSquare'
+  })
+}
+
+/** 查看图片的详情  */
+const handleDetailOpen = async () => {
+  isShowVideoDetail.value = true
+}
+
+/** 关闭图片的详情  */
+const handleDetailClose = async () => {
+  isShowVideoDetail.value = false
+}
+
+/** 获得 video 图片列表 */
+const getVideoList = async () => {
+  try {
+    // 1. 加载图片列表
+    videoListLoadingInstance.value = ElLoading.service({
+      target: videoListRef.value,
+      text: '加载中...'
+    } as LoadingOptionsResolved)
+    const { list, total } = await VideoApi.getVideoPageMy(queryParams)
+    videoList.value = list
+    pageTotal.value = total
+
+    // 2. 计算需要轮询的图片
+    const newWatVideos = {}
+    videoList.value.forEach((item) => {
+      if (item.status === AiVideoStatusEnum.IN_PROGRESS) {
+        newWatVideos[item.id] = item
+      }
+    })
+    inProgressVideoMap.value = newWatVideos
+  } finally {
+    // 关闭正在“加载中”的 Loading
+    if (videoListLoadingInstance.value) {
+      videoListLoadingInstance.value.close()
+      videoListLoadingInstance.value = null
+    }
+  }
+}
+
+/** 轮询生成中的 video 列表 */
+const refreshWatchVideos = async () => {
+  const videoIds = Object.keys(inProgressVideoMap.value).map(Number)
+  if (videoIds.length == 0) {
+    return
+  }
+  const list = (await VideoApi.getVideoListMyByIds(videoIds)) as VideoVO[]
+  const newWatchVideos = {}
+  list.forEach((video) => {
+    if (video.status === AiVideoStatusEnum.IN_PROGRESS) {
+      newWatchVideos[video.id] = video
+    } else {
+      const index = videoList.value.findIndex((oldVideo) => video.id === oldVideo.id)
+      if (index >= 0) {
+        // 更新 videoList
+        videoList.value[index] = video
+      }
+    }
+  })
+  inProgressVideoMap.value = newWatchVideos
+}
+
+/** 图片的点击事件 */
+const handleVideoButtonClick = async (type: string, videoDetail: VideoVO) => {
+  // 详情
+  if (type === 'more') {
+    showVideoDetailId.value = videoDetail.id
+    await handleDetailOpen()
+    return
+  }
+  // 删除
+  if (type === 'delete') {
+    await message.confirm(`是否删除照片?`)
+    await VideoApi.deleteVideoMy(videoDetail.id)
+    await getVideoList()
+    message.success('删除成功!')
+    return
+  }
+  // 下载
+  if (type === 'download') {
+    window.open(videoDetail.videoUrl)
+    // await download.video({ url: videoDetail.videoUrl })
+    return
+  }
+  // 重新生成
+  if (type === 'regeneration') {
+    // await emits('onRegeneration', videoDetail)
+    return
+  }
+}
+
+defineExpose({ getVideoList }) // 暴露组件方法
+
+const emits = defineEmits(['onRegeneration'])
+
+/** 组件挂在的时候 */
+onMounted(async () => {
+  // 获取 video 列表
+  await getVideoList()
+  // 自动刷新 video 列表
+  inProgressTimer.value = setInterval(async () => {
+    await refreshWatchVideos()
+  }, 1000 * 3)
+})
+
+/** 组件取消挂在的时候 */
+onUnmounted(async () => {
+  if (inProgressTimer.value) {
+    clearInterval(inProgressTimer.value)
+  }
+})
+</script>
+<style lang="scss">
+.dr-task {
+  width: 100%;
+  height: 100%;
+}
+.task-card {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  position: relative;
+}
+
+.task-video-list {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  height: 100%;
+  overflow: auto;
+  padding: 20px 20px 140px;
+  box-sizing: border-box; /* 确保内边距不会增加高度 */
+
+  > div {
+    margin-right: 20px;
+    margin-bottom: 20px;
+  }
+  > div:last-of-type {
+    //margin-bottom: 100px;
+  }
+}
+
+.task-video-pagination {
+  position: absolute;
+  bottom: 60px;
+  height: 50px;
+  line-height: 90px;
+  width: 100%;
+  z-index: 999;
+  background-color: #ffffff;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 222 - 0
src/views/ai/video/index/components/common/index.vue

@@ -0,0 +1,222 @@
+<!-- dall3 -->
+<template>
+  <div class="prompt">
+    <el-text tag="b">画面描述</el-text>
+    <el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
+    <el-input
+      v-model="prompt"
+      maxlength="1024"
+      :rows="5"
+      class="w-100% mt-15px"
+      input-style="border-radius: 7px;"
+      placeholder="例如:童话里的小屋应该是什么样子?"
+      show-word-limit
+      type="textarea"
+    />
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">平台</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select
+        v-model="otherPlatform"
+        placeholder="Select"
+        size="large"
+        class="!w-350px"
+        @change="handlerPlatformChange"
+      >
+        <el-option
+          v-for="item in AiVideoOtherPlatformEnum"
+          :key="item.key"
+          :label="item.name"
+          :value="item.key"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">模型</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
+        <el-option
+          v-for="item in platformModels"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">分辨率</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-button type="primary" v-model="resolution">1080P</el-button>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">视频时长</el-text>
+    </div>
+    <div class="duration">
+      <el-slider v-model="duration" show-input :min="2" :max="5" :step="1" />
+    </div>
+  </div>
+  <div class="btns">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0"
+      @click="handleGenerateVideo"
+    >
+      {{ drawIn ? '生成中' : '生成内容' }}
+    </el-button>
+  </div>
+</template>
+<script setup lang="ts">
+import { VideoApi, VideoDrawReqVO, VideoVO } from '@/api/ai/video'
+import { AiPlatformEnum, AiVideoOtherPlatformEnum } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 消息弹窗
+
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
+// 定义属性
+const drawIn = ref<boolean>(false) // 生成中
+const selectHotWord = ref<string>('') // 选中的热词
+// 表单
+const prompt = ref<string>('') // 提示词
+const width = ref<number>(1024) // 图片宽度
+const height = ref<number>(1024) // 图片高度
+const resolution = ref<string>('1080P') // 分辨率
+const duration = ref<number>(4) // 视频时长
+const otherPlatform = ref<string>(AiPlatformEnum.DOU_BAO) // 平台
+const platformModels = ref<ModelVO[]>([]) // 模型列表
+const modelId = ref<number>() // 选中的模型
+
+/** 选择热词 */
+const handleHotWordClick = async (hotWord: string) => {
+  // 情况一:取消选中
+  if (selectHotWord.value == hotWord) {
+    selectHotWord.value = ''
+    return
+  }
+
+  // 情况二:选中
+  selectHotWord.value = hotWord // 选中
+  prompt.value = hotWord // 替换提示词
+}
+
+/** 图片生成 */
+const handleGenerateVideo = async () => {
+  // 二次确认
+  await message.confirm(`确认生成内容?`)
+  try {
+    // 加载中
+    drawIn.value = true
+    // 回调
+    emits('onDrawStart', otherPlatform.value)
+    // 发送请求
+    const form = {
+      platform: otherPlatform.value,
+      modelId: modelId.value, // 模型
+      prompt: prompt.value, // 提示词
+      width: width.value, // 图片宽度
+      height: height.value, // 图片高度
+      resolution: resolution.value, // 分辨率
+      duration: duration.value, // 视频时长
+      options: {}
+    } as unknown as VideoDrawReqVO
+    await VideoApi.drawVideo(form)
+  } finally {
+    // 回调
+    emits('onDrawComplete', otherPlatform.value)
+    // 加载结束
+    drawIn.value = false
+  }
+}
+
+/** 填充值 */
+const settingValues = async (detail: VideoVO) => {
+  prompt.value = detail.prompt
+  width.value = detail.width
+  height.value = detail.height
+}
+
+/** 平台切换 */
+const handlerPlatformChange = async (platform: string) => {
+  // 根据选择的平台筛选模型
+  platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
+
+  // 切换平台,默认选择一个模型
+  if (platformModels.value.length > 0) {
+    modelId.value = platformModels.value[0].id // 使用 model 属性作为值
+  } else {
+    modelId.value = undefined
+  }
+}
+
+/** 监听 models 变化 */
+watch(
+  () => props.models,
+  () => {
+    handlerPlatformChange(otherPlatform.value)
+  },
+  { immediate: true, deep: true }
+)
+/** 暴露组件方法 */
+defineExpose({ settingValues })
+</script>
+<style scoped lang="scss">
+.hot-words {
+  display: flex;
+  flex-direction: column;
+  margin-top: 30px;
+
+  .word-list {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: start;
+    margin-top: 15px;
+
+    .btn {
+      margin: 0;
+    }
+  }
+}
+
+// 模型
+.group-item {
+  margin-top: 30px;
+
+  .group-item-body {
+    margin-top: 15px;
+    width: 100%;
+  }
+}
+
+.duration{
+  margin-left: 15px;
+}
+
+.btns {
+  display: flex;
+  justify-content: center;
+  margin-top: 50px;
+}
+</style>

+ 217 - 0
src/views/ai/video/index/components/videoImage/index.vue

@@ -0,0 +1,217 @@
+<!-- dall3 -->
+<template>
+  <div class="prompt">
+    <el-text tag="b">上传视频</el-text>
+    <el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
+    <UploadImg v-model="promptImage" />
+    <el-input
+      v-model="prompt"
+      maxlength="1024"
+      :rows="5"
+      class="w-100% mt-15px"
+      input-style="border-radius: 7px;"
+      placeholder="例如:童话里的小屋应该是什么样子?"
+      show-word-limit
+      type="textarea"
+    />
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">平台</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select
+        v-model="otherPlatform"
+        placeholder="Select"
+        size="large"
+        class="!w-350px"
+        @change="handlerPlatformChange"
+      >
+        <el-option
+          v-for="item in AiVideoOtherPlatformEnum"
+          :key="item.key"
+          :label="item.name"
+          :value="item.key"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">模型</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
+        <el-option
+          v-for="item in platformModels"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">视频尺寸</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-button type="primary">自动匹配</el-button>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">分辨率</el-text>
+    </div>
+    <el-space wrap class="group-item-body">
+      <el-button type="primary" v-model="resolution">1080P</el-button>
+    </el-space>
+  </div>
+  <div class="group-item">
+    <div>
+      <el-text tag="b">视频时长</el-text>
+    </div>
+    <div class="duration">
+      <el-slider v-model="duration" show-input :min="2" :max="5" :step="1" />
+    </div>
+  </div>
+  <div class="btns">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0 && promptImage === ''"
+      @click="handleGenerateVideo"
+    >
+      {{ drawIn ? '生成中' : '生成内容' }}
+    </el-button>
+  </div>
+</template>
+<script setup lang="ts">
+import { VideoApi, VideoDrawReqVO, VideoVO } from '@/api/ai/video'
+import { AiPlatformEnum, AiVideoOtherPlatformEnum } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 消息弹窗
+
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
+// 定义属性
+const drawIn = ref<boolean>(false) // 生成中
+const selectHotWord = ref<string>('') // 选中的热词
+// 表单
+const prompt = ref<string>('') // 提示词
+const promptImage = ref<string>('') // 参考视频
+const width = ref<number>(1024) // 视频宽度
+const height = ref<number>(1024) // 视频高度
+const resolution = ref<string>('1080P') // 分辨率
+const duration = ref<number>(4) // 视频时长
+const otherPlatform = ref<string>(AiPlatformEnum.DOU_BAO) // 平台
+const platformModels = ref<ModelVO[]>([]) // 模型列表
+const modelId = ref<number>() // 选中的模型
+
+/** 视频生成 */
+const handleGenerateVideo = async () => {
+  // 二次确认
+  await message.confirm(`确认生成内容?`)
+  try {
+    // 加载中
+    drawIn.value = true
+    // 回调
+    emits('onDrawStart', otherPlatform.value)
+    // 发送请求
+    const form = {
+      platform: otherPlatform.value,
+      modelId: modelId.value, // 模型
+      prompt: prompt.value, // 提示词
+      promptImage: "https://learn-ai.com.cn/admin-api/infra/file/29/get/20250914/33dbe291bd38b5db518434342be5d35c9a6c14c5c99c755ddfaf13ffe02e77d3_1757845073064.jpg", // 参考视频
+      width: width.value, // 视频宽度
+      height: height.value, // 视频高度
+      resolution: resolution.value, // 分辨率
+      duration: duration.value, // 视频时长
+      options: {}
+    } as unknown as VideoDrawReqVO
+    await VideoApi.drawVideo(form)
+  } finally {
+    // 回调
+    emits('onDrawComplete', otherPlatform.value)
+    // 加载结束
+    drawIn.value = false
+  }
+}
+
+/** 填充值 */
+const settingValues = async (detail: VideoVO) => {
+  prompt.value = detail.prompt
+  width.value = detail.width
+  height.value = detail.height
+}
+
+/** 平台切换 */
+const handlerPlatformChange = async (platform: string) => {
+  // platformModels.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.VIDEO)
+  // 根据选择的平台筛选模型
+  platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
+
+  // 切换平台,默认选择一个模型
+  if (platformModels.value.length > 0) {
+    modelId.value = platformModels.value[0].id // 使用 model 属性作为值
+  } else {
+    modelId.value = undefined
+  }
+}
+
+/** 监听 models 变化 */
+watch(
+  () => props.models,
+  () => {
+    handlerPlatformChange(otherPlatform.value)
+  },
+  { immediate: true, deep: true }
+)
+/** 暴露组件方法 */
+defineExpose({ settingValues })
+</script>
+<style scoped lang="scss">
+.hot-words {
+  display: flex;
+  flex-direction: column;
+  margin-top: 30px;
+
+  .word-list {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: start;
+    margin-top: 15px;
+
+    .btn {
+      margin: 0;
+    }
+  }
+}
+
+// 模型
+.group-item {
+  margin-top: 30px;
+
+  .group-item-body {
+    margin-top: 15px;
+    width: 100%;
+  }
+}
+
+.btns {
+  display: flex;
+  justify-content: center;
+  margin-top: 50px;
+}
+</style>

+ 125 - 0
src/views/ai/video/index/index.vue

@@ -0,0 +1,125 @@
+<!-- video -->
+<template>
+  <div class="ai-video">
+    <div class="left">
+      <div class="segmented">
+        <el-segmented v-model="selectPlatform" :options="platformOptions" />
+      </div>
+      <div class="modal-switch-container">
+        <Common
+          v-if="selectPlatform === 'common'"
+          ref="commonRef"
+          :models="models"
+          @on-draw-complete="handleDrawComplete"
+        />
+        <VideoImage
+          v-if="selectPlatform === 'videoImage'"
+          ref="VideoImageRef"
+          :models="models"
+          @on-draw-complete="handleDrawComplete"
+        />
+      </div>
+    </div>
+    <div class="main">
+      <VideoList ref="videoListRef" @on-regeneration="handleRegeneration" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import VideoList from './components/VideoList.vue'
+import { VideoVO } from '@/api/ai/video'
+import Common from './components/common/index.vue'
+import VideoImage from './components/videoImage/index.vue'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+const videoListRef = ref<any>() // video 列表 ref
+const commonRef = ref<any>() // stable diffusion ref
+
+// 定义属性
+const selectPlatform = ref('common') // 选中的平台
+const platformOptions = [
+  {
+    label: '通用',
+    value: 'common'
+  },
+  {
+    label: '图生视频',
+    value: "videoImage"
+  },
+]
+
+const models = ref<ModelVO[]>([]) // 模型列表
+
+/** 视频 start  */
+const handleDrawStart = async (platform: string) => {}
+
+/** 视频 complete */
+const handleDrawComplete = async (platform: string) => {
+  await videoListRef.value.getVideoList()
+}
+
+/** 重新生成:将画图详情填充到对应平台 */
+const handleRegeneration = async (video: VideoVO) => {
+  // 切换平台
+  selectPlatform.value = video.platform
+  // 根据不同平台填充 video
+  await nextTick()
+  // if (video.platform === AiPlatformEnum.DOU_BAO) {
+  //   //   // 新增:通用平台(包括豆包)使用 commonRef 填充数据
+  //   commonRef.value.settingValues(video)
+  // }
+}
+
+/** 组件挂载的时候 */
+onMounted(async () => {
+  // 获取模型列表
+  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.VIDEO)
+})
+</script>
+
+<style scoped lang="scss">
+.ai-video {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+
+  display: flex;
+  flex-direction: row;
+  height: 100%;
+  width: 100%;
+
+  .left {
+    display: flex;
+    flex-direction: column;
+    padding: 20px;
+    width: 390px;
+
+    .segmented .el-segmented {
+      --el-border-radius-base: 16px;
+      --el-segmented-item-selected-color: #fff;
+      background-color: #ececec;
+      width: 350px;
+    }
+
+    .modal-switch-container {
+      height: 100%;
+      overflow-y: auto;
+      margin-top: 30px;
+    }
+  }
+
+  .main {
+    flex: 1;
+    background-color: #fff;
+  }
+
+  .right {
+    width: 350px;
+    background-color: #f7f8fa;
+  }
+}
+</style>

+ 267 - 0
src/views/ai/video/manager/index.vue

@@ -0,0 +1,267 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="用户编号" prop="userId">
+        <el-select
+          v-model="queryParams.userId"
+          clearable
+          placeholder="请输入用户编号"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="item in userList"
+            :key="item.id"
+            :label="item.nickname"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="平台" prop="platform">
+        <el-select v-model="queryParams.platform" placeholder="请选择平台" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="视频状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择视频状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.AI_VIDEO_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否发布" prop="publicStatus">
+        <el-select
+          v-model="queryParams.publicStatus"
+          placeholder="请选择是否发布"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" width="180" fixed="left" />
+      <el-table-column label="视频" align="center" prop="videoUrl" width="110px" fixed="left">
+<!--        <template #default="{ row }">-->
+<!--          <el-video-->
+<!--            class="h-80px w-80px"-->
+<!--            lazy-->
+<!--            :src="row.videoUrl"-->
+<!--            :preview-src-list="[row.videoUrl]"-->
+<!--            preview-teleported-->
+<!--            fit="cover"-->
+<!--            v-if="row.videoUrl?.length > 0"-->
+<!--          />-->
+<!--        </template>-->
+      </el-table-column>
+      <el-table-column label="用户" align="center" prop="userId" width="180">
+        <template #default="scope">
+          <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="平台" align="center" prop="platform" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+        </template>
+      </el-table-column>
+      <el-table-column label="模型" align="center" prop="model" width="180" />
+      <el-table-column label="视频状态" align="center" prop="status" width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_VIDEO_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否发布" align="center" prop="publicStatus">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.publicStatus"
+            :active-value="true"
+            :inactive-value="false"
+            @change="handleUpdatePublicStatusChange(scope.row)"
+            :disabled="scope.row.status !== AiVideoStatusEnum.SUCCESS"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="提示词" align="center" prop="prompt" width="180" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="参考图" align="center" prop="promptImage" width="110px" fixed="left">
+        <template #default="{ row }">
+          <el-image
+            class="h-80px w-80px"
+            lazy
+            :src="row.promptImage"
+            :preview-src-list="[row.promptImage]"
+            preview-teleported
+            fit="cover"
+            v-if="row.promptImage?.length > 0"
+          />
+        </template>
+      </el-table-column>
+<!--      <el-table-column label="宽度" align="center" prop="width" />-->
+<!--      <el-table-column label="高度" align="center" prop="height" />-->
+      <el-table-column label="错误信息" align="center" prop="errorMessage" />
+      <el-table-column label="任务编号" align="center" prop="taskId" />
+      <el-table-column label="操作" align="center" width="100" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="danger"
+            v-if="scope.row.tenantId == getTenantId()"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:video:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { VideoApi, VideoVO } from '@/api/ai/video'
+import * as UserApi from '@/api/system/user'
+import { AiVideoStatusEnum } from '@/views/ai/utils/constants'
+import { getTenantId } from '@/utils/auth'
+import { ElButton } from 'element-plus'
+
+/** AI 视频 列表 */
+defineOptions({ name: 'AiVideoManager' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<VideoVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: undefined,
+  platform: undefined,
+  status: undefined,
+  publicStatus: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await VideoApi.getVideoPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await VideoApi.deleteVideo(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 修改是否发布 */
+const handleUpdatePublicStatusChange = async (row: VideoVO) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.publicStatus ? '公开' : '私有'
+    await message.confirm('确认要"' + text + '"该图片吗?')
+    // 发起修改状态
+    await VideoApi.updateVideo({
+      id: row.id,
+      publicStatus: row.publicStatus
+    })
+    await getList()
+  } catch {
+    row.publicStatus = !row.publicStatus
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  getList()
+  // 获得用户列表
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>