mirror of
http://36.133.248.69:3088/admin/RentWeAppFront.git
synced 2026-03-07 17:32:25 +08:00
需求变更进行调整
This commit is contained in:
@@ -1,438 +1,19 @@
|
||||
<template>
|
||||
<view class="vr-page">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-btn" @click="back">‹ 返回</view>
|
||||
|
||||
<!-- 主 Canvas -->
|
||||
<canvas canvas-id="vrCanvas" class="vr-canvas" @touchstart="onTouchStart" @touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"></canvas>
|
||||
|
||||
<!-- 左上角 3D mini preview -->
|
||||
<canvas v-if="currentSpace.type==='model'" canvas-id="miniCanvas" class="mini-canvas"></canvas>
|
||||
|
||||
<!-- Hotspot 点击由 Three.js raycaster 处理 -->
|
||||
|
||||
<!-- 底栏 -->
|
||||
<view class="bottom-bar">
|
||||
<!-- 左侧空间切换 -->
|
||||
<view class="left">
|
||||
<button @click="showSpaceList = !showSpaceList">
|
||||
<image src="/static/icons/space.png" />
|
||||
</button>
|
||||
</view>
|
||||
<!-- 右侧工具 -->
|
||||
<view class="right">
|
||||
<button @click="showTools = !showTools">
|
||||
<image src="/static/icons/tools.png" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空间缩略图弹窗 -->
|
||||
<view v-if="showSpaceList" class="space-list">
|
||||
<view v-for="space in spaces" :key="space.id" class="space-item" @click="switchSpace(space.id)">
|
||||
<image :src="space.thumbnail" />
|
||||
<text>{{ space.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工具栏弹窗 -->
|
||||
<view v-if="showTools" class="tools-panel">
|
||||
<button @click="resetView">重置视角</button>
|
||||
<button @click="toggleFullScreen">全屏</button>
|
||||
<button @click="takePhoto">拍照</button>
|
||||
</view>
|
||||
|
||||
<!-- 上一/下一空间按钮 -->
|
||||
<view class="switch-btn left" @click="prevSpace">‹</view>
|
||||
<view class="switch-btn right" @click="nextSpace">›</view>
|
||||
</view>
|
||||
<web-view :src="src + id"></web-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three'
|
||||
import {
|
||||
GLTFLoader
|
||||
} from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
|
||||
export default {
|
||||
name: 'VRView',
|
||||
data() {
|
||||
return {
|
||||
spaces: [], // VR 空间数组
|
||||
currentIndex: 0,
|
||||
currentSpace: null,
|
||||
|
||||
// Three.js 主场景
|
||||
renderer: null,
|
||||
scene: null,
|
||||
camera: null,
|
||||
sphere: null,
|
||||
videoTexture: null,
|
||||
|
||||
// Mini 3D模型
|
||||
miniRenderer: null,
|
||||
miniScene: null,
|
||||
miniCamera: null,
|
||||
miniModel: null,
|
||||
|
||||
// Hotspot
|
||||
raycaster: null,
|
||||
mouse: new THREE.Vector2(),
|
||||
|
||||
// 触控
|
||||
isDragging: false,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
lon: 0,
|
||||
lat: 0,
|
||||
|
||||
// UI
|
||||
showSpaceList: false,
|
||||
showTools: false
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.vrList) {
|
||||
try {
|
||||
// JSON.parse(decodeURIComponent(options.spaces))
|
||||
this.spaces = options.vrList
|
||||
} catch (e) {
|
||||
console.error('解析 VR 空间失败', e)
|
||||
}
|
||||
}
|
||||
this.currentSpace = this.spaces[this.currentIndex]
|
||||
this.initThree()
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Three.js 主场景 ====================
|
||||
initThree() {
|
||||
const canvasQuery = uni.createSelectorQuery()
|
||||
canvasQuery.select('#vrCanvas').node().exec(res => {
|
||||
const canvasNode = res[0].node
|
||||
const {
|
||||
windowWidth: width,
|
||||
windowHeight: height
|
||||
} = uni.getSystemInfoSync()
|
||||
|
||||
// 主 renderer
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvasNode,
|
||||
antialias: true
|
||||
})
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
// scene & camera
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 2000)
|
||||
this.camera.position.set(0, 0, 0.01)
|
||||
|
||||
// raycaster
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
|
||||
this.loadSpace(this.currentSpace)
|
||||
this.animate()
|
||||
})
|
||||
},
|
||||
|
||||
// 加载空间
|
||||
loadSpace(space) {
|
||||
// 清空场景
|
||||
while (this.scene.children.length) this.scene.remove(this.scene.children[0])
|
||||
if (this.videoTexture) {
|
||||
this.videoTexture = null
|
||||
}
|
||||
|
||||
// 图片
|
||||
if (space.type === 'image') {
|
||||
const geometry = new THREE.SphereGeometry(500, 60, 40)
|
||||
geometry.scale(-1, 1, 1)
|
||||
const texture = new THREE.TextureLoader().load(space.src)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture
|
||||
})
|
||||
this.sphere = new THREE.Mesh(geometry, material)
|
||||
this.scene.add(this.sphere)
|
||||
}
|
||||
// CubeMap
|
||||
else if (space.type === 'cubemap') {
|
||||
const urls = [space.cubemap.px, space.cubemap.nx, space.cubemap.py, space.cubemap.ny, space.cubemap.pz,
|
||||
space.cubemap.nz
|
||||
]
|
||||
const cubeTexture = new THREE.CubeTextureLoader().load(urls)
|
||||
this.scene.background = cubeTexture
|
||||
}
|
||||
// 视频
|
||||
else if (space.type === 'video') {
|
||||
const video = document.createElement('video')
|
||||
video.src = space.src
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.loop = true
|
||||
video.muted = false
|
||||
video.autoplay = true
|
||||
video.play()
|
||||
this.videoTexture = new THREE.VideoTexture(video)
|
||||
const geometry = new THREE.SphereGeometry(500, 60, 40)
|
||||
geometry.scale(-1, 1, 1)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: this.videoTexture
|
||||
})
|
||||
this.sphere = new THREE.Mesh(geometry, material)
|
||||
this.scene.add(this.sphere)
|
||||
}
|
||||
// 模型
|
||||
else if (space.type === 'model') {
|
||||
const loader = new GLTFLoader()
|
||||
loader.load(space.src, gltf => {
|
||||
this.sphere = gltf.scene
|
||||
this.sphere.scale.set(space.modelScale, space.modelScale, space.modelScale)
|
||||
this.scene.add(this.sphere)
|
||||
this.initMiniModel(gltf.scene.clone(), space.modelScale)
|
||||
})
|
||||
}
|
||||
|
||||
// Hotspots
|
||||
if (space.hotspots && space.hotspots.length) {
|
||||
space.hotspots.forEach(h => {
|
||||
const spriteMap = new THREE.TextureLoader().load('/static/icons/hotspot.png')
|
||||
const spriteMaterial = new THREE.SpriteMaterial({
|
||||
map: spriteMap
|
||||
})
|
||||
const sprite = new THREE.Sprite(spriteMaterial)
|
||||
const phi = THREE.MathUtils.degToRad(90 - h.position.lat)
|
||||
const theta = THREE.MathUtils.degToRad(h.position.lon)
|
||||
const radius = 500
|
||||
sprite.position.set(
|
||||
radius * Math.sin(phi) * Math.cos(theta),
|
||||
radius * Math.cos(phi),
|
||||
radius * Math.sin(phi) * Math.sin(theta)
|
||||
)
|
||||
sprite.userData.targetId = h.targetId
|
||||
this.scene.add(sprite)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(this.animate)
|
||||
// 相机旋转
|
||||
const phi = THREE.MathUtils.degToRad(90 - this.lat)
|
||||
const theta = THREE.MathUtils.degToRad(this.lon)
|
||||
this.camera.target = new THREE.Vector3(
|
||||
500 * Math.sin(phi) * Math.cos(theta),
|
||||
500 * Math.cos(phi),
|
||||
500 * Math.sin(phi) * Math.sin(theta)
|
||||
)
|
||||
this.camera.lookAt(this.camera.target)
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
// mini 模型同步旋转
|
||||
if (this.currentSpace.type === 'model' && this.miniModel) {
|
||||
this.miniModel.rotation.y = this.camera.rotation.y
|
||||
this.miniRenderer.render(this.miniScene, this.miniCamera)
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Mini 3D 模型 ====================
|
||||
initMiniModel(model, scale) {
|
||||
const canvasQuery = uni.createSelectorQuery()
|
||||
canvasQuery.select('#miniCanvas').node().exec(res => {
|
||||
const canvasNode = res[0].node
|
||||
this.miniRenderer = new THREE.WebGLRenderer({
|
||||
canvas: canvasNode,
|
||||
antialias: true,
|
||||
alpha: true
|
||||
})
|
||||
this.miniRenderer.setSize(150, 150)
|
||||
this.miniScene = new THREE.Scene()
|
||||
this.miniCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 2000)
|
||||
this.miniCamera.position.set(0, 50, 100)
|
||||
this.miniModel = model
|
||||
this.miniScene.add(this.miniModel)
|
||||
})
|
||||
},
|
||||
|
||||
// ==================== 空间切换 ====================
|
||||
switchSpace(id) {
|
||||
const index = this.spaces.findIndex(s => s.id === id)
|
||||
if (index >= 0) {
|
||||
this.currentIndex = index
|
||||
this.currentSpace = this.spaces[index]
|
||||
this.loadSpace(this.currentSpace)
|
||||
this.showSpaceList = false
|
||||
}
|
||||
},
|
||||
prevSpace() {
|
||||
this.currentIndex = (this.currentIndex - 1 + this.spaces.length) % this.spaces.length;
|
||||
this.switchSpace(this.spaces[this.currentIndex].id)
|
||||
},
|
||||
nextSpace() {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.spaces.length;
|
||||
this.switchSpace(this.spaces[this.currentIndex].id)
|
||||
},
|
||||
|
||||
// ==================== 手势 ====================
|
||||
onTouchStart(e) {
|
||||
this.isDragging = true;
|
||||
const t = e.touches[0];
|
||||
this.lastX = t.clientX;
|
||||
this.lastY = t.clientY
|
||||
},
|
||||
onTouchMove(e) {
|
||||
if (!this.isDragging) return;
|
||||
const t = e.touches[0];
|
||||
const dx = t.clientX - this.lastX;
|
||||
const dy = t.clientY - this.lastY;
|
||||
this.lastX = t.clientX;
|
||||
this.lastY = t.clientY;
|
||||
this.lon -= dx * 0.1;
|
||||
this.lat += dy * 0.1;
|
||||
this.lat = Math.max(-85, Math.min(85, this.lat))
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.isDragging = false
|
||||
},
|
||||
|
||||
// ==================== 工具栏功能示例 ====================
|
||||
resetView() {
|
||||
this.lon = 0;
|
||||
this.lat = 0
|
||||
},
|
||||
toggleFullScreen() {
|
||||
uni.setScreenBrightness({
|
||||
value: 1
|
||||
})
|
||||
}, // 示例
|
||||
takePhoto() {
|
||||
console.log('拍照功能可扩展')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vr-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.vr-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block
|
||||
}
|
||||
|
||||
.mini-canvas {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
z-index: 10;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6rpx
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 40rpx;
|
||||
left: 20rpx;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
z-index: 10
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20rpx;
|
||||
z-index: 10
|
||||
}
|
||||
|
||||
.bottom-bar .left,
|
||||
.bottom-bar .right {
|
||||
display: flex;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.space-list {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
padding: 10rpx
|
||||
}
|
||||
|
||||
.space-list .space-item {
|
||||
margin-right: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.space-list image {
|
||||
width: 120rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 6rpx
|
||||
}
|
||||
|
||||
.tools-panel {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
right: 0;
|
||||
width: 200rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10rpx
|
||||
}
|
||||
|
||||
.tools-panel button {
|
||||
margin-bottom: 10rpx;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
color: #fff;
|
||||
font-size: 60rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
.switch-btn.left {
|
||||
left: 20rpx
|
||||
}
|
||||
|
||||
.switch-btn.right {
|
||||
right: 20rpx
|
||||
}
|
||||
</style>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: '' ,// WebView URL
|
||||
src: this.$config.staticUrl + '/public/vr/vr.html?id='
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
onLoad(options) {
|
||||
this.id = options.id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user