Просмотр исходного кода

feat: 湖南省博物馆-南北有二石

chenlei 15 часов назад
Родитель
Сommit
20ab6c1005
86 измененных файлов с 3450 добавлено и 157 удалено
  1. 8 2
      README.md
  2. 4 0
      package.json
  3. BIN
      public/favicon/favicon-nb.ico
  4. BIN
      public/images/nb/VR.png
  5. BIN
      public/images/nb/antlist.png
  6. BIN
      public/images/nb/auto.png
  7. BIN
      public/images/nb/dislike.png
  8. BIN
      public/images/nb/enlarge_on.png
  9. BIN
      public/images/nb/hotlist.png
  10. BIN
      public/images/nb/like.png
  11. BIN
      public/images/nb/model.png
  12. BIN
      public/images/nb/narrow_off.png
  13. BIN
      public/images/nb/pause.png
  14. BIN
      public/images/nb/play.png
  15. BIN
      public/images/nb/share.png
  16. BIN
      public/images/xx/antlist.png
  17. BIN
      public/images/xx/auto.png
  18. BIN
      public/images/xx/chapter.png
  19. BIN
      public/images/xx/dislike.png
  20. BIN
      public/images/xx/enlarge_on.png
  21. BIN
      public/images/xx/hotlist.png
  22. BIN
      public/images/xx/like.png
  23. BIN
      public/images/xx/model.png
  24. BIN
      public/images/xx/narrow_off.png
  25. BIN
      public/images/xx/pause.png
  26. BIN
      public/images/xx/play.png
  27. BIN
      public/images/xx/share.png
  28. BIN
      public/images/xx/simple.png
  29. BIN
      src/hotspot/assets/images/nb/left.png
  30. BIN
      src/hotspot/assets/images/nb/right.png
  31. 4 17
      src/hotspot/views/hotspot/index.df.vue
  32. 385 0
      src/hotspot/views/hotspot/index.nb.vue
  33. 319 0
      src/hotspot/views/hotspot/index.xx.vue
  34. 0 11
      src/hotspot/views/hotspot/index.zb.vue
  35. 7 0
      src/index/assets/el.nb.scss
  36. 7 0
      src/index/assets/el.xx.scss
  37. BIN
      src/index/assets/images/nb/close.png
  38. BIN
      src/index/assets/images/xx/close.png
  39. BIN
      src/index/components/info-popup/images/nb/1.jpg
  40. BIN
      src/index/components/info-popup/images/xx/1.jpg
  41. 10 15
      src/index/components/info-popup/index.df.vue
  42. 152 0
      src/index/components/info-popup/index.nb.vue
  43. 152 0
      src/index/components/info-popup/index.xx.vue
  44. 1 0
      src/index/components/info-popup/index.zb.vue
  45. 21 0
      src/index/router/index.nb.ts
  46. 21 0
      src/index/router/index.xx.ts
  47. BIN
      src/index/views/cover/images/nb/bg.jpg
  48. BIN
      src/index/views/cover/images/nb/btn.png
  49. BIN
      src/index/views/cover/images/nb/mb-1.png
  50. BIN
      src/index/views/cover/images/nb/mb-bg.jpg
  51. BIN
      src/index/views/cover/images/nb/mb-date.png
  52. BIN
      src/index/views/cover/images/nb/mb-title.png
  53. BIN
      src/index/views/cover/images/xx/bg.jpg
  54. BIN
      src/index/views/cover/images/xx/btn.png
  55. BIN
      src/index/views/cover/images/xx/card.jpg
  56. BIN
      src/index/views/cover/images/xx/mb-bg.jpg
  57. BIN
      src/index/views/cover/images/xx/mb-card.jpg
  58. 51 0
      src/index/views/cover/index.nb.scss
  59. 34 0
      src/index/views/cover/index.nb.vue
  60. 49 0
      src/index/views/cover/index.xx.scss
  61. 35 0
      src/index/views/cover/index.xx.vue
  62. BIN
      src/index/views/home/components/ant-popup/images/nb/card-bg.png
  63. BIN
      src/index/views/home/components/ant-popup/images/nb/title.png
  64. BIN
      src/index/views/home/components/ant-popup/images/xx/input-bg.png
  65. BIN
      src/index/views/home/components/ant-popup/images/xx/reset.png
  66. BIN
      src/index/views/home/components/ant-popup/images/xx/search.png
  67. 1 1
      src/index/views/home/components/ant-popup/index.df.vue
  68. 261 0
      src/index/views/home/components/ant-popup/index.nb.vue
  69. 273 0
      src/index/views/home/components/ant-popup/index.xx.vue
  70. 204 0
      src/index/views/home/components/menu/index.nb.scss
  71. 256 0
      src/index/views/home/components/menu/index.nb.tsx
  72. 191 0
      src/index/views/home/components/menu/index.xx.scss
  73. 266 0
      src/index/views/home/components/menu/index.xx.tsx
  74. 46 0
      src/index/views/home/components/popup/index.nb.scss
  75. 16 0
      src/index/views/home/components/popup/index.nb.tsx
  76. 46 0
      src/index/views/home/components/popup/index.xx.scss
  77. 16 0
      src/index/views/home/components/popup/index.xx.tsx
  78. BIN
      src/index/views/home/images/nb/chapter.png
  79. BIN
      src/index/views/home/images/nb/popup-bg.png
  80. BIN
      src/index/views/home/images/nb/viewer.png
  81. BIN
      src/index/views/home/images/xx/viewer.png
  82. 138 0
      src/index/views/home/index.nb.scss
  83. 169 0
      src/index/views/home/index.nb.tsx
  84. 138 0
      src/index/views/home/index.xx.scss
  85. 169 0
      src/index/views/home/index.xx.tsx
  86. 0 111
      vite.config.ts.timestamp-1772251949654-9b6c30f4c4e0b.mjs

+ 8 - 2
README.md

@@ -1,6 +1,12 @@
-## 湖南省博物馆 - 遇见庞贝展
+## 湖南省博物馆
 
-SG-MKBSii25xMt
+湖南省博物馆-遇见庞贝展: SG-MKBSii25xMt
+
+湖南省博物馆-潇湘入海是通津:SG-M7XjDmSivmY
+
+湖南省博物馆-东方微笑:SG-SbTrgraKOyr
+
+湖南省博物馆-南北有二石:SG-wJztZUuoihV
 
 ### 初始化
 

+ 4 - 0
package.json

@@ -14,6 +14,10 @@
     "build:zb:test": "cross-env VITE_APP_SCENE=zb VITE_APP_TITLE=湖南省博物馆-书于竹帛展 VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
     "serve:df": "cross-env VITE_APP_SCENE=df VITE_APP_TITLE=湖南省博物馆-东方微笑 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
     "build:df:test": "cross-env VITE_APP_SCENE=df VITE_APP_TITLE=湖南省博物馆-东方微笑 VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
+    "serve:xx": "cross-env VITE_APP_SCENE=xx VITE_APP_TITLE=湖南省博物馆-潇湘入海是通津 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
+    "build:xx:test": "cross-env VITE_APP_SCENE=xx VITE_APP_TITLE=湖南省博物馆-潇湘入海是通津 VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
+    "serve:nb": "cross-env VITE_APP_SCENE=nb VITE_APP_TITLE=湖南省博物馆-南北有二石 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
+    "build:nb:test": "cross-env VITE_APP_SCENE=nb VITE_APP_TITLE=湖南省博物馆-南北有二石 VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
     "type-check": "vue-tsc --build --force"

BIN
public/favicon/favicon-nb.ico


BIN
public/images/nb/VR.png


BIN
public/images/nb/antlist.png


BIN
public/images/nb/auto.png


BIN
public/images/nb/dislike.png


BIN
public/images/nb/enlarge_on.png


BIN
public/images/nb/hotlist.png


BIN
public/images/nb/like.png


BIN
public/images/nb/model.png


BIN
public/images/nb/narrow_off.png


BIN
public/images/nb/pause.png


BIN
public/images/nb/play.png


BIN
public/images/nb/share.png


BIN
public/images/xx/antlist.png


BIN
public/images/xx/auto.png


BIN
public/images/xx/chapter.png


BIN
public/images/xx/dislike.png


BIN
public/images/xx/enlarge_on.png


BIN
public/images/xx/hotlist.png


BIN
public/images/xx/like.png


BIN
public/images/xx/model.png


BIN
public/images/xx/narrow_off.png


BIN
public/images/xx/pause.png


BIN
public/images/xx/play.png


BIN
public/images/xx/share.png


BIN
public/images/xx/simple.png


BIN
src/hotspot/assets/images/nb/left.png


BIN
src/hotspot/assets/images/nb/right.png


+ 4 - 17
src/hotspot/views/hotspot/index.df.vue

@@ -299,7 +299,7 @@
 
       > div {
         font-size: utils.vh-calc(16);
-        line-height: utils.vh-calc(30);
+        line-height: utils.vh-calc(34);
       }
     }
   }
@@ -310,28 +310,15 @@
       gap: utils.vh-calc(60);
 
       h3 {
-        position: relative;
         margin: utils.vh-calc(40) 0 utils.vh-calc(40) utils.vh-calc(113);
-        font-size: utils.vh-calc(48);
-        text-align: center;
-
-        &::after {
-          content: '';
-          position: absolute;
-          bottom: utils.vh-calc(-14);
-          left: 50%;
-          transform: translateX(-50%);
-          width: utils.vw-calc(353);
-          height: utils.vw-calc(9);
-          background: url('@hotspot/assets/images/zb/bd.png') no-repeat center / contain;
-        }
+        font-size: utils.vh-calc(32);
       }
       &-media {
         margin: utils.vh-calc(100) 0 0;
         padding: 0 utils.vw-calc(45);
         max-width: 100%;
         width: 100%;
-        height: utils.vh-calc(674);
+        height: utils.vh-calc(600);
       }
       &-wrap {
         flex: 1;
@@ -349,7 +336,7 @@
         }
         > div {
           font-size: utils.vh-calc(28);
-          line-height: utils.vh-calc(42);
+          line-height: utils.vh-calc(50);
         }
       }
     }

+ 385 - 0
src/hotspot/views/hotspot/index.nb.vue

