1
0

5 Commits 825222b572 ... 48fb2988cc

Autor SHA1 Mensagem Data
  Shinohara Haruna 48fb2988cc 合并分支 há 5 meses atrás
  Shinohara Haruna 54ebae0dd2 优化媒资组件在属性栏中的布局 há 5 meses atrás
  Shinohara Haruna ff7a9d0dac 支持媒资组件预览 há 5 meses atrás
  Shinohara Haruna 4fc0383158 支持画布背景预览 há 5 meses atrás
  Shinohara Haruna c0d31a9e2a 优化表格显示 há 5 meses atrás

+ 11 - 6
smsb-plus-ui/src/components/MediaFileSelector.vue

@@ -1,7 +1,7 @@
 <template>
   <div>
     <el-button type="primary" @click="dialogVisible = true">选择文件</el-button>
-    <div v-if="selectedFiles.length > 0" class="selected-files-list">
+    <div v-if="showSelected && selectedFiles.length > 0" class="selected-files-list">
       <el-tag v-for="(file, idx) in selectedFiles" :key="file.id" closable @close="removeFile(idx)" style="margin: 2px">
         {{ file.name }}
       </el-tag>
@@ -27,15 +27,15 @@
       <el-table v-loading="dialogLoading" ref="fileTable" :data="fileList" reserve-selection row-key="id"
         @selection-change="handleSelectionFile" @select="handleSelect" @select-all="handleSelectAll">
         <el-table-column type="selection" width="55" header-align="center" :selectable="isSelectableRow" />
-        <el-table-column label="类型" header-align="center" prop="type" width="80">
+        <el-table-column label="类型" header-align="left" prop="type" width="80">
           <template #default="scope">
             <dict-tag :options="smsb_source_type" :value="scope.row.type" />
           </template>
         </el-table-column>
         <el-table-column label="原名" header-align="left" prop="originalName" width="150" :show-overflow-tooltip="true" />
-        <el-table-column label="大小" header-align="center" prop="size" />
-        <el-table-column label="时长" header-align="center" prop="duration" />
-        <el-table-column label="截图" header-align="center" prop="screenshot">
+        <el-table-column label="大小" header-align="left" prop="size" />
+        <el-table-column label="时长" header-align="left" prop="duration" />
+        <el-table-column label="截图" header-align="left" prop="screenshot">
           <template #default="scope">
             <image-preview :src="scope.row.screenshot" style="width: 40px; height: 40px; cursor: pointer" />
           </template>
@@ -58,7 +58,12 @@ import { ref, watch, defineProps, defineEmits, nextTick } from 'vue';
 import { listMinioData } from '@/api/smsb/source/minioData';
 import type { MinioDataVO, MinioDataQuery } from '@/api/smsb/source/minioData_type';
 
-const props = defineProps<{ modelValue: string; single?: boolean; onlyImage?: boolean }>();
+const props = defineProps<{
+  modelValue: string;
+  single?: boolean;
+  onlyImage?: boolean;
+  showSelected: boolean | { type: BooleanConstructor; default: boolean };
+}>();
 const emit = defineEmits(['update:modelValue']);
 
 const dialogVisible = ref(false);

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

@@ -148,7 +148,7 @@
                 <el-switch v-model="selectedComponent[key]" active-text="开" inactive-text="关" />
               </template>
               <template v-else-if="key === 'mediaId'">
-                <MediaFileSelector v-model="selectedComponent[key]" />
+                <MediaFileSelector v-model="selectedComponent[key]" :showSelected="false" />
               </template>
               <template v-else-if="key === 'bg'">
                 <BackgroundSelector v-model="selectedComponent[key]" :isCanvas="selectedComponent.type === 'canvas'" />

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

@@ -15,18 +15,90 @@ interface Props {
   scale?: number;
 }
 const props = defineProps<Props>();
