Prechádzať zdrojové kódy

feat: 互动视频集成节点编辑、视频预览功能

dx 3 mesiacov pred
rodič
commit
673e189d53

+ 47 - 0
src/api/interactVideo/videoManage.js

@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+// 查询互动视频列表
+export function getInteractionVideoList(params) {
+  return request({
+    url: "/interactionVideo/list",
+    method: "get",
+    params,
+  });
+}
+// 新增视频
+export function interactionVideoAdd(data) {
+  return request({
+    url: "/interactionVideo",
+    method: "post",
+    data,
+  });
+}
+// 修改视频
+export function interactionVideoUpdate(data) {
+  return request({
+    url: "/interactionVideo",
+    method: "put",
+    data,
+  });
+}
+// 视频详情
+export function getInteractionVideoDetail(interactionId) {
+  return request({
+    url: `/interactionVideo/${interactionId}`,
+    method: "get",
+  });
+}
+// 获取互动视频关联素材详情
+export function getMaterialInfoDetail(interactionId) {
+  return request({
+    url: `/interactionVideo/detail/materialInfo/${interactionId}`,
+    method: "get",
+  });
+}
+// 视频删除
+export function interactionVideoDel(interactionIds) {
+  return request({
+    url: `/interactionVideo/${interactionIds}`,
+    method: "delete",
+  });
+}

+ 32 - 5
src/views/interactVideo/sourceMaterialManage/index.vue

@@ -81,6 +81,13 @@
           <el-button type="danger" icon="Delete" link @click="handleDelete(row)"
             >删除</el-button
           >
+          <el-button
+            type="primary"
+            link
+            @click="handlepreview(row)"
+            v-if="row.materialType == 0"
+            >视频预览</el-button
+          >
         </template>
       </el-table-column>
     </el-table>