@@ -0,0 +1,385 @@
+<template>
+  <div class="hotspot-page detail-popup">
+    <div class="detail-popup-media-wrap">
+      <button
+        v-if="showMediaNav"
+        type="button"
+        class="detail-popup-media-nav detail-popup-media-nav--prev"
+        aria-label="上一张"
+        @click="slidePrev"
+      >
+        <img :src="navLeftImg" alt="" />
+      </button>
+      <Swiper class="detail-popup-media" @swiper="initSwiper" @slideChange="handleChange">
+        <SwiperSlide v-for="(item, index) in curList" :key="item.url">
+          <template v-if="swiperInited">
+            <template v-if="myType === 'model'">
+              <iframe v-if="index === myInd" :src="item" frameborder="0" />
+            </template>
+            <template v-else-if="myType === 'video'">
+              <video ref="videos" controls :src="item.url" />
+            </template>
+            <template v-else-if="myType === 'img'">
+              <el-image :src="item" fit="contain" @click="handlePreview(index)" />
+            </template>
+          </template>
+        </SwiperSlide>
+      </Swiper>
+      <button
+        v-if="showMediaNav"
+        type="button"
+        class="detail-popup-media-nav detail-popup-media-nav--next"
+        aria-label="下一张"
+        @click="slideNext"
+      >
+        <img :src="navRightImg" alt="" />
+      </button>
+    </div>
+
+    <div class="detail-popup-wrap">
+      <div>
+        <h3>{{ myTitle }}</h3>
+      </div>
+      <el-scrollbar class="detail-popup-container">
+        <div v-html="myTxt" />
+      </el-scrollbar>
+    </div>
+
+    <!-- 音频播放器 -->
+    <audio
+      id="myAudio"
+      v-if="audio"
+      ref="volumeRef"
+      v-show="isOneAduio"
+      :src="audio"
+      controls
+    ></audio>
+  </div>
+</template>
+
+<script>
+  import { Swiper, SwiperSlide } from 'swiper/vue';
+  import 'swiper/css';
+
+  import { parseUrlParams, judgeIsMobile } from '@/utils';
+  import { MESSAGE_KEY } from '@/types';
+
+  import navLeftImg from '../../assets/images/nb/left.png';
+  import navRightImg from '../../assets/images/nb/right.png';
+  import ModelIcon from '@hotspot/assets/images/model-icon.png';
+  import ImageIcon from '@hotspot/assets/images/img-icon.png';
+  import VideoIcon from '@hotspot/assets/images/video-icon.png';
+  import VolumeOn from '@hotspot/assets/images/audio-icon.png';
+  import infoIcon from '@hotspot/assets/images/info-icon.png';
+
+  const urlParams = parseUrlParams(window.location.href);
+  const isMobile = judgeIsMobile();
+
+  export default {
+    name: 'hotspot',
+    components: {
+      Swiper,
+      SwiperSlide,
+    },
+    data() {
+      return {
+        isMobile,
+        VolumeOn,
+        m: urlParams.m,
+        id: urlParams.id,
+        // 音频地址
+        audio: '',
+        // 如果只有单独的音频
+        isOneAduio: false,
+        // 音频状态
+        audioSta: false,
+        swiperInited: false,
+
+        data: {
+          // 模型数组
+          model: [],
+          // 视频数组
+          video: [],
+          // 图片数组
+          img: [],
+        },
+        // 当前 type
+        myType: '',
+
+        // 当前索引
+        myInd: 0,
+
+        // 底部的tab
+        flooTab: [],
+
+        // 标题
+        myTitle: '',
+        // 内容
+        myTxt: '',
+        // 视频内容
+        videoTxt: [],
+        imgTxt: [],
+
+        // 只有标题和文字(没有视频,没有模型,没有图片)
+        oneTxt: false,
+
+        navLeftImg,
+        navRightImg,
+      };
+    },
+    computed: {
+      curList() {
+        return this.data[this.myType] || [];
+      },
+      showMediaNav() {
+        return this.curList.length > 1;
+      },
+      isTextType() {
+        return this.myType === 'text';
+      },
+    },
+    watch: {
+      audioSta(val) {
+        if (!this.$refs.volumeRef) return;
+        if (val) {
+          this.$refs.volumeRef.play();
+          this.$refs.volumeRef.onended = () => {
+            // console.log("----音频播放完毕");
+            this.audioSta = false;
+          };
+        } else this.$refs.volumeRef.pause();
+      },
+    },
+    mounted() {
+      this.getData();
+    },
+    methods: {
+      async getData() {
+        // https://www.4dmodel.com/
+        let url = `${
+          Boolean(Number(import.meta.env.VITE_APP_OFFLINE)) ? '.' : 'https://super.4dage.com'
+        }/data/${this.id}/hot/js/data.js?time=${Math.random()}`;
+        let result = await fetch(url).then((response) => response.json());
+        const resData = result[this.m];
+
+        if (resData) {
+          this.audio = resData.backgroundMusic;
+          // 只有单独的音频上传
+          if (resData.backgroundMusic && !resData.model && !resData.video && !resData.images) {
+            this.isOneAduio = true;
+          }
+          // 底部的tab
+          const arr = [];
+          const obj = {};
+          if (resData.images) {
+            obj.img = resData.images;
+            arr.push({ id: 3, type: 'img', name: '图片', icon: ImageIcon });
+          }
+          if (resData.video) {
+            obj.video = resData.video;
+            arr.push({ id: 2, type: 'video', name: '视频', icon: VideoIcon });
+          } else {
+            this.$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
+                )
+              ) {
+                this.audioSta = true;
+                this.$refs.volumeRef?.play();
+              }
+            });
+          }
+          if (resData.model) {
+            obj.model = resData.model;
+            arr.push({ id: 1, type: 'model', name: '模型', icon: ModelIcon });
+          }
+          if (isMobile) {
+            arr.push({ id: 4, type: 'text', name: '介绍', icon: infoIcon });
+          }
+
+          this.flooTab = arr;
+          this.data = obj;
+
+          // 当前type的值 应该为
+          if (resData.images) this.myType = 'img';
+          else if (resData.model) this.myType = 'model';
+          else if (resData.video) {
+            this.myType = 'video';
+            this.$nextTick(() => {
+              this.handleVideoPlay(this.data.video[0].url);
+            });
+          } else this.myType = 'text';
+
+          this.myTitle = resData.title || '';
+          this.myTxt = resData.content || '';
+          this.videoTxt = resData.videosDesc || [];
+          this.imgTxt = resData.imagesDesc || [];
+
+          // 只有 标题和 文字介绍(没有视频,没有模型,没有图片)
+          if (!obj.model && !obj.video && !obj.img && !resData.backgroundMusic) {
+            this.oneTxt = true;
+          }
+        }
+      },
+
+      handleTab(item) {
+        this.myInd = 0;
+        this.myType = item.type;
+        this.swiper?.slideTo(0);
+
+        switch (this.myType) {
+          case 'video':
+            this.$nextTick(() => {
+              this.handleVideoPlay(this.data.video[0].url);
+            });
+            break;
+        }
+      },
+
+      initSwiper(swiper) {
+        this.swiper = swiper;
+        this.swiperInited = true;
+      },
+      slidePrev() {
+        this.swiper?.slidePrev();
+      },
+      slideNext() {
+        this.swiper?.slideNext();
+      },
+      handleChange({ activeIndex }) {
+        this.myInd = activeIndex;
+
+        switch (this.myType) {
+          case 'video':
+            this.handleVideoPlay(this.data.video[activeIndex].url);
+            break;
+        }
+      },
+      handleVideoPlay(url) {
+        const video = this.$refs.videos?.find((i) => i.src === url);
+
+        this.lastVideo?.pause();
+        if (!video) return;
+
+        video.play();
+        this.lastVideo = video;
+      },
+      handlePreview(idx) {
+        window.parent.postMessage(
+          { type: MESSAGE_KEY.SHOW_VIEWER, images: [this.curList[idx]] },
+          '*'
+        );
+      },
+    },
+  };
+</script>
+
+<style lang="scss">
+  @use '@/assets/utils.scss';
+
+  .detail-popup {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: utils.vw-calc(120);
+    color: #ffffff;
+
+    &-media-wrap {
+      display: flex;
+      align-items: center;
+      gap: 50px;
+      flex-shrink: 0;
+    }
+    &-media-nav {
+      flex-shrink: 0;
+      padding: 0;
+      border: none;
+      background: transparent;
+      cursor: pointer;
+      line-height: 0;
+      img {
+        display: block;
+        width: utils.vw-calc(32);
+        height: auto;
+      }
+    }
+    &-media {
+      margin: unset !important;
+      flex: 1;
+      min-width: 0;
+      max-width: utils.vw-calc(600);
+      height: utils.vh-calc(620);
+    }
+    .el-image,
+    video {
+      width: 100%;
+      height: 100%;
+    }
+    video {
+      background: black;
+    }
+    h3 {
+      margin-bottom: utils.vh-calc(55);
+      font-size: utils.vh-calc(26);
+      font-weight: normal;
+      text-shadow: 0 0 5px #b3986d;
+    }
+    &-wrap {
+      flex-shrink: 0;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      height: utils.vh-calc(580);
+    }
+    &-container {
+      width: utils.vw-calc(640);
+      height: auto !important;
+
+      > div {
+        font-size: utils.vh-calc(18);
+        line-height: utils.vh-calc(34);
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .detail-popup {
+      flex-direction: column;
+      gap: utils.vh-calc(80);
+
+      &-media-wrap {
+        margin-top: utils.vh-calc(80);
+        padding: 0 utils.vw-calc(50);
+        gap: utils.vw-calc(40);
+        width: 100%;
+      }
+      &-media {
+        max-width: 100%;
+        width: utils.vw-calc(520);
+        height: utils.vh-calc(600);
+      }
+      &-wrap {
+        flex: 1;
+        height: 0 !important;
+      }
+      h3 {
+        padding: 0 utils.vw-calc(45);
+        font-size: utils.vh-calc(32);
+        line-height: utils.vh-calc(40);
+      }
+      &-container {
+        flex: 1;
+        height: 0 !important;
+        width: 100%;
+        padding: 0 utils.vw-calc(45) utils.vh-calc(200);
+
+        > div {
+          line-height: utils.vh-calc(60);
+          font-size: utils.vh-calc(28);
+        }
+      }
+    }
+  }
+</style>

+ 319 - 0
src/hotspot/views/hotspot/index.xx.vue

@@ -0,0 +1,319 @@
+<template>
+  <div class="hotspot-page detail-popup">
+    <div v-if="curList.length" class="detail-popup-media">
+      <el-image v-if="myType === 'img'" fit="contain" :src="curList[0]" @click="handlePreview(0)" />
+
+      <video v-else-if="myType === 'video'" ref="videos" controls :src="curList[0].url" />
+    </div>
+
+    <div class="detail-popup-wrap">
+      <div>
+        <h3>{{ myTitle }}</h3>
+      </div>
+      <el-scrollbar class="detail-popup-container">
+        <div v-html="myTxt" />
+      </el-scrollbar>
+    </div>
+
+    <!-- 音频播放器 -->
+    <audio
+      id="myAudio"
+      v-if="audio"
+      ref="volumeRef"
+      v-show="isOneAduio"
+      :src="audio"
+      controls
+    ></audio>
+  </div>
+</template>
+
+<script>
+  import { Swiper, SwiperSlide } from 'swiper/vue';
+  import { Navigation, Pagination } from 'swiper/modules';
+  import 'swiper/css';
+  import 'swiper/css/navigation';
+  import 'swiper/css/pagination';
+
+  import { parseUrlParams, judgeIsMobile } from '@/utils';
+  import { MESSAGE_KEY } from '@/types';
+
+  import ModelIcon from '@hotspot/assets/images/model-icon.png';
+  import ImageIcon from '@hotspot/assets/images/img-icon.png';
+  import VideoIcon from '@hotspot/assets/images/video-icon.png';
+  import VolumeOn from '@hotspot/assets/images/audio-icon.png';
+  import infoIcon from '@hotspot/assets/images/info-icon.png';
+
+  const urlParams = parseUrlParams(window.location.href);
+  const isMobile = judgeIsMobile();
+
+  export default {
+    name: 'hotspot',
+    components: {
+      Swiper,
+      SwiperSlide,
+    },
+    data() {
+      return {
+        isMobile,
+        VolumeOn,
+        m: urlParams.m,
+        id: urlParams.id,
+        // 音频地址
+        audio: '',
+        // 如果只有单独的音频
+        isOneAduio: false,
+        // 音频状态
+        audioSta: false,
+        swiperInited: false,
+
+        data: {
+          // 模型数组
+          model: [],
+          // 视频数组
+          video: [],
+          // 图片数组
+          img: [],
+        },
+        // 当前 type
+        myType: '',
+
+        // 当前索引
+        myInd: 0,
+
+        // 底部的tab
+        flooTab: [],
+
+        // 标题
+        myTitle: '',
+        // 内容
+        myTxt: '',
+        // 视频内容
+        videoTxt: [],
+        imgTxt: [],
+
+        // 只有标题和文字(没有视频,没有模型,没有图片)
+        oneTxt: false,
+
+        modules: [Navigation, Pagination],
+      };
+    },
+    computed: {
+      curList() {
+        return this.data[this.myType] || [];
+      },
+      isTextType() {
+        return this.myType === 'text';
+      },
+    },
+    watch: {
+      audioSta(val) {
+        if (!this.$refs.volumeRef) return;
+        if (val) {
+          this.$refs.volumeRef.play();
+          this.$refs.volumeRef.onended = () => {
+            // console.log("----音频播放完毕");
+            this.audioSta = false;
+          };
+        } else this.$refs.volumeRef.pause();
+      },
+    },
+    mounted() {
+      this.getData();
+    },
+    methods: {
+      async getData() {
+        // https://www.4dmodel.com/
+        let url = `${
+          Boolean(Number(import.meta.env.VITE_APP_OFFLINE)) ? '.' : 'https://super.4dage.com'
+        }/data/${this.id}/hot/js/data.js?time=${Math.random()}`;
+        let result = await fetch(url).then((response) => response.json());
+        const resData = result[this.m];
+
+        if (resData) {
+          this.audio = resData.backgroundMusic;
+          // 只有单独的音频上传
+          if (resData.backgroundMusic && !resData.model && !resData.video && !resData.images) {
+            this.isOneAduio = true;
+          }
+          // 底部的tab
+          const arr = [];
+          const obj = {};
+          if (resData.images) {
+            obj.img = resData.images;
+            arr.push({ id: 3, type: 'img', name: '图片', icon: ImageIcon });
+          }
+          if (resData.video) {
+            obj.video = resData.video;
+            arr.push({ id: 2, type: 'video', name: '视频', icon: VideoIcon });
+          } else {
+            this.$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
+                )
+              ) {
+                this.audioSta = true;
+                this.$refs.volumeRef?.play();
+              }
+            });
+          }
+          if (resData.model) {
+            obj.model = resData.model;
+            arr.push({ id: 1, type: 'model', name: '模型', icon: ModelIcon });
+          }
+          if (isMobile) {
+            arr.push({ id: 4, type: 'text', name: '介绍', icon: infoIcon });
+          }
+
+          this.flooTab = arr;
+          this.data = obj;
+
+          // 当前type的值 应该为
+          if (resData.images) this.myType = 'img';
+          else if (resData.model) this.myType = 'model';
+          else if (resData.video) {
+            this.myType = 'video';
+            this.$nextTick(() => {
+              this.handleVideoPlay(this.data.video[0].url);
+            });
+          } else this.myType = 'text';
+
+          this.myTitle = resData.title || '';
+          this.myTxt = resData.content || '';
+          this.videoTxt = resData.videosDesc || [];
+          this.imgTxt = resData.imagesDesc || [];
+
+          // 只有 标题和 文字介绍(没有视频,没有模型,没有图片)
+          if (!obj.model && !obj.video && !obj.img && !resData.backgroundMusic) {
+            this.oneTxt = true;
+          }
+        }
+      },
+
+      handleTab(item) {
+        this.myInd = 0;
+        this.myType = item.type;
+        this.swiper?.slideTo(0);
+
+        switch (this.myType) {
+          case 'video':
+            this.$nextTick(() => {
+              this.handleVideoPlay(this.data.video[0].url);
+            });
+            break;
+        }
+      },
+
+      initSwiper(swiper) {
+        this.swiper = swiper;
+        this.swiperInited = true;
+      },
+      handleChange({ activeIndex }) {
+        this.myInd = activeIndex;
+
+        switch (this.myType) {
+          case 'video':
+            this.handleVideoPlay(this.data.video[activeIndex].url);
+            break;
+        }
+      },
+      handleVideoPlay(url) {
+        const video = this.$refs.videos?.find((i) => i.src === url);
+
+        this.lastVideo?.pause();
+        if (!video) return;
+
+        video.play();
+        this.lastVideo = video;
+      },
+      handlePreview(idx) {
+        window.parent.postMessage(
+          { type: MESSAGE_KEY.SHOW_VIEWER, images: [this.curList[idx]] },
+          '*'
+        );
+      },
+    },
+  };
+</script>
+
+<style lang="scss">
+  @use '@/assets/utils.scss';
+
+  .detail-popup {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: utils.vw-calc(200);
+    color: #e9decc;
+
+    &-media {
+      max-width: utils.vw-calc(1000);
+      height: utils.vh-calc(620);
+    }
+    .el-image,
+    video {
+      width: 100%;
+      height: 100%;
+    }
+    video {
+      background: black;
+    }
+    h3 {
+      text-align: center;
+      margin-bottom: utils.vh-calc(70);
+      font-size: utils.vh-calc(30);
+      font-weight: normal;
+    }
+    &-wrap {
+      flex-shrink: 0;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      height: utils.vh-calc(580);
+    }
+    &-container {
+      width: utils.vw-calc(640);
+      height: auto !important;
+
+      > div {
+        font-size: utils.vh-calc(18);
+        line-height: utils.vh-calc(34);
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .detail-popup {
+      flex-direction: column;
+      gap: utils.vh-calc(150);
+
+      &-media {
+        margin: utils.vh-calc(100) 0 0;
+        padding: 0 utils.vw-calc(45);
+        max-width: 100%;
+        width: 100%;
+        height: utils.vh-calc(600);
+      }
+      &-wrap {
+        flex: 1;
+        height: 0 !important;
+      }
+      h3 {
+        font-size: utils.vh-calc(36);
+        line-height: utils.vh-calc(40);
+      }
+      &-container {
+        flex: 1;
+        height: 0 !important;
+        width: 100%;
+        padding: 0 utils.vw-calc(45) utils.vh-calc(200);
+
+        > div {
+          font-size: utils.vh-calc(28);
+          line-height: utils.vh-calc(50);
+        }
+      }
+    }
+  }
+</style>

+ 0 - 11
src/hotspot/views/hotspot/index.zb.vue

@@ -298,17 +298,6 @@
         font-size: utils.vh-calc(48);
         text-align: center;
         color: #e7a770;
-
-        &::after {
-          content: '';
-          position: absolute;
-          bottom: utils.vh-calc(-14);
-          left: 50%;
-          transform: translateX(-50%);
-          width: utils.vw-calc(353);
-          height: utils.vw-calc(9);
-          background: url('@hotspot/assets/images/zb/bd.png') no-repeat center / contain;
-        }
       }
       &-media {
         margin: 0 0 utils.vh-calc(24);

+ 7 - 0
src/index/assets/el.nb.scss

@@ -0,0 +1,7 @@
+@forward 'element-plus/theme-chalk/src/common/var.scss' with (
+  $colors: (
+    'primary': (
+      'base': #000000,
+    ),
+  )
+);

+ 7 - 0
src/index/assets/el.xx.scss

@@ -0,0 +1,7 @@
+@forward 'element-plus/theme-chalk/src/common/var.scss' with (
+  $colors: (
+    'primary': (
+      'base': #426f4b,
+    ),
+  )
+);

BIN
src/index/assets/images/nb/close.png


BIN
src/index/assets/images/xx/close.png


BIN
src/index/components/info-popup/images/nb/1.jpg


BIN
src/index/components/info-popup/images/xx/1.jpg


+ 10 - 15
src/index/components/info-popup/index.df.vue

@@ -13,10 +13,10 @@
     <div class="info-popup-container">
       <h3 />
       <div class="info-popup-contact">
-        <p>展览时间:2025.11.21-2026.05.24</p>
+        <p>展览时间:2025.11.21-2026.5.24</p>
         <p>展览城市:湖南-长沙</p>
-        <p>展览地点:湖南博物馆一楼特展一厅</p>
-        <p>主办单位:湖南博物馆、青州市博物馆、诸城博物馆、山东博物馆</p>
+        <p>展览地点:湖南博物馆一楼特展一厅</p>
+        <p>主办单位:湖南博物馆、青州市博物馆、诸城博物馆、山东博物馆</p>
       </div>
       <el-scrollbar class="info-popup-info">
         <p>
@@ -31,7 +31,7 @@
         <p>
           来自青州市博物馆、诸城市博物馆、山东博物馆等机构的每一尊造像皆是青州风格的代言和千年时光见证者:它们曾伫立于古青州寺院的金色晨光中,历经朝代更迭、战火沧桑,最终以或残缺,或完好的姿态与我们相遇。让我们走进展览,共同开启一场东方美的发现之旅。
         </p>
-        <p style="text-indent: 2em">
+        <p style="text-align: right">
           <b>——谨以此展,致敬永恒的东方微笑,致敬艺术永恒的生命力与治愈力。</b>
         </p>
       </el-scrollbar>
@@ -92,6 +92,7 @@
       flex: 1;
       color: #ffeac7;
       font-size: utils.vh-calc(18);
+      line-height: utils.vh-calc(40);
 
       h3 {
         margin: 0 auto;
@@ -101,17 +102,12 @@
       }
     }
     &-contact {
-      margin: utils.vh-calc(40) 0;
-      line-height: utils.vh-calc(30);
+      margin: utils.vh-calc(30) 0;
     }
     &-info {
-      line-height: utils.vh-calc(28);
       padding-right: 20px;
-      height: utils.vh-calc(380);
-
-      p {
-        margin-bottom: utils.vh-calc(30);
-      }
+      height: utils.vh-calc(380) !important;
+      text-indent: 2em;
     }
     &-img {
       flex-shrink: 0;
@@ -141,6 +137,7 @@
         height: 0;
         padding: 0 utils.vh-calc(40);
         font-size: utils.vh-calc(28);
+        line-height: utils.vh-calc(60);
 
         h3 {
           width: utils.vh-calc(600);
@@ -149,13 +146,11 @@
       }
       &-contact {
         padding: 0 utils.vw-calc(10);
-        line-height: utils.vh-calc(50);
       }
       &-info {
         flex: 1;
-        height: 0;
+        height: 0 !important;
         padding: 0 utils.vw-calc(10);
-        line-height: utils.vh-calc(50);
       }
       &-img {
         width: 100%;

Разница между файлами не показана из-за своего большого размера
+ 152 - 0
src/index/components/info-popup/index.nb.vue


Разница между файлами не показана из-за своего большого размера
+ 152 - 0
src/index/components/info-popup/index.xx.vue


+ 1 - 0
src/index/components/info-popup/index.zb.vue

@@ -103,6 +103,7 @@
       line-height: utils.vh-calc(28);
       padding-right: 20px;
       height: utils.vh-calc(380);
+      text-indent: 2em;
 
       p {
         margin-bottom: utils.vh-calc(30);

+ 21 - 0
src/index/router/index.nb.ts

@@ -0,0 +1,21 @@
+import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'cover',
+    component: () => import('@/views/cover/index.nb.vue'),
+  },
+  {
+    path: '/scene',
+    name: 'home',
+    component: () => import('@/views/home'),
+  },
+];
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes,
+});
+
+export default router;

