Pārlūkot izejas kodu

添加时钟组件支持

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

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

@@ -22,6 +22,7 @@
             <template v-else-if="item.type === 'mediaAsset'">媒资</template>
             <template v-else-if="item.type === 'live'">直播</template>
             <template v-else-if="item.type === 'webPage'">网页</template>
+            <template v-else-if="item.type === 'clock'">时钟</template>
             <!-- 未来可扩展图片等类型 -->
           </div>
         </div>
@@ -36,6 +37,7 @@
         <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 class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('clock')">时钟</div>
         <!-- 可扩展更多组件 -->
       </div>
       <div class="editor-canvas" ref="editorCanvasRef" @dragover.prevent @drop="onCanvasDrop">
@@ -95,6 +97,14 @@
                       item.height = height / canvasScale;
                     }
                   " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
+                <ClockBoard v-if="item.type === 'clock'" :width="item.width * canvasScale"
+                  :height="item.height * canvasScale" :format="item.format" :selected="selectedComponent === item"
+                  @resize="
+                    ({ width, height }) => {
+                      item.width = width / canvasScale;
+                      item.height = height / canvasScale;
+                    }
+                  " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
                 <!-- 未来可扩展更多类型 -->
               </div>
             </template>
@@ -137,6 +147,13 @@
                 v-else-if="key === 'color' && (selectedComponent.type === 'text' || selectedComponent.type === 'scrollingText')">
                 <BackgroundSelector v-model="selectedComponent[key]" :isCanvas="false" />
               </template>
+              <template v-else-if="selectedComponent.type === 'clock' && key === 'format'">
+                <el-select v-model="selectedComponent[key]" style="width: 100%">
+                  <el-option label="24小时制 (HH:mm:ss)" value="24h" />
+                  <el-option label="12小时制 (hh:mm:ss A)" value="12h" />
+                  <el-option label="日期+时间 (YYYY-MM-DD HH:mm:ss)" value="date" />
+                </el-select>
+              </template>
               <template v-else>
                 <el-input v-model="selectedComponent[key]" />
               </template>
@@ -289,13 +306,15 @@ import MediaFileSelector from '@/components/MediaFileSelector.vue';
 import BackgroundSelector from '@/components/BackgroundSelector.vue';
 import LiveBoard from './component/LiveBoard.vue';
 import WebPageBoard from './component/WebPageBoard.vue';
+import ClockBoard from './component/ClockBoard.vue';
 import {
   canvasPropNameMap,
   textPropNameMap,
   scrollingTextPropNameMap,
   mediaAssetPropNameMap,
   livePropNameMap,
-  webPagePropNameMap
+  webPagePropNameMap,
+  clockPropNameMap
 } from './component/propNameMaps';
 // 拖拽类型
 const dragType = ref<string | null>(null);
@@ -352,6 +371,8 @@ function getPropLabel(key: string) {
     return livePropNameMap[key] || key;
   } else if (selectedComponent.value?.type === 'webPage') {
     return webPagePropNameMap[key] || key;
+  } else if (selectedComponent.value?.type === 'clock') {
+    return clockPropNameMap[key] || key;
   }
   return key;
 }
@@ -555,6 +576,18 @@ function onCanvasDrop(e: DragEvent) {
     };
     editorContent.value.elements.push(newWebPage);
     nextTick(() => selectComponent(newWebPage));
+  } else if (dragType.value === 'clock') {
+    const newClock = {
+      type: 'clock',
+      format: '24h', // default format, can be '24h', '12h', or 'dateTime'
+      x: x,
+      y: y,
+      width: 200,
+      height: 60,
+      depth: getMaxDepth() + 1
+    };
+    editorContent.value.elements.push(newClock);
+    nextTick(() => selectComponent(newClock));
   }
   dragType.value = null;
 }

+ 1 - 1
smsb-plus-ui/src/views/smsb/itemProgram/component/CanvasBoard.vue