-const canvasStyle = computed(() => ({
-  width: '100%',
-  height: '100%',
-  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',
-  fontSize: '20px',
-  color: '#aaa',
-  borderRadius: '12px',
-  boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
-}));
+const canvasStyle = computed(() => {
+  // console.log('props.bg', props.bg);
+  let bgValue = props.bg;
+  let imgUrl: string | undefined;
+
+  // 尝试解析 bg 为数组并取 url
+  if (typeof bgValue === 'string' && bgValue.trim().startsWith('[')) {
+    try {
+      const arr = JSON.parse(bgValue);
+      if (Array.isArray(arr) && arr.length > 0 && arr[0].url) {
+        imgUrl = arr[0].url;
+      }
+    } catch (e) {
+      // 解析失败,忽略
+    }
+  }
+
+  const isHex = typeof bgValue === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(bgValue);
+  const isImg = typeof bgValue === 'string' && /^https?:\/\/.+\.(png|jpe?g|webp|gif|bmp|svg)(\?.*)?$/i.test(bgValue);
+
+  if (isHex) {
+    // console.log('Is Hex BG');
+    return {
+      width: '100%',
+      height: '100%',
+      background: props.bg,
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+      fontSize: '20px',
+      color: '#aaa',
+      borderRadius: '12px',
+      boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
+    };
+  } else if (isImg) {
+    // console.log('Is Img BG');
+    return {
+      width: '100%',
+      height: '100%',
+      backgroundImage: `url(${bgValue})`,
+      backgroundSize: 'cover',
+      backgroundPosition: 'center',
+      backgroundRepeat: 'no-repeat',
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+      fontSize: '20px',
+      color: '#aaa',
+      borderRadius: '12px',
+      boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
+    };
+  } else if (imgUrl) {
+    // console.log('Is Img BG (from array)');
+    return {
+      width: '100%',
+      height: '100%',
+      backgroundImage: `url(${imgUrl})`,
+      backgroundSize: 'cover',
+      backgroundPosition: 'center',
+      backgroundRepeat: 'no-repeat',
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+      fontSize: '20px',
+      color: '#aaa',
+      borderRadius: '12px',
+      boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
+    };
+  } else {
+    // console.log('Is Default BG');
+    return {
+      width: '100%',
+      height: '100%',
+      background: '#fff',
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+      fontSize: '20px',
+      color: '#aaa',
+      borderRadius: '12px',
+      boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
+    };
+  }
+});
 
 const wrapperStyle = computed(() => {
   const width = typeof props.width === 'number' ? props.width : parseFloat(props.width || '600');

+ 115 - 6
smsb-plus-ui/src/views/smsb/itemProgram/component/MediaAssetBoard.vue

@@ -5,15 +5,30 @@
     position: 'relative',
     display: 'flex',
     alignItems: 'center',
-    justifyContent: 'center'
-  }">
-    <div class="media-asset-icon">
+    justifyContent: 'center',
+    overflow: 'hidden'
+  }" @click="$emit('click', $event)">
+    <!-- 预览窗口 -->
+    <div v-if="mediaItems.length > 0" class="preview-container">
+      <template v-for="(item, index) in mediaItems" :key="index">
+        <div v-show="currentMediaIndex === index" class="media-item" :class="{ 'active': currentMediaIndex === index }">
+          <img v-if="item.type === 1" :src="item.url" :alt="item.name" class="media-content" />
+          <video v-else-if="item.type === 2" :src="item.url" class="media-content" preload="metadata" muted
+            @loadeddata="onVideoLoaded"></video>
+        </div>
+      </template>
+    </div>
+
+    <!-- 无媒体时的默认状态 -->
+    <div v-else class="media-asset-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" />
         <path d="M8 22l6-6 4 4 6-6" stroke="#409eff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
         <circle cx="11" cy="12" r="2" fill="#409eff" />
       </svg>
     </div>
+
+    <!-- Resize -->
     <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)" />
@@ -22,30 +37,90 @@
 </template>
 
 <script setup lang="ts">
-import { computed, withDefaults, defineEmits } from 'vue';
-const emit = defineEmits(['resize']);
+import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
+
+const emit = defineEmits(['resize', 'click']);
+
 interface Props {
   width?: number;
   height?: number;
   mediaId?: string;
   selected?: boolean;
 }
+
 const props = withDefaults(defineProps<Props>(), {
   width: 120,
-  height: 120
+  height: 120,
+  mediaId: ''
 });
+
 const selected = computed(() => !!props.selected);
+const mediaItems = ref<any[]>([]);
+const currentMediaIndex = ref(0);
+let slideInterval: number | null = null;
+const SLIDE_DURATION = 10000; // 暂且先写死 10s
+
+const parseMediaItems = () => {
+  try {
+    if (props.mediaId) {
+      const parsed = JSON.parse(props.mediaId);
+      mediaItems.value = Array.isArray(parsed) ? parsed : [parsed];
+    } else {
+      mediaItems.value = [];
+    }
+    currentMediaIndex.value = 0;
+    startSlideShow();
+  } catch (e) {
+    console.error('Error parsing media items:', e);
+    mediaItems.value = [];
+  }
+};
+
+const startSlideShow = () => {
+  clearSlideShow();
+  if (mediaItems.value.length <= 1) return;
 
+  slideInterval = window.setInterval(() => {
+    currentMediaIndex.value = (currentMediaIndex.value + 1) % mediaItems.value.length;
+  }, SLIDE_DURATION);
+};
+
+const clearSlideShow = () => {
+  if (slideInterval !== null) {
+    clearInterval(slideInterval);
+    slideInterval = null;
+  }
+};
+
+const onVideoLoaded = (event: Event) => {
+  const video = event.target as HTMLVideoElement;
+  video.pause();
+  video.currentTime = 0;
+};
+
+watch(
+  () => props.mediaId,
+  () => {
+    parseMediaItems();
+  },
+  { immediate: true }
+);
+
+onUnmounted(() => {
+  clearSlideShow();
+});
 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;
