|
@@ -6,7 +6,10 @@
|
|
|
display: 'flex',
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
justifyContent: 'center',
|
|
|
- overflow: 'hidden'
|
|
|
|
|
|
|
+ overflow: 'hidden',
|
|
|
|
|
+ flexDirection: 'column',
|
|
|
|
|
+ gap: '8px',
|
|
|
|
|
+ padding: '8px'
|
|
|
}" @click="$emit('click', $event)">
|
|
}" @click="$emit('click', $event)">
|
|
|
<!-- 预览窗口 -->
|
|
<!-- 预览窗口 -->
|
|
|
<div v-if="mediaItems.length > 0" class="preview-container">
|
|
<div v-if="mediaItems.length > 0" class="preview-container">
|
|
@@ -17,6 +20,11 @@
|
|
|
@loadeddata="onVideoLoaded"></video>
|
|
@loadeddata="onVideoLoaded"></video>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 轮播组名称显示 -->
|
|
|
|
|
+ <div v-if="carouselGroup.name" class="group-name-overlay">
|
|
|
|
|
+ {{ carouselGroup.name }}
|
|
|
|
|
+ </div>
|
|
|
</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" />
|
|
<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" />
|
|
<circle cx="11" cy="12" r="2" fill="#409eff" />
|
|
|
</svg>
|
|
</svg>
|
|
|
|
|
+ <div v-if="!props.readonly" class="select-hint">
|
|
|
|
|
+ {{ carouselGroup.name ? `轮播组:${carouselGroup.name}` : '请选择轮播组' }}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Resize -->
|
|
<!-- 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"
|
|
<div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
|
|
|
@mousedown.stop="onResizeMouseDown(dir, $event)" />
|
|
@mousedown.stop="onResizeMouseDown(dir, $event)" />
|
|
|
</template>
|
|
</template>
|
|
@@ -37,78 +48,184 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
|
|
|
|
|
|
|
+import { ref, computed, watch, onUnmounted, defineProps, defineEmits } from 'vue';
|
|
|
|
|
+import { getItem, getItemFileRelList } from '@/api/smsb/source/item';
|
|
|
|
|
+import { getMinioData } from '@/api/smsb/source/minioData';
|
|
|
|
|
|
|
|
-const emit = defineEmits(['resize', 'click']);
|
|
|
|
|
|
|
+const emit = defineEmits(['update:mediaId', 'resize', 'click']);
|
|
|
|
|
|
|
|
-interface Props {
|
|
|
|
|
- width?: number;
|
|
|
|
|
- height?: number;
|
|
|
|
|
- mediaId?: string;
|
|
|
|
|
- selected?: boolean;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const props = withDefaults(
|
|
|
|
|
+ defineProps<{
|
|
|
|
|
+ width: number;
|
|
|
|
|
+ height: number;
|
|
|
|
|
+ mediaId?: string;
|
|
|
|
|
+ selected?: boolean;
|
|
|
|
|
+ readonly?: boolean;
|
|
|
|
|
+ modelValue?: any;
|
|
|
|
|
+ }>(),
|
|
|
|
|
+ {
|
|
|
|
|
+ selected: false,
|
|
|
|
|
+ readonly: false,
|
|
|
|
|
+ modelValue: null
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
-const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
- width: 120,
|
|
|
|
|
- height: 120,
|
|
|
|
|
- mediaId: ''
|
|
|
|
|
-});
|
|
|
|
|
|
|
+interface MediaItem {
|
|
|
|
|
+ id: string | number;
|
|
|
|
|
+ type: number; // 1-图片 2-视频
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ duration: number; // 秒
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-const selected = computed(() => !!props.selected);
|
|
|
|
|
-const mediaItems = ref<any[]>([]);
|
|
|
|
|
|
|
+const mediaItems = ref<MediaItem[]>([]);
|
|
|
const currentMediaIndex = ref(0);
|
|
const currentMediaIndex = ref(0);
|
|
|
let slideInterval: number | null = null;
|
|
let slideInterval: number | null = null;
|
|
|
-const SLIDE_DURATION = 10000; // 暂且先写死 10s
|
|
|
|
|
|
|
|
|
|
-const parseMediaItems = () => {
|
|
|
|
|
|
|
+// 解析轮播组数据
|
|
|
|
|
+const carouselGroup = computed(() => {
|
|
|
try {
|
|
try {
|
|
|
if (props.mediaId) {
|
|
if (props.mediaId) {
|
|
|
- const parsed = JSON.parse(props.mediaId);
|
|
|
|
|
- mediaItems.value = Array.isArray(parsed) ? parsed : [parsed];
|
|
|
|
|
- } else {
|
|
|
|
|
- mediaItems.value = [];
|
|
|
|
|
|
|
+ return JSON.parse(props.mediaId);
|
|
|
}
|
|
}
|
|
|
- currentMediaIndex.value = 0;
|
|
|
|
|
- startSlideShow();
|
|
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('Error parsing media items:', e);
|
|
|
|
|
- mediaItems.value = [];
|
|
|
|
|
|
|
+ console.error('Failed to parse mediaId:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ return {};
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const selected = computed(() => !!props.selected);
|
|
|
|
|
+
|
|
|
|
|
+// 清理轮播
|
|
|
|
|
+const clearSlideShow = () => {
|
|
|
|
|
+ if (slideInterval !== null) {
|
|
|
|
|
+ clearInterval(slideInterval);
|
|
|
|
|
+ slideInterval = null;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 开始轮播
|
|
|
const startSlideShow = () => {
|
|
const startSlideShow = () => {
|
|
|
clearSlideShow();
|
|
clearSlideShow();
|
|
|
if (mediaItems.value.length <= 1) return;
|
|
if (mediaItems.value.length <= 1) return;
|
|
|
|
|
|
|
|
- slideInterval = window.setInterval(() => {
|
|
|
|
|
- currentMediaIndex.value = (currentMediaIndex.value + 1) % mediaItems.value.length;
|
|
|
|
|
- }, SLIDE_DURATION);
|
|
|
|
|
|
|
+ const playNext = () => {
|
|
|
|
|
+ const currentItem = mediaItems.value[currentMediaIndex.value];
|
|
|
|
|
+ const duration = (currentItem?.duration || 5) * 1000; // 转为毫秒,默认5秒
|
|
|
|
|
+
|
|
|
|
|
+ // console.log(`播放第 ${currentMediaIndex.value + 1}/${mediaItems.value.length} 项,类型: ${currentItem.type},时长: ${duration}ms`);
|
|
|
|
|
+
|
|
|
|
|
+ slideInterval = window.setTimeout(() => {
|
|
|
|
|
+ currentMediaIndex.value = (currentMediaIndex.value + 1) % mediaItems.value.length;
|
|
|
|
|
+ playNext();
|
|
|
|
|
+ }, duration);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 开始第一项播放
|
|
|
|
|
+ playNext();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const clearSlideShow = () => {
|
|
|
|
|
- if (slideInterval !== null) {
|
|
|
|
|
- clearInterval(slideInterval);
|
|
|
|
|
- slideInterval = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 获取文件类型(1-图片,2-视频)
|
|
|
|
|
+const getFileType = (type: number): number => {
|
|
|
|
|
+ // 1-图片,2-视频,3-音频
|
|
|
|
|
+ return type === 2 ? 2 : 1; // 视频为2,其他都视为图片
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const onVideoLoaded = (event: Event) => {
|
|
|
|
|
- const video = event.target as HTMLVideoElement;
|
|
|
|
|
- video.pause();
|
|
|
|
|
- video.currentTime = 0;
|
|
|
|
|
|
|
+// 加载轮播组媒体项
|
|
|
|
|
+const loadCarouselGroupMedia = async () => {
|
|
|
|
|
+ // console.log('开始加载轮播组媒体项', carouselGroup.value);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!carouselGroup.value || !carouselGroup.value.id) {
|
|
|
|
|
+ // console.log('轮播组ID不存在');
|
|
|
|
|
+ mediaItems.value = [];
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 获取轮播组关联的文件关系列表
|
|
|
|
|
+ const relRes = await getItemFileRelList(carouselGroup.value.id);
|
|
|
|
|
+ // console.log('获取到轮播组关联文件关系:', relRes.data);
|
|
|
|
|
+
|
|
|
|
|
+ if (!Array.isArray(relRes.data) || relRes.data.length === 0) {
|
|
|
|
|
+ // console.log('轮播组没有关联的文件');
|
|
|
|
|
+ mediaItems.value = [];
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 处理每个关联文件
|
|
|
|
|
+ const items: MediaItem[] = [];
|
|
|
|
|
+ for (const rel of relRes.data) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取文件详情
|
|
|
|
|
+ // console.log(`加载文件详情: ${rel.fileId}`);
|
|
|
|
|
+ const fileRes = await getMinioData(rel.fileId);
|
|
|
|
|
+ const fileInfo = fileRes.data;
|
|
|
|
|
+ // console.log('文件详情:', fileInfo);
|
|
|
|
|
+
|
|
|
|
|
+ if (fileInfo) {
|
|
|
|
|
+ // 使用 fileUrl 如果存在,否则回退到构建的 URL
|
|
|
|
|
+ const fileUrl = fileInfo.fileUrl || `/api/source/file/${rel.fileId}`;
|
|
|
|
|
+ // console.log(`文件 ${fileInfo.originalName} 的 URL:`, fileUrl);
|
|
|
|
|
+
|
|
|
|
|
+ items.push({
|
|
|
|
|
+ id: rel.fileId,
|
|
|
|
|
+ type: getFileType(fileInfo.type),
|
|
|
|
|
+ url: fileUrl,
|
|
|
|
|
+ duration: rel.duration || 5, // 使用关联关系中的 duration
|
|
|
|
|
+ name: fileInfo.originalName
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (fileError) {
|
|
|
|
|
+ console.error(`加载文件 ${rel.fileId} 详情失败:`, fileError);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 按sort排序
|
|
|
|
|
+ items.sort((a, b) => (a as any).sort - (b as any).sort);
|
|
|
|
|
+ mediaItems.value = items;
|
|
|
|
|
+ // console.log('最终媒体项列表:', mediaItems.value);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 开始轮播
|
|
|
|
|
+ currentMediaIndex.value = 0;
|
|
|
|
|
+ startSlideShow();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载轮播组媒体项失败:', error);
|
|
|
|
|
+ mediaItems.value = [];
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 监听mediaId变化
|
|
|
watch(
|
|
watch(
|
|
|
() => props.mediaId,
|
|
() => props.mediaId,
|
|
|
- () => {
|
|
|
|
|
- parseMediaItems();
|
|
|
|
|
|
|
+ (newVal) => {
|
|
|
|
|
+ if (newVal) {
|
|
|
|
|
+ loadCarouselGroupMedia();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mediaItems.value = [];
|
|
|
|
|
+ clearSlideShow();
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
{ immediate: true }
|
|
{ immediate: true }
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+const onVideoLoaded = (event: Event) => {
|
|
|
|
|
+ const video = event.target as HTMLVideoElement;
|
|
|
|
|
+ video.pause();
|
|
|
|
|
+ video.currentTime = 0;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
clearSlideShow();
|
|
clearSlideShow();
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+// 暴露方法,供父组件调用
|
|
|
|
|
+const getCarouselGroupId = () => {
|
|
|
|
|
+ return props.mediaId;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ getCarouselGroupId
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
let startX = 0,
|
|
let startX = 0,
|
|
|
startY = 0,
|
|
startY = 0,
|
|
|
startW = 0,
|
|
startW = 0,
|
|
@@ -151,6 +268,13 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
|
|
|
transition: border-color 0.2s;
|
|
transition: border-color 0.2s;
|
|
|
position: relative;
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-hint {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.media-asset-board-wrapper.selected {
|
|
.media-asset-board-wrapper.selected {
|
|
@@ -189,6 +313,7 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
|
|
|
|
|
|
|
|
.media-asset-icon {
|
|
.media-asset-icon {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
@@ -196,6 +321,21 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
|
|
|
background: #f5f7fa;
|
|
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 {
|
|
.resize-handle {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
width: 10px;
|
|
width: 10px;
|