+ 21 - 0
src/index/router/index.xx.ts

@@ -0,0 +1,21 @@
+import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'cover',
+    component: () => import('@/views/cover/index.xx.vue'),
+  },
+  {
+    path: '/scene',
+    name: 'home',
+    component: () => import('@/views/home'),
+  },
+];
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes,
+});
+
+export default router;

BIN
src/index/views/cover/images/nb/bg.jpg


BIN
src/index/views/cover/images/nb/btn.png


BIN
src/index/views/cover/images/nb/mb-1.png


BIN
src/index/views/cover/images/nb/mb-bg.jpg


BIN
src/index/views/cover/images/nb/mb-date.png


BIN
src/index/views/cover/images/nb/mb-title.png


BIN
src/index/views/cover/images/xx/bg.jpg


BIN
src/index/views/cover/images/xx/btn.png


BIN
src/index/views/cover/images/xx/card.jpg


BIN
src/index/views/cover/images/xx/mb-bg.jpg


BIN
src/index/views/cover/images/xx/mb-card.jpg


+ 51 - 0
src/index/views/cover/index.nb.scss

@@ -0,0 +1,51 @@
+@use '@/assets/utils.scss';
+
+.cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/nb/bg.jpg') no-repeat center / 100% 100%;
+
+  &__btn {
+    position: absolute;
+    left: utils.vw-calc(505);
+    bottom: utils.vw-calc(276);
+    width: utils.vw-calc(67);
+    height: utils.vw-calc(239);
+    cursor: pointer;
+  }
+}
+
+.m-cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/nb/mb-bg.jpg') no-repeat top center / cover;
+
+  &__title {
+    position: absolute;
+    top: utils.vh-calc(253);
+    right: utils.vw-calc(105);
+    width: utils.vh-calc(426);
+    height: utils.vh-calc(867);
+  }
+  &__date {
+    position: absolute;
+    left: utils.vw-calc(63);
+    bottom: utils.vh-calc(190);
+    width: utils.vh-calc(113);
+    height: utils.vh-calc(197);
+  }
+  &__img {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: utils.vw-calc(750);
+    height: utils.vw-calc(179);
+  }
+  &__btn {
+    position: absolute;
+    left: utils.vw-calc(175);
+    bottom: utils.vh-calc(420);
+    width: utils.vh-calc(67);
+    height: utils.vh-calc(239);
+  }
+}

