瀏覽代碼

feat: 书于竹帛展

chenlei 13 小時之前
父節點
當前提交
dd96557741
共有 46 個文件被更改,包括 1755 次插入6 次删除
  1. 1 0
      components.d.ts
  2. 2 0
      package.json
  3. 二進制
      public/images/zb/antlist.png
  4. 二進制
      public/images/zb/auto.png
  5. 二進制
      public/images/zb/chapter.png
  6. 二進制
      public/images/zb/dislike.png
  7. 二進制
      public/images/zb/enlarge_on.png
  8. 二進制
      public/images/zb/hotlist.png
  9. 二進制
      public/images/zb/like.png
  10. 二進制
      public/images/zb/model.png
  11. 二進制
      public/images/zb/narrow_off.png
  12. 二進制
      public/images/zb/pause.png
  13. 二進制
      public/images/zb/play.png
  14. 二進制
      public/images/zb/share.png
  15. 二進制
      src/hotspot/assets/images/zb/audio.png
  16. 二進制
      src/hotspot/assets/images/zb/bd.png
  17. 342 0
      src/hotspot/views/hotspot/index.zb.vue
  18. 7 0
      src/index/assets/el.zb.scss
  19. 二進制
      src/index/assets/images/zb/close.png
  20. 二進制
      src/index/components/info-popup/images/zb/1.jpg
  21. 159 0
      src/index/components/info-popup/index.zb.vue
  22. 21 0
      src/index/router/index.zb.ts
  23. 8 6
      src/index/utils/index.ts
  24. 二進制
      src/index/views/cover/images/zb/bg.jpg
  25. 二進制
      src/index/views/cover/images/zb/btn.png
  26. 二進制
      src/index/views/cover/images/zb/info.png
  27. 二進制
      src/index/views/cover/images/zb/info2.png
  28. 二進制
      src/index/views/cover/images/zb/logo.png
  29. 二進制
      src/index/views/cover/images/zb/mb-bg.jpg
  30. 二進制
      src/index/views/cover/images/zb/mb-info.png
  31. 二進制
      src/index/views/cover/images/zb/mb-info2.png
  32. 二進制
      src/index/views/cover/images/zb/mb-title.png
  33. 二進制
      src/index/views/cover/images/zb/title.png
  34. 87 0
      src/index/views/cover/index.zb.scss
  35. 39 0
      src/index/views/cover/index.zb.vue
  36. 二進制
      src/index/views/home/components/ant-popup/images/zb/icon.png
  37. 二進制
      src/index/views/home/components/ant-popup/images/zb/mb-close.png
  38. 二進制
      src/index/views/home/components/ant-popup/images/zb/title.png
  39. 289 0
      src/index/views/home/components/ant-popup/index.zb.vue
  40. 182 0
      src/index/views/home/components/menu/index.zb.scss
  41. 256 0
      src/index/views/home/components/menu/index.zb.tsx
  42. 45 0
      src/index/views/home/components/popup/index.zb.scss
  43. 16 0
      src/index/views/home/components/popup/index.zb.tsx
  44. 二進制
      src/index/views/home/images/zb/viewer.png
  45. 132 0
      src/index/views/home/index.zb.scss
  46. 169 0
      src/index/views/home/index.zb.tsx

+ 1 - 0
components.d.ts

@@ -8,6 +8,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']

+ 2 - 0
package.json

@@ -10,6 +10,8 @@
     "push:test": "cross-env node ./scripts/publish.js",
     "serve:pb": "cross-env VITE_APP_SCENE=pb VITE_APP_TITLE=湖南省博物馆-遇见庞贝展 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
     "build:pb:test": "cross-env VITE_APP_SCENE=pb VITE_APP_TITLE=湖南省博物馆-遇见庞贝展 VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
+    "serve:zb": "cross-env VITE_APP_SCENE=zb VITE_APP_TITLE=书于竹帛展 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
+    "build:zb:test": "cross-env VITE_APP_SCENE=zb 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"

二進制
public/images/zb/antlist.png


二進制
public/images/zb/auto.png


二進制
public/images/zb/chapter.png


二進制
public/images/zb/dislike.png


二進制
public/images/zb/enlarge_on.png


二進制
public/images/zb/hotlist.png


二進制
public/images/zb/like.png


二進制
public/images/zb/model.png


二進制
public/images/zb/narrow_off.png


二進制
public/images/zb/pause.png


二進制
public/images/zb/play.png


二進制
public/images/zb/share.png


二進制
src/hotspot/assets/images/zb/audio.png


