Browse Source

初步实现轮播组选择

Shinohara Haruna 5 months ago
parent
commit
f6a57c4207

+ 221 - 0
smsb-plus-ui/src/components/CarouselGroupSelector.vue

@@ -0,0 +1,221 @@
+<template>
+  <div>
+    <el-button type="primary" @click="dialogVisible = true">选择轮播组</el-button>
+    <div v-if="showSelected && selectedGroup" class="selected-group">
+      <el-tag closable @close="removeGroup" style="margin: 2px">
+        {{ selectedGroup.name }}
+      </el-tag>
+    </div>
+    <el-dialog title="选择轮播组" v-model="dialogVisible" width="900px" append-to-body>
+      <el-form :inline="true" :model="queryParams" class="mb-2">
+        <el-form-item label="名称">
+          <el-input v-model="queryParams.itemName" placeholder="轮播组名称" clearable style="width: 180px"
+            @keyup.enter="getCarouselList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="getCarouselList">搜索</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="dialogLoading" :data="carouselList" @row-click="handleRowClick" highlight-current-row>
+        <el-table-column width="50">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.selected" @click.stop @change="handleSelectChange(row)" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="轮播组名称" min-width="150" />
+        <el-table-column prop="itemCount" label="轮播项数量" width="100" />
+        <el-table-column prop="duration" label="轮播间隔(秒)" width="120" />
+        <el-table-column prop="createTime" label="创建时间" width="180" />
+      </el-table>
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize" @pagination="getCarouselList" />
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSelect" :disabled="!currentRow">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, defineProps, defineEmits, nextTick } from 'vue';
+import { listItem } from '@/api/smsb/source/item';
+import type { ItemVO, ItemQuery } from '@/api/smsb/source/item_type';
+
+export interface CarouselGroup {
+  id: string | number;
+  name: string;
+  itemCount: number;
+  duration: number;
+  createTime: string;
+  items: any[];
+  selected?: boolean;
+  [key: string]: any; // Allow additional properties
+}
+
+const props = defineProps<{
+  modelValue: string;
+  showSelected?: boolean;
+}>();
+
+const emit = defineEmits(['update:modelValue']);
+
+const dialogVisible = ref(false);
+const dialogLoading = ref(false);
+const carouselList = ref<CarouselGroup[]>([]);
+const total = ref(0);
+const currentRow = ref<CarouselGroup | null>(null);
+const selectedGroup = ref<CarouselGroup | null>(null);
+
+// 定义查询参数类型
+interface CarouselQuery extends ItemQuery {
+  pageNum: number;
+  pageSize: number;
+  itemName: string;
+  itemType: number;
+}
+
+const queryParams = ref<CarouselQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  itemName: '',
+  itemType: 1 // 1表示轮播组类型
+});
+
+// 初始化时,如果有值,反序列化
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (val) {
+      try {
+        const parsedValue = JSON.parse(val);
+        selectedGroup.value = {
+          id: parsedValue.id,
+          name: parsedValue.name,
+          itemCount: parsedValue.itemCount,
+          duration: parsedValue.duration,
+          createTime: parsedValue.createTime,
+          items: parsedValue.items
+        };
+      } catch {
+        selectedGroup.value = null;
+      }
+    } else {
+      selectedGroup.value = null;
+    }
+  },
+  { immediate: true }
+);
+
+// 查询轮播组列表
+const getCarouselList = async () => {
+  dialogLoading.value = true;
+  try {
+    // 构建查询参数,确保类型安全
+    const query: ItemQuery = {
+      itemName: queryParams.value.itemName,
+      itemType: 1, // 1表示轮播组类型
+      pageNum: queryParams.value.pageNum,
+      pageSize: queryParams.value.pageSize
+    };
+
+    const res = await listItem(query);
+
+    // 转换数据格式,并标记已选中的项
+    carouselList.value = (res.rows || []).map((item: ItemVO) => ({
+      id: item.id,
+      name: item.itemName || '',
+      itemName: item.itemName || '', // 保持与API返回的字段名一致
+      itemCount: item.sourceNum || 0,
+      duration: 5, // 默认5秒,可以根据实际需求调整
+      createTime: item.createTime || '',
+      selected: selectedGroup.value?.id === item.id,
+      items: [] // 不需要在UI中显示,仅用于内部处理
+    }));
+
+    // 如果当前有选中的组,确保它在列表中也被选中
+    if (selectedGroup.value?.id) {
+      const selectedItem = carouselList.value.find((item) => item.id === selectedGroup.value?.id);
+      if (selectedItem) {
+        selectedItem.selected = true;
+        currentRow.value = selectedItem;
+      }
+    }
+
+    total.value = res.total || 0;
+  } finally {
+    dialogLoading.value = false;
+  }
+};
+
+// 行点击
+const handleRowClick = (row: CarouselGroup) => {
+  currentRow.value = row;
+};
+
+// 处理选择变化
+const handleSelectChange = (row: CarouselGroup) => {
+  // 确保只有一个被选中
+  carouselList.value.forEach((item) => {
+    if (item.id !== row.id) {
+      item.selected = false;
+    }
+  });
+  currentRow.value = row.selected ? row : null;
+};
+
+// 选择轮播组
+const selectGroup = (row: CarouselGroup) => {
+  row.selected = true;
+  handleSelectChange(row);
+  confirmSelect();
+};
+
+// 移除已选轮播组
+const removeGroup = () => {
+  selectedGroup.value = null;
+  emit('update:modelValue', '');
+};
+
+// 确认选择
+const confirmSelect = () => {
+  if (!currentRow.value) return;
+
+  // 确保ID是数字类型
+  const id = typeof currentRow.value.id === 'string' ? parseInt(currentRow.value.id, 10) : currentRow.value.id;
+
+  if (isNaN(id)) {
+    console.error('Invalid group ID:', currentRow.value.id);
+    return;
+  }
+
+  // 只存储必要的信息
+  selectedGroup.value = {
+    id,
+    name: currentRow.value.name,
+    itemCount: 0,
+    duration: 0,
+    createTime: '',
+    items: []
+  };
+
+  // 更新所有行的选中状态
+  carouselList.value.forEach((item) => {
+    item.selected = item.id === currentRow.value.id;
+  });
+
+  emit('update:modelValue', JSON.stringify(selectedGroup.value));
+  dialogVisible.value = false;
+};
+
+// 弹窗首次打开时自动加载
+watch(dialogVisible, (val) => {
+  if (val) getCarouselList();
+});
+</script>
+
+<style scoped>
+.selected-group {
+  margin-top: 8px;
+}
+</style>

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