+ 34 - 0
src/index/views/cover/index.nb.vue

@@ -0,0 +1,34 @@
+<template>
+  <div :class="isMobile ? 'm-cover' : 'cover'">
+    <template v-if="!isMobile">
+      <img
+        class="cover__btn"
+        src="./images/nb/btn.png"
+        draggable="false"
+        @click="$router.push({ name: 'home' })"
+      />
+    </template>
+
+    <template v-else>
+      <img class="m-cover__title" src="./images/nb/mb-title.png" draggable="false" />
+      <img class="m-cover__date" src="./images/nb/mb-date.png" draggable="false" />
+      <img class="m-cover__img" src="./images/nb/mb-1.png" draggable="false" />
+      <img
+        class="m-cover__btn"
+        src="./images/nb/btn.png"
+        draggable="false"
+        @click="$router.push({ name: 'home' })"
+      />
+    </template>
+  </div>
+</template>
+
+<script setup>
+  import { judgeIsMobile } from '@/utils';
+
+  const isMobile = judgeIsMobile();
+</script>
+
+<style lang="scss" scoped>
+  @use './index.nb.scss';
+</style>

+ 49 - 0
src/index/views/cover/index.xx.scss

@@ -0,0 +1,49 @@
+@use '@/assets/utils.scss';
+
+.cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/xx/bg.jpg') no-repeat center bottom / 100% 100%;
+
+  &__info {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: utils.vw-calc(1583);
+    height: utils.vw-calc(824);
+    transform: translate(-50%, -50%);
+    background: url('./images/xx/card.jpg') no-repeat center / contain;
+  }
+  &__btn {
+    position: absolute;
+    right: utils.vw-calc(583);
+    bottom: utils.vw-calc(64);
+    width: utils.vw-calc(240);
+    height: utils.vw-calc(60);
+    cursor: pointer;
+  }
+}
+
+.m-cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/xx/mb-bg.jpg') no-repeat center top / 100% 100%;
+
+  &__info {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: utils.vh-calc(630);
+    height: utils.vh-calc(1504);
+    transform: translate(-50%, -50%);
+    background: url('./images/xx/mb-card.jpg') no-repeat center / contain;
+  }
+  &__btn {
+    position: absolute;
+    left: 50%;
+    bottom: utils.vh-calc(91);
+    width: utils.vh-calc(240);
+    height: utils.vh-calc(60);
+    transform: translateX(-50%);
+  }
+}

+ 35 - 0
src/index/views/cover/index.xx.vue

@@ -0,0 +1,35 @@
+<template>
+  <div :class="isMobile ? 'm-cover' : 'cover'">
+    <template v-if="!isMobile">
+      <div class="cover__info">
+        <img
+          class="cover__btn"
+          src="./images/xx/btn.png"
+          draggable="false"
+          @click="$router.push({ name: 'home' })"
+        />
+      </div>
+    </template>
+
+    <template v-else>
+      <div class="m-cover__info">
+        <img
+          class="m-cover__btn"
+          src="./images/xx/btn.png"
+          draggable="false"
+          @click="$router.push({ name: 'home' })"
+        />
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+  import { judgeIsMobile } from '@/utils';
+
+  const isMobile = judgeIsMobile();
+</script>
+
+<style lang="scss" scoped>
+  @use './index.xx.scss';
+</style>

BIN
src/index/views/home/components/ant-popup/images/nb/card-bg.png


BIN
src/index/views/home/components/ant-popup/images/nb/title.png


BIN
src/index/views/home/components/ant-popup/images/xx/input-bg.png


BIN
src/index/views/home/components/ant-popup/images/xx/reset.png


BIN
src/index/views/home/components/ant-popup/images/xx/search.png


+ 1 - 1
src/index/views/home/components/ant-popup/index.df.vue

@@ -178,7 +178,7 @@
         --el-input-bg-color: rgba(242, 220, 180, 0.4);
         --el-input-border-radius: 0;
         --el-border-color: transparent;
-        --el-input-text-color: transparent;
+        --el-input-text-color: #f2dcb4;
         --el-input-placeholder-color: #f2dcb4;
 
         flex: 1;

+ 261 - 0
src/index/views/home/components/ant-popup/index.nb.vue

