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

feat: MKT1112 全国总工会-第三届全国工匠大会线上展会系统

chenlei 3 месяцев назад
Родитель
Сommit
e3836097bb

+ 2 - 0
package.json

@@ -8,6 +8,8 @@
     "build:test": "cross-env VITE_APP_TITLE=DEMO VITE_APP_HOT_DOMAIN=./hotspot.html run-p type-check \"build-only {@}\" --",
     "build:offline": "cross-env VITE_APP_OFFLINE=1 npm run build:test && cross-env node ./scripts/generate-offline-config.js && replace-in-file --configFile ./scripts/offline-replace-config.json",
     "push:test": "cross-env node ./scripts/publish.js",
+    "serve:zgh": "cross-env VITE_APP_SCENE=zgh VITE_APP_TITLE=全国总工会-第三届全国工匠大会线上展会系统 VITE_APP_HOT_DOMAIN=./hotspot.html vite",
+    "build:zgh:test": "cross-env VITE_APP_SCENE=zgh 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"

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

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

BIN
src/index/assets/images/zgh/auto-active-min.png


BIN
src/index/assets/images/zgh/auto-min.png


BIN
src/index/assets/images/zgh/dollhouse-active-min.png


BIN
src/index/assets/images/zgh/dollhouse-min.png


BIN
src/index/assets/images/zgh/egg.png


BIN
src/index/assets/images/zgh/floor-active-min.png


BIN
src/index/assets/images/zgh/floor-min.png


BIN
src/index/assets/images/zgh/hot-label-min.png


BIN
src/index/assets/images/zgh/hotlist-active-min.png


BIN
src/index/assets/images/zgh/hotlist-min.png


BIN
src/index/assets/images/zgh/play-active-min.png


BIN
src/index/assets/images/zgh/play-min.png


+ 10 - 0
src/index/types/home.ts

@@ -34,3 +34,13 @@ export const CameraTypeMap = {
   dollhouse: CAMERA_TYPE_ENUM.DOLLHOUSE,
   panorama: CAMERA_TYPE_ENUM.PANORAMA,
 };
+
+export type HotResourceType = {
+  imgUrl: string;
+  /** 彩蛋是否解锁 */
+  flag: boolean;
+  name: string;
+  id: string;
+  /** 拿sort当id */
+  sort: number;
+};

+ 7 - 0
src/index/views/home/components/guide/index.scss

@@ -201,6 +201,13 @@
   }
   .frame .thumbImg {
     width: 103px;
+
+    .overlay {
+      font-size: 12px;
+    }
+  }
+  #drawer.open {
+    height: 110px;
   }
   #drawer.open {
     height: 110px;

+ 2 - 1
src/index/views/home/components/hot-spot-list/index.scss

@@ -107,7 +107,8 @@
     width: 100%;
     max-width: 100%;
     height: 250px;
