介绍

@nicepkg/vr360-core 是一个基于 threejs在新窗口打开 的全景库,非常适合用来做全景看房、全景街景、全景景点等业务需求。

它支持 json 配置驱动视图,你可以用 json 配置的方式快速实现常见全景需求。

特性

  • json 驱动视图,快速实现全景业务需求
  • 高性能,支持自动找出 json 变更的部分,最小化更新到 3d 全景里,不会整个场景重建。
  • 支持增删改提示点,hover 提示点的弹窗支持自定义定制,支持自定义提示点的贴图。
  • 内置场景切换穿梭动画
  • 暴露 scene、camera、renderer 等 threejs在新窗口打开 对象,方便你更高的定制化
  • 基于事件驱动的数据交互,设计框架无关性
  • 完整的 ts 支持

安装

npm i @nicepkg/vr360-core threejs
yarn add @nicepkg/vr360-core threejs
pnpm add @nicepkg/vr360-core threejs

浏览器(CDN)

<!-- 引入 threejs -->
<script src="https://unpkg.com/three@0.145.0/build/three.min.js"></script>

<!-- 引入 vr360-core -->
<script src="https://unpkg.com/@nicepkg/vr360-core@0.3.1"></script>

<script>
// 使用
const {Vr360} = Vr360Core // 原生使用时,所有的导出都在 window.Vr360Core 里

// 初始化全景实例
const vr360 = new Vr360({...})

// 开始渲染全景
vr360.render()
</script>
<!-- 引入 threejs -->
<script src="https://cdn.jsdelivr.net/npm/three@0.145.0/build/three.min.js"></script>

<!-- 引入 vr360-core -->
<script src="https://cdn.jsdelivr.net/npm/@nicepkg/vr360-core@0.3.1"></script>

<script>
// 使用
const {Vr360} = Vr360Core // 原生使用时,所有的导出都在 window.Vr360Core 里

// 初始化全景实例
const vr360 = new Vr360({...})

// 开始渲染全景
vr360.render()
</script>

为什么

如果产品叫你实现一个类似全景看房的功能,而且时间紧,你会怎么做?

学 threejs 知识,然后徒手撸出全景,然后自己抽象出一个 json 配置驱动和后端数据联调对接?

可能还要自己写一个全景编辑器?

可以但没必要,你完全可以把时间省下来做些有意义的事,你只要用 @nicepkg/vr360-core 就可以了。

简单点的业务需求仅需 json 配置就能实现,重要是这种代码拉条哈士奇也能维护。

编辑器还在开发中,复杂度不会消失,只会转移,当你岁月静好,一定是作者在为你负重前行。

使用

请阅读我们的 属性文档函数文档事件文档,了解如何使用 @nicepkg/vr360-core

除了下面的简易示例,你也可以浏览我们的 完整项目示例

简易示例效果


简易示例代码(cv 专用)


<template>
  <div class="demo">
    <!-- 提示 -->
    <div
      ref="tipRef"
      class="demo-tip"
      :style="{
        transform: `translate(${tip.left}px, ${tip.top + 50}px)`,
        zIndex: tip.show ? 99 : -1,
        visibility: tip.show ? 'visible' : 'hidden'
      }"
    >
      <!-- 提示标题 -->
      <div class="demo-tip-title">{{ tip.title }}</div>

      <!-- 提示内容 -->
      <div class="demo-tip-content">{{ tip.content }}</div>
    </div>

    <!-- 360全景容器 -->
    <div ref="containerRef" class="demo-container"></div>

    <!-- 底部切换场景 -->
    <div class="demo-bottom-bar">
      <div
        v-for="space in spacesConfig"
        :key="space.id"
        class="demo-bottom-card"
        @click="handleSwitchSpace(space)"
        :style="{
          backgroundImage: `url(${space.cubeSpaceTextureUrls.left})`
        }"
      ></div>
    </div>
  </div>
