Selaa lähdekoodia

添加对节目布局的预览支持

Shinohara Haruna 5 kuukautta sitten
vanhempi
sitoutus
c52a28f667

+ 1 - 1
smsb-plus-ui/src/layout/components/Sidebar/index.vue

@@ -30,7 +30,7 @@ const topMenus = computed(() => permissionStore.getTopbarRoutes().filter((menu)
 const currentTopMenuPath = computed(() => {
   // console.log("[Sidebar] route.path", route.path);
   // 特例
-  if (route.path === '/source/push/approval' || route.path.startsWith('/source/split/edit') || route.path.startsWith('/smsb/itemProgram/edit')) {
+  if (route.path === '/source/push/approval' || route.path.startsWith('/source/split/edit') || route.path.startsWith('/smsb/itemProgram/edit') || route.path.startsWith('/smsb/itemProgram/preview')) {
     return '/source';
   }
   // 取当前路由的一级菜单 path

+ 6 - 0
smsb-plus-ui/src/router/index.ts

@@ -208,6 +208,12 @@ export const dynamicRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/smsb/itemProgram/EditProgram.vue'),
         name: 'EditItemProgram',
         meta: { title: '编辑节目', activeMenu: '/smsb/itemProgram', icon: '' }
+      },
+      {
+        path: 'preview/:id',
+        component: () => import('@/views/smsb/itemProgram/PreviewProgram.vue'),
+        name: 'PreviewItemProgram',
+        meta: { title: '预览节目', activeMenu: '/smsb/itemProgram', icon: '' }
       }
     ]
   },

+ 241 - 0
smsb-plus-ui/src/views/smsb/itemProgram/PreviewProgram.vue

@@ -0,0 +1,241 @@
+<template>
+  <div class="preview-program-layout">
+    <!-- 只保留中央预览区 -->
+    <div class="main-preview">
+      <div class="preview-canvas" ref="previewCanvasRef">
+        <CanvasBoard v-if="canvasItem" :width="canvasItem.width" :height="canvasItem.height" :bg="canvasItem.bg"
+          :scale="canvasScale">
+          <template #default>
+            <template
+              v-for="item in editorContent.elements.filter((el) => el.type !== 'canvas').sort((a, b) => a.depth - b.depth)"
+              :key="item.depth + '-' + item.type">
+              <div :style="{
+                position: 'absolute',
+                left: (item.x || 0) * canvasScale + 'px',
+                top: (item.y || 0) * canvasScale + 'px',
+                width: (item.width || 200) * canvasScale + 'px',
+                height: (item.height || 40) * canvasScale + 'px',
+                zIndex: 10,
+                pointerEvents: 'none' // 禁用所有交互
+              }">
+                <TextBoard v-if="item.type === 'text'" :text="item.text" :color="item.color" :font-size="item.fontSize"
+                  :font-weight="item.fontWeight" :align="item.align" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" />
+                <ScrollingTextBoard v-if="item.type === 'scrollingText'" :text="item.text" :color="item.color"
+                  :font-size="item.fontSize" :font-weight="item.fontWeight" :align="item.align"
+                  :width="item.width * canvasScale" :height="item.height * canvasScale" :speed="item.speed" />
+                <MediaAssetBoard v-if="item.type === 'mediaAsset'" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" :media-id="item.mediaId" v-model="item.mediaGroup" />
+                <LiveBoard v-if="item.type === 'live'" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" :live-url="item.liveUrl" :play-audio="item.playAudio" />
+                <WebPageBoard v-if="item.type === 'webPage'" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" :url="item.url" />
+                <ClockBoard v-if="item.type === 'clock'" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" :format="item.format" />
+              </div>
+            </template>
+          </template>
+        </CanvasBoard>
+      </div>
+      <div class="preview-controls">
+        <el-button type="primary" @click="goBack" class="back-btn">返回</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick, computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { getItemProgram, listItemProgram } from '@/api/smsb/source/item_program';
+import CanvasBoard from './component/CanvasBoard.vue';
+import TextBoard from './component/TextBoard.vue';
+import ScrollingTextBoard from './component/ScrollingTextBoard.vue';
+import MediaAssetBoard from './component/MediaAssetBoard.vue';
+import LiveBoard from './component/LiveBoard.vue';
+import WebPageBoard from './component/WebPageBoard.vue';
+import ClockBoard from './component/ClockBoard.vue';
+
+const router = useRouter();
+const route = useRoute();
+
+// 自动修正 id 类型,确保为 string 或 number
+const rawId = route.params.id;
+const id = ref<string | number>(Array.isArray(rawId) ? rawId[0] : rawId);
+
+// 节目信息
+const programName = ref('');
+const programResolution = ref('');
+
+// 画布缩放
+const previewCanvasRef = ref<HTMLElement | null>(null);
+const containerSize = ref({ width: 0, height: 0 });
+const canvasScale = ref(1);
+
+// 编辑器内容
+interface EditorContent {
+  elements: any[];
+}
+
+const editorContent = ref<EditorContent>({ elements: [] });
+const canvasItem = computed(() => {
+  return editorContent.value.elements.find((el) => el.type === 'canvas');
+});
+
+// 更新容器尺寸和缩放
+const updateContainerSize = () => {
+  if (previewCanvasRef.value) {
+    const container = previewCanvasRef.value;
+    containerSize.value = {
+      width: container.clientWidth,
+      height: container.clientHeight
+    };
+
+    if (canvasItem.value) {
+      const scaleX = (container.clientWidth - 40) / canvasItem.value.width;
+      const scaleY = (container.clientHeight - 40) / canvasItem.value.height;
+      canvasScale.value = Math.min(scaleX, scaleY, 1); // 限制最大缩放为1
+      console.log('Canvas scale updated:', {
+        scale: canvasScale.value,
+        container: containerSize.value,
+        canvas: { width: canvasItem.value.width, height: canvasItem.value.height }
+      });
+    }
+  }
+};
+
+// 获取节目详情
+const fetchProgramDetail = async () => {
+  try {
+    console.log('Fetching program detail for ID:', id.value);
+
+    // 使用列表接口获取节目数据,因为详情接口可能返回 null
+    const listRes = await listItemProgram({ programId: id.value });
+    console.log('Program list response:', listRes);
+
+    if (listRes.code === 200 && listRes.rows && listRes.rows.length > 0) {
+      const programData = listRes.rows[0];
+      programName.value = programData.name || '未命名节目';
+      programResolution.value = programData.resolutionRatio || '1920x1080';
+
+      // 尝试从 itemJsonStr 或 content 中获取节目内容
+      let contentStr = programData.itemJsonStr || programData.content;
+
+      if (!contentStr) {
+        console.warn('No content found in program data');
+        ElMessage.warning('该节目没有可预览的内容');
+        return;
+      }
+
+      try {
+        // 处理可能的转义字符
+        if (typeof contentStr === 'string') {
+          // 先尝试直接解析
+          try {
+            contentStr = JSON.parse(contentStr);
+          } catch (e) {
+            // 如果解析失败,可能是双重转义的 JSON 字符串
+            try {
+              contentStr = JSON.parse(JSON.parse(`"${contentStr}"`));
+            } catch (e2) {
+              console.error('Failed to parse content string:', e2);
+              throw new Error('内容格式不正确');
+            }
+          }
+        }
+
+        // 确保 elements 数组存在
+        if (!contentStr.elements || !Array.isArray(contentStr.elements)) {
+          contentStr.elements = [];
+        }
+
+        editorContent.value = contentStr;
+
+        // 确保有画布元素
+        if (!editorContent.value.elements.some((el) => el.type === 'canvas')) {
+          console.warn('No canvas element found in program content, adding default canvas');
+          editorContent.value.elements.unshift({
+            type: 'canvas',
+            width: 1920,
+            height: 1080,
+            bg: { type: 'color', value: '#ffffff' },
+            depth: 0
+          });
+        }
+
+        // 更新容器尺寸
+        nextTick(updateContainerSize);
+      } catch (e) {
+        console.error('Failed to parse program content:', e);
+        ElMessage.error('节目内容解析失败: ' + (e as Error).message);
+      }
+    } else {
+      throw new Error(listRes.msg || '获取节目详情失败,未找到对应节目');
+    }
+  } catch (error) {
+    console.error('Error fetching program detail:', error);
+    ElMessage.error('获取节目详情失败: ' + (error as Error).message);
+  }
+};
+
+// 返回上一页
+const goBack = () => {
+  router.go(-1);
+};
+
+onMounted(async () => {
+  await fetchProgramDetail();
+  window.addEventListener('resize', updateContainerSize);
+
+  // 添加页面标题
+  document.title = `预览 - ${programName.value || '节目预览'}`;
+
+  return () => {
+    window.removeEventListener('resize', updateContainerSize);
+  };
+});
+</script>
+
+<style scoped>
+.preview-program-layout {
+  display: flex;
+  height: 100vh;
+  background-color: #f0f2f5;
+  overflow: hidden;
+}
+
+.main-preview {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  padding: 20px;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.preview-canvas {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #f5f5f5;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  overflow: hidden;
+  margin-bottom: 16px;
+}
+
+.preview-controls {
+  display: flex;
+  justify-content: center;
+  padding: 16px 0;
+  background-color: #fff;
+  border-top: 1px solid #eee;
+}
+
+.back-btn {
+  min-width: 120px;
+}
+</style>

+ 15 - 0
smsb-plus-ui/src/views/smsb/itemProgram/index.vue

@@ -79,6 +79,9 @@
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
                 v-hasPermi="['system:itemProgram:remove']"></el-button>
             </el-tooltip>
+            <el-tooltip content="预览" placement="top">
+              <el-button link type="primary" icon="View" @click="handlePreview(scope.row)"></el-button>
+            </el-tooltip>
           </template>
         </el-table-column>
       </el-table>
@@ -120,8 +123,11 @@ import { listItemProgram, getItemProgram, delItemProgram, addItemProgram, update
 import { getUser, optionSelect, UserVO } from '@/api/system/user';
 import { useUserStore } from '@/store/modules/user';
 import { ItemProgramVO, ItemProgramQuery, ItemProgramForm } from '@/api/smsb/source/item_program_type';
+import { useRouter } from 'vue-router';
+import { getCurrentInstance, reactive, toRefs } from 'vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
 
 const itemProgramList = ref<ItemProgramVO[]>([]);
 const userNickNameMap = reactive<Record<string, string>>({});
@@ -287,6 +293,15 @@ const handleExport = () => {
   );
 };
 
+/** 预览按钮操作 */
+const handlePreview = (row: any) => {
+  console.log('Preview program:', row);
+  router.push({
+    name: 'PreviewItemProgram',
+    params: { id: row.programId }
+  });
+};
+
 onMounted(() => {
   getList();
 });