This commit is contained in:
root 2025-12-11 09:49:02 +08:00
parent 79b8268ff8
commit 1ed382f126
7 changed files with 194 additions and 237 deletions

Binary file not shown.

View File

@ -8,6 +8,12 @@ let mainWindow;
let localServer;
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) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) throw new Error('窗口未找到');
@ -193,7 +199,6 @@ function createWindow() {
nodeIntegration: false,
contextIsolation: true,
// sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
// backgroundThrottling: false,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, '../public/logo.png'),
@ -311,7 +316,7 @@ function startLocalServer(callback) {
// 应用事件处理
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
app.disableHardwareAcceleration();
// app.disableHardwareAcceleration();
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
@ -334,4 +339,4 @@ app.on('activate', () => {
// 应用退出前清理资源
app.on('before-quit', () => {
stopBackendService();
});
});

View File

@ -1,6 +1,15 @@
const { contextBridge, ipcRenderer } = require('electron');
const { contextBridge } = require('electron');
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',
getSocketTransports: () => {
const allowPolling = process.env.ALLOW_POLLING === '1'
return allowPolling ? ['websocket', 'polling'] : ['websocket']
}
});

View File

@ -133,7 +133,7 @@
<span class="currencytext2">{{ headlist.rotation }}°</span>
</div>
<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>
@ -585,6 +585,11 @@ let imuSocket = null
let pressureSocket = null
let restartSocket = null
let frameCount = 0
let latestFrameCamera1 = ''
let latestFrameCamera2 = ''
let rafScheduled = false
let lastRenderTs = 0
const MAX_RENDER_FPS = 30
//
const BACKEND_URL = getBackendUrl()
@ -1216,13 +1221,30 @@ function displayFrame(base64Image) {
function displayCameraFrameById(deviceId, base64Image) {
if (base64Image && base64Image.length > 0) {
const url = 'data:image/jpeg;base64,' + base64Image
if (String(deviceId).toLowerCase() === 'camera2') {
camera2ImgSrc.value = url
latestFrameCamera2 = base64Image
} else {
camera1ImgSrc.value = url
//
rtspImgSrc.value = url
latestFrameCamera1 = base64Image
}
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 {
console.warn('⚠️ 收到空的视频帧数据')

View File

@ -579,10 +579,11 @@
<div v-if="isBig" style="position: fixed;top: 0;right: 0;
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>
<!-- <img v-if="isBig" :src="(cameraStatus === '已连接' && camera1ImgSrc) ? camera1ImgSrc : noImageSvg" alt=""
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" /> -->
<img :src="(camera1ImgSrc || camera2ImgSrc) || ''" alt="video"
style="position:absolute; left:0; top:0; width:100%; height:100%; object-fit:contain; background:#323232;" />
</div>
</div>
</template>
<script setup>
@ -2994,4 +2995,4 @@ function refreshClick(type) {
.historyDialogVisible .el-dialog__body{
padding: 0;
}
</style>
</style>

View File

@ -3,112 +3,142 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import { onMounted, onUnmounted, watch } from 'vue'
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import maleUrl from '@/assets/glb/male.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({
rotation: { type: [Number, String], default: 0 },
tilt: { type: [Number, String], default: 0 },
pitch: { type: [Number, String], default: 0 },
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(() => {
initThreeJS()
})
onUnmounted(() => {
cleanupThreeJS()
})
function initThreeJS() {
//
const container = document.getElementById('containermodel');
if (!container) return;
// 1.
scene = new THREE.Scene();
let containermodel = document.getElementById('containermodel');
//
camera = new THREE.PerspectiveCamera(75, containermodel.offsetWidth / containermodel.offsetHeight, 0.1, 1000);
// 2.
camera = new THREE.PerspectiveCamera(75, container.offsetWidth / container.offsetHeight, 0.1, 1000);
camera.position.set(0, 0, 4);
camera.lookAt(0, 0, 0);
//
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
scene.background = null; //
renderer.setClearColor(0x000000, 0.02);
renderer.setSize(containermodel.offsetWidth, containermodel.offsetHeight);
// 3. ( WebGL2 -> WebGL1)
const canvas = document.createElement('canvas')
let gl = null
const contextAttributes = { alpha: true, antialias: false, preserveDrawingBuffer: false, powerPreference: 'high-performance' };
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.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace; //
renderer.toneMapping = THREE.ACESFilmicToneMapping; //
renderer.toneMappingExposure = 1.2; //
document.getElementById('containermodel').appendChild(renderer.domElement);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
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);
scene.add(ambientLight);
// -
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 3);
directionalLight1.position.set(0, 0, 6);
scene.add(directionalLight1);
// -
const directionalLight2 = new THREE.DirectionalLight(0xbbddff, 1.0);
directionalLight2.position.set(-8, 6, 6);
scene.add(directionalLight2);
//
const directionalLight3 = new THREE.DirectionalLight(0xffddbb, 0.8);
directionalLight3.position.set(8, 6, 6);
scene.add(directionalLight3);
//
const topLight = new THREE.DirectionalLight(0xffffff, 1.0);
topLight.position.set(0, 15, 0);
scene.add(topLight);
//
const bottomLight = new THREE.DirectionalLight(0x8899bb, 0.5);
bottomLight.position.set(0, -6, 0);
scene.add(bottomLight);
// 3D
loadModel();
//
animate();
}
function loadModel() {
const loader = new GLTFLoader();
// Model.glb
const url = (props.gender === '女') ? femaleUrl : maleUrl;
loader.load(url,
(gltf) => {
if (!scene) return; //
model = gltf.scene;
//
//
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 5 / maxDim; //
model.scale.set(scale, scale, scale);
const scale = 5 / maxDim;
//
model.scale.set(scale, scale, scale);
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.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
@ -117,21 +147,17 @@ function loadModel() {
});
scene.add(model);
console.log('3D模型加载成功');
},
(progress) => {
console.log('加载进度:', (progress.loaded / progress.total * 100) + '%');
},
undefined,
(error) => {
console.error('模型加载失败:', error);
//
createFallbackModel();
}
);
}
function createFallbackModel() {
console.log('使用默认立方体模型');
if (!scene) return;
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshLambertMaterial({
color: 0x00ff00,
@ -145,33 +171,65 @@ function createFallbackModel() {
scene.add(model);
}
function animate() {
function animate(currentTime) {
animationId = requestAnimationFrame(animate);
if (!currentTime) currentTime = performance.now();
// SLERP
//
if (currentTime - lastRenderTime < FRAME_INTERVAL) {
return;
}
lastRenderTime = currentTime - (currentTime % FRAME_INTERVAL);
// 姿
if (model) {
currentQuaternion.slerp(targetQuaternion, 0.1);
model.quaternion.copy(currentQuaternion);
}
renderer.render(scene, camera);
if (renderer && scene && camera) {
renderer.render(scene, camera);
}
}
//
onResizeHandler = () => {
let containermodel = document.getElementById('containermodel');
camera.aspect = containermodel.offsetWidth / containermodel.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize(containermodel.offsetWidth, containermodel.offsetHeight);
};
window.addEventListener('resize', onResizeHandler);
function cleanupThreeJS() {
if (animationId) cancelAnimationFrame(animationId);
if (onResizeHandler) window.removeEventListener('resize', onResizeHandler);
if (scene) {
scene.traverse((obj) => {
if (obj.isMesh) {
obj.geometry && obj.geometry.dispose();
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m && m.dispose && m.dispose());
} else {
obj.material && obj.material.dispose && obj.material.dispose();
}
}
});
}
if (renderer) {
renderer.dispose();
const container = document.getElementById('containermodel');
if (container && renderer.domElement) {
container.removeChild(renderer.domElement);
}
}
scene = null;
camera = null;
renderer = 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);
@ -179,61 +237,30 @@ watch(
{ immediate: true }
)
//
watch(
() => props.gender,
() => {
if (model) {
try {
(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();
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;
model = null;
}
// loadModel();
loadModel(); //
}
)
onUnmounted(() => {
try { animationId && cancelAnimationFrame(animationId); } catch {}
try { window.removeEventListener('resize', onResizeHandler || (()=>{})); } catch {}
try {
if (scene) {
scene.traverse((obj) => {
if (obj.isMesh) {
obj.geometry && obj.geometry.dispose();
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m && m.dispose && m.dispose());
} else {
obj.material && obj.material.dispose && obj.material.dispose();
}
}
});
}
} catch {}
try { renderer && renderer.dispose && renderer.dispose(); } catch {}
try {
const container = document.getElementById('containermodel');
if (container) {
while (container.firstChild) container.removeChild(container.firstChild);
}
} catch {}
scene = null;
camera = null;
renderer = null;
model = null;
})
//
</script>
<style scoped>
@ -241,113 +268,6 @@ onUnmounted(() => {
width: 100%;
height: 100%;
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>

View File

@ -24,7 +24,7 @@ export default defineConfig({
},
server: {
port: 3000,
host: 'localhost',
host: '0.0.0.0',
// 开发服务器配置
cors: true,
strictPort: false