</template>
<script lang="ts">
import {Vr360} from '@nicepkg/vr360-core'
import type {SpaceConfig} from '@nicepkg/vr360-core'

export default {
  data() {
    // 全景空间配置
    const spacesConfig: SpaceConfig[] = [
      {
        id: 'spaceA', // 空间 id,用于切换空间,必须唯一
        tips: [
          // 提示,可选
          {
            id: '1', // 提示 id,用于缓存,在当前 tips 数组里要唯一,必须
            position: {x: 0, y: -10, z: 40}, // 提示位置,必须
            content: {
              // 提示内容,在 showTip 事件会暴露,要包含什么你自己决定。
              title: '豪华跑车',
              text: '比奥迪还贵的豪华跑车'
            }
          },
          {
            id: '2',
            // 自定义提示图标贴图,可选
            textureUrl:
              'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
            targetSpaceId: 'spaceB', // 提示点击后跳转的空间 id,可选
            position: {x: -10, y: -4, z: 40},
            content: {
              title: '去客厅',
              text: '一起去尊贵的客厅吧'
            }
          }
        ],
        cubeSpaceTextureUrls: {
          // 立方体贴图,分别为:背侧、下侧、前侧、左侧、右侧、上侧,必须
          back: 'https://m.360buyimg.com/babel/jfs/t1/40814/31/19646/41953/63398297E0707fe35/4e831e60cf579899.jpg',
          down: 'https://m.360buyimg.com/babel/jfs/t1/43941/23/19369/84038/633982d7E838acd9a/9e7a89cc910d3409.jpg',
          front: 'https://m.360buyimg.com/babel/jfs/t1/150573/35/27827/113528/633982faE8556c0c0/b455284fd91885c6.jpg',
          left: 'https://m.360buyimg.com/babel/jfs/t1/189204/25/29491/61430/63398310E3c180e43/26d9b459cf714f9f.jpg',
          right: 'https://m.360buyimg.com/babel/jfs/t1/184948/34/28448/131232/63398323E61ff80fb/89ab84eda0421260.jpg',
          up: 'https://m.360buyimg.com/babel/jfs/t1/86258/24/33602/70616/63398334Ea2dcf448/e2f5c275792fb3d6.jpg'
        }
      },
      {
        id: 'spaceB',
        tips: [
          {
            id: '3',
            position: {x: -2, y: -25, z: 40},
            content: {
              title: '香奈儿垃圾桶',
              text: '里面装着主人不用的奢侈品'
            }
          },
          {
            id: '4',
            position: {x: -20, y: 0, z: 40},
            content: {
              title: '宇宙牌冰箱',
              text: '装着超级多零食'
            }
          },
          {
            id: '5',
            textureUrl:
              'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
            targetSpaceId: 'spaceA',
            position: {
              x: -8,
              y: 0,
              z: -40
            },
            content: {
              title: '去门口',
              text: '一起去门口吧'
            }
          }
        ],
        cubeSpaceTextureUrls: {
          back: 'https://m.360buyimg.com/babel/jfs/t1/48117/28/21445/120448/63398366Ede81497b/a46e362df5f7d0ed.jpg',
          down: 'https://m.360buyimg.com/babel/jfs/t1/101209/24/26762/106253/63398376Eedb0db22/4f335c4ecd72ad74.jpg',
          front: 'https://m.360buyimg.com/babel/jfs/t1/154056/6/26449/110652/63398388E8ecda044/22e1646534839a95.jpg',
          left: 'https://m.360buyimg.com/babel/jfs/t1/198990/28/28201/74687/6339839aE28806a5e/43b311d3379397df.jpg',
          right: 'https://m.360buyimg.com/babel/jfs/t1/209711/14/25233/92186/633983b0E8f4df687/750ba84061ea64a6.jpg',
          up: 'https://m.360buyimg.com/babel/jfs/t1/186545/35/29054/29678/633983c2E72ef4848/92043b945a03fc29.jpg'
        }
      }
    ]

    return {
      vr360: null as InstanceType<typeof Vr360> | null, // 全景实例
      spacesConfig, // 全景空间配置
      tip: {
        // 提示属性
        top: 0, // 提示容器的 top 或 translateY 值
        left: 0, // 提示容器的 left 或 translateX 值
        title: '', // 提示标题
        content: '', // 提示内容
        show: false // 是否显示提示
      }
    }
  },
  mounted() {
    // 初始化全景实例
    this.vr360 = new Vr360({
      container: this.$refs.containerRef!,
      tipContainer: this.$refs.tipRef!,
      spacesConfig: this.spacesConfig
    })

    // 设置全景自动旋转
    this.vr360.controls.autoRotate = true

    // 开始渲染全景
    this.vr360.render()

    // 实时监听页面尺寸变化,更新全景尺寸
    this.vr360.listenResize()

    // 当需要显示提示时
    this.vr360.on('showTip', e => {
      // 停止旋转场景
      this.vr360!.controls.autoRotate = false

      // 设置提示内容和提示容器位置
      const {top, left, tip} = e
      this.tip = {
        top,
        left,
        title: tip.content.title,
        content: tip.content.text,
        show: true
      }
    })

    // 当需要隐藏提示时
    this.vr360.on('hideTip', () => {
      // 重新开启旋转场景
      this.vr360!.controls.autoRotate = true

      // 隐藏提示容器
      this.tip.show = false
    })
  },
  destroy() {
    // 页面卸载时注销全景实例
    this.vr360?.destroy?.()
  },
  methods: {
    // 切换全景空间
    handleSwitchSpace(space: SpaceConfig) {
      this.vr360?.switchSpace?.(space.id)
    }
  }
}
</script>
<style>
/* 引入自己的样式 */
@import './demo.css';
</style>
<template>
  <div class="demo">
    <!-- 提示 -->
    <div
      ref="tipRef"
      class="demo-tip"
      :style="{
        transform: `translate(${tipLeft}px, ${tipTop + 50}px)`,
        zIndex: showTip ? 99 : -1,
        visibility: showTip ? 'visible' : 'hidden'
      }"
    >
      <!-- 提示标题 -->
      <div class="demo-tip-title">{{ tipTitle }}</div>

      <!-- 提示内容 -->
      <div class="demo-tip-content">{{ tipContent }}</div>
    </div>

    <!-- 360全景容器 -->
    <div ref="containerRef" class="demo-container"></div>

    <!-- 底部切换场景 -->
    <div class="demo-bottom-bar">
      <div
        v-for="space in spacesConfig"
        :key="space.id"
        class="demo-bottom-card"
        @click="handleSwitchSpace(space)"
        :style="{
          backgroundImage: `url(${space.cubeSpaceTextureUrls.left})`
        }"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {onMounted, onUnmounted, ref} from 'vue'
