|
|
@@ -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>
|