|
@@ -0,0 +1,266 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-dialog class="hot-popup" v-model="show" append-to-body destroy-on-close>
|
|
|
|
|
+ <div class="hotspot-page">
|
|
|
|
|
+ <div v-if="!isTextType" class="hotspot-page-container">
|
|
|
|
|
+ <!-- 音频播放器 -->
|
|
|
|
|
+ <audio
|
|
|
|
|
+ id="myAudio"
|
|
|
|
|
+ v-if="audio"
|
|
|
|
|
+ ref="volumeRef"
|
|
|
|
|
+ v-show="isOneAduio"
|
|
|
|
|
+ :src="audio"
|
|
|
|
|
+ controls
|
|
|
|
|
+ ></audio>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 模型页面 -->
|
|
|
|
|
+ <Swiper
|
|
|
|
|
+ :modules="[Navigation, Pagination]"
|
|
|
|
|
+ class="hotspot-page-swiper"
|
|
|
|
|
+ :slides-per-view="isMobile ? 1 : 3"
|
|
|
|
|
+ :centered-slides="true"
|
|
|
|
|
+ :navigation="Boolean(curList.length) && !isMobile"
|
|
|
|
|
+ :pagination="
|
|
|
|
|
+ isMobile
|
|
|
|
|
+ ? false
|
|
|
|
|
+ : {
|
|
|
|
|
+ clickable: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ "
|
|
|
|
|
+ @swiper="initSwiper"
|
|
|
|
|
+ @slideChange="handleChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <SwiperSlide v-for="(item, index) in curList" :key="item.url">
|
|
|
|
|
+ <template v-if="swiperInited">
|
|
|
|
|
+ <template v-if="currentType === 'video'">
|
|
|
|
|
+ <video ref="videoRef" controls :src="item.src" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else-if="currentType === 'img'">
|
|
|
|
|
+ <el-image :src="item.src" fit="contain" @click="handlePreview(index)" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </SwiperSlide>
|
|
|
|
|
+ </Swiper>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 底部的tab -->
|
|
|
|
|
+ <ul v-if="flooTab.length > 1 || (audio && !isOneAduio)" class="hotspot-page-nav">
|
|
|
|
|
+ <!-- 音频图标 -->
|
|
|
|
|
+ <li
|
|
|
|
|
+ v-if="audio && !isOneAduio"
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'hotspot-page-nav__item',
|
|
|
|
|
+ {
|
|
|
|
|
+ active: audioSta,
|
|
|
|
|
+ },
|
|
|
|
|
+ ]"
|
|
|
|
|
+ @click="audioSta = !audioSta"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :src="VolumeOn" :alt="audioSta ? '关闭音频' : '打开音频'" />
|
|
|
|
|
+ <span>音频</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li
|
|
|
|
|
+ v-for="item in flooTab"
|
|
|
|
|
+ :key="item.type"
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'hotspot-page-nav__item',
|
|
|
|
|
+ {
|
|
|
|
|
+ active: currentType === item.type,
|
|
|
|
|
+ },
|
|
|
|
|
+ ]"
|
|
|
|
|
+ @click="handleTab(item)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :class="`${item.type}-icon`" :src="item.icon" />
|
|
|
|
|
+ <span>{{ item.name }}</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="isMobile ? isTextType : true"
|
|
|
|
|
+ class="hotspot-page-scrollbar"
|
|
|
|
|
+ :class="{ isTop: !flooTab.length && !isMobile }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <h3>{{ hotData?.title }}</h3>
|
|
|
|
|
+ <div v-html="hotData?.content" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+ import type { HotDataType } from '@/types';
|
|
|
|
|
+ import { computed, nextTick, ref, watch } from 'vue';
|
|
|
|
|
+ import { Swiper, SwiperSlide } from 'swiper/vue';
|
|
|
|
|
+ import { Navigation, Pagination } from 'swiper/modules';
|
|
|
|
|
+ import 'swiper/css';
|
|
|
|
|
+ import 'swiper/css/navigation';
|
|
|
|
|
+ import 'swiper/css/pagination';
|
|
|
|
|
+
|
|
|
|
|
+ import { judgeIsMobile } from '@/utils';
|
|
|
|
|
+
|
|
|
|
|
+ import ImageIcon from '@/assets/images/img-icon.png';
|
|
|
|
|
+ import VideoIcon from '@/assets/images/video-icon.png';
|
|
|
|
|
+ import VolumeOn from '@/assets/images/audio-icon.png';
|
|
|
|
|
+ import infoIcon from '@/assets/images/info-icon.png';
|
|
|
|
|
+
|
|
|
|
|
+ import { api as viewerApi } from 'v-viewer';
|
|
|
|
|
+ import 'viewerjs/dist/viewer.css';
|
|
|
|
|
+ import type { SwiperClass } from 'swiper/react';
|
|
|
|
|
+ import { useRoute } from 'vue-router';
|
|
|
|
|
+ import { HOTSPOT_TYPE, type HotspotTabType } from '@/types/hotspot';
|
|
|
|
|
+
|
|
|
|
|
+ const props = defineProps<{
|
|
|
|
|
+ visible: boolean;
|
|
|
|
|
+ hotData: HotDataType | null;
|
|
|
|
|
+ }>();
|
|
|
|
|
+ const emits = defineEmits(['update:visible']);
|
|
|
|
|
+
|
|
|
|
|
+ const route = useRoute();
|
|
|
|
|
+ const isMobile = judgeIsMobile();
|
|
|
|
|
+ const videoRef = ref<HTMLVideoElement | null>(null);
|
|
|
|
|
+ const volumeRef = ref<HTMLAudioElement | null>(null);
|
|
|
|
|
+ // 音频地址
|
|
|
|
|
+ const audio = ref('');
|
|
|
|
|
+ // 如果只有单独的音频
|
|
|
|
|
+ const isOneAduio = ref(false);
|
|
|
|
|
+ // 音频状态
|
|
|
|
|
+ const audioSta = ref(false);
|
|
|
|
|
+ const swiperInited = ref(false);
|
|
|
|
|
+ // 当前 type
|
|
|
|
|
+ const currentType = ref<HOTSPOT_TYPE>(HOTSPOT_TYPE.TEXT);
|
|
|
|
|
+ const isTextType = computed(() => currentType.value === HOTSPOT_TYPE.TEXT);
|
|
|
|
|
+ const data = ref<any>({
|
|
|
|
|
+ // 视频数组
|
|
|
|
|
+ video: [],
|
|
|
|
|
+ // 图片数组
|
|
|
|
|
+ img: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ const myInd = ref(0);
|
|
|
|
|
+ const flooTab = ref<HotspotTabType[]>([]);
|
|
|
|
|
+ const curList = computed(() => data.value[currentType.value] || []);
|
|
|
|
|
+ const show = computed({
|
|
|
|
|
+ get() {
|
|
|
|
|
+ return props.visible;
|
|
|
|
|
+ },
|
|
|
|
|
+ set(v) {
|
|
|
|
|
+ emits('update:visible', v);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ let swiper: null | SwiperClass = null;
|
|
|
|
|
+
|
|
|
|
|
+ const initSwiper = (_swiper) => {
|
|
|
|
|
+ swiper = _swiper;
|
|
|
|
|
+ swiperInited.value = true;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleChange = ({ activeIndex }) => {
|
|
|
|
|
+ myInd.value = activeIndex;
|
|
|
|
|
+
|
|
|
|
|
+ switch (currentType.value) {
|
|
|
|
|
+ case 'video':
|
|
|
|
|
+ handleVideoPlay(data.value.video[activeIndex].src);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let lastVideo: null | HTMLVideoElement = null;
|
|
|
|
|
+ const handleVideoPlay = (url: string) => {
|
|
|
|
|
+ if (!Array.isArray(videoRef.value)) return;
|
|
|
|
|
+
|
|
|
|
|
+ const video = videoRef.value?.find((i) => i.src === url);
|
|
|
|
|
+ lastVideo?.pause();
|
|
|
|
|
+ if (!video) return;
|
|
|
|
|
+ video.play();
|
|
|
|
|
+ lastVideo = video;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleTab = (item: any) => {
|
|
|
|
|
+ myInd.value = 0;
|
|
|
|
|
+ currentType.value = item.type;
|
|
|
|
|
+ swiper?.slideTo(0);
|
|
|
|
|
+
|
|
|
|
|
+ switch (currentType.value) {
|
|
|
|
|
+ case 'video':
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ handleVideoPlay(data.value.video[0].src);
|
|
|
|
|
+ });
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handlePreview = (idx: number) => {
|
|
|
|
|
+ viewerApi({ images: curList.value, options: { initialViewIndex: idx, zIndex: 9999 } });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ watch(audioSta, (val) => {
|
|
|
|
|
+ if (!volumeRef.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (val) {
|
|
|
|
|
+ volumeRef.value.play();
|
|
|
|
|
+ volumeRef.value.onended = () => {
|
|
|
|
|
+ // console.log("----音频播放完毕");
|
|
|
|
|
+ audioSta.value = false;
|
|
|
|
|
+ };
|
|
|
|
|
+ } else volumeRef.value.pause();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.hotData,
|
|
|
|
|
+ (v) => {
|
|
|
|
|
+ if (!v) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (v.bgm) {
|
|
|
|
|
+ audio.value = `${import.meta.env.VITE_APP_BACKEND_URL}/scene_view_data/${
|
|
|
|
|
+ route.query.m
|
|
|
|
|
+ }/user/${v.bgm.src}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 只有单独的音频上传
|
|
|
|
|
+ if (!v.media.length) isOneAduio.value = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 底部的tab
|
|
|
|
|
+ const arr: HotspotTabType[] = [];
|
|
|
|
|
+ const imgs = v?.media.filter((i) => i.type === 'image') ?? [];
|
|
|
|
|
+ const videos = v?.media.filter((i) => i.type === 'video') ?? [];
|
|
|
|
|
+
|
|
|
|
|
+ if (imgs.length) {
|
|
|
|
|
+ arr.push({ type: HOTSPOT_TYPE.IMG, name: '图片', icon: ImageIcon });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (videos.length) {
|
|
|
|
|
+ arr.push({ type: HOTSPOT_TYPE.VIDEO, name: '视频', icon: VideoIcon });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (
|
|
|
|
|
+ !window.navigator.userAgent.match(
|
|
|
|
|
+ /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ audioSta.value = true;
|
|
|
|
|
+ volumeRef.value?.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isMobile) {
|
|
|
|
|
+ arr.push({ type: HOTSPOT_TYPE.TEXT, name: '介绍', icon: infoIcon });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const baseUrl = `${import.meta.env.VITE_APP_BACKEND_URL}/scene_view_data`;
|
|
|
|
|
+ data.value = {
|
|
|
|
|
+ video: videos.map((i) => ({ ...i, src: `${baseUrl}/${route.query.m}/user/${i.src}` })),
|
|
|
|
|
+ img: imgs.map((i) => ({
|
|
|
|
|
+ ...i,
|
|
|
|
|
+ src: `${baseUrl}/${route.query.m}/user/hotspot/${i.sid}/${i.src}`,
|
|
|
|
|
+ })),
|
|
|
|
|
+ };
|
|
|
|
|
+ flooTab.value = arr;
|
|
|
|
|
+ if (imgs.length) currentType.value = HOTSPOT_TYPE.IMG;
|
|
|
|
|
+ else if (videos.length) currentType.value = HOTSPOT_TYPE.VIDEO;
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ immediate: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+ @use './index.scss';
|
|
|
|
|
+</style>
|