二進制
src/hotspot/assets/images/zb/bd.png


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

@@ -0,0 +1,342 @@
+<template>
+  <div class="hotspot-page detail-popup">
+    <h3 v-if="isMobile">{{ myTitle }}</h3>
+
+    <div v-if="curList.length" class="detail-popup-media">
+      <el-image
+        v-if="myType === 'img'"
+        fit="contain"
+        :src="curList[0]"
+        :preview-src-list="curList"
+      />
+
+      <video v-else-if="myType === 'video'" ref="videos" controls :src="curList[0].url" />
+    </div>
+
+    <el-scrollbar class="detail-popup-container">
+      <h3 v-if="!isMobile">{{ myTitle }}</h3>
+      <div v-html="myTxt" />
+    </el-scrollbar>
+
+    <!-- 音频播放器 -->
+    <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);
+
+    &-media {
+      max-width: utils.vw-calc(1000);
+      height: utils.vh-calc(620);
+    }
+    .el-image,
+    video {
+      width: 100%;
+      height: 100%;
+    }
+    video {
+      background: black;
+    }
+    &-container {
+      flex-shrink: 0;
+      width: utils.vw-calc(605);
+      height: auto;
+      color: #e7a770;
+
+      h3 {
+        position: relative;
+        margin-bottom: utils.vh-calc(55);
+        font-size: utils.vh-calc(30);
+        line-height: utils.vh-calc(40);
+        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;
+        }
+      }
+      > div {
+        font-size: utils.vh-calc(18);
+        line-height: utils.vh-calc(30);
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .detail-popup {
+      flex-direction: column;
+      gap: utils.vh-calc(60);
+
+      h3 {
+        position: relative;
+        margin-top: utils.vh-calc(60);
+        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);
+        padding: 0 utils.vw-calc(45);
+        max-width: 100%;
+        width: 100%;
+        height: utils.vh-calc(674);
+      }
+      &-container {
+        flex: 1;
+        height: 0;
+        width: 100%;
+        padding: 0 utils.vw-calc(45) utils.vh-calc(158);
+
+        h3 {
+          font-size: utils.vh-calc(36);
+          line-height: utils.vh-calc(48);
+        }
+        > div {
+          font-size: utils.vh-calc(28);
+          line-height: utils.vh-calc(42);
+        }
+      }
+    }
+  }
+</style>

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

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

二進制
src/index/assets/images/zb/close.png


二進制
src/index/components/info-popup/images/zb/1.jpg


文件差異過大導致無法顯示
+ 159 - 0
src/index/components/info-popup/index.zb.vue


+ 21 - 0
src/index/router/index.zb.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.zb.vue'),
+  },
+  {
+    path: '/scene',
+    name: 'home',
+    component: () => import('@/views/home'),
+  },
+];
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes,
+});
+
+export default router;

+ 8 - 6
src/index/utils/index.ts

@@ -21,11 +21,13 @@ export function parseUrlParams(url: string): Record<string, string> {
 }
 
 export function judgeIsMobile() {
-  const userAgentInfo = navigator.userAgent;
-  const mobileAgents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'];
-  const mobileFlag = mobileAgents.some((mobileAgent) => {
-    return userAgentInfo.indexOf(mobileAgent) > 0;
-  });
+  const userAgent = navigator.userAgent.toLowerCase();
+  // 匹配移动端核心特征关键词,排除桌面端误判
+  const mobileKeywords =
+    /android|iphone|ipod|ipad|ios|blackberry|webos|windows phone|mobile|symbian|iemobile|opera mini/i;
 
-  return mobileFlag;
+  const hasMobileUA = mobileKeywords.test(userAgent);
+
+  // 核心逻辑:匹配移动端UA且非桌面端,或明确是平板设备
+  return hasMobileUA || /ipad|tablet/i.test(userAgent);
 }

二進制
src/index/views/cover/images/zb/bg.jpg


二進制
src/index/views/cover/images/zb/btn.png


二進制
src/index/views/cover/images/zb/info.png


二進制
src/index/views/cover/images/zb/info2.png


二進制
src/index/views/cover/images/zb/logo.png


二進制
src/index/views/cover/images/zb/mb-bg.jpg


二進制
src/index/views/cover/images/zb/mb-info.png


二進制
src/index/views/cover/images/zb/mb-info2.png


二進制
src/index/views/cover/images/zb/mb-title.png


二進制
src/index/views/cover/images/zb/title.png


+ 87 - 0
src/index/views/cover/index.zb.scss

