PreviewProgram.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <template>
  2. <div class="preview-program-layout">
  3. <!-- 只保留中央预览区 -->
  4. <div class="main-preview">
  5. <div class="preview-canvas" ref="previewCanvasRef">
  6. <CanvasBoard v-if="canvasItem" :width="canvasItem.width" :height="canvasItem.height" :bg="canvasItem.bg"
  7. :scale="canvasScale">
  8. <template #default>
  9. <template
  10. v-for="item in editorContent.elements.filter((el) => el.type !== 'canvas').sort((a, b) => a.depth - b.depth)"
  11. :key="item.depth + '-' + item.type">
  12. <div :style="{
  13. position: 'absolute',
  14. left: (item.x || 0) * canvasScale + 'px',
  15. top: (item.y || 0) * canvasScale + 'px',
  16. width: (item.width || 200) * canvasScale + 'px',
  17. height: (item.height || 40) * canvasScale + 'px',
  18. zIndex: 10,
  19. pointerEvents: 'none' // 禁用所有交互
  20. }">
  21. <TextBoard v-if="item.type === 'text'" :text="item.text" :color="item.color" :font-size="item.fontSize"
  22. :font-weight="item.fontWeight" :align="item.align" :width="item.width * canvasScale"
  23. :height="item.height * canvasScale" />
  24. <ScrollingTextBoard v-if="item.type === 'scrollingText'" :text="item.text" :color="item.color"
  25. :font-size="item.fontSize" :font-weight="item.fontWeight" :align="item.align"
  26. :width="item.width * canvasScale" :height="item.height * canvasScale" :speed="item.speed" />
  27. <MediaAssetBoard v-if="item.type === 'mediaAsset'" :width="item.width * canvasScale"
  28. :height="item.height * canvasScale" :media-id="item.mediaId" v-model="item.mediaGroup" />
  29. <LiveBoard v-if="item.type === 'live'" :width="item.width * canvasScale"
  30. :height="item.height * canvasScale" :live-url="item.liveUrl" :play-audio="item.playAudio" />
  31. <WebPageBoard v-if="item.type === 'webPage'" :width="item.width * canvasScale"
  32. :height="item.height * canvasScale" :url="item.url" />
  33. <ClockBoard v-if="item.type === 'clock'" :width="item.width * canvasScale"
  34. :height="item.height * canvasScale" :format="item.format" />
  35. </div>
  36. </template>
  37. </template>
  38. </CanvasBoard>
  39. </div>
  40. <div class="preview-controls">
  41. <el-button type="primary" @click="goBack" class="back-btn">返回</el-button>
  42. </div>
  43. </div>
  44. </div>
  45. </template>
  46. <script setup lang="ts">
  47. import { ref, onMounted, nextTick, computed } from 'vue';
  48. import { useRoute, useRouter } from 'vue-router';
  49. import { ElMessage } from 'element-plus';
  50. import { getItemProgram, listItemProgram } from '@/api/smsb/source/item_program';
  51. import CanvasBoard from './component/CanvasBoard.vue';
  52. import TextBoard from './component/TextBoard.vue';
  53. import ScrollingTextBoard from './component/ScrollingTextBoard.vue';
  54. import MediaAssetBoard from './component/MediaAssetBoard.vue';
  55. import LiveBoard from './component/LiveBoard.vue';
  56. import WebPageBoard from './component/WebPageBoard.vue';
  57. import ClockBoard from './component/ClockBoard.vue';
  58. const router = useRouter();
  59. const route = useRoute();
  60. // 自动修正 id 类型,确保为 string 或 number
  61. const rawId = route.params.id;
  62. const id = ref<string | number>(Array.isArray(rawId) ? rawId[0] : rawId);
  63. // 节目信息
  64. const programName = ref('');
  65. const programResolution = ref('');
  66. // 画布缩放
  67. const previewCanvasRef = ref<HTMLElement | null>(null);
  68. const containerSize = ref({ width: 0, height: 0 });
  69. const canvasScale = ref(1);
  70. // 编辑器内容
  71. interface EditorContent {
  72. elements: any[];
  73. }
  74. const editorContent = ref<EditorContent>({ elements: [] });
  75. const canvasItem = computed(() => {
  76. return editorContent.value.elements.find((el) => el.type === 'canvas');
  77. });
  78. // 更新容器尺寸和缩放
  79. const updateContainerSize = () => {
  80. if (previewCanvasRef.value) {
  81. const container = previewCanvasRef.value;
  82. containerSize.value = {
  83. width: container.clientWidth,
  84. height: container.clientHeight
  85. };
  86. if (canvasItem.value) {
  87. const scaleX = (container.clientWidth - 40) / canvasItem.value.width;
  88. const scaleY = (container.clientHeight - 40) / canvasItem.value.height;
  89. canvasScale.value = Math.min(scaleX, scaleY, 1); // 限制最大缩放为1
  90. console.log('Canvas scale updated:', {
  91. scale: canvasScale.value,
  92. container: containerSize.value,
  93. canvas: { width: canvasItem.value.width, height: canvasItem.value.height }
  94. });
  95. }
  96. }
  97. };
  98. // 获取节目详情
  99. const fetchProgramDetail = async () => {
  100. try {
  101. console.log('Fetching program detail for ID:', id.value);
  102. // 使用列表接口获取节目数据,因为详情接口可能返回 null
  103. const listRes = await listItemProgram({ programId: id.value });
  104. console.log('Program list response:', listRes);
  105. if (listRes.code === 200 && listRes.rows && listRes.rows.length > 0) {
  106. const programData = listRes.rows[0];
  107. programName.value = programData.name || '未命名节目';
  108. programResolution.value = programData.resolutionRatio || '1920x1080';
  109. // 尝试从 itemJsonStr 或 content 中获取节目内容
  110. let contentStr = programData.itemJsonStr || programData.content;
  111. if (!contentStr) {
  112. console.warn('No content found in program data');
  113. ElMessage.warning('该节目没有可预览的内容');
  114. return;
  115. }
  116. try {
  117. // 处理可能的转义字符
  118. if (typeof contentStr === 'string') {
  119. // 先尝试直接解析
  120. try {
  121. contentStr = JSON.parse(contentStr);
  122. } catch (e) {
  123. // 如果解析失败,可能是双重转义的 JSON 字符串
  124. try {
  125. contentStr = JSON.parse(JSON.parse(`"${contentStr}"`));
  126. } catch (e2) {
  127. console.error('Failed to parse content string:', e2);
  128. throw new Error('内容格式不正确');
  129. }
  130. }
  131. }
  132. // 确保 elements 数组存在
  133. if (!contentStr.elements || !Array.isArray(contentStr.elements)) {
  134. contentStr.elements = [];
  135. }
  136. editorContent.value = contentStr;
  137. // 确保有画布元素
  138. if (!editorContent.value.elements.some((el) => el.type === 'canvas')) {
  139. console.warn('No canvas element found in program content, adding default canvas');
  140. editorContent.value.elements.unshift({
  141. type: 'canvas',
  142. width: 1920,
  143. height: 1080,
  144. bg: { type: 'color', value: '#ffffff' },
  145. depth: 0
  146. });
  147. }
  148. // 更新容器尺寸
  149. nextTick(updateContainerSize);
  150. } catch (e) {
  151. console.error('Failed to parse program content:', e);
  152. ElMessage.error('节目内容解析失败: ' + (e as Error).message);
  153. }
  154. } else {
  155. throw new Error(listRes.msg || '获取节目详情失败,未找到对应节目');
  156. }
  157. } catch (error) {
  158. console.error('Error fetching program detail:', error);
  159. ElMessage.error('获取节目详情失败: ' + (error as Error).message);
  160. }
  161. };
  162. // 返回上一页
  163. const goBack = () => {
  164. router.go(-1);
  165. };
  166. onMounted(async () => {
  167. await fetchProgramDetail();
  168. window.addEventListener('resize', updateContainerSize);
  169. // 添加页面标题
  170. document.title = `预览 - ${programName.value || '节目预览'}`;
  171. return () => {
  172. window.removeEventListener('resize', updateContainerSize);
  173. };
  174. });
  175. </script>
  176. <style scoped>
  177. .preview-program-layout {
  178. display: flex;
  179. height: 100vh;
  180. background-color: #f0f2f5;
  181. overflow: hidden;
  182. }
  183. .main-preview {
  184. flex: 1;
  185. display: flex;
  186. flex-direction: column;
  187. height: 100%;
  188. padding: 20px;
  189. box-sizing: border-box;
  190. overflow: hidden;
  191. }
  192. .preview-canvas {
  193. flex: 1;
  194. display: flex;
  195. align-items: center;
  196. justify-content: center;
  197. background-color: #f5f5f5;
  198. border: 1px solid #ddd;
  199. border-radius: 4px;
  200. overflow: hidden;
  201. margin-bottom: 16px;
  202. }
  203. .preview-controls {
  204. display: flex;
  205. justify-content: center;
  206. padding: 16px 0;
  207. background-color: #fff;
  208. border-top: 1px solid #eee;
  209. }
  210. .back-btn {
  211. min-width: 120px;
  212. }
  213. </style>