@@ -18,7 +18,7 @@ const props = defineProps<Props>();
 const canvasStyle = computed(() => ({
   width: '100%',
   height: '100%',
-  background: props.bg || '#fff',
+  background: typeof props.bg === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(props.bg) ? props.bg : '#fff',
   display: 'flex',
   alignItems: 'center',
   justifyContent: 'center',

+ 171 - 0
smsb-plus-ui/src/views/smsb/itemProgram/component/ClockBoard.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="clock-board-wrapper" :class="{ selected }"
+    :style="{ width: '100%', height: '100%', position: 'relative' }">
+    <div class="clock-board" :style="clockStyle">{{ formattedTime }}</div>
+    <template v-if="selected">
+      <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="dir" class="resize-handle" :class="dir"
+        @mousedown.stop="onResizeMouseDown($event, dir)"></div>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, onUnmounted } from 'vue';
+const emit = defineEmits(['resize']);
+interface Props {
+  color?: string;
+  fontSize?: string | number;
+  fontWeight?: string | number;
+  align?: 'left' | 'center' | 'right';
+  width?: number;
+  height?: number;
+  selected?: boolean;
+  format?: '24h' | '12h' | 'date';
+}
+const props = defineProps<Props>();
+const selected = computed(() => !!props.selected);
+const clockStyle = computed(() => ({
+  color: props.color || '#222',
+  fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
+  fontWeight: props.fontWeight || 'normal',
+  textAlign: props.align || 'center',
+  width: '100%',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center',
+  userSelect: 'none'
+}));
+
+const formats = {
+  '24h': '24小时制 (HH:mm:ss)',
+  '12h': '12小时制 (hh:mm:ss A)',
+  'date': '日期+时间 (YYYY-MM-DD HH:mm:ss)'
+};
+import { watch } from 'vue';
+
+const format = ref<'24h' | '12h' | 'date'>(props.format || '24h');
+
+// 保持 format 与 props.format 同步
+watch(
+  () => props.format,
+  (val) => {
+    if (val && val !== format.value) {
+      format.value = val;
+    }
+  }
+);
+
+watch(format, (val) => { });
+
+const now = ref(new Date());
+
+const formattedTime = computed(() => {
+  const d = now.value;
+  let result;
+  if (format.value === '24h') {
+    result = d.toLocaleTimeString('zh-CN', { hour12: false });
+  } else if (format.value === '12h') {
+    result = d.toLocaleTimeString('zh-CN', { hour12: true });
+  } else {
+    // 日期+时间
+    const date = d.toLocaleDateString('zh-CN');
+    const time = d.toLocaleTimeString('zh-CN', { hour12: false });
+    result = `${date} ${time}`;
+  }
+
+  return result;
+});
+
+let timer: number | undefined;
+onMounted(() => {
+  timer = window.setInterval(() => {
+    now.value = new Date();
+  }, 1000);
+});
+onUnmounted(() => {
+  if (timer) clearInterval(timer);
+});
+
+function onResizeMouseDown(e: MouseEvent, dir: string) {
+  e.stopPropagation();
+  const startX = e.clientX,
+    startY = e.clientY;
+  const startWidth = Number(props.width) || 200;
+  const startHeight = Number(props.height) || 40;
+  function onMove(ev: MouseEvent) {
+    let newWidth = startWidth,
+      newHeight = startHeight;
+    if (dir.includes('r')) newWidth += ev.clientX - startX;
+    if (dir.includes('l')) newWidth -= ev.clientX - startX;
+    if (dir.includes('b')) newHeight += ev.clientY - startY;
+    if (dir.includes('t')) newHeight -= ev.clientY - startY;
+    emit('resize', { width: Math.max(20, newWidth), height: Math.max(20, newHeight) });
+  }
+  function onUp() {
+    document.removeEventListener('mousemove', onMove);
+    document.removeEventListener('mouseup', onUp);
+  }
+  document.addEventListener('mousemove', onMove);
+  document.addEventListener('mouseup', onUp);
+}
+</script>
+
+<style scoped>
+.clock-board-wrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.clock-board {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.resize-handle {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: #fff;
+  border: 1px solid #aaa;
+  z-index: 10;
+}
+
+.resize-handle.tr {
+  top: -5px;
+  right: -5px;
+  cursor: ne-resize;
+}
+
+.resize-handle.tl {
+  top: -5px;
+  left: -5px;
+  cursor: nw-resize;
+}
+
+.resize-handle.br {
+  bottom: -5px;
+  right: -5px;
+  cursor: se-resize;
+}
+
+.resize-handle.bl {
+  bottom: -5px;
+  left: -5px;
+  cursor: sw-resize;
+}
+
+.clock-format-selector {
+  position: absolute;
+  bottom: 8px;
+  right: 8px;
+  z-index: 20;
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 4px;
+  padding: 2px 6px;
+}
+</style>

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

@@ -45,6 +45,14 @@ export const webPagePropNameMap = {
   y: '纵坐标'
 };
 
+export const clockPropNameMap = {
+  format: '时间格式',
+  width: '宽度',
+  height: '高度',
+  x: '横坐标',
+  y: '纵坐标'
+};
+
 export const livePropNameMap = {
   liveUrl: '直播地址',
   playAudio: '播放音频',