@@ -35,7 +35,7 @@
               fill="currentColor"></path>
           </svg>
         </el-icon>
-        <div style="font-size: 12px; color: #bbb">回收站</div>
+        <div style="font-size: 12px; color: #bbb">拖拽移除组件</div>
       </div>
     </div>
 
@@ -86,7 +86,7 @@
                   " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
                 <MediaAssetBoard v-if="item.type === 'mediaAsset'" :width="item.width * canvasScale"
                   :height="item.height * canvasScale" :media-id="item.mediaId" :selected="selectedComponent === item"
-                  @resize="
+                  v-model="item.mediaGroup" @resize="
                     ({ width, height }) => {
                       item.width = width / canvasScale;
                       item.height = height / canvasScale;
@@ -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]" :showSelected="false" />
+                <CarouselGroupSelector v-model="selectedComponent[key]" :showSelected="false" />
               </template>
               <template v-else-if="key === 'bg'">
                 <BackgroundSelector v-model="selectedComponent[key]" :isCanvas="selectedComponent.type === 'canvas'" />
@@ -328,7 +328,7 @@ 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 MediaFileSelector from '@/components/MediaFileSelector.vue';
+import CarouselGroupSelector from '@/components/CarouselGroupSelector.vue';
 import BackgroundSelector from '@/components/BackgroundSelector.vue';
 import LiveBoard from './component/LiveBoard.vue';
 import WebPageBoard from './component/WebPageBoard.vue';

+ 113 - 32
smsb-plus-ui/src/views/smsb/itemProgram/component/MediaAssetBoard.vue

@@ -6,7 +6,10 @@
     display: 'flex',
     alignItems: 'center',
     justifyContent: 'center',
-    overflow: 'hidden'
+    overflow: 'hidden',
+    flexDirection: 'column',
+    gap: '8px',
+    padding: '8px'
   }" @click="$emit('click', $event)">
     <!-- 预览窗口 -->
     <div v-if="mediaItems.length > 0" class="preview-container">
@@ -17,6 +20,11 @@
             @loadeddata="onVideoLoaded"></video>
         </div>
       </template>
+
+      <!-- 轮播组名称显示 -->
+      <div v-if="carouselGroup.name" class="group-name-overlay">
+        {{ carouselGroup.name }}
+      </div>
     </div>
 
     <!-- 无媒体时的默认状态 -->
@@ -26,10 +34,13 @@
         <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 v-if="!props.readonly" class="select-hint">