@@ -0,0 +1,261 @@
+<template>
+  <el-dialog
+    class="ant-popup"
+    v-model="show"
+    append-to-body
+    destroy-on-close
+    :close-on-click-modal="false"
+    :show-close="false"
+  >
+    <img class="title" src="./images/nb/title.png" />
+    <div class="close" @click="show = false" />
+
+    <div class="ant-popup-search">
+      <el-input
+        v-model="query"
+        placeholder="请输入展品名称"
+        @keydown.stop
+        @keyup.stop
+        @keyup.enter="search"
+      />
+
+      <div class="search btn" @click="search">搜索</div>
+      <div class="reset btn" @click="reset">重置</div>
+    </div>
+
+    <el-scrollbar class="ant-popup-scrollbar">
+      <ul>
+        <template v-for="item in filteredList" :key="item.id">
+          <li v-if="item.info.images.length > 0" @click="handleChecked(item)">
+            <el-image :src="item.info.images[0]" fit="contain" />
+            <p class="limit-line">{{ item.info.title }}</p>
+          </li>
+        </template>
+      </ul>
+    </el-scrollbar>
+  </el-dialog>
+
+  <!-- <DetailPopup v-model:visible="detailVisible" :checkedItem="checkedItem" /> -->
+</template>
+
+<script setup lang="ts">
+  import { computed, ref, watch } from 'vue';
+  // import { ANT_LIST } from './constants';
+  // import DetailPopup from './detail.vue';
+
+  const emits = defineEmits(['update:visible']);
+  const props = defineProps<{
+    visible: boolean;
+  }>();
+
+  const show = computed({
+    get() {
+      return props.visible;
+    },
+    set(v) {
+      emits('update:visible', v);
+    },
+  });
+
+  const query = ref('');
+  // const checkedItem = ref(null);
+  // const detailVisible = ref(false);
+
+  // @ts-ignore
+  let originalList = window.myHotList || [];
+
+  const filteredList = computed(() => {
+    const q = query.value.trim().toLowerCase();
+    if (!q) return originalList;
+    return originalList.filter((item) => item.info.title.toLowerCase().includes(q));
+  });
+
+  const search = () => {
+    // computed `filteredList` reacts to `query`, so no extra logic required
+  };
+
+  const reset = () => {
+    query.value = '';
+  };
+
+  const handleChecked = (item: any) => {
+    if (item && item.examine) {
+      show.value = false;
+      setTimeout(() => {
+        // @ts-ignore
+        item.examine(window.player, true);
+      }, 200);
+    }
+  };
+
+  watch(show, (v) => {
+    if (v) {
+      // @ts-ignore
+      originalList = window.myHotList || [];
+    }
+  });
+</script>
+
+<style lang="scss">
+  @use '@/assets/utils.scss';
+
+  .ant-popup {
+    --el-dialog-bg-color: transparent;
+    --el-dialog-box-shadow: none;
+    --el-dialog-padding-primary: 0;
+    margin: 0 auto;
+    padding-top: utils.vh-calc(150);
+    width: utils.vw-calc(1060);
+    height: 100%;
+
+    .el-dialog__body {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+    }
+    .title {
+      position: fixed;
+      top: utils.vh-calc(74);
+      left: utils.vw-calc(116);
+      width: utils.vw-calc(173);
+      height: utils.vw-calc(65);
+    }
+    .close {
+      position: fixed;
+      top: utils.vh-calc(60);
+      right: utils.vw-calc(50);
+      width: utils.vw-calc(50);
+      height: utils.vw-calc(50);
+      background: url('@/assets/images/nb/close.png') no-repeat center / contain;
+      cursor: pointer;
+    }
+    &-scrollbar {
+      flex: 1;
+      height: 0;
+
+      ul {
+        list-style: none;
+        padding-bottom: 30px;
+        margin: 0;
+        display: grid;
+        grid-template-columns: repeat(5, 1fr);
+        gap: utils.vw-calc(30) utils.vw-calc(40);
+      }
+
+      li {
+        display: inline-block;
+        width: 100%;
+        cursor: pointer;
+
+        .el-image {
+          padding: utils.vh-calc(12);
+          height: utils.vh-calc(246);
+          background: url('./images/nb/card-bg.png') no-repeat center / 100% 100%;
+        }
+        p {
+          margin: utils.vh-calc(16) 0 0;
+          font-size: utils.vh-calc(20);
+          line-height: utils.vh-calc(20);
+          color: #ffffff;
+          text-align: center;
+        }
+      }
+
+      li > .el-image,
+      li .el-image {
+        display: block;
+        width: 100%;
+      }
+    }
+    &-search {
+      margin: utils.vh-calc(40) 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      height: utils.vh-calc(45);
+
+      .el-input {
+        --el-input-bg-color: #ffffff;
+        --el-input-border-radius: 0;
+        --el-border-color: #b89d5e;
+        --el-input-text-color: #958474;
+        --el-input-placeholder-color: #958474;
+
+        flex: 1;
+        height: 100%;
+        font-size: utils.vh-calc(18);
+
+        .el-input__inner {
+          font-family: 'fzq';
+          height: 100%;
+          line-height: 100%;
+        }
+      }
+      .btn {
+        flex-shrink: 0;
+        width: utils.vh-calc(112);
+        height: utils.vh-calc(45);
+        text-align: center;
+        font-size: utils.vh-calc(21);
+        line-height: utils.vh-calc(45);
+        color: #ffffff;
+        cursor: pointer;
+
+        &.search {
+          margin: 0 utils.vw-calc(15) 0 utils.vw-calc(20);
+          background: #9f7e5e;
+        }
+        &.reset {
+          background: #5a524b;
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .ant-popup {
+      padding-top: utils.vh-calc(150);
+      width: 100%;
+
+      .title {
+        top: utils.vh-calc(60);
+        left: utils.vw-calc(50);
+        width: utils.vw-calc(173);
+        height: utils.vw-calc(65);
+      }
+      .close {
+        top: utils.vh-calc(50);
+        right: utils.vw-calc(30);
+        width: utils.vw-calc(70);
+        height: utils.vw-calc(70);
+      }
+      &-scrollbar {
+        padding: 0 utils.vw-calc(50);
+
+        ul {
+          grid-template-columns: repeat(3, 1fr);
+          gap: utils.vw-calc(55) utils.vw-calc(24);
+        }
+        li {
+          p {
+            margin: utils.vh-calc(16) 0 0;
+            font-size: utils.vh-calc(24);
+            line-height: utils.vh-calc(24);
+          }
+          .el-image {
+            padding: 5px;
+            height: utils.vh-calc(247);
+          }
+        }
+      }
+      &-search {
+        margin: utils.vh-calc(40) utils.vw-calc(50);
+        height: utils.vh-calc(50);
+
+        .el-input {
+          font-size: utils.vh-calc(24);
+        }
+      }
+    }
+  }
+</style>

+ 273 - 0
src/index/views/home/components/ant-popup/index.xx.vue

@@ -0,0 +1,273 @@
+<template>
+  <el-dialog
+    class="ant-popup"
+    v-model="show"
+    append-to-body
+    destroy-on-close
+    :close-on-click-modal="false"
+    :show-close="false"
+  >
+    <div class="close" @click="show = false" />
+
+    <div class="ant-popup-search">
+      <p class="title">展品说明</p>
+
+      <el-input
+        v-model="query"
+        placeholder="请输入展品名称"
+        @keydown.stop
+        @keyup.stop
+        @keyup.enter="search"
+      />
+
+      <div class="search btn" @click="search">搜索</div>
+      <div class="reset btn" @click="reset">重置</div>
+    </div>
+
+    <el-scrollbar class="ant-popup-scrollbar">
+      <ul>
+        <template v-for="item in filteredList" :key="item.id">
+          <li v-if="item.info.images.length > 0" @click="handleChecked(item)">
+            <el-image :src="item.info.images[0]" fit="contain" />
+            <p class="limit-line">{{ item.info.title }}</p>
+          </li>
+        </template>
+      </ul>
+    </el-scrollbar>
+  </el-dialog>
+
+  <!-- <DetailPopup v-model:visible="detailVisible" :checkedItem="checkedItem" /> -->
+</template>
+
+<script setup lang="ts">
+  import { computed, ref, watch } from 'vue';
+  // import { ANT_LIST } from './constants';
+  // import DetailPopup from './detail.vue';
+
+  const emits = defineEmits(['update:visible']);
+  const props = defineProps<{
+    visible: boolean;
+  }>();
+
+  const show = computed({
+    get() {
+      return props.visible;
+    },
+    set(v) {
+      emits('update:visible', v);
+    },
+  });
+
+  const query = ref('');
+  // const checkedItem = ref(null);
+  // const detailVisible = ref(false);
+
+  // @ts-ignore
+  let originalList = window.myHotList || [];
+
+  const filteredList = computed(() => {
+    const q = query.value.trim().toLowerCase();
+    if (!q) return originalList;
+    return originalList.filter((item) => item.info.title.toLowerCase().includes(q));
+  });
+
+  const search = () => {
+    // computed `filteredList` reacts to `query`, so no extra logic required
+  };
+
+  const reset = () => {
+    query.value = '';
+  };
+
+  const handleChecked = (item: any) => {
+    if (item && item.examine) {
+      show.value = false;
+      setTimeout(() => {
+        // @ts-ignore
+        item.examine(window.player, true);
+      }, 200);
+    }
+  };
+
+  watch(show, (v) => {
+    if (v) {
+      // @ts-ignore
+      originalList = window.myHotList || [];
+    }
+  });
+</script>
+
+<style lang="scss">
+  @use '@/assets/utils.scss';
+
+  .ant-popup {
+    --el-dialog-bg-color: transparent;
+    --el-dialog-box-shadow: none;
+    --el-dialog-padding-primary: 0;
+    margin: 0 auto;
+    padding-top: utils.vh-calc(35);
+    width: utils.vw-calc(1060);
+    height: 100%;
+
+    .el-dialog__body {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+    }
+    .close {
+      position: fixed;
+      top: utils.vh-calc(84);
+      right: utils.vw-calc(84);
+      width: utils.vw-calc(24);
+      height: utils.vw-calc(24);
+      background: url('@/assets/images/xx/close.png') no-repeat center / contain;
+      cursor: pointer;
+    }
+    &-scrollbar {
+      flex: 1;
+      height: 0;
+
+      ul {
+        list-style: none;
+        padding-bottom: utils.vw-calc(30);
+        margin: 0;
+        display: grid;
+        grid-template-columns: repeat(5, 1fr);
+        gap: utils.vw-calc(40);
+      }
+
+      li {
+        display: inline-block;
+        width: 100%;
+        cursor: pointer;
+
+        .el-image {
+          padding: utils.vh-calc(12);
+          height: utils.vh-calc(200);
+          background: #e9decc;
+        }
+        p {
+          margin: utils.vh-calc(12) 0 0;
+          font-size: utils.vh-calc(18);
+          line-height: utils.vh-calc(20);
+          color: #e9decc;
+          text-align: center;
+        }
+      }
+
+      li > .el-image,
+      li .el-image {
+        display: block;
+        width: 100%;
+      }
+    }
+    &-search {
+      margin: utils.vh-calc(35) 0;
+      display: flex;
+      align-items: center;
+
+      .title {
+        flex: 1;
+        color: #e9decc;
+        font-size: utils.vw-calc(32);
+      }
+      .el-input {
+        --el-input-bg-color: transparent;
+        --el-input-border-radius: 0;
+        --el-border-color: transparent;
+        --el-input-text-color: #e9decc;
+        --el-input-placeholder-color: #e9decc;
+
+        flex: 1;
+        font-size: utils.vw-calc(18);
+        width: utils.vw-calc(386);
+        height: utils.vw-calc(40);
+        background: url('./images/xx/input-bg.png') no-repeat center / 100% 100%;
+
+        &__wrapper {
+          padding: 0 utils.vw-calc(30);
+          box-shadow: none !important;
+        }
+        .el-input__inner {
+          font-family: 'fzq';
+          height: 100%;
+          line-height: 100%;
+        }
+      }
+      .btn {
+        flex-shrink: 0;
+        margin-left: utils.vw-calc(25);
+        width: utils.vw-calc(92);
+        height: utils.vw-calc(40);
+        text-align: center;
+        font-size: utils.vw-calc(18);
+        line-height: utils.vw-calc(44);
+        color: #e9decc;
+        cursor: pointer;
+
+        &.search {
+          background: url('./images/xx/search.png') no-repeat center / contain;
+        }
+        &.reset {
+          background: url('./images/xx/reset.png') no-repeat center / contain;
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .ant-popup {
+      padding-top: utils.vh-calc(145);
+      width: 100%;
+
+      .close {
+        top: utils.vh-calc(77);
+        right: utils.vw-calc(30);
+        width: utils.vw-calc(36);
+        height: utils.vw-calc(36);
+      }
+      &-scrollbar {
+        padding: 0 utils.vw-calc(30);
+
+        ul {
+          grid-template-columns: repeat(2, 1fr);
+          gap: utils.vw-calc(30);
+        }
+        li {
+          p {
+            margin: utils.vh-calc(20) 0 0;
+            font-size: utils.vh-calc(28);
+            line-height: utils.vh-calc(30);
+          }
+          .el-image {
+            padding: 5px;
+            height: utils.vh-calc(350);
+          }
+        }
+      }
+      &-search {
+        margin: utils.vh-calc(40) utils.vw-calc(30);
+
+        .title {
+          position: absolute;
+          top: utils.vh-calc(70);
+          left: 50%;
+          font-size: utils.vw-calc(48);
+          transform: translateX(-50%);
+        }
+        .el-input {
+          flex: 1;
+          height: utils.vh-calc(70);
+          font-size: utils.vh-calc(24);
+        }
+        .btn {
+          margin-left: utils.vw-calc(30);
+          width: utils.vw-calc(130);
+          height: utils.vw-calc(70);
+          font-size: utils.vw-calc(30);
+          line-height: utils.vw-calc(74);
+        }
+      }
+    }
+  }
+</style>

+ 204 - 0
src/index/views/home/components/menu/index.nb.scss

@@ -0,0 +1,204 @@
+@use '@/assets/utils.scss';
+
+.el-popper.is-dark {
+  --el-text-color-primary: linear-gradient(
+    90deg,
+    rgba(0, 0, 0, 0.8) 70%,
+    transparent 100%
+  ) !important;
+  --el-bg-color: #f5d9ac !important;
+}
+
+.pinBottom-container {
+  position: absolute;
+  bottom: 25px;
+  width: 100%;
+  transition: all 0.5s;
+  z-index: var(--z-index-top);
+
+  &.open {
+    bottom: 155px;
+
+    &.playing {
+      bottom: 175px;
+    }
+    &.noScroll {
+      bottom: 135px;
+
+      &.playing {
+        bottom: 155px;
+      }
+    }
+  }
+  &.playing:not(.open) {
+    bottom: 45px;
+  }
+}
+
+.pinBottom {
+  position: absolute;
+  bottom: 0;
+  line-height: 1;
+  transition: all 0.5s;
+
+  #hotList {
+    display: none !important;
+  }
+  &.left {
+    left: 48px;
+    bottom: 0;
+    overflow: hidden;
+
+    > * {
+      margin-right: 30px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+  &.right {
+    display: none;
+    right: 20px;
+    bottom: 0;
+
+    > div {
+      width: 40px;
+      height: 40px;
+
+      img {
+        width: 30px;
+        height: 30px;
+      }
+    }
+  }
+  > * {
+    float: left;
+    width: 50px;
+    height: 50px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+
+    &.active,
+    &:hover,
+    &.opened {
+      opacity: 0.7;
+    }
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  &-exit-simple {
+    position: fixed;
+    left: 50%;
+    bottom: -120px;
+    width: 120px;
+    height: 40px;
+    line-height: 40px;
+    text-align: center;
+    color: #e0b387;
+    border-radius: 50px;
+    background-color: rgba(0, 0, 0, 0.5);
+    cursor: pointer;
+    transform: translateX(-50%);
+
+    &.active {
+      bottom: 20px;
+    }
+  }
+}
+
+#thumb {
+  .icon-slot {
+    width: 100%;
+    height: 100%;
+    transition: background ease-in 0.2s;
+    background: url('/images/nb/dislike.png') no-repeat center / contain;
+  }
+
+  &.active .icon-slot {
+    background-image: url('/images/nb/like.png');
+  }
+}
+
+div.el-popper.is-dark {
+  background: none;
+  color: #d2d2d2;
+  border: none;
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: -5px;
+    width: 107px;
+    height: 23px;
+    transform: translateY(-50%);
+    background: url('../../images/nb/popup-bg.png') no-repeat center / contain;
+    z-index: -1;
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .pinBottom-container {
+    bottom: 20px;
+
+    &.open {
+      bottom: 120px;
+
+      &.noScroll {
+        bottom: 110px;
+
+        &.playing {
+          bottom: 130px;
+        }
+      }
+    }
+    &.playing:not(.open) {
+      bottom: 40px;
+    }
+  }
+  .pinBottom {
+    display: flex;
+
+    &.left {
+      left: utils.vw-calc(20);
+      right: utils.vw-calc(20);
+      justify-content: center;
+      gap: utils.vw-calc(20);
+
+      > * {
+        margin-right: 0;
+      }
+    }
+    > div {
+      flex-shrink: 0;
+      width: utils.vw-calc(55);
+      height: utils.vw-calc(55);
+    }
+  }
+}
+
+.pinBottom-container,
+.home-viewer,
+.home_img {
+  transition:
+    bottom 0.5s,
+    opacity 0.38s ease,
+    transform 0.38s ease,
+    visibility 0.38s;
+}
+
+.simple-mode {
+  .pinBottom-container,
+  .home-viewer,
+  .home_img {
+    opacity: 0;
+    transform: translateY(12px) scale(0.995);
+    pointer-events: none;
+    visibility: hidden;
+  }
+}

+ 256 - 0
src/index/views/home/components/menu/index.nb.tsx

@@ -0,0 +1,256 @@
+import { defineComponent, ref, watch } from 'vue';
+import { homeApi } from '@/api';
+import SharePopup from '../share-popup/index.vue';
+import AntPopup from '../ant-popup/index.nb.vue';
+import InfoPopup from '@/components/info-popup/index.nb.vue';
+import './index.nb.scss';
+
+export default defineComponent({
+  name: 'HomeMenu',
+  props: {
+    likeCount: {
+      type: Number,
+      required: true,
+    },
+    handleLikeCounter: {
+      type: Function,
+      required: true,
+    },
+    manageJsLoaded: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  setup(props) {
+    const isSimpleMode = ref(false);
+    const shareVisible = ref(false);
+    const antVisible = ref(false);
+    const animationThumb = ref(false);
+    const isMobile = ref(false);
+    const infoVisible = ref(false);
+
+    const toggleSimpleMode = (on?: boolean) => {
+      const next = typeof on === 'boolean' ? on : !isSimpleMode.value;
+      isSimpleMode.value = next;
+      try {
+        if (next) document.documentElement.classList.add('simple-mode');
+        else document.documentElement.classList.remove('simple-mode');
+      } catch (e) {
+        // ignore
+      }
+    };
+    const handleThumb = () => {
+      if (animationThumb.value) return;
+
+      animationThumb.value = true;
+      homeApi.saveStar(window.number);
+      props.handleLikeCounter();
+
+      setTimeout(() => {
+        animationThumb.value = false;
+      }, 200);
+    };
+
+    watch(
+      () => props.manageJsLoaded,
+      (v) => {
+        if (v) {
+          isMobile.value = window.browser.isMobile();
+        }
+      },
+      {
+        immediate: true,
+      }
+    );
+
+    return {
+      isMobile,
+      isSimpleMode,
+      shareVisible,
+      antVisible,
+      animationThumb,
+      infoVisible,
+      handleThumb,
+      toggleSimpleMode,
+    };
+  },
+  render() {
+    return (
+      <div>
+        <div class="pinBottom-container">
+          <div class="pinBottom left">
+            <div id="previous" class="previous desktop-only ui-icon" style="display: none;">
+              <a>
+                <img
+                  src="images/nb/play.png"
+                  width="24"
+                  height="24"
+                  data-original-title="自动漫游"
+                />
+              </a>
+            </div>
+            <el-tooltip content="自动漫游" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div id="play" class="ui-icon">
+                <a>
+                  <img src="images/nb/play.png" width="24" height="24" />
+                </a>
+              </div>
+            </el-tooltip>
+            <div id="pause" class="ui-icon" style="display: none;">
+              <a>
+                <img title="暂停漫游" src="images/nb/pause.png" width="24" height="24" />
+              </a>
+            </div>
+            <div id="next" class="next desktop-only ui-icon wide" style="display: none;">
+              <a>
+                <i title="" class="icon icon-dpad-right" data-original-title="下一个"></i>
+              </a>
+            </div>
+            <el-tooltip content="展厅场景" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div id="pullTab" title="">
+                <img
+                  class="icon icon-inside"
+                  src="images/nb/auto.png"
+                  title="展厅场景"
+                  data-default-url="images/nb/auto.png"
+                  data-active-url="images/nb/auto.png"
+                />
+              </div>
+            </el-tooltip>
+            <div id="hotList" title="" style="display: none">
+              <el-tooltip content="热点列表" offset={0} show-arrow={false} disabled={this.isMobile}>
+                <img class="icon icon-inside" src="images/nb/hotlist.png" title="热点列表" />
+              </el-tooltip>
+            </div>
+            <div
+              data-original-title="全景漫游"
+              id="gui-modes-inside"
+              title=""
+              class=""
+              style="display:none"
+            >
+              <img class="icon icon-inside" src="images/inside.png" title="全景漫游" />
+            </div>
+            <el-tooltip content="迷你模型" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div id="gui-modes-dollhouse" title="" class="">
+                <img class="icon icon-inside" src="images/nb/model.png" title="迷你模型" />
+              </div>
+            </el-tooltip>
+            <div id="description" title="">
+              <el-tooltip content="展览介绍" offset={0} show-arrow={false} disabled={this.isMobile}>
+                <img
+                  class="icon icon-inside"
+                  src="images/nb/hotlist.png"
+                  title="展览介绍"
+                  onClick={() => (this.infoVisible = true)}
+                />
+              </el-tooltip>
+            </div>
+            <div
+              data-original-title="俯视图"
+              id="gui-modes-floorplan"
+              title=""
+              style="display:none"
+            >
+              <img class="icon icon-inside" src="images/floor.png" title="俯视图" />
+            </div>
+            <el-tooltip content="展品说明" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div
+                id="gui-modes-antlist"
+                title=""
+                class=""
+                onClick={() => (this.antVisible = true)}
+              >
+                <img class="icon icon-inside" src="images/nb/antlist.png" title="展品说明" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content={`点赞${this.likeCount}`} offset={0} show-arrow={false}>
+              <div
+                id="thumb"
+                data-original-title="点赞"
+                class={{ active: this.animationThumb }}
+                onClick={this.handleThumb}
+              >
+                <div class="icon icon-slot" title="点赞" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content="分享" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div id="sharing" onClick={() => (this.shareVisible = true)}>
+                <img class="icon icon-inside" src="images/nb/share.png" title="分享" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content="全屏" offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div
+                id="gui-fullscreen"
+                class="ui-icon wide"
+                data-placement="top"
+                title="{[{ VIEW_FULLSCREEN }]}"
+              >
+                <a>
+                  <img class="icon icon-fullscreen" src="images/nb/enlarge_on.png" />
+                </a>
+              </div>
+            </el-tooltip>
+            <div
+              id="gui-fullscreen-exit"
+              class="ui-icon wide"
+              data-placement="top"
+              title="{[{ EXIT_FULLSCREEN }]}"
+              style="display: none;"
+            >
+              <a>
+                <img class="icon icon-fullscreen-exit" src="images/nb/narrow_off.png" />
+              </a>
+            </div>
+            {/* <el-tooltip content="清屏"  offset={0} show-arrow={false} disabled={this.isMobile}>
+              <div onClick={() => this.toggleSimpleMode(true)}>
+                <img class="icon icon-inside" src="images/nb/simple.png" title="清屏" />
+              </div>
+            </el-tooltip> */}
+
+            <div data-original-title="VR" id="vr" title="" style="display: none;">
+              <img class="icon icon-inside" src="images/VR.png" title="VR" />
+            </div>
+            <div
+              data-original-title="消除外壳"
+              id="gui-remove-face"
+              title=""
+              style="display: none; float: left;"
+            >
+              <img class="icon icon-inside" src="images/face.jpg" />
+            </div>
+          </div>
+        </div>
+        <div class="pinBottom right">
+          <div id="volume" class="ui-icon wide" style="display: none">
+            <a>
+              <img
+                src="images/Volume btn_on.png"
+                width="24"
+                height="24"
+                data-default-url="images/Volume btn_on.png"
+                data-active-url="images/Volume btn_off.png"
+              />
+            </a>
+          </div>
+          <div id="vr" class="ui-icon wide hidden">
+            <a>
+              <i title="{[{ VIEW_IN_VR }]}" class="icon icon-webvr"></i>
+            </a>
+          </div>
+        </div>
+
+        <div
+          class={['pinBottom-exit-simple', { active: this.isSimpleMode }]}
+          onClick={() => this.toggleSimpleMode(false)}
+        >
+          退出清屏
+        </div>
+
+        <InfoPopup visible={this.infoVisible} onUpdate:visible={() => (this.infoVisible = false)} />
+        <AntPopup visible={this.antVisible} onUpdate:visible={(v) => (this.antVisible = v)} />
+        <SharePopup visible={this.shareVisible} onUpdate:visible={(v) => (this.shareVisible = v)} />
+      </div>
+    );
+  },
+});

+ 191 - 0
src/index/views/home/components/menu/index.xx.scss

@@ -0,0 +1,191 @@
+@use '@/assets/utils.scss';
+
+.el-popper.is-dark {
+  --el-text-color-primary: linear-gradient(
+    90deg,
+    rgba(0, 0, 0, 0.8) 70%,
+    transparent 100%
+  ) !important;
+  --el-bg-color: #f5d9ac !important;
+}
+
+.pinBottom-container {
+  position: absolute;
+  bottom: 25px;
+  width: 100%;
+  transition: all 0.5s;
+  z-index: var(--z-index-top);
+
+  &.open {
+    bottom: 155px;
+
+    &.playing {
+      bottom: 175px;
+    }
+    &.noScroll {
+      bottom: 135px;
+
+      &.playing {
+        bottom: 155px;
+      }
+    }
+  }
+  &.playing:not(.open) {
+    bottom: 45px;
+  }
+}
+
+.pinBottom {
+  position: absolute;
+  bottom: 0;
+  line-height: 1;
+  transition: all 0.5s;
+
+  #hotList {
+    display: none !important;
+  }
+  &.left {
+    left: 48px;
+    bottom: 0;
+    overflow: hidden;
+
+    > * {
+      margin-right: 25px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+  &.right {
+    display: none;
+    right: 20px;
+    bottom: 0;
+
+    > div {
+      width: 40px;
+      height: 40px;
+
+      img {
+        width: 30px;
+        height: 30px;
+      }
+    }
+  }
+  > * {
+    float: left;
+    width: 40px;
+    height: 40px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+
+    &.active,
+    &:hover,
+    &.opened {
+      opacity: 0.7;
+    }
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  &-exit-simple {
+    position: fixed;
+    left: 50%;
+    bottom: -120px;
+    width: 120px;
+    height: 40px;
+    line-height: 40px;
+    text-align: center;
+    color: #e0b387;
+    border-radius: 50px;
+    background-color: rgba(0, 0, 0, 0.5);
+    cursor: pointer;
+    transform: translateX(-50%);
+
+    &.active {
+      bottom: 20px;
+    }
+  }
+}
+
+#thumb {
+  .icon-slot {
+    width: 100%;
+    height: 100%;
+    transition: background ease-in 0.2s;
+    background: url('/images/xx/dislike.png') no-repeat center / contain;
+  }
+
+  &.active .icon-slot {
+    background-image: url('/images/xx/like.png');
+  }
+}
+
+.el-popper.is-dark {
+  background: rgba(#000000, 0.3) !important;
+  color: #ebe0ce !important;
+  border: none;
+}
+
+@media only screen and (max-width: 600px) {
+  .pinBottom-container {
+    bottom: 20px;
+
+    &.open {
+      bottom: 120px;
+
+      &.noScroll {
+        bottom: 110px;
+
+        &.playing {
+          bottom: 130px;
+        }
+      }
+    }
+    &.playing:not(.open) {
+      bottom: 40px;
+    }
+  }
+  .pinBottom {
+    display: flex;
+
+    &.left {
+      left: utils.vw-calc(20);
+      right: utils.vw-calc(20);
+      justify-content: center;
+      gap: utils.vw-calc(20);
+
+      > * {
+        margin-right: 0;
+      }
+    }
+    > div {
+      width: utils.vw-calc(50);
+      height: utils.vw-calc(50);
+    }
+  }
+}
+
+.pinBottom-container,
+.home-viewer,
+.home_img {
+  transition:
+    bottom 0.5s,
+    opacity 0.38s ease,
+    transform 0.38s ease,
+    visibility 0.38s;
+}
+
+.simple-mode {
+  .pinBottom-container,
+  .home-viewer,
+  .home_img {
+    opacity: 0;
+    transform: translateY(12px) scale(0.995);
+    pointer-events: none;
+    visibility: hidden;
+  }
+}

+ 266 - 0
src/index/views/home/components/menu/index.xx.tsx

@@ -0,0 +1,266 @@
+import { defineComponent, ref, watch } from 'vue';
+import { homeApi } from '@/api';
+import SharePopup from '../share-popup/index.vue';
+import AntPopup from '../ant-popup/index.xx.vue';
+import InfoPopup from '@/components/info-popup/index.xx.vue';
+import './index.xx.scss';
+
+export default defineComponent({
+  name: 'HomeMenu',
+  props: {
+    likeCount: {
+      type: Number,
+      required: true,
+    },
+    handleLikeCounter: {
+      type: Function,
+      required: true,
+    },
+    manageJsLoaded: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  setup(props) {
+    const isSimpleMode = ref(false);
+    const shareVisible = ref(false);
+    const antVisible = ref(false);
+    const animationThumb = ref(false);
+    const isMobile = ref(false);
+    const infoVisible = ref(false);
+
+    const toggleSimpleMode = (on?: boolean) => {
+      const next = typeof on === 'boolean' ? on : !isSimpleMode.value;
+      isSimpleMode.value = next;
+      try {
+        if (next) document.documentElement.classList.add('simple-mode');
+        else document.documentElement.classList.remove('simple-mode');
+      } catch (e) {
+        // ignore
+      }
+    };
+    const handleThumb = () => {
+      if (animationThumb.value) return;
+
+      animationThumb.value = true;
+      homeApi.saveStar(window.number);
+      props.handleLikeCounter();
+
+      setTimeout(() => {
+        animationThumb.value = false;
+      }, 200);
+    };
+
+    watch(
+      () => props.manageJsLoaded,
+      (v) => {
+        if (v) {
+          isMobile.value = window.browser.isMobile();
+        }
+      },
+      {
+        immediate: true,
+      }
+    );
+
+    return {
+      isMobile,
+      isSimpleMode,
+      shareVisible,
+      antVisible,
+      animationThumb,
+      infoVisible,
+      handleThumb,
+      toggleSimpleMode,
+    };
+  },
+  render() {
+    return (
+      <div>
+        <div class="pinBottom-container">
+          <div class="pinBottom left">
+            <div id="previous" class="previous desktop-only ui-icon" style="display: none;">
+              <a>
+                <img
+                  src="images/xx/play.png"
+                  width="24"
+                  height="24"
+                  data-original-title="自动漫游"
+                />
+              </a>
+            </div>
+            <el-tooltip content="自动漫游" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div id="play" class="ui-icon">
+                <a>
+                  <img src="images/xx/play.png" width="24" height="24" />
+                </a>
+              </div>
+            </el-tooltip>
+            <div id="pause" class="ui-icon" style="display: none;">
+              <a>
+                <img title="暂停漫游" src="images/xx/pause.png" width="24" height="24" />
+              </a>
+            </div>
+            <div id="next" class="next desktop-only ui-icon wide" style="display: none;">
+              <a>
+                <i title="" class="icon icon-dpad-right" data-original-title="下一个"></i>
+              </a>
+            </div>
+            <el-tooltip content="展厅场景" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div id="pullTab" title="">
+                <img
+                  class="icon icon-inside"
+                  src="images/xx/auto.png"
+                  title="展厅场景"
+                  data-default-url="images/xx/auto.png"
+                  data-active-url="images/xx/auto.png"
+                />
+              </div>
+            </el-tooltip>
+            <div id="hotList" title="" style="display: none">
+              <el-tooltip
+                content="热点列表"
+                offset={10}
+                show-arrow={false}
+                disabled={this.isMobile}
+              >
+                <img class="icon icon-inside" src="images/xx/hotlist.png" title="热点列表" />
+              </el-tooltip>
+            </div>
+            <div
+              data-original-title="全景漫游"
+              id="gui-modes-inside"
+              title=""
+              class=""
+              style="display:none"
+            >
+              <img class="icon icon-inside" src="images/inside.png" title="全景漫游" />
+            </div>
+            <el-tooltip content="迷你模型" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div id="gui-modes-dollhouse" title="" class="">
+                <img class="icon icon-inside" src="images/xx/model.png" title="迷你模型" />
+              </div>
+            </el-tooltip>
+            <div id="description" title="">
+              <el-tooltip
+                content="展览介绍"
+                offset={10}
+                show-arrow={false}
+                disabled={this.isMobile}
+              >
+                <img
+                  class="icon icon-inside"
+                  src="images/xx/hotlist.png"
+                  title="展览介绍"
+                  onClick={() => (this.infoVisible = true)}
+                />
+              </el-tooltip>
+            </div>
+            <div
+              data-original-title="俯视图"
+              id="gui-modes-floorplan"
+              title=""
+              style="display:none"
+            >
+              <img class="icon icon-inside" src="images/floor.png" title="俯视图" />
+            </div>
+            <el-tooltip content="展品说明" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div
+                id="gui-modes-antlist"
+                title=""
+                class=""
+                onClick={() => (this.antVisible = true)}
+              >
+                <img class="icon icon-inside" src="images/xx/antlist.png" title="展品说明" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content={`点赞${this.likeCount}`} offset={10} show-arrow={false}>
+              <div
+                id="thumb"
+                data-original-title="点赞"
+                class={{ active: this.animationThumb }}
+                onClick={this.handleThumb}
+              >
+                <div class="icon icon-slot" title="点赞" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content="分享" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div id="sharing" onClick={() => (this.shareVisible = true)}>
+                <img class="icon icon-inside" src="images/xx/share.png" title="分享" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content="全屏" offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div
+                id="gui-fullscreen"
+                class="ui-icon wide"
+                data-placement="top"
+                title="{[{ VIEW_FULLSCREEN }]}"
+              >
+                <a>
+                  <img class="icon icon-fullscreen" src="images/xx/enlarge_on.png" />
+                </a>
+              </div>
+            </el-tooltip>
+            <div
+              id="gui-fullscreen-exit"
+              class="ui-icon wide"
+              data-placement="top"
+              title="{[{ EXIT_FULLSCREEN }]}"
+              style="display: none;"
+            >
+              <a>
+                <img class="icon icon-fullscreen-exit" src="images/xx/narrow_off.png" />
+              </a>
+            </div>
+            {/* <el-tooltip content="清屏"  offset={10} show-arrow={false} disabled={this.isMobile}>
+              <div onClick={() => this.toggleSimpleMode(true)}>
+                <img class="icon icon-inside" src="images/xx/simple.png" title="清屏" />
+              </div>
+            </el-tooltip> */}
+
+            <div data-original-title="VR" id="vr" title="" style="display: none;">
+              <img class="icon icon-inside" src="images/VR.png" title="VR" />
+            </div>
+            <div
+              data-original-title="消除外壳"
+              id="gui-remove-face"
+              title=""
+              style="display: none; float: left;"
+            >
+              <img class="icon icon-inside" src="images/face.jpg" />
+            </div>
+          </div>
+        </div>
+        <div class="pinBottom right">
+          <div id="volume" class="ui-icon wide" style="display: none">
+            <a>
+              <img
+                src="images/Volume btn_on.png"
+                width="24"
+                height="24"
+                data-default-url="images/Volume btn_on.png"
+                data-active-url="images/Volume btn_off.png"
+              />
+            </a>
+          </div>
+          <div id="vr" class="ui-icon wide hidden">
+            <a>
+              <i title="{[{ VIEW_IN_VR }]}" class="icon icon-webvr"></i>
+            </a>
+          </div>
+        </div>
+
+        <div
+          class={['pinBottom-exit-simple', { active: this.isSimpleMode }]}
+          onClick={() => this.toggleSimpleMode(false)}
+        >
+          退出清屏
+        </div>
+
+        <InfoPopup visible={this.infoVisible} onUpdate:visible={() => (this.infoVisible = false)} />
+        <AntPopup visible={this.antVisible} onUpdate:visible={(v) => (this.antVisible = v)} />
+        <SharePopup visible={this.shareVisible} onUpdate:visible={(v) => (this.shareVisible = v)} />
+      </div>
+    );
+  },
+});

+ 46 - 0
src/index/views/home/components/popup/index.nb.scss

@@ -0,0 +1,46 @@
+#popup {
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.8);
+  z-index: var(--z-hot-popper);
+
+  &.wait {
+    opacity: 0.1;
+  }
+}
+#id1 {
+  width: 100%;
+  height: 100%;
+}
+.popup-content {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+#closepop {
+  position: absolute;
+  top: 60px;
+  right: 60px;
+  width: 50px;
+  height: 50px;
+  cursor: pointer;
+  text-indent: -999em;
+  background: url('@/assets/images/nb/close.png') no-repeat center / contain;
+}
+
+@media only screen and (max-width: 600px) {
+  #closepop {
+    top: unset;
+    left: 50%;
+    right: unset;
+    bottom: 33px;
+    width: 25px;
+    height: 25px;
+    transform: translateX(-50%);
+  }
+}

+ 16 - 0
src/index/views/home/components/popup/index.nb.tsx

@@ -0,0 +1,16 @@
+import { defineComponent } from 'vue';
+import './index.nb.scss';
+
+export default defineComponent({
+  name: 'HomePopup',
+  render() {
+    return (
+      <div id="popup">
+        <div class="popup-wrap">
+          <div class="popup-content"></div>
+          <div id="closepop">close</div>
+        </div>
+      </div>
+    );
+  },
+});

+ 46 - 0
src/index/views/home/components/popup/index.xx.scss

@@ -0,0 +1,46 @@
+#popup {
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.8);
+  z-index: var(--z-hot-popper);
+
+  &.wait {
+    opacity: 0.1;
+  }
+}
+#id1 {
+  width: 100%;
+  height: 100%;
+}
+.popup-content {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+#closepop {
+  position: absolute;
+  top: 80px;
+  right: 80px;
+  width: 24px;
+  height: 24px;
+  cursor: pointer;
+  text-indent: -999em;
+  background: url('@/assets/images/xx/close.png') no-repeat center / contain;
+}
+
+@media only screen and (max-width: 600px) {
+  #closepop {
+    top: unset;
+    left: 50%;
+    right: unset;
+    bottom: 33px;
+    width: 18px;
+    height: 18px;
+    transform: translateX(-50%);
+  }
+}

+ 16 - 0
src/index/views/home/components/popup/index.xx.tsx

@@ -0,0 +1,16 @@
+import { defineComponent } from 'vue';
+import './index.xx.scss';
+
+export default defineComponent({
+  name: 'HomePopup',
+  render() {
+    return (
+      <div id="popup">
+        <div class="popup-wrap">
+          <div class="popup-content"></div>
+          <div id="closepop">close</div>
+        </div>
+      </div>
+    );
+  },
+});

BIN
src/index/views/home/images/nb/chapter.png


BIN
src/index/views/home/images/nb/popup-bg.png


BIN
src/index/views/home/images/nb/viewer.png


BIN
src/index/views/home/images/xx/viewer.png


+ 138 - 0
src/index/views/home/index.nb.scss

@@ -0,0 +1,138 @@
+@use '@/assets/utils.scss';
+
+.home {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+
+  &-viewer {
+    position: absolute;
+    top: utils.vh-calc(34);
+    left: utils.vw-calc(26);
+    width: utils.vw-calc(250);
+    height: utils.vw-calc(77);
+    background: url('./images/nb/viewer.png') no-repeat center / contain;
+    z-index: 1;
+
+    span {
+      position: absolute;
+      right: utils.vw-calc(15);
+      bottom: utils.vw-calc(14);
+      color: var(--el-color-primary);
+      font-size: utils.vw-calc(25);
+      width: utils.vw-calc(100);
+      letter-spacing: 2px;
+      text-align: center;
+    }
+  }
+  &_img {
+    position: absolute;
+    top: utils.vh-calc(50);
+    right: utils.vw-calc(50);
+    width: utils.vw-calc(178);
+    height: utils.vw-calc(58);
+    cursor: pointer;
+    z-index: 1;
+  }
+  &_logo {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    left: 50%;
+    bottom: 20px;
+    width: 300px;
+    text-align: center;
+    font-size: 14px;
+    transform: translateX(-50%);
+    color: rgba(255, 255, 255, 0.8);
+
+    img {
+      width: 50%;
+    }
+    span {
+      font-size: 16px;
+      padding: 5px 0;
+      color: rgba(255, 255, 255, 0.8);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.8);
+    }
+  }
+}
+
+.home-iframe {
+  width: 100%;
+  height: 100%;
+
+  iframe {
+    width: 100%;
+    height: 100%;
+    display: block;
+  }
+}
+
+#player {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+
+  canvas {
+    position: relative;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+  }
+}
+
+#hot {
+  position: absolute;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+
+  > div[pos='right'] {
+    transform: translate(20px, -50%);
+  }
+  > div[pos='top'] {
+    transform: translate(-50%, calc(-100% - 20px));
+  }
+  > div[pos='middle'] {
+    transform: translate(-50%, -50%);
+  }
+  > div[pos='bottom'] {
+    transform: translate(-50%, 20px);
+  }
+  > div[pos='left'] {
+    transform: translate(calc(-100% - 20px), -50%);
+  }
+  > div {
+    position: absolute;
+    color: #fff;
+    user-select: none;
+    border-radius: 5px;
+    background-color: rgba(34, 34, 34, 0.3);
+    padding: 10px;
+    max-width: 400px;
+    letter-spacing: 1px;
+    line-height: 20px;
+    z-index: var(--z-index-top);
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .home {
+    &_logo {
+      width: 200px;
+
+      span {
+        font-size: 14px;
+      }
+    }
+  }
+}

+ 169 - 0
src/index/views/home/index.nb.tsx

@@ -0,0 +1,169 @@
+import { defineComponent, onMounted, ref } from 'vue';
+import { storeToRefs } from 'pinia';
+import useBaseStore from '@/store/module/base';
+import JsScript from '@/components/js-script';
+import Title from './components/title';
+import WebVr from './components/web-vr';
+import Other from './components/other';
+import Guide from './components/guide';
+import Vrcon from './components/vrcon';
+import Menu from './components/menu/index.nb';
+import GuiLoading from './components/gui-loading';
+import Popup from './components/popup';
+import HotSpotList from './components/hot-spot-list';
+import { homeApi } from '@/api';
+import './index.nb.scss';
+
+// 自定义热点图标
+// @ts-ignore
+// window.hoticon = {
+//   default: '/images/point.png',
+//   higt: '/images/point2.png',
+// };
+
+export default defineComponent({
+  name: 'home',
+  components: {
+    Title,
+    WebVr,
+    Other,
+    Vrcon,
+    GuiLoading,
+    JsScript,
+    Popup,
+  },
+  setup() {
+    let isInIframe = false;
+    try {
+      isInIframe = window.self !== window.top;
+    } catch {
+      // 如果由于跨域/安全限制导致访问 window.top 抛错,则保守按 iframe 渲染内容
+      isInIframe = true;
+    }
+
+    const iframeSrc = (() => {
+      try {
+        const url = new URL(window.location.href);
+        // 强制内部脚本按 autoplay 分支继续加载,避免 iframe 嵌入模式卡住进度条
+        url.searchParams.set('play', '1');
+        return url.toString();
+      } catch {
+        return window.location.href;
+      }
+    })();
+
+    const baseStore = useBaseStore();
+    const { manageJsLoaded } = storeToRefs(baseStore);
+    const hotJsLoaded = ref(false);
+    const visitCount = ref(0);
+    const likeCount = ref(0);
+
+    const getSceneDetail = async () => {
+      const { data } = await homeApi.getSceneDetail(window.number);
+      if (!data) return;
+      visitCount.value = data.visitSum;
+      likeCount.value = data.starSum;
+    };
+
+    const handleLikeCounter = () => {
+      likeCount.value += 1;
+    };
+
+    onMounted(() => {
+      if (!isInIframe) return;
+      getSceneDetail();
+      homeApi.saveSceneVisit(window.number);
+    });
+
+    return {
+      iframeSrc,
+      isInIframe,
+      manageJsLoaded,
+      hotJsLoaded,
+      visitCount,
+      likeCount,
+      handleLikeCounter,
+    };
+  },
+  render() {
+    // 顶层路由只负责 iframe 隔离;iframe 内再渲染原始 home 内容。
+    if (!this.isInIframe) {
+      return (
+        <div class="home home-iframe">
+          <iframe src={this.iframeSrc} title="home-iframe" />
+        </div>
+      );
+    }
+
+    return (
+      <div class="home">
+        {/* 进度条加载 */}
+        <GuiLoading />
+
+        <div class="home-viewer">
+          <span class="limit-line">{this.visitCount}</span>
+        </div>
+
+        {/* 加载初始页面 */}
+        <div id="gui-thumb" />
+
+        {/* 热点弹出框 */}
+        <Popup />
+
+        {/* 场景canvs主容器 */}
+        <div id="player" />
+
+        {/* 底部菜单 */}
+        <div id="gui-parent">
+          {/* 热点气泡 */}
+          <div id="hot" />
+
+          <div id="gui" style="display: none;">
+            {/* 标题 */}
+            <Title style={{ display: 'none' }} />
+
+            {/* 热点列表 */}
+            <HotSpotList />
+
+            {/* 底部菜单 */}
+            <Menu
+              manageJsLoaded={this.manageJsLoaded}
+              likeCount={this.likeCount}
+              handleLikeCounter={this.handleLikeCounter}
+            />
+
+            {/* 导览 */}
+            <Guide />
+
+            {/* <div class="home_logo">
+              <img src="images/btm_logo.png" />
+              <span>提供技术支持</span>
+            </div> */}
+          </div>
+
+          <WebVr />
+          <Vrcon />
+          <Other />
+        </div>
+
+        {/* TODO: 没有控制权,耦合严重;放在此处为了防止元素未渲染导致报错 */}
+        <JsScript src="./js/manage.js" onLoad={() => (this.manageJsLoaded = true)} />
+        {this.manageJsLoaded && (
+          <div>
+            <JsScript src="./js/Hot.js" onLoad={() => (this.hotJsLoaded = true)} />
+            {this.hotJsLoaded && (
+              <div>
+                <JsScript src="./js/main_2020_show.js" />
+                {/* 延迟加载 */}
+                <JsScript src="./js/lib/player-0.0.12.min.js" />
+                <JsScript src="./js/lib/Tween.js" />
+                <JsScript src="./js/SpecialScene.js" />
+                <JsScript src="./js/loadCAD.js" />
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  },
+});

+ 138 - 0
src/index/views/home/index.xx.scss

@@ -0,0 +1,138 @@
+@use '@/assets/utils.scss';
+
+.home {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+
+  &-viewer {
+    position: absolute;
+    top: utils.vh-calc(30);
+    left: utils.vw-calc(30);
+    width: utils.vw-calc(240);
+    height: utils.vw-calc(60);
+    background: url('./images/xx/viewer.png') no-repeat center / contain;
+    z-index: 1;
+
+    span {
+      position: absolute;
+      right: utils.vw-calc(15);
+      bottom: utils.vw-calc(6);
+      color: #ebe0ce;
+      font-size: utils.vw-calc(22);
+      width: utils.vw-calc(80);
+      letter-spacing: 2px;
+      text-align: center;
+    }
+  }
+  &_img {
+    position: absolute;
+    top: utils.vh-calc(50);
+    right: utils.vw-calc(50);
+    width: utils.vw-calc(178);
+    height: utils.vw-calc(58);
+    cursor: pointer;
+    z-index: 1;
+  }
+  &_logo {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    left: 50%;
+    bottom: 20px;
+    width: 300px;
+    text-align: center;
+    font-size: 14px;
+    transform: translateX(-50%);
+    color: rgba(255, 255, 255, 0.8);
+
+    img {
+      width: 50%;
+    }
+    span {
+      font-size: 16px;
+      padding: 5px 0;
+      color: rgba(255, 255, 255, 0.8);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.8);
+    }
+  }
+}
+
+.home-iframe {
+  width: 100%;
+  height: 100%;
+
+  iframe {
+    width: 100%;
+    height: 100%;
+    display: block;
+  }
+}
+
+#player {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+
+  canvas {
+    position: relative;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+  }
+}
+
+#hot {
+  position: absolute;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+
+  > div[pos='right'] {
+    transform: translate(20px, -50%);
+  }
+  > div[pos='top'] {
+    transform: translate(-50%, calc(-100% - 20px));
+  }
+  > div[pos='middle'] {
+    transform: translate(-50%, -50%);
+  }
+  > div[pos='bottom'] {
+    transform: translate(-50%, 20px);
+  }
+  > div[pos='left'] {
+    transform: translate(calc(-100% - 20px), -50%);
+  }
+  > div {
+    position: absolute;
+    color: #fff;
+    user-select: none;
+    border-radius: 5px;
+    background-color: rgba(34, 34, 34, 0.3);
+    padding: 10px;
+    max-width: 400px;
+    letter-spacing: 1px;
+    line-height: 20px;
+    z-index: var(--z-index-top);
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .home {
+    &_logo {
+      width: 200px;
+
+      span {
+        font-size: 14px;
+      }
+    }
+  }
+}

+ 169 - 0
src/index/views/home/index.xx.tsx

@@ -0,0 +1,169 @@
+import { defineComponent, onMounted, ref } from 'vue';
+import { storeToRefs } from 'pinia';
+import useBaseStore from '@/store/module/base';
+import JsScript from '@/components/js-script';
+import Title from './components/title';
+import WebVr from './components/web-vr';
+import Other from './components/other';
+import Guide from './components/guide';
+import Vrcon from './components/vrcon';
+import Menu from './components/menu/index.xx';
+import GuiLoading from './components/gui-loading';
+import Popup from './components/popup';
+import HotSpotList from './components/hot-spot-list';
+import { homeApi } from '@/api';
+import './index.xx.scss';
+
+// 自定义热点图标
+// @ts-ignore
+// window.hoticon = {
+//   default: '/images/point.png',
+//   higt: '/images/point2.png',
+// };
+
+export default defineComponent({
+  name: 'home',
+  components: {
+    Title,
+    WebVr,
+    Other,
+    Vrcon,
+    GuiLoading,
+    JsScript,
+    Popup,
+  },
+  setup() {
+    let isInIframe = false;
+    try {
+      isInIframe = window.self !== window.top;
+    } catch {
+      // 如果由于跨域/安全限制导致访问 window.top 抛错,则保守按 iframe 渲染内容
+      isInIframe = true;
+    }
+
+    const iframeSrc = (() => {
+      try {
+        const url = new URL(window.location.href);
+        // 强制内部脚本按 autoplay 分支继续加载,避免 iframe 嵌入模式卡住进度条
+        url.searchParams.set('play', '1');
+        return url.toString();
+      } catch {
+        return window.location.href;
+      }
+    })();
+
+    const baseStore = useBaseStore();
+    const { manageJsLoaded } = storeToRefs(baseStore);
+    const hotJsLoaded = ref(false);
+    const visitCount = ref(0);
+    const likeCount = ref(0);
+
+    const getSceneDetail = async () => {
+      const { data } = await homeApi.getSceneDetail(window.number);
+      if (!data) return;
+      visitCount.value = data.visitSum;
+      likeCount.value = data.starSum;
+    };
+
+    const handleLikeCounter = () => {
+      likeCount.value += 1;
+    };
+
+    onMounted(() => {
+      if (!isInIframe) return;
+      getSceneDetail();
+      homeApi.saveSceneVisit(window.number);
+    });
+
+    return {
+      iframeSrc,
+      isInIframe,
+      manageJsLoaded,
+      hotJsLoaded,
+      visitCount,
+      likeCount,
+      handleLikeCounter,
+    };
+  },
+  render() {
+    // 顶层路由只负责 iframe 隔离;iframe 内再渲染原始 home 内容。
+    if (!this.isInIframe) {
+      return (
+        <div class="home home-iframe">
+          <iframe src={this.iframeSrc} title="home-iframe" />
+        </div>
+      );
+    }
+
+    return (
+      <div class="home">
+        {/* 进度条加载 */}
+        <GuiLoading />
+
+        <div class="home-viewer">
+          <span class="limit-line">{this.visitCount}</span>
+        </div>
+
+        {/* 加载初始页面 */}
+        <div id="gui-thumb" />
+
+        {/* 热点弹出框 */}
+        <Popup />
+
+        {/* 场景canvs主容器 */}
+        <div id="player" />
+
+        {/* 底部菜单 */}
+        <div id="gui-parent">
+          {/* 热点气泡 */}
+          <div id="hot" />
+
+          <div id="gui" style="display: none;">
+            {/* 标题 */}
+            <Title style={{ display: 'none' }} />
+
+            {/* 热点列表 */}
+            <HotSpotList />
+
+            {/* 底部菜单 */}
+            <Menu
+              manageJsLoaded={this.manageJsLoaded}
+              likeCount={this.likeCount}
+              handleLikeCounter={this.handleLikeCounter}
+            />
+
+            {/* 导览 */}
+            <Guide />
+
+            {/* <div class="home_logo">
+              <img src="images/btm_logo.png" />
+              <span>提供技术支持</span>
+            </div> */}
+          </div>
+
+          <WebVr />
+          <Vrcon />
+          <Other />
+        </div>
+
+        {/* TODO: 没有控制权,耦合严重;放在此处为了防止元素未渲染导致报错 */}
+        <JsScript src="./js/manage.js" onLoad={() => (this.manageJsLoaded = true)} />
+        {this.manageJsLoaded && (
+          <div>
+            <JsScript src="./js/Hot.js" onLoad={() => (this.hotJsLoaded = true)} />
+            {this.hotJsLoaded && (
+              <div>
+                <JsScript src="./js/main_2020_show.js" />
+                {/* 延迟加载 */}
+                <JsScript src="./js/lib/player-0.0.12.min.js" />
+                <JsScript src="./js/lib/Tween.js" />
+                <JsScript src="./js/SpecialScene.js" />
+                <JsScript src="./js/loadCAD.js" />
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  },
+});

Разница между файлами не показана из-за своего большого размера
+ 0 - 111
vite.config.ts.timestamp-1772251949654-9b6c30f4c4e0b.mjs