import {Vr360} from '@nicepkg/vr360-core'
import type {SpaceConfig} from '@nicepkg/vr360-core'

const containerRef = ref<HTMLElement>() // 全景容器

const tipRef = ref<HTMLElement>() // 提示容器
const tipLeft = ref(0) // 提示容器的 left 或 translateX 值
const tipTop = ref(0) // 提示容器的 top 或 translateY 值
const showTip = ref(false) // 是否显示提示容器
const tipTitle = ref('') // 提示容器的标题
const tipContent = ref('') // 提示容器的内容

let vr360: InstanceType<typeof Vr360> // 全景实例

// 全景空间配置
const spacesConfig: SpaceConfig[] = [
  {
    id: 'spaceA', // 空间 id,用于切换空间,必须唯一
    tips: [
      // 提示,可选
      {
        id: '1', // 提示 id,用于缓存,在当前 tips 数组里要唯一,必须
        position: {x: 0, y: -10, z: 40}, // 提示位置,必须
        content: {
          // 提示内容,在 showTip 事件会暴露,要包含什么你自己决定。
          title: '豪华跑车',
          text: '比奥迪还贵的豪华跑车'
        }
      },
      {
        id: '2',
        // 自定义提示图标贴图,可选
        textureUrl: 'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
        targetSpaceId: 'spaceB', // 提示点击后跳转的空间 id,可选
        position: {x: -10, y: -4, z: 40},
        content: {
          title: '去客厅',
          text: '一起去尊贵的客厅吧'
        }
      }
    ],
    cubeSpaceTextureUrls: {
      // 立方体贴图,分别为:背侧、下侧、前侧、左侧、右侧、上侧,必须
      back: 'https://m.360buyimg.com/babel/jfs/t1/40814/31/19646/41953/63398297E0707fe35/4e831e60cf579899.jpg',
      down: 'https://m.360buyimg.com/babel/jfs/t1/43941/23/19369/84038/633982d7E838acd9a/9e7a89cc910d3409.jpg',
      front: 'https://m.360buyimg.com/babel/jfs/t1/150573/35/27827/113528/633982faE8556c0c0/b455284fd91885c6.jpg',
      left: 'https://m.360buyimg.com/babel/jfs/t1/189204/25/29491/61430/63398310E3c180e43/26d9b459cf714f9f.jpg',
      right: 'https://m.360buyimg.com/babel/jfs/t1/184948/34/28448/131232/63398323E61ff80fb/89ab84eda0421260.jpg',
      up: 'https://m.360buyimg.com/babel/jfs/t1/86258/24/33602/70616/63398334Ea2dcf448/e2f5c275792fb3d6.jpg'
    }
  },
  {
    id: 'spaceB',
    tips: [
      {
        id: '3',
        position: {x: -2, y: -25, z: 40},
        content: {
          title: '香奈儿垃圾桶',
          text: '里面装着主人不用的奢侈品'
        }
      },
      {
        id: '4',
        position: {x: -20, y: 0, z: 40},
        content: {
          title: '宇宙牌冰箱',
          text: '装着超级多零食'
        }
      },
      {
        id: '5',
        textureUrl: 'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
        targetSpaceId: 'spaceA',
        position: {
          x: -8,
          y: 0,
          z: -40
        },
        content: {
          title: '去门口',
          text: '一起去门口吧'
        }
      }
    ],
    cubeSpaceTextureUrls: {
      back: 'https://m.360buyimg.com/babel/jfs/t1/48117/28/21445/120448/63398366Ede81497b/a46e362df5f7d0ed.jpg',
      down: 'https://m.360buyimg.com/babel/jfs/t1/101209/24/26762/106253/63398376Eedb0db22/4f335c4ecd72ad74.jpg',
      front: 'https://m.360buyimg.com/babel/jfs/t1/154056/6/26449/110652/63398388E8ecda044/22e1646534839a95.jpg',
      left: 'https://m.360buyimg.com/babel/jfs/t1/198990/28/28201/74687/6339839aE28806a5e/43b311d3379397df.jpg',
      right: 'https://m.360buyimg.com/babel/jfs/t1/209711/14/25233/92186/633983b0E8f4df687/750ba84061ea64a6.jpg',
      up: 'https://m.360buyimg.com/babel/jfs/t1/186545/35/29054/29678/633983c2E72ef4848/92043b945a03fc29.jpg'
    }
  }
]