@@ -0,0 +1,87 @@
+@use '@/assets/utils.scss';
+
+.cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/zb/bg.jpg') no-repeat center bottom / 100% 100%;
+
+  &__logo {
+    position: absolute;
+    top: utils.vh-calc(42);
+    left: utils.vw-calc(55);
+    width: utils.vw-calc(219);
+    height: utils.vw-calc(74);
+  }
+  &__title {
+    position: absolute;
+    top: utils.vh-calc(66);
+    left: utils.vw-calc(305);
+    width: utils.vw-calc(1192);
+    height: utils.vw-calc(373);
+  }
+  &__info {
+    position: absolute;
+    right: utils.vw-calc(47);
+    bottom: utils.vh-calc(41);
+    width: utils.vw-calc(312);
+    height: utils.vw-calc(492);
+  }
+  &__info2 {
+    position: absolute;
+    left: utils.vw-calc(305);
+    bottom: utils.vh-calc(7);
+    width: utils.vw-calc(1147);
+    height: utils.vw-calc(246);
+  }
+  &__btn {
+    position: absolute;
+    right: utils.vw-calc(847);
+    bottom: utils.vh-calc(224);
+    width: utils.vw-calc(216);
+    height: utils.vw-calc(65);
+    cursor: pointer;
+  }
+}
+
+.m-cover {
+  position: absolute;
+  inset: 0;
+  background: url('./images/zb/mb-bg.jpg') no-repeat center bottom / 100% 100%;
+
+  &__logo {
+    position: absolute;
+    top: utils.vh-calc(51);
+    left: utils.vw-calc(20);
+    width: utils.vw-calc(219);
+    height: utils.vw-calc(74);
+  }
+  &__title {
+    position: absolute;
+    top: utils.vh-calc(104);
+    left: utils.vw-calc(332);
+    width: utils.vh-calc(282);
+    height: utils.vh-calc(1436);
+  }
+  &__info {
+    position: absolute;
+    top: utils.vh-calc(190);
+    left: 0;
+    width: utils.vw-calc(357);
+    height: utils.vw-calc(216);
+  }
+  &__info2 {
+    position: absolute;
+    left: utils.vw-calc(20);
+    bottom: utils.vh-calc(95);
+    width: utils.vh-calc(312);
+    height: utils.vh-calc(492);
+  }
+  &__btn {
+    position: absolute;
+    left: utils.vw-calc(33);
+    bottom: utils.vh-calc(646);
+    width: utils.vh-calc(216);
+    height: utils.vh-calc(65);
+    cursor: pointer;
+  }
+}

+ 39 - 0
src/index/views/cover/index.zb.vue

@@ -0,0 +1,39 @@
+<template>
+  <div :class="isMobile ? 'm-cover' : 'cover'">
+    <template v-if="!isMobile">
+      <img class="cover__logo" src="./images/zb/logo.png" draggable="false" />
+      <img class="cover__title" src="./images/zb/title.png" draggable="false" />
+      <img class="cover__info" src="./images/zb/info.png" draggable="false" />
+      <img class="cover__info2" src="./images/zb/info2.png" draggable="false" />
+      <img
+        class="cover__btn"
+        src="./images/zb/btn.png"
+        draggable="false"
+        @click="$router.push({ name: 'home' })"
+      />
+    </template>
+
+    <template v-else>
+      <img class="m-cover__logo" src="./images/zb/logo.png" draggable="false" />
+      <img class="m-cover__title" src="./images/zb/mb-title.png" draggable="false" />
+      <img class="m-cover__info" src="./images/zb/mb-info.png" draggable="false" />
+      <img class="m-cover__info2" src="./images/zb/mb-info2.png" draggable="false" />
+      <img
+        class="m-cover__btn"
+        src="./images/zb/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.zb.scss';
+</style>

二進制
src/index/views/home/components/ant-popup/images/zb/icon.png


二進制
src/index/views/home/components/ant-popup/images/zb/mb-close.png


二進制
src/index/views/home/components/ant-popup/images/zb/title.png


+ 289 - 0
src/index/views/home/components/ant-popup/index.zb.vue

