瀏覽代碼

1. 实现媒资选择;2. 修复新节目自动生成画布失败的bug

Shinohara Haruna 5 月之前
父節點
當前提交
604c7f592b
共有 2 個文件被更改,包括 213 次插入22 次删除
  1. 188 0
      smsb-plus-ui/src/components/MediaFileSelector.vue
  2. 25 22
      smsb-plus-ui/src/views/smsb/itemProgram/EditProgram.vue

+ 188 - 0
smsb-plus-ui/src/components/MediaFileSelector.vue

@@ -0,0 +1,188 @@
+<template>
+  <div>
+    <el-button type="primary" @click="dialogVisible = true">选择文件</el-button>
+    <div v-if="selectedFiles.length > 0" class="selected-files-list">
+      <el-tag v-for="(file, idx) in selectedFiles" :key="file.id" closable @close="removeFile(idx)" style="margin: 2px">
+        {{ file.name }}
+      </el-tag>
+    </div>
+    <el-dialog title="选择媒资文件" v-model="dialogVisible" width="900px" append-to-body @close="restoreSelection">
+      <el-form :inline="true" :model="queryParams" class="mb-2">
+        <el-form-item label="名称">
+          <el-input v-model="queryParams.originalName" placeholder="文件名" clearable style="width: 180px"
+            @keyup.enter="getFileList" />
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="queryParams.type" clearable placeholder="全部" style="width: 120px">
+            <el-option label="全部" :value="''" />
+            <el-option label="图片" :value="1" />
+            <el-option label="视频" :value="2" />
+            <el-option label="音频" :value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="getFileList">搜索</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="dialogLoading" ref="fileTable" :data="fileList" reserve-selection row-key="id"
+        @selection-change="handleSelectionFile" @select="handleSelect" @select-all="handleSelectAll">
+        <el-table-column type="selection" width="55" header-align="center" />
+        <el-table-column label="类型" header-align="center" prop="type" width="80">
+          <template #default="scope">
+            <dict-tag :options="smsb_source_type" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column label="原名" header-align="left" prop="originalName" width="150" :show-overflow-tooltip="true" />
+        <el-table-column label="大小" header-align="center" prop="size" />
+        <el-table-column label="时长" header-align="center" prop="duration" />
+        <el-table-column label="截图" header-align="center" prop="screenshot">
+          <template #default="scope">
+            <image-preview :src="scope.row.screenshot" style="width: 40px; height: 40px; cursor: pointer" />
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination v-show="fileTotal > 0" :total="fileTotal" v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize" @pagination="getFileList" />
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSelect">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { smsb_source_type } = toRefs<any>(proxy?.useDict('smsb_source_type'));
+import { ref, watch, defineProps, defineEmits, nextTick } from 'vue';
+import { listMinioData } from '@/api/smsb/source/minioData';
+import type { MinioDataVO, MinioDataQuery } from '@/api/smsb/source/minioData_type';
+
+const props = defineProps<{ modelValue: string }>();
+const emit = defineEmits(['update:modelValue']);
+
+const dialogVisible = ref(false);
+const dialogLoading = ref(false);
+const fileList = ref<MinioDataVO[]>([]);
+const fileTotal = ref(0);
+const selectedFiles = ref<any[]>([]);
+const fileTable = ref();
+
+const queryParams = ref<MinioDataQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  originalName: '',
+  type: ''
+});
+
+// 初始化时,如果有值,反序列化
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (val) {
+      try {
+        selectedFiles.value = JSON.parse(val);
+      } catch {
+        selectedFiles.value = [];
+      }
+    } else {
+      selectedFiles.value = [];
+    }
+  },
+  { immediate: true }
+);
+
+// 查询文件资源列表
+const getFileList = async () => {
+  dialogLoading.value = true;
+  try {
+    const res = await listMinioData(queryParams.value);
+    console.log('[getFileList] raw:', res);
+    console.log('[getFileList] rows:', res.rows);
+    const mapped = (res.rows || []).map((item: any, idx: number) => {
+      const sizeNum = Number(item.size);
+      const sizeStr = !isNaN(sizeNum) && sizeNum > 0 ? (sizeNum / 1024).toFixed(3) + 'MB' : '0MB';
+      if (isNaN(sizeNum)) {
+        console.warn(`[getFileList] row[${idx}] 非法size:`, item.size, item);
+      }
+      return {
+        ...item,
+        size: sizeStr
+      };
+    });
+    fileList.value = mapped;
+    console.log('[getFileList] mapped fileList:', mapped);
+    console.log('[getFileList] fileList.value:', fileList.value, 'isArray:', Array.isArray(fileList.value), 'length:', fileList.value.length);
+
+    fileTotal.value = res.total || 0;
+    await nextTick();
+    restoreSelection();
+  } finally {
+    dialogLoading.value = false;
+  }
+};
+
+// 多选框选中文件数据
+function handleSelectionFile(selection: MinioDataVO[]) {
+  // 只做新增
+  selection.forEach((item) => {
+    if (!selectedFiles.value.some((f) => String(f.id) === String(item.id))) {
+      selectedFiles.value.push({
+        id: item.id,
+        name: item.originalName,
+        type: item.type,
+        duration: item.type === 1 ? 10 : item.duration,
+        order: 0
+      });
+    }
+  });
+  selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
+}
+// 取消单个选中
+function handleSelect(selection: MinioDataVO[], row: MinioDataVO) {
+  if (!selection.some((item) => String(item.id) === String(row.id))) {
+    selectedFiles.value = selectedFiles.value.filter((f) => String(f.id) !== String(row.id));
+    selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
+  }
+}
+// 取消全选
+function handleSelectAll(selection: MinioDataVO[]) {
+  const currentPageIds = new Set(fileList.value.map((item) => String(item.id)));
+  const selectedIds = new Set(selection.map((item) => String(item.id)));
+  selectedFiles.value = selectedFiles.value.filter((f) => !currentPageIds.has(String(f.id)) || selectedIds.has(String(f.id)));
+  selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
+}
+
+function removeFile(idx: number) {
+  selectedFiles.value.splice(idx, 1);
+  emitChange();
+}
+function confirmSelect() {
+  dialogVisible.value = false;
+  emitChange();
+}
+function emitChange() {
+  emit('update:modelValue', JSON.stringify(selectedFiles.value));
+}
+// 弹窗关闭时还原选中状态
+function restoreSelection() {
+  nextTick(() => {
+    if (!fileTable.value) return;
+    const selectedIds = new Set(selectedFiles.value.map((f) => String(f.id)));
+    fileList.value.forEach((row) => {
+      fileTable.value.toggleRowSelection(row, selectedIds.has(String(row.id)));
+    });
+  });
+}
+
+// 弹窗首次打开时自动加载
+watch(dialogVisible, (val) => {
+  if (val) getFileList();
+});
+</script>
+
+<style scoped>
+.selected-files-list {
+  margin: 8px 0;
+}
+</style>