-    transition: top 0.4s, width 0.5s;
+    will-change: top;
+    transition: top linear 0.4s;
     background: rgba(34, 36, 37, 0.9);
   }
   .hotListActive {

+ 8 - 3
src/index/views/home/components/hot-spot-list/index.tsx

@@ -1,4 +1,4 @@
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 import className from 'classname';
 import { storeToRefs } from 'pinia';
 import useBaseStore from '@/store/module/base';
@@ -10,9 +10,14 @@ export default defineComponent({
   setup() {
     const baseStore = useBaseStore();
     const { hotListVisible, hotList } = storeToRefs(baseStore);
+    const realHotList = computed(() =>
+      hotList.value.filter((i) => i.title.indexOf('彩蛋&') === -1)
+    );
 
     const handleClose = () => {
       baseStore.setState('hotListVisible', false);
+      // @ts-ignore
+      window.parent?.FaIconStateFu?.(true);
     };
 
     const openDetail = (data: HotDataType) => {
@@ -25,7 +30,7 @@ export default defineComponent({
     };
 
     return {
-      hotList,
+      realHotList,
       hotListVisible,
       handleClose,
       openDetail,
@@ -41,7 +46,7 @@ export default defineComponent({
         </div>
         <div id="hotListContent">
           <ul>
-            {this.hotList.map((i) => (
+            {this.realHotList.map((i) => (
               <li key={i.sid} onClick={this.openDetail.bind(undefined, i)}>
                 <span>{i.title}</span>
               </li>

+ 93 - 0
src/index/views/home/components/menu/index.zgh.scss

@@ -0,0 +1,93 @@
+.pinBottom-container {
+  position: absolute;
+  bottom: 10px;
+  width: 100%;
+  transition: all 0.5s;
+  z-index: var(--z-index-top);
+
+  &.open {
+    bottom: 140px;
+
+    &.playing {
+      bottom: 140px;
+    }
+    &.noScroll {
+      bottom: 120px;
+    }
+  }
+  &.playing:not(.open) {
+    bottom: 30px;
+  }
+}
+
+.pinBottom {
+  position: absolute;
+  bottom: 0;
+  line-height: 1;
+  transition: all 0.5s;
+
+  &.left {
+    display: flex;
+    gap: 15px;
+    left: 10px;
+    bottom: 0;
+    overflow: hidden;
+  }
+  > div {
+    width: 60px;
+    height: 60px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+
+    &.active,
+    &:hover,
+    &.opened {
+      opacity: 0.8;
+    }
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+}
+
+@media only screen and (max-width: 600px) {
+  .pinBottom-container {
+    bottom: 50px;
+
+    &.open {
+      bottom: 150px;
+
+      &.noScroll {
+        bottom: 140px;
+      }
+      &.playing {
+        bottom: 155px;
+      }
+    }
+    &.playing:not(.open) {
+      bottom: 70px;
+    }
+  }
+  .pinBottom {
+    display: flex;
+
+    &.left {
+      gap: 10px;
+      flex-direction: column;
+      left: 10px;
+    }
+    > div {
+      width: 44px;
+      height: 44px;
+    }
+  }
+  #play,
+  #pause {
+    width: 44px;
+    height: 44px;
+  }
+}

+ 186 - 0
src/index/views/home/components/menu/index.zgh.tsx

@@ -0,0 +1,186 @@
+import { defineComponent, ref, watch } from 'vue';
+import className from 'classname';
+import useBaseStore from '@/store/module/base';
+import { storeToRefs } from 'pinia';
+import { useRoute } from 'vue-router';
+import Guide from '../guide';
+import { useFullscreen } from '@vueuse/core';
+import { CAMERA_TYPE_ENUM } from '@/types/home';
+import Rules from '../rules/index.vue';
+import type { IRulesMethods } from '@/types';
+import AutoIcon from '@/assets/images/zgh/auto-min.png';
+import AutoActiveIcon from '@/assets/images/zgh/auto-active-min.png';
+import DollhouseIcon from '@/assets/images/zgh/dollhouse-min.png';
+import DollhouseActiveIcon from '@/assets/images/zgh/dollhouse-active-min.png';
+import FloorIcon from '@/assets/images/zgh/floor-min.png';
+import FloorActiveIcon from '@/assets/images/zgh/floor-active-min.png';
+import HotlistIcon from '@/assets/images/zgh/hotlist-min.png';
+import HotlistActiveIcon from '@/assets/images/zgh/hotlist-active-min.png';
+import PlayIcon from '@/assets/images/zgh/play-min.png';
+import PlayActiveIcon from '@/assets/images/zgh/play-active-min.png';
+import './index.zgh.scss';
+
+export default defineComponent({
+  name: 'HomeMenu',
+  props: {
+    mode: {
+      type: Number,
+      default: CAMERA_TYPE_ENUM.PANORAMA,
+    },
+  },
+  setup() {
+    const baseStore = useBaseStore();
+    const route = useRoute();
+    const { isFullscreen, isSupported, toggle } = useFullscreen();
+    const { kankanInited, guidePlaying, hotList, hotListVisible, bgMusicDom, bgMusicPlaying } =
+      storeToRefs(baseStore);
+    const guideVisible = ref(false);
+    const rulesRef = ref<IRulesMethods>();
+
+    const handlePlayGuide = async () => {
+      const player = await window.kankan.TourManager.player;
+      const playing = guidePlaying.value;
+
+      if (playing) {
+        player.pause();
+      } else {
+        player.play();
+        guideVisible.value = true;
+      }
+
+      baseStore.setState('guidePlaying', !playing);
+    };
+
+    const handleDollhouse = () => {
+      window.kankan.Camera.dollhouse();
+    };
+    const handleFloorplan = () => {
+      window.kankan.Camera.floorplan();
+    };
+    const handlePanorama = () => {
+      window.kankan.Camera.panorama();
+    };
+    const openHotList = () => {
+      const type = !hotListVisible.value;
+      baseStore.setState('hotListVisible', type);
+      // @ts-ignore
+      window.parent?.FaIconStateFu?.(!type);
+    };
+    const startMeasure = () => {
+      rulesRef.value?.startMeasure();
+    };
+
+    const handleBgMusic = () => {
+      if (bgMusicPlaying.value) {
+        bgMusicDom?.value?.pause();
+      } else {
+        bgMusicDom?.value?.play();
+      }
+
+      baseStore.setState('bgMusicPlaying', !bgMusicPlaying.value);
+    };
+
+    watch(kankanInited, async (v) => {
+      if (!v) return;
+
+      const detail = await window.kankan.store.get('metadata');
+
+      if (detail.music) {
+        // 存在背景音乐
+        const dom = document.createElement('audio');
+        dom.src = `${import.meta.env.VITE_APP_BACKEND_URL}/scene_view_data/${
+          route.query.m
+        }/user/music-user.mp3`;
+        dom.loop = true;
+        document.body.appendChild(dom);
+        baseStore.setState('bgMusicDom', dom);
+      }
+    });
+
+    return {
+      hotList,
+      hotListVisible,
+      isSupported,
+      isFullscreen,
+      guideVisible,
+      guidePlaying,
+      bgMusicDom,
+      bgMusicPlaying,
+      rulesRef,
+      toggle,
+      openHotList,
+      startMeasure,
+      handlePlayGuide,
+      handleFloorplan,
+      handleDollhouse,
+      handlePanorama,
+      handleBgMusic,
+    };
+  },
+  render() {
+    return (
+      <>
+        {!this.rulesRef?.showRuleBox && (
+          <>
+            <div
+              class={className('pinBottom-container', {
+                open: this.guideVisible,
+                playing: this.guidePlaying,
+                noScroll: !this.guidePlaying,
+              })}
+            >
+              <div class="pinBottom left">
+                <div id="play" class="ui-icon" onClick={this.handlePlayGuide}>
+                  {!this.guidePlaying ? (
+                    <img src={PlayIcon} />
+                  ) : (
+                    <img title="暂停" src={PlayActiveIcon} />
+                  )}
+                </div>
+                <div
+                  id="pullTab"
+                  class={className({ opened: this.guideVisible })}
+                  onClick={() => (this.guideVisible = !this.guideVisible)}
+                >
+                  <img
+                    class="icon icon-inside"
+                    src={this.guideVisible ? AutoActiveIcon : AutoIcon}
+                    title="导览"
+                  />
+                </div>
+                {Boolean(this.hotList.length) && (
+                  <div id="hotList" onClick={this.openHotList}>
+                    <img
+                      class="icon icon-inside"
+                      src={this.hotListVisible ? HotlistActiveIcon : HotlistIcon}
+                      title="热点列表"
+                    />
+                  </div>
+                )}
+                <div id="gui-modes-dollhouse" onClick={this.handleDollhouse}>
+                  <img
+                    class="icon icon-inside"
+                    src={
+                      this.mode === CAMERA_TYPE_ENUM.DOLLHOUSE ? DollhouseActiveIcon : DollhouseIcon
+                    }
+                    title="迷你模型"
+                  />
+                </div>
+                <div id="gui-modes-floorplan" onClick={this.handleFloorplan}>
+                  <img
+                    class="icon icon-inside"
+                    src={this.mode === CAMERA_TYPE_ENUM.FLOORPLAN ? FloorActiveIcon : FloorIcon}
+                    title="俯视图"
+                  />
+                </div>
+              </div>
+            </div>
+          </>
+        )}
+
+        <Guide open={this.guideVisible} playing={this.guidePlaying} />
+        <Rules ref="rulesRef" />
+      </>
+    );
+  },
+});

+ 131 - 0
src/index/views/home/index.zgh.scss

@@ -0,0 +1,131 @@
+.home {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+
+  &_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);
+    }
+  }
+}
+
+#player {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+
+  .player 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);
+  }
+}
+
+.hot {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 150px;
+  height: 150px;
+  cursor: pointer;
+  transform: translate(-50%, -50%);
+  background: url('@/assets/images/zgh/egg.png') no-repeat center / contain;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: -20px;
+    left: 50%;
+    width: 94px;
+    height: 40px;
+    transform: translateX(-50%);
+    background: url('@/assets/images/zgh/hot-label-min.png') no-repeat center / contain;
+  }
+}
+
+[xui_min_map] {
+  top: 20px !important;
+  right: 20px !important;
+  transition: all 0.3s;
+}
+
+@media only screen and (max-width: 600px) {
+  .home {
+    &_logo {
+      width: 200px;
+
+      span {
+        font-size: 14px;
+      }
+    }
+  }
+
+  [xui_min_map] {
+    width: 101px !important;
+    height: 88px !important;
+    top: 18px !important;
+    right: 17px !important;
+  }
+}

+ 245 - 0
src/index/views/home/index.zgh.tsx

@@ -0,0 +1,245 @@
+import { defineComponent, onMounted, ref } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useRoute } from 'vue-router';
+import Other from './components/other';
+import Menu from './components/menu';
+import GuiLoading from './components/gui-loading';
+import Popup from './components/popup/index.vue';
+import HotSpotList from './components/hot-spot-list';
+import type { HotDataType } from '@/types';
+import useBaseStore from '@/store/module/base';
+import { CAMERA_TYPE_ENUM, CameraTypeMap, type HotResourceType } from '@/types/home';
+import './index.zgh.scss';
+
+export default defineComponent({
+  name: 'home',
+  components: {
+    Other,
+    GuiLoading,
+    Popup,
+  },
+  setup() {
+    const baseStore = useBaseStore();
+    const { checkedHotData, hotVisible } = storeToRefs(baseStore);
+    const route = useRoute();
+    const cameraMode = ref(CAMERA_TYPE_ENUM.PANORAMA);
+    let hotResources: HotResourceType[] = [];
+    let hotTagLoaded = false;
+
+    const init = () => {
+      // @ts-ignore
+      const kankan = new KanKan({
+        dom: '#player',
+        num: route.query.m,
+        server: 'https://www.4dkankan.com',
+      });
+
+      kankan.use('MinMap', {
+        theme: {
+          camera_fillStyle: '#fc7107',
+        },
+      });
+
+      // 全部热点数据
+      kankan.store.on('tags', (tags) => {
+        baseStore.setState('hotList', tags.tags);
+      });
+
+      // 监听看看的模式
+      kankan.Camera.on('mode.beforeChange', ({ toMode }) => {
+        cameraMode.value = CameraTypeMap[toMode];
+      });
+
+      // 有关球幕视频控制背景音乐
+      kankan.Scene.on('panorama.videorenderer.startvideo', () => {
+        // 进入球幕视频
+        handleBgMusic(false);
+      });
+      kankan.Scene.on('panorama.videorenderer.resumerender', () => {
+        // 进入球幕视频
+        handleBgMusic(false);
+      });
+      kankan.Scene.on('panorama.videorenderer.suspendrender', () => {
+        // 退出球幕视频
+        handleBgMusic(true);
+      });
+
+      window.kankan = kankan;
+      baseStore.setState('kankanInited', true);
+    };
+
+    const initHot = () => {
+      console.log('热点初始化', typeof window.kankan);
+
+      // 热点
+      window.kankan
+        .use('TagView', {
+          // 自定义热点图标
+          render(data) {
+            const prefix = '彩蛋&';
+            const preIndex = data.title.indexOf(prefix);
+
+            console.log('初始化彩蛋:', preIndex, data.title);
+            if (preIndex === -1) return null;
+
+            const number = data.title.slice(preIndex + prefix.length);
+            const item = hotResources.find((i) => i.sort === Number(number));
+
+            if (item?.flag) return '<div></div>';
+
+            return `<div class='hot'></div>`;
+          },
+        })
+        .then((TagView) => {
+          window.TagView = TagView;
+
+          // 监听手动点击事件
+          TagView.on('click', (e) => {
+            const prefix = '彩蛋&';
+            const preIndex = e.data.title.indexOf(prefix);
+
+            if (preIndex > -1) {
+              // 触发彩蛋
+              const number = e.data.title.slice(preIndex + prefix.length);
+              // @ts-ignore
+              window.parent?.FaStrikeEggFu?.(Number(number));
+
+              // const $tag = $('[data-tag-id="' + e.data.sid + '"]');
+              // $tag && $tag.addClass('hidden');
+              return;
+            }
+
+            const isLink = e.data.title.startsWith('(link)');
+
+            if (isLink) {
+              const regex = new RegExp(`<\/?p[^>]*>`, 'g');
+              const link = e.data.content.replace(regex, '');
+              const isHttp = link.startsWith('http');
+              window.location.href = isHttp ? link : `https://scene.4dage.com${link}`;
+            } else {
+              // 隐藏工具栏
+              // @ts-ignore
+              window.parent?.FaIconStateFu?.(false);
+              baseStore.setState('checkedHotData', e.data);
+              baseStore.setState('hotVisible', true);
+            }
+          });
+        });
+
+      window.kankan.render();
+    };
+
+    const handleBgMusic = (type = true) => {
+      if (type) {
+        baseStore.bgMusicDom?.play();
+      } else {
+        baseStore.bgMusicDom?.pause();
+      }
+      baseStore.setState('bgMusicPlaying', type);
+    };
+
+    const closeHotDetail = () => {
+      baseStore.setState('hotVisible', false);
+
+      // @ts-ignore
+      window.parent?.FaIconStateFu?.(true);
+    };
+
+    let once = false;
+    const handlePageClick = () => {
+      if (once) return;
+
+      handleBgMusic();
+      once = true;
+    };
+
+    onMounted(() => {
+      init();
+
+      // initHot();
+
+      // @ts-ignore
+      window.sonGetListFu = (val, code) => {
+        // 子页面接受父页面传过来的数据
+        hotResources = val;
+
+        console.log('热点初始化状态:', hotTagLoaded);
+        if (hotTagLoaded) {
+          $('[data-tag-id]').each(function (idx, el) {
+            const $current = $(this);
+            const tagId = $current.attr('data-tag-id');
+            const item = baseStore.hotList.find((i) => i.sid === tagId);
+            if (!item) return;
+            const item2 = hotResources.find((i) => item.title === '彩蛋&' + i.sort);
+            if (!item2 || item2.flag) {
+              const $tag = $('[data-tag-id="' + item.sid + '"]');
+              $tag && $tag.addClass('hidden');
+            }
+          });
+        } else {
+          initHot();
+          hotTagLoaded = true;
+        }
+      };
+
+      // @ts-ignore
+      window.sonEggUpFu = (id) => {
+        // console.log('子页面接受父页面-关闭彩蛋', id)
+        const item = baseStore.hotList.find((i) => i.title === '彩蛋&' + id);
+        if (!item) return;
+        const $tag = $('[data-tag-id="' + item.sid + '"]');
+        $tag && $tag.addClass('hidden');
+      };
+    });
+
+    return {
+      hotVisible,
+      checkedHotData,
+      cameraMode,
+      closeHotDetail,
+      handlePageClick,
+    };
+  },
+  render() {
+    return (
+      <div class="home" onClick={this.handlePageClick}>
+        {/* 进度条加载 */}
+        <GuiLoading />
+
+        {/* 加载初始页面 */}
+        <div id="gui-thumb" />
+
+        {/* 热点弹出框 */}
+        <Popup
+          hotData={this.checkedHotData}
+          visible={this.hotVisible}
+          onUpdate:visible={this.closeHotDetail}
+        />
+
+        {/* 场景canvs主容器 */}
+        <div id="player" />
+
+        {/* 底部菜单 */}
+        <div id="gui-parent">
+          {/* 热点气泡 */}
+          <div id="hot" />
+
+          <div id="gui">
+            {/* 热点列表 */}
+            <HotSpotList />
+
+            {/* 底部菜单 */}
+            <Menu mode={this.cameraMode} />
+
+            {/* <div class="home_logo">
+              <img src="images/btm_logo.png" />
+              <span>提供技术支持</span>
+            </div> */}
+          </div>
+
+          <Other />
+        </div>
+      </div>
+    );
+  },
+});