@@ -0,0 +1,289 @@
+<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/zb/title.png" />
+    <div class="close" @click="show = false" />
+
+    <div class="ant-popup-search">
+      <div class="ant-popup-search-input">
+        <el-input
+          v-model="query"
+          placeholder="请输入展品名称"
+          @keydown.stop
+          @keyup.stop
+          @keyup.enter="search"
+        />
+        <div class="search" @click="search"></div>
+      </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(100);
+    width: utils.vw-calc(1060);
+    height: 100%;
+
+    .el-dialog__body {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+    }
+    .title {
+      position: fixed;
+      top: utils.vh-calc(20);
+      left: utils.vw-calc(111);
+      width: utils.vw-calc(197);
+      height: utils.vw-calc(65);
+    }
+    .close {
+      position: fixed;
+      top: utils.vh-calc(46);
+      right: utils.vw-calc(44);
+      width: utils.vw-calc(52);
+      height: utils.vw-calc(52);
+      background: url('@/assets/images/zb/close.png') no-repeat center / contain;
+      cursor: pointer;
+    }
+    &-scrollbar {
+      flex: 1;
+      height: 0;
+
+      ul {
+        list-style: none;
+        padding: 0;
+        margin: 0;
+        display: grid;
+        grid-template-columns: repeat(5, 1fr);
+        gap: utils.vw-calc(40);
+      }
+
+      li {
+        display: inline-block;
+        width: 100%;
+        margin: 0 0 utils.vh-calc(30) 0;
+        cursor: pointer;
+
+        .el-image {
+          padding: 10px;
+          height: utils.vh-calc(180);
+          background: linear-gradient(0deg, #595959 0%, #292929 100%);
+        }
+        p {
+          margin: utils.vh-calc(6) 0 0;
+          font-size: utils.vh-calc(14);
+          color: #ffd865;
+          text-align: center;
+        }
+      }
+
+      li > .el-image,
+      li .el-image {
+        display: block;
+        width: 100%;
+      }
+    }
+    &-search {
+      margin: utils.vh-calc(30) 0;
+      display: flex;
+      justify-content: space-between;
+      height: utils.vh-calc(40);
+      gap: utils.vw-calc(50);
+
+      &-input {
+        flex: 1;
+        display: flex;
+        border-radius: 25px;
+        padding-left: utils.vw-calc(20);
+        overflow: hidden;
+        border: 1px solid #ffd865;
+
+        .search {
+          position: relative;
+          width: utils.vw-calc(85);
+          height: 100%;
+          background-color: #ffd865;
+          cursor: pointer;
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            width: utils.vw-calc(30);
+            height: utils.vw-calc(31);
+            transform: translate(-50%, -50%);
+            background: url('./images/zb/icon.png') no-repeat center / contain;
+          }
+        }
+      }
+      .el-input {
+        --el-input-bg-color: transparent;
+        --el-input-border-color: none;
+        --el-input-hover-border-color: none;
+        --el-input-focus-border-color: none;
+        --el-input-text-color: #ffd865;
+        --el-input-placeholder-color: rgba(255, 216, 101, 0.7);
+        font-size: utils.vh-calc(18);
+
+        .el-input__inner {
+          height: 100%;
+          line-height: 100%;
+        }
+      }
+      .btn {
+        flex-shrink: 0;
+        width: utils.vw-calc(78);
+        height: 100%;
+        text-align: center;
+        font-size: utils.vw-calc(18);
+        line-height: utils.vh-calc(44);
+        border-radius: 25px;
+        cursor: pointer;
+
+        &.reset {
+          background: #ffd865;
+          color: #000000;
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: 600px) {
+    .ant-popup {
+      width: 100%;
+
+      .title {
+        display: none;
+      }
+      .close {
+        top: utils.vh-calc(29);
+        right: utils.vw-calc(44);
+        width: utils.vw-calc(60);
+        height: utils.vw-calc(60);
+        background: url('./images/zb/mb-close.png') no-repeat center / contain;
+      }
+      &-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(15) 0 0;
+            font-size: utils.vh-calc(28);
+          }
+          .el-image {
+            padding: 5px;
+            height: utils.vh-calc(320);
+          }
+        }
+      }
+      &-search {
+        margin: utils.vh-calc(40) utils.vw-calc(30);
+        height: utils.vh-calc(50);
+
+        &-input {
+          padding-left: 0;
+        }
+        .el-input {
+          font-size: utils.vh-calc(30);
+        }
+        .btn {
+          width: utils.vw-calc(115);
+          text-align: center;
+          font-size: utils.vh-calc(30);
+          line-height: utils.vh-calc(56);
+        }
+      }
+    }
+  }
+</style>

+ 182 - 0
src/index/views/home/components/menu/index.zb.scss

@@ -0,0 +1,182 @@
+@use '@/assets/utils.scss';
+
+.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: 70px;
+    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: 46px;
+    height: 46px;
+    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/zb/dislike.png') no-repeat center / contain;
+  }
+
+  &.active .icon-slot {
+    background-image: url('/images/zb/like.png');
+  }
+}
+
+.el-popper.is-dark {
+  --el-text-color-primary: #171616;
+  --el-bg-color: #ffcd4f;
+  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;
+  }
+}

