Pārlūkot izejas kodu

添加网页组件支持

Shinohara Haruna 5 mēneši atpakaļ
vecāks
revīzija
325e3552d4

+ 34 - 10
smsb-plus-ui/src/views/smsb/itemProgram/EditProgram.vue

@@ -22,6 +22,7 @@
             <template v-else-if="item.type === 'scrollingText'">滚动文本</template>
             <template v-else-if="item.type === 'mediaAsset'">媒资</template>
             <template v-else-if="item.type === 'live'">直播</template>
+            <template v-else-if="item.type === 'webPage'">网页</template>
             <!-- 未来可扩展图片等类型 -->
           </div>
         </div>
@@ -35,6 +36,7 @@
         <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('scrollingText')">滚动文本</div>
         <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('mediaAsset')">媒资</div>
         <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('live')">直播</div>
+        <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('webPage')">网页</div>
         <!-- 可扩展更多组件 -->
       </div>
       <div class="editor-canvas" ref="editorCanvasRef" @dragover.prevent @drop="onCanvasDrop">
@@ -82,6 +84,13 @@
                   item.height = height;
                 }
               " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
+            <WebPageBoard v-if="item.type === 'webPage'" :width="item.width" :height="item.height" :url="item.url"
+              :selected="selectedComponent === item" @resize="
+                ({ width, height }) => {
+                  item.width = width;
+                  item.height = height;
+                }
+              " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
             <!-- 未来可扩展更多类型 -->
           </div>
         </template>
@@ -164,7 +173,15 @@ 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 { canvasPropNameMap, textPropNameMap, scrollingTextPropNameMap, mediaAssetPropNameMap, livePropNameMap } from './component/propNameMaps';
+import WebPageBoard from './component/WebPageBoard.vue';
+import {
+  canvasPropNameMap,
+  textPropNameMap,
+  scrollingTextPropNameMap,
+  mediaAssetPropNameMap,
+  livePropNameMap,
+  webPagePropNameMap
+} from './component/propNameMaps';
 // 拖拽类型
 const dragType = ref<string | null>(null);
 // 获取最大 depth
@@ -213,6 +230,8 @@ function getPropLabel(key: string) {
     return mediaAssetPropNameMap[key] || key;
   } else if (selectedComponent.value?.type === 'live') {
     return livePropNameMap[key] || key;
+  } else if (selectedComponent.value?.type === 'webPage') {
+    return webPagePropNameMap[key] || key;
   }
   return key;
 }
@@ -298,6 +317,7 @@ const editorContent = ref({ elements: [] });
 
 // editor-canvas 缩放逻辑
 const editorCanvasRef = ref<HTMLElement | null>(null);
+const containerSize = ref({ width: 0, height: 0 });
 const dragComponentType = ref<string | null>(null);
 
 const draggingId = ref<number | null>(null);
@@ -397,12 +417,22 @@ function onCanvasDrop(e: DragEvent) {
     };
     editorContent.value.elements.push(newLive);
     nextTick(() => selectComponent(newLive));
+  } else if (dragType.value === 'webPage') {
+    const newWebPage = {
+      type: 'webPage',
+      url: '',
+      x: x,
+      y: y,
+      width: 120,
+      height: 120,
+      depth: getMaxDepth() + 1
+    };
+    editorContent.value.elements.push(newWebPage);
+    nextTick(() => selectComponent(newWebPage));
   }
   dragType.value = null;
 }
 