+ 25 - 22
smsb-plus-ui/src/views/smsb/itemProgram/EditProgram.vue

@@ -124,6 +124,9 @@
               <template v-if="selectedComponent.type === 'live' && key === 'playAudio'">
                 <el-switch v-model="selectedComponent[key]" active-text="开" inactive-text="关" />
               </template>
+              <template v-else-if="key === 'mediaId'">
+                <MediaFileSelector v-model="selectedComponent[key]" />
+              </template>
               <template v-else>
                 <el-input v-model="selectedComponent[key]" />
               </template>
@@ -366,13 +369,6 @@ onMounted(async () => {
   }
 });
 
-// 模拟 program 对象
-const program = ref<any>({});
-
-// 假设 json_str 为后端获取或 props 传入的 JSON 字符串
-let json_str = '';
-// json_str = props.jsonStr || '';
-
 function ensureCanvasAndDepth(elements, resolutionRatio?: string) {
   let width = 600,
     height = 400;
@@ -556,25 +552,32 @@ onMounted(async () => {
   // 获取节目详细信息并补充到 editorContent
   try {
     const res = await getItemProgram(id.value);
-    if (res.data) {
-      // itemJsonStr 是画布内容
-      let content = {};
+    let name = res.data?.name || '';
+    let resolutionRatio = res.data?.resolutionRatio || '';
+    let parsed: any = { elements: [] };
+    if (res.data && res.data.itemJsonStr) {
       try {
-        content = res.data.itemJsonStr ? JSON.parse(res.data.itemJsonStr) : {};
-      } catch (e) {
-        content = {};
+        parsed = JSON.parse(res.data.itemJsonStr);
+      } catch (err) {
+        parsed = { elements: [] };
       }
-      // 合并 name、resolutionRatio 等基础字段
-      editorContent.value = {
-        ...content,
-        name: res.data.name,
-        resolutionRatio: res.data.resolutionRatio,
-        elements: content.elements || []
-      };
     }
+    // 合并 name、resolutionRatio 字段,保证结构完整
+    editorContent.value = {
+      ...parsed,
+      name,
+      resolutionRatio,
+      elements: Array.isArray(parsed.elements) ? parsed.elements : []
+    };
+    ensureCanvasAndDepth(editorContent.value.elements, editorContent.value.resolutionRatio);
   } catch (e) {
-    // 错误处理
-    editorContent.value = { elements: [] };
+    // fallback: 初始化 elements 并插入默认画布,且补齐基础字段
+    editorContent.value = {
+      name: '',
+      resolutionRatio: '',
+      elements: []
+    };
+    ensureCanvasAndDepth(editorContent.value.elements);
   }
 });