+        {{ carouselGroup.name ? `轮播组:${carouselGroup.name}` : '请选择轮播组' }}
+      </div>
     </div>
 
     <!-- Resize -->
-    <template v-if="selected">
+    <template v-if="selected && !props.readonly">
       <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
         @mousedown.stop="onResizeMouseDown(dir, $event)" />
     </template>
@@ -37,45 +48,90 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
-
-const emit = defineEmits(['resize', 'click']);
+import { ref, computed, watch, onUnmounted, defineProps, defineEmits } from 'vue';
+import { getItem } from '@/api/smsb/source/item';
 
-interface Props {
-  width?: number;
-  height?: number;
-  mediaId?: string;
-  selected?: boolean;
-}
+const emit = defineEmits(['update:mediaId', 'resize', 'click']);
 
-const props = withDefaults(defineProps<Props>(), {
-  width: 120,
-  height: 120,
-  mediaId: ''
-});
+const props = withDefaults(
+  defineProps<{
+    width: number;
+    height: number;
+    mediaId?: string;
+    selected?: boolean;
+    readonly?: boolean;
+    modelValue?: any;
+  }>(),
+  {
+    selected: false,
+    readonly: false,
+    modelValue: null
+  }
+);
 
-const selected = computed(() => !!props.selected);
 const mediaItems = ref<any[]>([]);
 const currentMediaIndex = ref(0);
 let slideInterval: number | null = null;
-const SLIDE_DURATION = 10000; // 暂且先写死 10s
+const SLIDE_DURATION = 10000; // 10秒切换一次
 
-const parseMediaItems = () => {
+// 解析轮播组数据
+const carouselGroup = computed(() => {
   try {
     if (props.mediaId) {
-      const parsed = JSON.parse(props.mediaId);
-      mediaItems.value = Array.isArray(parsed) ? parsed : [parsed];
+      return JSON.parse(props.mediaId);
+    }
+  } catch (e) {
+    console.error('Failed to parse mediaId:', e);
+  }
+  return {};
+});
+
+const selected = computed(() => !!props.selected);
+
+// 加载轮播组媒体项
+const loadCarouselGroupMedia = async () => {
+  try {
+    if (!carouselGroup.value || !carouselGroup.value.id) {
+      mediaItems.value = [];
+      return;
+    }
+
+    // 获取轮播组详情
+    const res = await getItem(carouselGroup.value.id);
+
+    if (res.data && Array.isArray(res.data.fileIdList)) {
+      mediaItems.value = res.data.fileIdList.map((fileId: string | number) => ({
+        id: fileId,
+        type: 1, // 默认为图片类型
+        url: `/api/source/file/${fileId}`
+      }));
     } else {
       mediaItems.value = [];
     }
+  } catch (error) {
+    console.error('Failed to load carousel group media:', error);
+    mediaItems.value = [];
+  } finally {
+    // 重置索引并开始轮播
     currentMediaIndex.value = 0;
     startSlideShow();
-  } catch (e) {
-    console.error('Error parsing media items:', e);
-    mediaItems.value = [];
   }
 };
 
+// 监听mediaId变化
+watch(
+  () => props.mediaId,
+  (newVal) => {
+    if (newVal) {
+      loadCarouselGroupMedia();
+    } else {
+      mediaItems.value = [];
+      clearSlideShow();
+    }
+  },
+  { immediate: true }
+);
+
 const startSlideShow = () => {
   clearSlideShow();
   if (mediaItems.value.length <= 1) return;
@@ -98,17 +154,19 @@ const onVideoLoaded = (event: Event) => {
   video.currentTime = 0;
 };
 
-watch(
-  () => props.mediaId,
-  () => {
-    parseMediaItems();
-  },
-  { immediate: true }
-);
-
 onUnmounted(() => {
   clearSlideShow();
 });
+
+// 暴露方法,供父组件调用
+const getCarouselGroupId = () => {
+  return props.mediaId;
+};
+
+defineExpose({
+  getCarouselGroupId
+});
+
 let startX = 0,
   startY = 0,
   startW = 0,
@@ -151,6 +209,13 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   transition: border-color 0.2s;
   position: relative;
   overflow: hidden;
+  box-sizing: border-box;
+}
+
+.select-hint {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
 }
 
 .media-asset-board-wrapper.selected {
@@ -189,6 +254,7 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
 
 .media-asset-icon {
   display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
   width: 100%;
@@ -196,6 +262,21 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   background: #f5f7fa;
 }
 
+.group-name-overlay {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  color: white;
+  padding: 4px 8px;
+  font-size: 12px;
+  text-align: center;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 .resize-handle {
   position: absolute;
   width: 10px;

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

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