onMounted(() => {
  // 初始化全景实例
  vr360 = new Vr360({
    container: containerRef.value!,
    tipContainer: tipRef.value!,
    spacesConfig
  })

  // 设置全景自动旋转
  vr360.controls.autoRotate = true

  // 开始渲染全景
  vr360.render()

  // 实时监听页面尺寸变化,更新全景尺寸
  vr360.listenResize()

  // 当需要显示提示时
  vr360.on('showTip', e => {
    // 停止旋转场景
    vr360!.controls.autoRotate = false

    // 设置提示内容和提示容器位置
    const {top, left, tip} = e
    showTip.value = true
    tipLeft.value = left
    tipTop.value = top
    tipTitle.value = tip.content.title
    tipContent.value = tip.content.text
  })

  // 当需要隐藏提示时
  vr360.on('hideTip', () => {
    // 重新开启旋转场景
    vr360!.controls.autoRotate = true

    // 隐藏提示容器
    showTip.value = false
  })
})

// 切换全景空间
function handleSwitchSpace(space: SpaceConfig) {
  vr360.switchSpace(space.id)
}

onUnmounted(() => {
  // 页面卸载时注销全景实例
  vr360?.destroy?.()
})
</script>
<style>
/* 引入自己的样式 */
@import './demo.css';
</style>
import React, {useEffect, useState, useRef} from 'react'
import {Vr360} from '@nicepkg/vr360-core'
import type {SpaceConfig} from '@nicepkg/vr360-core'
import './demo.css' // 引入自己的样式