@@ -57,10 +132,12 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
     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);
 }
@@ -73,18 +150,50 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   border-radius: 10px;
   transition: border-color 0.2s;
   position: relative;
+  overflow: hidden;
 }
 
 .media-asset-board-wrapper.selected {
   border-color: #409eff;
 }
 
+.preview-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.media-item {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.5s ease-in-out;
+}
+
+.media-item.active {
+  opacity: 1;
+}
+
+.media-content {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
 .media-asset-icon {
   display: flex;
   align-items: center;
   justify-content: center;
   width: 100%;
   height: 100%;
+  background: #f5f7fa;
 }
 
 .resize-handle {

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

@@ -30,7 +30,7 @@ export const scrollingTextPropNameMap = {
 };
 
 export const mediaAssetPropNameMap = {
-  mediaId: '媒资ID',
+  mediaId: '媒资文件',
   width: '宽度',
   height: '高度',
   x: '横坐标',

+ 29 - 12
smsb-plus-ui/src/views/smsb/itemProgram/index.vue

@@ -5,9 +5,9 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="节目ID" prop="programId">
+            <!-- <el-form-item label="节目ID" prop="programId">
               <el-input v-model="queryParams.programId" placeholder="请输入节目ID" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
+            </el-form-item> -->
             <el-form-item label="节目名称" prop="name">
               <el-input v-model="queryParams.name" placeholder="请输入节目名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
@@ -15,12 +15,12 @@
               <el-input v-model="queryParams.resolutionRatio" placeholder="请输入分辨率" clearable
                 @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="图片地址" prop="imgUrl">
+            <!-- <el-form-item label="图片地址" prop="imgUrl">
               <el-input v-model="queryParams.imgUrl" placeholder="请输入图片地址" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="节目时长" prop="duration">
+            </el-form-item> -->
+            <!-- <el-form-item label="节目时长" prop="duration">
               <el-input v-model="queryParams.duration" placeholder="请输入节目时长" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
+            </el-form-item> -->
             <el-form-item label="所属个人" prop="user">
               <el-input v-model="queryParams.user" placeholder="请输入所属个人" clearable @keyup.enter="handleQuery" />
             </el-form-item>
@@ -61,10 +61,14 @@
         <el-table-column label="节目ID" align="center" prop="programId" />
         <el-table-column label="节目名称" align="center" prop="name" />
         <el-table-column label="分辨率" align="center" prop="resolutionRatio" />
-        <el-table-column label="图片地址" align="center" prop="imgUrl" />
-        <el-table-column label="状态" align="center" prop="status" />
-        <el-table-column label="节目时长" align="center" prop="duration" />
-        <el-table-column label="所属个人" align="center" prop="user" />
+        <!-- <el-table-column label="图片地址" align="center" prop="imgUrl" /> -->
+        <!-- <el-table-column label="状态" align="center" prop="status" /> -->
+        <!-- <el-table-column label="节目时长" align="center" prop="duration" /> -->
+        <el-table-column label="所属个人" align="center">
+          <template #default="scope">
+            <span>{{ userNickNameMap[scope.row.user] || scope.row.user || '-' }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
@@ -97,9 +101,9 @@
             <el-option label="1024x768" value="1024x768" />
           </el-select>
         </el-form-item>
-        <el-form-item label="图片地址" prop="imgUrl">
+        <!-- <el-form-item label="图片地址" prop="imgUrl">
           <el-input v-model="form.imgUrl" placeholder="请输入图片地址(可选)" />
-        </el-form-item>
+        </el-form-item> -->
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -113,12 +117,14 @@
 
 <script setup name="ItemProgram" lang="ts">
 import { listItemProgram, getItemProgram, delItemProgram, addItemProgram, updateItemProgram } from '@/api/smsb/source/item_program';
+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';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const itemProgramList = ref<ItemProgramVO[]>([]);
+const userNickNameMap = reactive<Record<string, string>>({});
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -171,6 +177,17 @@ const getList = async () => {
   const res = await listItemProgram(queryParams.value);
   itemProgramList.value = res.rows;
   total.value = res.total;
+  // 收集所有 userId
+  const userIds = Array.from(new Set(res.rows.map((item: any) => item.user).filter(Boolean)));
+  if (userIds.length > 0) {
+    try {
+      // 批量获取用户信息
+      const userRes = await optionSelect(userIds);
+      (userRes.data || []).forEach((u: UserVO) => {
+        userNickNameMap[u.userId] = u.nickName;
+      });
+    } catch (e) { }
+  }
   loading.value = false;
 };