合并冲突
This commit is contained in:
commit
d9b6141d54
@ -8,6 +8,12 @@ let mainWindow;
|
|||||||
let localServer;
|
let localServer;
|
||||||
let backendProcess;
|
let backendProcess;
|
||||||
|
|
||||||
|
app.disableDomainBlockingFor3DAPIs();
|
||||||
|
// app.disableHardwareAcceleration();
|
||||||
|
app.commandLine.appendSwitch('ignore-gpu-blocklist');
|
||||||
|
app.commandLine.appendSwitch('enable-webgl');
|
||||||
|
app.commandLine.appendSwitch('use-angle', 'd3d11');
|
||||||
|
|
||||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (!win) throw new Error('窗口未找到');
|
if (!win) throw new Error('窗口未找到');
|
||||||
@ -193,7 +199,6 @@ function createWindow() {
|
|||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
// sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
// sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
||||||
// backgroundThrottling: false,
|
|
||||||
preload: path.join(__dirname, 'preload.js')
|
preload: path.join(__dirname, 'preload.js')
|
||||||
},
|
},
|
||||||
icon: path.join(__dirname, '../public/logo.png'),
|
icon: path.join(__dirname, '../public/logo.png'),
|
||||||
@ -311,7 +316,7 @@ function startLocalServer(callback) {
|
|||||||
|
|
||||||
// 应用事件处理
|
// 应用事件处理
|
||||||
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
||||||
app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
generateReportPdf: (payload) => ipcRenderer.invoke('generate-report-pdf', payload),
|
generateReportPdf: (payload) => {
|
||||||
|
try {
|
||||||
|
return window?.electron?.ipcRenderer?.invoke('generate-report-pdf', payload)
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
getBackendUrl: () => process.env.BACKEND_URL || 'http://localhost:5000',
|
getBackendUrl: () => process.env.BACKEND_URL || 'http://localhost:5000',
|
||||||
|
getSocketTransports: () => {
|
||||||
|
const allowPolling = process.env.ALLOW_POLLING === '1'
|
||||||
|
return allowPolling ? ['websocket', 'polling'] : ['websocket']
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
<span class="currencytext2">{{ headlist.rotation }}°</span>
|
<span class="currencytext2">{{ headlist.rotation }}°</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100%;height: 80%;">
|
<div style="width: 100%;height: 80%;">
|
||||||
<Model v-if="patientInfo != null && patientInfo.id != null&& patientInfo.id != ''" :rotation="Number(headlist.rotation)" :tilt="Number(headlist.tilt)" :pitch="Number(headlist.pitch)" :gender="patientInfo.gender || '男'" />
|
<Model :rotation="Number(headlist.rotation)" :tilt="Number(headlist.tilt)" :pitch="Number(headlist.pitch)" :gender="patientInfo.gender || '男'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -585,6 +585,11 @@ let imuSocket = null
|
|||||||
let pressureSocket = null
|
let pressureSocket = null
|
||||||
let restartSocket = null
|
let restartSocket = null
|
||||||
let frameCount = 0
|
let frameCount = 0
|
||||||
|
let latestFrameCamera1 = ''
|
||||||
|
let latestFrameCamera2 = ''
|
||||||
|
let rafScheduled = false
|
||||||
|
let lastRenderTs = 0
|
||||||
|
const MAX_RENDER_FPS = 30
|
||||||
|
|
||||||
// 后端服务器地址配置
|
// 后端服务器地址配置
|
||||||
const BACKEND_URL = getBackendUrl()
|
const BACKEND_URL = getBackendUrl()
|
||||||
@ -1216,13 +1221,30 @@ function displayFrame(base64Image) {
|
|||||||
|
|
||||||
function displayCameraFrameById(deviceId, base64Image) {
|
function displayCameraFrameById(deviceId, base64Image) {
|
||||||
if (base64Image && base64Image.length > 0) {
|
if (base64Image && base64Image.length > 0) {
|
||||||
const url = 'data:image/jpeg;base64,' + base64Image
|
|
||||||
if (String(deviceId).toLowerCase() === 'camera2') {
|
if (String(deviceId).toLowerCase() === 'camera2') {
|
||||||
camera2ImgSrc.value = url
|
latestFrameCamera2 = base64Image
|
||||||
} else {
|
} else {
|
||||||
camera1ImgSrc.value = url
|
latestFrameCamera1 = base64Image
|
||||||
// 旧变量保留(避免其它位置引用出错)
|
}
|
||||||
rtspImgSrc.value = url
|
if (!rafScheduled) {
|
||||||
|
rafScheduled = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()
|
||||||
|
const minInterval = 1000 / MAX_RENDER_FPS
|
||||||
|
if (now - lastRenderTs >= minInterval) {
|
||||||
|
if (latestFrameCamera1) {
|
||||||
|
const url1 = 'data:image/jpeg;base64,' + latestFrameCamera1
|
||||||
|
camera1ImgSrc.value = url1
|
||||||
|
rtspImgSrc.value = url1
|
||||||
|
}
|
||||||
|
if (latestFrameCamera2) {
|
||||||
|
const url2 = 'data:image/jpeg;base64,' + latestFrameCamera2
|
||||||
|
camera2ImgSrc.value = url2
|
||||||
|
}
|
||||||
|
lastRenderTs = now
|
||||||
|
}
|
||||||
|
rafScheduled = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ 收到空的视频帧数据')
|
console.warn('⚠️ 收到空的视频帧数据')
|
||||||
|
|||||||
@ -579,10 +579,11 @@
|
|||||||
<div v-if="isBig" style="position: fixed;top: 0;right: 0;
|
<div v-if="isBig" style="position: fixed;top: 0;right: 0;
|
||||||
width: 100%;height: 100vh;z-index: 9999;background: red;border: 2px solid #b0b0b0">
|
width: 100%;height: 100vh;z-index: 9999;background: red;border: 2px solid #b0b0b0">
|
||||||
<svg @click="isBig=false" style="position: absolute;right: 10px;top:10px;cursor: pointer;" t="1760175800150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5743" width="24" height="24"><path d="M796 163.1L511.1 448l-285-285-63.9 64 285 285-285 285 63.8 63.8 285-285 285 285 63.8-63.8-285-285 285-285-63.8-63.9z" fill="#ffffff" p-id="5744"></path></svg>
|
<svg @click="isBig=false" style="position: absolute;right: 10px;top:10px;cursor: pointer;" t="1760175800150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5743" width="24" height="24"><path d="M796 163.1L511.1 448l-285-285-63.9 64 285 285-285 285 63.8 63.8 285-285 285 285 63.8-63.8-285-285 285-285-63.8-63.9z" fill="#ffffff" p-id="5744"></path></svg>
|
||||||
<!-- <img v-if="isBig" :src="(cameraStatus === '已连接' && camera1ImgSrc) ? camera1ImgSrc : noImageSvg" alt=""
|
<img :src="(camera1ImgSrc || camera2ImgSrc) || ''" alt="video"
|
||||||
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" /> -->
|
style="position:absolute; left:0; top:0; width:100%; height:100%; object-fit:contain; background:#323232;" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -3,112 +3,142 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
import maleUrl from '@/assets/glb/male.glb?url'
|
import maleUrl from '@/assets/glb/male.glb?url'
|
||||||
import femaleUrl from '@/assets/glb/female.glb?url'
|
import femaleUrl from '@/assets/glb/female.glb?url'
|
||||||
|
|
||||||
// const { ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// Three.js场景设置
|
|
||||||
let scene, camera, renderer, model;
|
|
||||||
let targetQuaternion = new THREE.Quaternion();
|
|
||||||
let currentQuaternion = new THREE.Quaternion();
|
|
||||||
let animationId = 0;
|
|
||||||
let onResizeHandler = null;
|
|
||||||
|
|
||||||
let axisMapping = { roll: 'z', pitch: 'x', yaw: 'y' };
|
|
||||||
let manualOffsets = { x: 0, y: 0, z: 0 };
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rotation: { type: [Number, String], default: 0 },
|
rotation: { type: [Number, String], default: 0 },
|
||||||
tilt: { type: [Number, String], default: 0 },
|
tilt: { type: [Number, String], default: 0 },
|
||||||
pitch: { type: [Number, String], default: 0 },
|
pitch: { type: [Number, String], default: 0 },
|
||||||
gender: { type: String, default: '' }
|
gender: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Three.js 核心变量
|
||||||
|
let scene, camera, renderer, model;
|
||||||
|
let targetQuaternion = new THREE.Quaternion();
|
||||||
|
let currentQuaternion = new THREE.Quaternion();
|
||||||
|
let animationId = 0;
|
||||||
|
let onResizeHandler = null;
|
||||||
|
|
||||||
|
// 渲染循环控制
|
||||||
|
let lastRenderTime = 0;
|
||||||
|
const TARGET_FPS = 10; // 限制为25帧以平衡流畅度与性能
|
||||||
|
const FRAME_INTERVAL = 1000 / TARGET_FPS;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initThreeJS()
|
initThreeJS()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupThreeJS()
|
||||||
|
})
|
||||||
|
|
||||||
function initThreeJS() {
|
function initThreeJS() {
|
||||||
// 创建场景
|
const container = document.getElementById('containermodel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 1. 创建场景
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
let containermodel = document.getElementById('containermodel');
|
|
||||||
// 创建相机
|
// 2. 创建相机
|
||||||
camera = new THREE.PerspectiveCamera(75, containermodel.offsetWidth / containermodel.offsetHeight, 0.1, 1000);
|
camera = new THREE.PerspectiveCamera(75, container.offsetWidth / container.offsetHeight, 0.1, 1000);
|
||||||
camera.position.set(0, 0, 4);
|
camera.position.set(0, 0, 4);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
// 创建渲染器
|
// 3. 创建渲染器 (尝试 WebGL2 -> WebGL1)
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
|
const canvas = document.createElement('canvas')
|
||||||
scene.background = null; // 置空场景背景
|
let gl = null
|
||||||
renderer.setClearColor(0x000000, 0.02);
|
const contextAttributes = { alpha: true, antialias: false, preserveDrawingBuffer: false, powerPreference: 'high-performance' };
|
||||||
renderer.setSize(containermodel.offsetWidth, containermodel.offsetHeight);
|
|
||||||
|
try { gl = canvas.getContext('webgl2', contextAttributes) } catch {}
|
||||||
|
if (!gl) {
|
||||||
|
try { gl = canvas.getContext('webgl', contextAttributes) } catch {}
|
||||||
|
}
|
||||||
|
if (!gl) return;
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ canvas, context: gl })
|
||||||
|
scene.background = null;
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace; // 改善颜色输出
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 添加色调映射
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
renderer.toneMappingExposure = 1.2; // 增加曝光
|
renderer.toneMappingExposure = 1.2;
|
||||||
document.getElementById('containermodel').appendChild(renderer.domElement);
|
|
||||||
|
|
||||||
// 高强度照明系统
|
container.appendChild(renderer.domElement)
|
||||||
// 环境光 - 大幅提升整体亮度
|
|
||||||
|
// 处理 WebGL 上下文丢失
|
||||||
|
renderer.domElement.addEventListener('webglcontextlost', (e) => { e.preventDefault(); }, false)
|
||||||
|
renderer.domElement.addEventListener('webglcontextrestored', () => { animate(); }, false)
|
||||||
|
|
||||||
|
// 4. 添加灯光
|
||||||
|
setupLights();
|
||||||
|
|
||||||
|
// 5. 加载模型
|
||||||
|
loadModel();
|
||||||
|
|
||||||
|
// 6. 窗口大小监听
|
||||||
|
onResizeHandler = () => {
|
||||||
|
if (!container || !camera || !renderer) return;
|
||||||
|
camera.aspect = container.offsetWidth / container.offsetHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', onResizeHandler);
|
||||||
|
|
||||||
|
// 7. 开始渲染循环
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLights() {
|
||||||
const ambientLight = new THREE.AmbientLight(0x606060, 10.0);
|
const ambientLight = new THREE.AmbientLight(0x606060, 10.0);
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
|
||||||
// 主方向光 - 从正前方照射,高强度
|
|
||||||
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 3);
|
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 3);
|
||||||
directionalLight1.position.set(0, 0, 6);
|
directionalLight1.position.set(0, 0, 6);
|
||||||
scene.add(directionalLight1);
|
scene.add(directionalLight1);
|
||||||
|
|
||||||
// 左侧补光 - 更强的强度
|
|
||||||
const directionalLight2 = new THREE.DirectionalLight(0xbbddff, 1.0);
|
const directionalLight2 = new THREE.DirectionalLight(0xbbddff, 1.0);
|
||||||
directionalLight2.position.set(-8, 6, 6);
|
directionalLight2.position.set(-8, 6, 6);
|
||||||
scene.add(directionalLight2);
|
scene.add(directionalLight2);
|
||||||
|
|
||||||
// 右侧补光
|
|
||||||
const directionalLight3 = new THREE.DirectionalLight(0xffddbb, 0.8);
|
const directionalLight3 = new THREE.DirectionalLight(0xffddbb, 0.8);
|
||||||
directionalLight3.position.set(8, 6, 6);
|
directionalLight3.position.set(8, 6, 6);
|
||||||
scene.add(directionalLight3);
|
scene.add(directionalLight3);
|
||||||
|
|
||||||
// 顶部强光
|
|
||||||
const topLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
const topLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
topLight.position.set(0, 15, 0);
|
topLight.position.set(0, 15, 0);
|
||||||
scene.add(topLight);
|
scene.add(topLight);
|
||||||
|
|
||||||
// 底部补光(避免底部过暗)
|
|
||||||
const bottomLight = new THREE.DirectionalLight(0x8899bb, 0.5);
|
const bottomLight = new THREE.DirectionalLight(0x8899bb, 0.5);
|
||||||
bottomLight.position.set(0, -6, 0);
|
bottomLight.position.set(0, -6, 0);
|
||||||
scene.add(bottomLight);
|
scene.add(bottomLight);
|
||||||
|
|
||||||
// 加载3D模型
|
|
||||||
loadModel();
|
|
||||||
|
|
||||||
// 开始渲染循环
|
|
||||||
animate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadModel() {
|
function loadModel() {
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
// 尝试加载Model.glb文件
|
|
||||||
const url = (props.gender === '女') ? femaleUrl : maleUrl;
|
const url = (props.gender === '女') ? femaleUrl : maleUrl;
|
||||||
|
|
||||||
loader.load(url,
|
loader.load(url,
|
||||||
(gltf) => {
|
(gltf) => {
|
||||||
|
if (!scene) return; // 防止加载完成前组件已销毁
|
||||||
model = gltf.scene;
|
model = gltf.scene;
|
||||||
// 调整模型大小和位置
|
|
||||||
|
// 自动调整大小和位置
|
||||||
const box = new THREE.Box3().setFromObject(model);
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
const size = box.getSize(new THREE.Vector3());
|
const size = box.getSize(new THREE.Vector3());
|
||||||
const maxDim = Math.max(size.x, size.y, size.z);
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
const scale = 5 / maxDim; // 调整为合适大小
|
const scale = 5 / maxDim;
|
||||||
model.scale.set(scale, scale, scale);
|
|
||||||
|
|
||||||
// 将模型置于中心
|
model.scale.set(scale, scale, scale);
|
||||||
const center = box.getCenter(new THREE.Vector3());
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
model.position.set(-center.x * scale, -center.y * scale+0.2 , -center.z * scale);
|
model.position.set(-center.x * scale, -center.y * scale + 0.2, -center.z * scale);
|
||||||
model.rotation.set(0, 0, 0);
|
model.rotation.set(0, 0, 0);
|
||||||
|
|
||||||
// 启用阴影
|
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child.isMesh) {
|
if (child.isMesh) {
|
||||||
child.castShadow = true;
|
child.castShadow = true;
|
||||||
@ -117,21 +147,17 @@ function loadModel() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
scene.add(model);
|
scene.add(model);
|
||||||
console.log('3D模型加载成功');
|
|
||||||
},
|
|
||||||
(progress) => {
|
|
||||||
console.log('加载进度:', (progress.loaded / progress.total * 100) + '%');
|
|
||||||
},
|
},
|
||||||
|
undefined,
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('模型加载失败:', error);
|
console.error('模型加载失败:', error);
|
||||||
// 如果模型加载失败,创建一个替代的立方体
|
|
||||||
createFallbackModel();
|
createFallbackModel();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFallbackModel() {
|
function createFallbackModel() {
|
||||||
console.log('使用默认立方体模型');
|
if (!scene) return;
|
||||||
const geometry = new THREE.BoxGeometry(2, 2, 2);
|
const geometry = new THREE.BoxGeometry(2, 2, 2);
|
||||||
const material = new THREE.MeshLambertMaterial({
|
const material = new THREE.MeshLambertMaterial({
|
||||||
color: 0x00ff00,
|
color: 0x00ff00,
|
||||||
@ -145,67 +171,32 @@ function createFallbackModel() {
|
|||||||
scene.add(model);
|
scene.add(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate(currentTime) {
|
||||||
animationId = requestAnimationFrame(animate);
|
animationId = requestAnimationFrame(animate);
|
||||||
|
|
||||||
// 应用SLERP平滑插值到模型
|
if (!currentTime) currentTime = performance.now();
|
||||||
|
|
||||||
|
// 帧率节流
|
||||||
|
if (currentTime - lastRenderTime < FRAME_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastRenderTime = currentTime - (currentTime % FRAME_INTERVAL);
|
||||||
|
|
||||||
|
// 姿态平滑插值
|
||||||
if (model) {
|
if (model) {
|
||||||
currentQuaternion.slerp(targetQuaternion, 0.1);
|
currentQuaternion.slerp(targetQuaternion, 0.1);
|
||||||
model.quaternion.copy(currentQuaternion);
|
model.quaternion.copy(currentQuaternion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderer && scene && camera) {
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口大小调整
|
function cleanupThreeJS() {
|
||||||
onResizeHandler = () => {
|
if (animationId) cancelAnimationFrame(animationId);
|
||||||
let containermodel = document.getElementById('containermodel');
|
if (onResizeHandler) window.removeEventListener('resize', onResizeHandler);
|
||||||
camera.aspect = containermodel.offsetWidth / containermodel.offsetHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(containermodel.offsetWidth, containermodel.offsetHeight);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', onResizeHandler);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [Number(props.rotation), Number(props.tilt), Number(props.pitch)],
|
|
||||||
([rotation, tilt, pitch]) => {
|
|
||||||
const toRad = (deg) => (deg || 0) * Math.PI / 180;
|
|
||||||
const euler = new THREE.Euler(toRad(pitch), toRad(rotation), toRad(tilt), 'XYZ');
|
|
||||||
const q = new THREE.Quaternion().setFromEuler(euler);
|
|
||||||
targetQuaternion.copy(q);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.gender,
|
|
||||||
() => {
|
|
||||||
if (model) {
|
|
||||||
try {
|
|
||||||
scene.remove(model);
|
|
||||||
model.traverse((child) => {
|
|
||||||
if (child.isMesh) {
|
|
||||||
child.geometry && child.geometry.dispose();
|
|
||||||
if (Array.isArray(child.material)) {
|
|
||||||
child.material.forEach(m => m && m.dispose && m.dispose());
|
|
||||||
} else {
|
|
||||||
child.material && child.material.dispose && child.material.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
model = null;
|
|
||||||
}
|
|
||||||
// loadModel();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
try { animationId && cancelAnimationFrame(animationId); } catch {}
|
|
||||||
try { window.removeEventListener('resize', onResizeHandler || (()=>{})); } catch {}
|
|
||||||
try {
|
|
||||||
if (scene) {
|
if (scene) {
|
||||||
scene.traverse((obj) => {
|
scene.traverse((obj) => {
|
||||||
if (obj.isMesh) {
|
if (obj.isMesh) {
|
||||||
@ -218,22 +209,58 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
try { renderer && renderer.dispose && renderer.dispose(); } catch {}
|
if (renderer) {
|
||||||
try {
|
renderer.dispose();
|
||||||
const container = document.getElementById('containermodel');
|
const container = document.getElementById('containermodel');
|
||||||
if (container) {
|
if (container && renderer.domElement) {
|
||||||
while (container.firstChild) container.removeChild(container.firstChild);
|
container.removeChild(renderer.domElement);
|
||||||
}
|
}
|
||||||
} catch {}
|
}
|
||||||
|
|
||||||
scene = null;
|
scene = null;
|
||||||
camera = null;
|
camera = null;
|
||||||
renderer = null;
|
renderer = null;
|
||||||
model = null;
|
model = null;
|
||||||
})
|
}
|
||||||
|
|
||||||
// 添加键盘控制(可选)
|
// 监听姿态数据
|
||||||
|
watch(
|
||||||
|
() => [Number(props.rotation), Number(props.tilt), Number(props.pitch)],
|
||||||
|
([rotation, tilt, pitch]) => {
|
||||||
|
const toRad = (deg) => (deg || 0) * Math.PI / 180;
|
||||||
|
// 注意:这里欧拉角的顺序可能需要根据实际模型骨骼进行微调,目前保持原样 'XYZ'
|
||||||
|
const euler = new THREE.Euler(toRad(pitch), toRad(rotation), toRad(tilt), 'XYZ');
|
||||||
|
const q = new THREE.Quaternion().setFromEuler(euler);
|
||||||
|
targetQuaternion.copy(q);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听性别切换
|
||||||
|
watch(
|
||||||
|
() => props.gender,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
if (newVal === oldVal) return;
|
||||||
|
|
||||||
|
if (model && scene) {
|
||||||
|
scene.remove(model);
|
||||||
|
// 清理旧模型资源
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.geometry && child.geometry.dispose();
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
child.material.forEach(m => m && m.dispose && m.dispose());
|
||||||
|
} else {
|
||||||
|
child.material && child.material.dispose && child.material.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
model = null;
|
||||||
|
}
|
||||||
|
loadModel(); // 重新加载新性别的模型
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -241,113 +268,6 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
overflow: hidden; /* 防止 canvas 溢出 */
|
||||||
|
|
||||||
#info {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
color: white;
|
|
||||||
background: rgba(0,0,0,0.8);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
z-index: 100;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
max-width: 350px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#qr-section {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qr-canvas {
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#debug-panel {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
color: white;
|
|
||||||
background: rgba(0,0,0,0.8);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
z-index: 100;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-section {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 8px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-section h4 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #4CAF50;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 5px 0;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row label {
|
|
||||||
min-width: 20px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row select {
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
color: white;
|
|
||||||
border: 1px solid rgba(255,255,255,0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row input[type="range"] {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row .value {
|
|
||||||
min-width: 40px;
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connected {
|
|
||||||
color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disconnected {
|
|
||||||
color: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qr-code {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
background: white;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -24,7 +24,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: 'localhost',
|
host: '0.0.0.0',
|
||||||
// 开发服务器配置
|
// 开发服务器配置
|
||||||
cors: true,
|
cors: true,
|
||||||
strictPort: false
|
strictPort: false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user