function Example() {
  const containerRef = useRef<HTMLDivElement>(null) // 全景容器
  const tipRef = useRef<HTMLDivElement>(null) // 提示容器
  const [tipLeft, setTipLeft] = useState(0) // 提示容器的 left 或 translateX 值
  const [tipTop, setTipTop] = useState(0) // 提示容器的 top 或 translateY 值
  const [showTip, setShowTip] = useState(false) // 是否显示提示容器
  const [tipTitle, setTipTitle] = useState('') // 提示容器的标题
  const [tipContent, setTipContent] = useState('') // 提示容器的内容
  const [vr360, setVr360] = useState<InstanceType<typeof Vr360>>() // 全景实例

  // 全景空间配置
  const spacesConfig: SpaceConfig[] = [
    {
      id: 'spaceA', // 空间 id,用于切换空间,必须唯一
      tips: [
        // 提示,可选
        {
          id: '1', // 提示 id,用于缓存,在当前 tips 数组里要唯一,必须
          position: {x: 0, y: -10, z: 40}, // 提示位置,必须
          content: {
            // 提示内容,在 showTip 事件会暴露,要包含什么你自己决定。
            title: '豪华跑车',
            text: '比奥迪还贵的豪华跑车'
          }
        },
        {
          id: '2',
          // 自定义提示图标贴图,可选
          textureUrl:
            'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
          targetSpaceId: 'spaceB', // 提示点击后跳转的空间 id,可选
          position: {x: -10, y: -4, z: 40},
          content: {
            title: '去客厅',
            text: '一起去尊贵的客厅吧'
          }
        }
      ],
      cubeSpaceTextureUrls: {
        // 立方体贴图,分别为:背侧、下侧、前侧、左侧、右侧、上侧,必须
        back: 'https://m.360buyimg.com/babel/jfs/t1/40814/31/19646/41953/63398297E0707fe35/4e831e60cf579899.jpg',
        down: 'https://m.360buyimg.com/babel/jfs/t1/43941/23/19369/84038/633982d7E838acd9a/9e7a89cc910d3409.jpg',
        front: 'https://m.360buyimg.com/babel/jfs/t1/150573/35/27827/113528/633982faE8556c0c0/b455284fd91885c6.jpg',
        left: 'https://m.360buyimg.com/babel/jfs/t1/189204/25/29491/61430/63398310E3c180e43/26d9b459cf714f9f.jpg',
        right: 'https://m.360buyimg.com/babel/jfs/t1/184948/34/28448/131232/63398323E61ff80fb/89ab84eda0421260.jpg',
        up: 'https://m.360buyimg.com/babel/jfs/t1/86258/24/33602/70616/63398334Ea2dcf448/e2f5c275792fb3d6.jpg'
      }
    },
    {
      id: 'spaceB',
      tips: [
        {
          id: '3',
          position: {x: -2, y: -25, z: 40},
          content: {
            title: '香奈儿垃圾桶',
            text: '里面装着主人不用的奢侈品'
          }
        },
        {
          id: '4',
          position: {x: -20, y: 0, z: 40},
          content: {
            title: '宇宙牌冰箱',
            text: '装着超级多零食'
          }
        },
        {
          id: '5',
          textureUrl:
            'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
          targetSpaceId: 'spaceA',
          position: {
            x: -8,
            y: 0,
            z: -40
          },
          content: {
            title: '去门口',
            text: '一起去门口吧'
          }
        }
      ],
      cubeSpaceTextureUrls: {
        back: 'https://m.360buyimg.com/babel/jfs/t1/48117/28/21445/120448/63398366Ede81497b/a46e362df5f7d0ed.jpg',
        down: 'https://m.360buyimg.com/babel/jfs/t1/101209/24/26762/106253/63398376Eedb0db22/4f335c4ecd72ad74.jpg',
        front: 'https://m.360buyimg.com/babel/jfs/t1/154056/6/26449/110652/63398388E8ecda044/22e1646534839a95.jpg',
        left: 'https://m.360buyimg.com/babel/jfs/t1/198990/28/28201/74687/6339839aE28806a5e/43b311d3379397df.jpg',
        right: 'https://m.360buyimg.com/babel/jfs/t1/209711/14/25233/92186/633983b0E8f4df687/750ba84061ea64a6.jpg',
        up: 'https://m.360buyimg.com/babel/jfs/t1/186545/35/29054/29678/633983c2E72ef4848/92043b945a03fc29.jpg'
      }
    }
  ]

  useEffect(() => {
    // 初始化全景实例
    setVr360(
      new Vr360({
        container: containerRef.current!,
        tipContainer: tipRef.current!,
        spacesConfig
      })
    )

    return () => {
      // 页面卸载时注销全景实例
      vr360?.destroy?.()
    }
  }, [])

  useEffect(() => {
    if (vr360) {
      // 设置全景自动旋转
      vr360.controls.autoRotate = true

      // 开始渲染全景
      vr360.render()

      // 实时监听页面尺寸变化,更新全景尺寸
      vr360.listenResize()

      // 当需要显示提示时
      vr360.on('showTip', e => {
        // 停止旋转场景
        vr360!.controls.autoRotate = false

        // 设置提示内容和提示容器位置
        const {top, left, tip} = e
        setShowTip(true)
        setTipLeft(left)
        setTipTop(top)
        setTipTitle(tip.content.title)
        setTipContent(tip.content.text)
      })

      // 当需要隐藏提示时
      vr360.on('hideTip', () => {
        // 重新开启旋转场景
        vr360!.controls.autoRotate = true

        // 隐藏提示容器
        setShowTip(false)
      })
    }
  }, [vr360])

  // 切换全景空间
  function handleSwitchSpace(space: SpaceConfig) {
    vr360?.switchSpace?.(space.id)
  }

  return (
    <div className="demo">
      {/* 提示 */}
      <div
        ref={tipRef}
        className="demo-tip"
        style={{
          transform: `translate(${tipLeft}px, ${tipTop + 50}px)`,
          zIndex: showTip ? 99 : -1,
          visibility: showTip ? 'visible' : 'hidden'
        }}
      >
        {/* 提示标题 */}
        <div className="demo-tip-title">{tipTitle}</div>

        {/* 提示内容 */}
        <div className="demo-tip-content">{tipContent}</div>
      </div>

      {/* 360全景容器 */}
      <div ref={containerRef} className="demo-container"></div>

      {/* 底部切换场景 */}
      <div className="demo-bottom-bar">
        {spacesConfig.map(space => (
          <div
            key={space.id}
            className="demo-bottom-card"
            onClick={() => handleSwitchSpace(space)}
            style={{
              backgroundImage: `url(${space.cubeSpaceTextureUrls.left})`
            }}
          ></div>
        ))}
      </div>
    </div>
  )
}