-const containerSize = ref({ width: 0, height: 0 });
-
 function updateContainerSize() {
   if (editorCanvasRef.value) {
     containerSize.value.width = editorCanvasRef.value.clientWidth;
@@ -427,13 +457,7 @@ const canvasScale = computed(() => {
   return Math.min(boxW / cW, boxH / cH, 1);
 });
 
-try {
-  let parsed = json_str ? JSON.parse(json_str) : { elements: [] };
-  editorContent.value.elements = ensureCanvasAndDepth(parsed.elements || []);
-} catch (e) {
-  // fallback
-  editorContent.value.elements = ensureCanvasAndDepth([]);
-}
+// 已移除多余的 return 和重复代码,仅保留 canvasScale 的 computed 实现
 
 const handleSave = () => {
   program.value.content = JSON.stringify(editorContent.value);

+ 150 - 0
smsb-plus-ui/src/views/smsb/itemProgram/component/WebPageBoard.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="web-page-board-wrapper" :class="{ selected }" :style="{
+    width: props.width + 'px',
+    height: props.height + 'px',
+    position: 'relative',
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center'
+  }">
+    <div class="web-page-icon">
+      <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
+        <rect x="3" y="3" width="26" height="26" rx="6" fill="#f3f6fa" stroke="#409eff" stroke-width="2" />
+        <rect x="8" y="10" width="16" height="12" rx="2" fill="#fff" stroke="#409eff" stroke-width="1.5" />
+        <circle cx="16" cy="16" r="2" fill="#409eff" />
+      </svg>
+    </div>
+
+    <template v-if="selected">
+      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
+        @mousedown.stop="onResizeMouseDown(dir, $event)" />
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, withDefaults, defineEmits } from 'vue';
+const emit = defineEmits(['resize']);
+interface Props {
+  width?: number;
+  height?: number;
+  url?: string;
+  selected?: boolean;
+}
+const props = withDefaults(defineProps<Props>(), {
+  width: 120,
+  height: 120,
+  url: ''
+});
+const selected = computed(() => !!props.selected);
+
+let startX = 0,
+  startY = 0,
+  startW = 0,
+  startH = 0;
+function onResizeMouseDown(dir: string, e: MouseEvent) {
+  e.stopPropagation();
+  startX = e.clientX;
+  startY = e.clientY;
+  startW = props.width;
+  startH = props.height;
+  function onMouseMove(ev: MouseEvent) {
+    let dx = ev.clientX - startX;
+    let dy = ev.clientY - startY;
+    let newW = startW;
+    let newH = startH;
+    if (dir.includes('e')) newW = Math.max(40, startW + dx);
+    if (dir.includes('s')) newH = Math.max(40, startH + dy);
+    if (dir.includes('w')) newW = Math.max(40, startW - dx);
+    if (dir.includes('n')) newH = Math.max(40, startH - dy);
+    emit('resize', { width: newW, height: newH });
+  }
+  function onMouseUp() {
+    window.removeEventListener('mousemove', onMouseMove);
+    window.removeEventListener('mouseup', onMouseUp);
+  }
+  window.addEventListener('mousemove', onMouseMove);
+  window.addEventListener('mouseup', onMouseUp);
+}
+</script>
+
+<style scoped>
+.web-page-board-wrapper {
+  background: #f9fbfd;
+  border: 2px dashed #b3c7e6;
+  border-radius: 10px;
+  transition: border-color 0.2s;
+  position: relative;
+}
+
+.web-page-board-wrapper.selected {
+  border-color: #409eff;
+}
+
+.web-page-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.web-page-info {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  justify-content: center;
+}
+
+.web-page-url {
+  font-size: 13px;
+  color: #409eff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 120px;
+}
+
+.label {
+  color: #409eff;
+  font-weight: bold;
+  margin-right: 4px;
+}
+
+.resize-handle {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: #fff;
+  border: 2px solid #409eff;
+  border-radius: 50%;
+  z-index: 10;
+  cursor: pointer;
+}
+
+.resize-nw {
+  left: -6px;
+  top: -6px;
+  cursor: nwse-resize;
+}
+
+.resize-ne {
+  right: -6px;
+  top: -6px;
+  cursor: nesw-resize;
+}
+
+.resize-sw {
+  left: -6px;
+  bottom: -6px;
+  cursor: nesw-resize;
+}
+
+.resize-se {
+  right: -6px;
+  bottom: -6px;
+  cursor: nwse-resize;
+}
+</style>

+ 8 - 0
smsb-plus-ui/src/views/smsb/itemProgram/component/propNameMaps.ts

@@ -37,6 +37,14 @@ export const mediaAssetPropNameMap = {
   y: '纵坐标'
 };
 
+export const webPagePropNameMap = {
+  url: '网页链接',
+  width: '宽度',
+  height: '高度',
+  x: '横坐标',
+  y: '纵坐标'
+};
+
 export const livePropNameMap = {
   liveUrl: '直播地址',
   playAudio: '播放音频',