+ 256 - 0
src/index/views/home/components/menu/index.zb.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.zb.vue';
+import InfoPopup from '@/components/info-popup/index.zb.vue';
+import './index.zb.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/zb/play.png"
+                  width="24"
+                  height="24"
+                  data-original-title="自动漫游"
+                />
+              </a>
+            </div>
+            <el-tooltip content="自动漫游" offset={10} disabled={this.isMobile}>
+              <div id="play" class="ui-icon">
+                <a>
+                  <img src="images/zb/play.png" width="24" height="24" />
+                </a>
+              </div>
+            </el-tooltip>
+            <div id="pause" class="ui-icon" style="display: none;">
+              <a>
+                <img title="暂停漫游" src="images/zb/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} disabled={this.isMobile}>
+              <div id="pullTab" title="">
+                <img
+                  class="icon icon-inside"
+                  src="images/zb/auto.png"
+                  title="展厅场景"
+                  data-default-url="images/zb/auto.png"
+                  data-active-url="images/zb/auto.png"
+                />
+              </div>
+            </el-tooltip>
+            <div id="hotList" title="" style="display: none">
+              <el-tooltip content="热点列表" offset={10} disabled={this.isMobile}>
+                <img class="icon icon-inside" src="images/zb/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} disabled={this.isMobile}>
+              <div id="gui-modes-dollhouse" title="" class="">
+                <img class="icon icon-inside" src="images/zb/model.png" title="迷你模型" />
+              </div>
+            </el-tooltip>
+            <div id="description" title="">
+              <el-tooltip content="展览介绍" offset={10} disabled={this.isMobile}>
+                <img
+                  class="icon icon-inside"
+                  src="images/zb/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} disabled={this.isMobile}>
+              <div
+                id="gui-modes-antlist"
+                title=""
+                class=""
+                onClick={() => (this.antVisible = true)}
+              >
+                <img class="icon icon-inside" src="images/zb/antlist.png" title="展品说明" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content={`点赞${this.likeCount}`} offset={10}>
+              <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} disabled={this.isMobile}>
+              <div id="sharing" onClick={() => (this.shareVisible = true)}>
+                <img class="icon icon-inside" src="images/zb/share.png" title="分享" />
+              </div>
+            </el-tooltip>
+            <el-tooltip content="全屏" offset={10} 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/zb/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/zb/narrow_off.png" />
+              </a>
+            </div>
+            {/* <el-tooltip content="清屏"  offset={10} disabled={this.isMobile}>
+              <div onClick={() => this.toggleSimpleMode(true)}>
+                <img class="icon icon-inside" src="images/zb/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>
+    );
+  },
+});

+ 45 - 0
src/index/views/home/components/popup/index.zb.scss

@@ -0,0 +1,45 @@
+#popup {
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  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: 46px;
+  right: 44px;
+  width: 50px;
+  height: 50px;
+  cursor: pointer;
+  text-indent: -999em;
+  background: url('@/assets/images/zb/close.png') no-repeat center / contain;
+}
+
+@media only screen and (max-width: 600px) {
+  #closepop {
+    top: unset;
+    left: 50%;
+    right: unset;
+    bottom: 33px;
+    width: 18px;
+    transform: translateX(-50%);
+  }
+}

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

@@ -0,0 +1,16 @@
+import { defineComponent } from 'vue';
+import './index.zb.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>
+    );
+  },
+});

二進制
src/index/views/home/images/zb/viewer.png


+ 132 - 0
src/index/views/home/index.zb.scss

@@ -0,0 +1,132 @@
+@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(260);
+    height: utils.vw-calc(70);
+    background: url('./images/zb/viewer.png') no-repeat center / contain;
+    z-index: 1;
+
+    span {
+      position: absolute;
+      right: utils.vw-calc(15);
+      bottom: utils.vw-calc(10);
+      color: var(--el-color-primary);
+      font-size: utils.vw-calc(18);
+      width: utils.vw-calc(85);
+      letter-spacing: 2px;
+      text-align: center;
+    }
+  }
+  &_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;
+      }
+    }
+    &-viewer {
+      display: none;
+    }
+  }
+}

+ 169 - 0
src/index/views/home/index.zb.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.zb';
+import GuiLoading from './components/gui-loading';
+import Popup from './components/popup';
+import HotSpotList from './components/hot-spot-list';
+import { homeApi } from '@/api';
+import './index.zb.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>
+    );
+  },
+});