export default Example
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Html App</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
    />

    <!-- 引入 threejs -->
    <script src="https://unpkg.com/three@0.145.0/build/three.min.js"></script>

    <!-- 引入 vr360-core -->
    <script src="https://unpkg.com/@nicepkg/vr360-core@^0.3.1/dist/index.min.umd.js"></script>

    <!-- 引入自己的 css -->
    <link href="./demo.css" rel="stylesheet" />
  </head>
  <body>
    <div class="demo">
      <!-- 提示 -->
      <div class="demo-tip">

        <!-- 提示标题 -->
        <div class="demo-tip-title"></div>

        <!-- 提示内容 -->
        <div class="demo-tip-content"></div>
      </div>

      <!-- 360全景容器 -->
      <div class="demo-container"></div>

      <!-- 底部切换场景 -->
      <div class="demo-bottom-bar"></div>
    </div>
    <script>
      const {Vr360} = Vr360Core // 原生使用时,所有的导出都在 window.Vr360Core 里
      const container = document.querySelector('.demo-container') // 360全景容器
      const tip = document.querySelector('.demo-tip') // 提示容器
      const tipTitle = document.querySelector('.demo-tip-title') // 提示标题 el
      const tipContent = document.querySelector('.demo-tip-content') // 提示内容 el
      const bottomBar = document.querySelector('.demo-bottom-bar') // 底部切换场景容器

      // 全景空间配置
      const spacesConfig = [
        {
          id: 'spaceA', // 空间 id,用于切换空间,必须唯一
          tips: [
            // 提示,可选
            {
              id: '1', // 提示 id,用于缓存,在当前 tips 数组里要唯一,必须
              position: {x: 0, y: -10, z: 40}, // 提示位置,必须
              content: {
                // 提示内容,在 showTip 事件会暴露,要包含什么你自己决定。
                title: '豪华跑车',
                text: '比奥迪还贵的豪华跑车'
              }
            },
            {
              id: '2',
              // 自定义提示图标贴图,可选
              textureUrl:
                'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
              targetSpaceId: 'spaceB', // 提示点击后跳转的空间 id,可选
              position: {x: -10, y: -4, z: 40},
              content: {
                title: '去客厅',
                text: '一起去尊贵的客厅吧'
              }
            }
          ],
          cubeSpaceTextureUrls: {
            // 立方体贴图,分别为:背侧、下侧、前侧、左侧、右侧、上侧,必须
            back: 'https://m.360buyimg.com/babel/jfs/t1/40814/31/19646/41953/63398297E0707fe35/4e831e60cf579899.jpg',
            down: 'https://m.360buyimg.com/babel/jfs/t1/43941/23/19369/84038/633982d7E838acd9a/9e7a89cc910d3409.jpg',
            front: 'https://m.360buyimg.com/babel/jfs/t1/150573/35/27827/113528/633982faE8556c0c0/b455284fd91885c6.jpg',
            left: 'https://m.360buyimg.com/babel/jfs/t1/189204/25/29491/61430/63398310E3c180e43/26d9b459cf714f9f.jpg',
            right: 'https://m.360buyimg.com/babel/jfs/t1/184948/34/28448/131232/63398323E61ff80fb/89ab84eda0421260.jpg',
            up: 'https://m.360buyimg.com/babel/jfs/t1/86258/24/33602/70616/63398334Ea2dcf448/e2f5c275792fb3d6.jpg'
          }
        },
        {
          id: 'spaceB',
          tips: [
            {
              id: '3',
              position: {x: -2, y: -25, z: 40},
              content: {
                title: '香奈儿垃圾桶',
                text: '里面装着主人不用的奢侈品'
              }
            },
            {
              id: '4',
              position: {x: -20, y: 0, z: 40},
              content: {
                title: '宇宙牌冰箱',
                text: '装着超级多零食'
              }
            },
            {
              id: '5',
              textureUrl:
                'https://m.360buyimg.com/babel/jfs/t1/125314/12/31594/6260/6339b149E14068522/5c0d35a3e149936a.png',
              targetSpaceId: 'spaceA',
              position: {
                x: -8,
                y: 0,
                z: -40
              },
              content: {
                title: '去门口',
                text: '一起去门口吧'
              }
            }
          ],
          cubeSpaceTextureUrls: {
            back: 'https://m.360buyimg.com/babel/jfs/t1/48117/28/21445/120448/63398366Ede81497b/a46e362df5f7d0ed.jpg',
            down: 'https://m.360buyimg.com/babel/jfs/t1/101209/24/26762/106253/63398376Eedb0db22/4f335c4ecd72ad74.jpg',
            front: 'https://m.360buyimg.com/babel/jfs/t1/154056/6/26449/110652/63398388E8ecda044/22e1646534839a95.jpg',
            left: 'https://m.360buyimg.com/babel/jfs/t1/198990/28/28201/74687/6339839aE28806a5e/43b311d3379397df.jpg',
            right: 'https://m.360buyimg.com/babel/jfs/t1/209711/14/25233/92186/633983b0E8f4df687/750ba84061ea64a6.jpg',
            up: 'https://m.360buyimg.com/babel/jfs/t1/186545/35/29054/29678/633983c2E72ef4848/92043b945a03fc29.jpg'
          }
        }
      ]

      // 初始化全景实例
      const vr360 = new Vr360({
        container, // 全景挂载容器
        tipContainer: tip, // 提示挂载容器
        spacesConfig // 全景配置
      })

      // 设置全景自动旋转
      vr360.controls.autoRotate = true

      // 开始渲染全景
      vr360.render()

      // 实时监听页面尺寸变化,更新全景尺寸
      vr360.listenResize()

      // 当需要显示提示时
      vr360.on('showTip', e => {
        // 停止旋转场景
        vr360.controls.autoRotate = false

        // 设置提示内容和提示容器位置
        const {top, left} = e
        Object.assign(tip.style, {
          transform: `translate(${left}px, ${top + 50}px)`,
          zIndex: 99,
          visibility: 'visible'
        })
        tip.style.t
        tipTitle.innerText = e.tip.content.title
        tipContent.innerText = e.tip.content.text
      })

      // 当需要隐藏提示时
      vr360.on('hideTip', () => {
        // 重新开启旋转场景
        vr360.controls.autoRotate = true

        // 隐藏提示容器
        tip.style.zIndex = -1
        tip.style.visibility = 'hidden'
      })

      // 页面卸载时注销全景实例
      window.addEventListener('unload', () => {
        vr360.destroy()
      })

      // 切换全景空间
      function handleSwitchSpace(space) {
        vr360.switchSpace(space.id)
      }

      bottomBar.append(
        ...spacesConfig.map(space => {
          const card = document.createElement('div')
          card.className = 'demo-bottom-card'
          Object.assign(card.style, {
            backgroundImage: `url(${space.cubeSpaceTextureUrls.left})`
          })
          card.addEventListener('click', () => {
            handleSwitchSpace(space)
          })
          return card
        })
      )
    </script>
  </body>
</html>
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.demo {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background-color: #fff;
}

.demo-tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 240px;
  height: 60px;
  padding: 4px;
  color: #fff;
  cursor: pointer;
  visibility: hidden;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 4px;
}

.demo-tip-title {
  font-weight: bold;
}

.demo-container {
  width: 100%;
  height: 100%;
}

.demo-bottom-bar {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100px;
}

.demo-bottom-card {
  width: 140px;
  height: 70px;
  margin-left: 1rem;
  cursor: pointer;
  background-repeat: no-repeat;
  background-size: cover;
  border-radius: 4px;
}