@@ -147,10 +154,20 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 视频预览 -->
+    <el-dialog title="视频预览" v-model="videoViewOpen" width="1200">
+      <XGVideoViewer
+        :videoUrl="videoUrl"
+        :autoplay="true"
+        :autoplayMuted="false"
+      />
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="SourceMaterialManage">
+import XGVideoViewer from "@/components/XGVideoViewer";
 import {
   getLibraryList,
   libraryAdd,
@@ -170,6 +187,8 @@ const single = ref(true);
 const multiple = ref(true);
 const dialogTitle = ref("新增素材");
 const materialOpen = ref(false);
+const videoViewOpen = ref(false);
+const videoUrl = ref("");
 const data = reactive({
   form: {
     materialName: undefined,
@@ -242,22 +261,24 @@ function handleDelete(row) {
 function handleAdd() {
   form.value = {
     materialName: undefined,
-    materialType: "0",
+    materialType: 0,
     fileList: [],
   };
   dialogTitle.value = "新增素材";
   materialOpen.value = true;
 }
 function handleUpdate(row) {
+  const materialId = row.materialId || ids.value[0];
   dialogTitle.value = "编辑素材";
   materialOpen.value = true;
   detailLoading.value = true;
-  getLibraryDetail(row.materialId)
+  getLibraryDetail(materialId)
     .then((res) => {
       form.value = {
         materialId: res.data.materialId,
         materialName: res.data.materialName,
         materialType: res.data.materialType,
+        fileList: [],
       };
     })
     .finally(() => {
@@ -271,7 +292,6 @@ function dialogSubmit() {
   proxy.$refs["dataForm"].validate((valid) => {
     if (valid) {
       confirmLoading.value = true;
-      console.log(form.value);
       const { materialName, materialType, fileList = [] } = form.value;
       formData.append("materialName", materialName);
       formData.append("materialType", materialType);
@@ -282,7 +302,6 @@ function dialogSubmit() {
         formData.append("materialId", form.value.materialId);
         libraryUpdate(formData)
           .then((res) => {
-            console.log(res);
             if (res.code == 200) {
               proxy.$modal.msgSuccess("编辑成功");
               materialOpen.value = false;
@@ -295,7 +314,6 @@ function dialogSubmit() {
       } else {
         libraryAdd(formData)
           .then((res) => {
-            console.log(res);
             if (res.code == 200) {
               proxy.$modal.msgSuccess("新增成功");
               materialOpen.value = false;
@@ -325,4 +343,13 @@ function uploadChange(file) {
 function uploadRemove() {
   form.value.fileList = [];
 }
+
+function handlepreview(row) {
+  if (!row.url) {
+    proxy.$modal.msgError("视频文件错误,请重新上传视频");
+    return;
+  }
+  videoUrl.value = row.url;
+  videoViewOpen.value = true;
+}
 </script>

+ 363 - 0
src/views/interactVideo/videoManage/formFields.vue

@@ -0,0 +1,363 @@
+<template>
+  <div>
+    <!-- 新增互动视频基本信息 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="materialOpen"
+      width="1200"
+      append-to-body
+    >
+      <el-form
+        :model="form"
+        ref="dataForm"
+        :rules="rules"
+        label-width="110px"
+        inline
+      >
+        <el-row :gutter="10">
+          <el-col :span="8">
+            <el-form-item label="互动视频名称" prop="interactionName">
+              <el-input
+                v-model="form.interactionName"
+                placeholder="请输入互动视频名称"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="节点类型" prop="nodeType">
+              <el-select v-model="form.nodeType" placeholder="请选择节点类型">
+                <el-option label="根节点" value="root-node" />
+                <el-option label="场景节点" value="plot-node" />
+                <el-option label="跳转节点" value="jump-node" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="素材" prop="selectMaterial">
+              <el-radio-group v-model="form.selectMaterial">
+                <el-radio :label="1">选择已有素材</el-radio>
+                <el-radio :label="2" @click="uploadSource">上传素材</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" v-show="form.selectMaterial == 1">
+            <el-form-item label="">
+              <!-- 列表 -->
+              <el-table
+                border
+                row-key="materialId"
+                v-loading="loading"
+                :data="libraryList"
+                @selection-change="handleSelectionChange"
+              >
+                <el-table-column
+                  type="selection"
+                  width="100"
+                  align="center"
+                  :reserve-selection="true"
+                />
+                <el-table-column
+                  type="index"
+                  label="序号"
+                  width="100"
+                  align="center"
+                />
+                <el-table-column
+                  prop="materialName"
+                  label="素材名称"
+                  align="center"
+                />
+                <!-- 素材类型,0-视频、1-图片 -->
+                <el-table-column
+                  prop="materialType"
+                  label="素材类型"
+                  align="center"
+                >
+                  <template #default="{ row }">
+                    {{ libraryType[row.materialType] }}
+                  </template>
+                </el-table-column>
+                <!-- 0=未发布,1=发布 -->
+                <el-table-column prop="status" label="状态" align="center">
+                  <template #default="{ row }">
+                    {{ libraryStatus[row.status] }}
+                  </template>
+                </el-table-column>
+              </el-table>
+              <pagination
+                v-show="total > 0"
+                :total="total"
+                v-model:page="queryParams.pageNum"
+                v-model:limit="queryParams.pageSize"
+                @pagination="getList"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            type="primary"
+            :loading="confirmLoading"
+            @click="dialogSubmit"
+            >确 定</el-button
+          >
+          <el-button :loading="confirmLoading" @click="dialogCancel"
+            >取 消</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="新增素材" v-model="addSourceOpen" width="800">
+      <el-form
+        :model="materialForm"
+        ref="materialFormRef"
+        :rules="materialRules"
+        label-width="100"
+      >
+        <el-form-item label="素材名称" prop="materialName">
+          <el-input
+            v-model="materialForm.materialName"
+            placeholder="请输入素材名称"
+          />
+        </el-form-item>
+        <el-form-item label="素材类型" prop="materialType">
+          <el-select
+            v-model="materialForm.materialType"
+            placeholder="请选择素材类型"
+          >
+            <el-option :value="0" label="视频" />
+            <el-option :value="1" label="图片" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="素材文件">
+          <el-upload
+            ref="uploadRef"
+            action=""
+            :file-list="materialForm.fileList"
+            :on-change="uploadChange"
+            :on-remove="uploadRemove"
+            :auto-upload="false"
+            :limit="1"
+          >
+            <template #trigger>
+              <el-button type="primary">选择文件</el-button>
+            </template>
+            <template #tip>
+              <div class="el-upload__tip">仅支持上传图片或视频文件</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            type="primary"
+            :loading="materialConfirmLoading"
+            @click="sourceAdd"
+            >确 定</el-button
+          >
+          <el-button :loading="materialConfirmLoading" @click="sourceAddCancel"
+            >取 消</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="FormFields">
+import {
+  getLibraryList,
+  libraryAdd,
+} from "@/api/interactVideo/sourceMaterialManage";
+import { interactionVideoAdd } from "@/api/interactVideo/videoManage";
+const { proxy } = getCurrentInstance();
+const router = useRouter();
+const props = defineProps({
+  open: {
+    type: Boolean,
+    default: false,
+  },
+});
+const emits = defineEmits(["close"]);
+const materialOpen = ref(false);
+const dialogTitle = ref("新增互动视频");
+const confirmLoading = ref(false);
+const loading = ref(false);
+const libraryList = ref([]);
+const total = ref(0);
+const materialConfirmLoading = ref(false);
+const addSourceOpen = ref(false);
+const ids = ref([]);
+const uploadId = ref([]);
+const data = reactive({
+  form: {
+    nodeType: "root-node",
+    selectMaterial: 1,
+  },
+  rules: {
+    interactionName: [
+      { required: true, message: "互动视频名称不能为空", trigger: "blur" },
+    ],
+  },
+  materialForm: {
+    materialName: undefined,
+    materialType: undefined,
+    fileList: [],
+  },
+  materialRules: {
+    materialName: [
+      { required: true, message: "素材名称不能为空", trigger: "blur" },
+    ],
+    materialType: [
+      { required: true, message: "素材类型不能为空", trigger: "change" },
+    ],
+  },
+  libraryType: {
+    0: "视频",
+    1: "图片",
+  },
+  libraryStatus: {
+    0: "未发布",
+    1: "已发布",
+  },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+  },
+});
+
+const {
+  form,
+  rules,
+  libraryType,
+  libraryStatus,
+  queryParams,
+  materialRules,
+  materialForm,
+} = toRefs(data);
+
+watchEffect(() => {
+  if (props.open) {
+    materialOpen.value = true;
+    getList();
+  } else {
+    materialOpen.value = false;
+  }
+});
+
+// 互动视频新增并关联素材
+function dialogSubmit() {
+  proxy.$refs["dataForm"].validate((valid) => {
+    if (valid) {
+      // 判断选中素材id和上传素材id是否为空
+      if (ids.value.length == 0 && uploadId.value.length == 0) {
+        proxy.$modal.msgError("请选择素材");
+        return;
+      }
+      confirmLoading.value = true;
+      const { interactionName, remark, nodeType } = form.value;
+      const params = {
+        materialIds: ids.value,
+        interactionName,
+        remark,
+        nodeType,
+      };
+      interactionVideoAdd(params)
+        .then((res) => {
+          // 新增成功后拿到id跳转互动视频节点编辑
+          if (res.code == 200) {
+            proxy.$modal.msgSuccess("新增成功");
+            dialogCancel();
+            router.push({
+              path: "/interactVideo/videoOperation",
+              query: { interactionId: res.data },
+            });
+          }
+        })
+        .finally(() => {
+          confirmLoading.value = false;
+        });
+      confirmLoading.value = true;
+    }
+  });
+}
+function dialogCancel() {
+  emits("close", false);
+  confirmLoading.value = false;
+}
+
+/** 查询素材列表 */
+function getList() {
+  loading.value = true;
+  getLibraryList(queryParams.value).then((response) => {
+    libraryList.value = response.rows;
+    total.value = response.total;
+    loading.value = false;
+  });
+}
+// 选中素材id
+function handleSelectionChange(selection) {
+  ids.value = selection.map((item) => item.materialId);
+}
+
+// 素材新增
+function uploadSource() {
+  addSourceOpen.value = true;
+  materialForm.value = {
+    materialName: undefined,
+    materialType: 0,
+    fileList: [],
+  };
+}
+function sourceAdd() {
+  const formData = new FormData();
+  proxy.$refs["materialFormRef"].validate((valid) => {
+    if (valid) {
+      materialConfirmLoading.value = true;
+      const { materialName, materialType, fileList } = materialForm.value;
+      formData.append("materialName", materialName);
+      formData.append("materialType", materialType);
+      fileList.forEach((file) => {
+        formData.append("file", file.raw);
+      });
+      libraryAdd(formData)
+        .then((res) => {
+          if (res.code == 200) {
+            proxy.$modal.msgSuccess("新增成功");
+            uploadId.value.push(res.data);
+            addSourceOpen.value = false;
+          }
+        })
+        .finally(() => {
+          materialConfirmLoading.value = false;
+        })
+        .catch((err) => {
+          console.log(err);
+        });
+    }
+  });
+}
+// 取消素材新增
+function sourceAddCancel() {
+  materialConfirmLoading.value = false;
+  addSourceOpen.value = false;
+}
+
+// 文件上传回调
+function uploadChange(file) {
+  materialForm.value.fileList.push(file);
+}
+
+function uploadRemove() {
+  materialForm.value.fileList = [];
+}
+</script>

+ 170 - 0
src/views/interactVideo/videoManage/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="app-container">
+    <!-- 查询条件 -->
+    <el-form
+      :model="queryParams"
+      ref="queryRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="互动视频名称" prop="interactionName">
+        <el-input
+          v-model="queryParams.interactionName"
+          placeholder="请输入互动视频名称"
+          clearable
+          style="width: 240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery"
+          >搜索</el-button
+        >
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    <!-- 操作按钮 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
+          >新增</el-button
+        >
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="Edit"
+          :disabled="single"
+          @click="handleUpdate"
+          >修改</el-button
+        >
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="Delete"
+          :disabled="multiple"
+          @click="handleDelete"
+          >删除</el-button
+        >
+      </el-col>
+    </el-row>
+    <!-- 列表 -->
+    <el-table
+      border
+      v-loading="loading"
+      :data="videoList"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="50" />
+      <el-table-column type="index" label="序号" align="center" width="80" />
+      <el-table-column prop="interactionName" label="视频名称" align="center" />
+      <el-table-column label="操作" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" icon="Edit" link @click="handleUpdate(row)"
+            >编辑</el-button
+          >
+          <el-button type="danger" link @click="handleDelete(row)"
+            >删除</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNum"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+    <!-- 新增编辑 -->
+    <FormFields :open="materialOpen" @close="formFieldsClose" />
+  </div>
+</template>
+
+<script setup name="VideoManage">
+import {
+  getInteractionVideoList,
+  interactionVideoDel,
+} from "@/api/interactVideo/videoManage";
+import FormFields from "./formFields.vue";
+const { proxy } = getCurrentInstance();
+const router = useRouter();
+const loading = ref(false);
+const videoList = ref([]);
+const total = ref(0);
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const materialOpen = ref(false);
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    interactionName: undefined,
+  },
+});
+const { queryParams } = toRefs(data);
+function handleSelectionChange(selection) {
+  ids.value = selection.map((item) => item.interactionId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+getList();
+/** 查询视频列表 */
+function getList() {
+  loading.value = true;
+  getInteractionVideoList(queryParams.value)
+    .then((response) => {
+      videoList.value = response.rows;
+      total.value = response.total;
+      loading.value = false;
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+}
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  handleQuery();
+}
+/** 删除按钮操作 */
+function handleDelete(row) {
+  const interactionIds = row.interactionId || ids.value;
+  proxy.$modal
+    .confirm('是否确认删除互动视频编号为"' + interactionIds + '"的数据项?')
+    .then(function () {
+      return interactionVideoDel(interactionIds);
+    })
+    .then(() => {
+      getList();
+      proxy.$modal.msgSuccess("删除成功");
+    })
+    .catch(() => {});
+}
+
+function handleAdd() {
+  // router.push("/interactVideo/videoOperation");
+  materialOpen.value = true;
+}
+function handleUpdate(row) {
+  const interactionId = row.interactionId || ids.value[0];
+  router.push({
+    path: "/interactVideo/videoOperation",
+    query: { interactionId },
+  });
+}
+
+function formFieldsClose() {
+  materialOpen.value = false;
+  getList();
+}
+</script>

+ 150 - 0
src/views/interactVideo/videoManage/videoOperation.vue

@@ -0,0 +1,150 @@
+<!-- 新增编辑视频 -->
+<template>
+  <div class="app-container">
+    <div style="display: flex">
+      <el-button type="primary" @click="formSubmit">保存</el-button>
+      <el-button type="info" @click="videoView">视频预览</el-button>
+      <el-button @click="returnList">取消</el-button>
+    </div>
+    <div class="border_box">
+      <InteractiveVideoEditor
+        ref="editorRef"
+        :graphData="graphData"
+        :videoSources="videoSources"
+        :imageSources="imageSources"
+      />
+    </div>
+    <el-dialog title="视频预览" v-model="videoViewOpen" width="1200">
+      <XGVideoViewer
+        :question="videoDetail.data.question"
+        :options="videoDetail.data.optionsList"
+        :videoUrl="videoDetail.data.url"
+        :autoplay="true"
+        :autoplayMuted="false"
+        :poster="videoDetail.data.image"
+        @onClickOption="onClickOption"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="VideoOperation">
+import {
+  interactionVideoUpdate,
+  getInteractionVideoDetail,
+  getMaterialInfoDetail,
+} from "@/api/interactVideo/videoManage";
+// 编辑器
+import {
+  transformGraphdata,
+  transfromReturnGraphdata,
+} from "@/hooks/transformGraphdata";
+import InteractiveVideoEditor from "@/components/InteractiveVideoEditor";
+import XGVideoViewer from "@/components/XGVideoViewer";
+import { watchEffect } from "vue";
+
+const { proxy } = getCurrentInstance();
+const route = useRoute();
+// 互动视频基本信息
+const { interactionId } = route.query;
+const interactionVideoInfo = ref({});
+// 资源数据
+const videoSources = ref([]);
+const imageSources = ref([]);
+// 组件实例
+const editorRef = ref(null);
+// 默认流程图数据
+const graphData = ref({
+  id: "0",
+  type: "root-node",
+  preNodeId: null,
+  children: [],
+  data: {
+    name: "默认节点",
+    image: "",
+    url: "",
+    materialId: "1",
+    question: "",
+    optionsList: [],
+  },
+});
+/**
+ * 视频ID变化,则获取相关素材以及节点数据
+ */
+watchEffect(() => {
+  // 获取互动视频基本信息以及节点
+  getInteractionVideoDetail(interactionId).then((res) => {
+    if (res.code == 200) {
+      interactionVideoInfo.value = res.data.interactionVideoInfo;
+      graphData.value = transfromReturnGraphdata(res.data.rootNode);
+    }
+  });
+  // 获取相关素材信息
+  getMaterialInfoDetail(interactionId).then((res) => {
+    // 素材类型 0 视频  1 图片
+    videoSources.value = res.data.materialLibraryVOList
+      .filter((i) => i.materialType == 0)
+      .map((i) => {
+        return {
+          id: i.materialId,
+          name: i.materialName,
+          url: i.accessory?.url,
+        };
+      });
+    imageSources.value = res.data.materialLibraryVOList
+      .filter((i) => i.materialType == 1)
+      .map((i) => {
+        return {
+          id: i.materialId,
+          name: i.materialName,
+          url: i.accessory?.url,
+        };
+      });
+  });
+});
+
+// 关闭当前页签
+function returnList() {
+  proxy.$tab.closePage();
+}
+
+function formSubmit() {
+  // 剧情节点信息
+  const rootNode = transformGraphdata(graphData.value);
+  interactionVideoUpdate({
+    interactionVideoInfo: interactionVideoInfo.value,
+    rootNode,
+  }).then((res) => {
+    if (res.code == 200) {
+      proxy.$modal.msgSuccess(res.msg);
+      returnList();
+    }
+  });
+}
+
+// 视频预览
+const videoViewOpen = ref(false);
+const videoDetail = ref({});
+
+function videoView() {
+  videoDetail.value = graphData.value;
+  if (!videoDetail.value.data.url) {
+    proxy.$modal.msgError("暂无可播放视频,可添加视频素材后预览");
+    return;
+  }
+  videoViewOpen.value = true;
+}
+// 点击选中选项
+const onClickOption = (option) => {
+  videoDetail.value = graphData.value.children.filter(
+    (i) => i.id == option.nodeId
+  )[0];
+};
+</script>
+
+<style>
+.border_box {
+  border: 1px solid #ccc;
+  margin-top: 10px;
+}
+</style>