合并冲突

This commit is contained in:
limengnan 2025-12-11 09:51:58 +08:00
commit d9b6141d54
6 changed files with 194 additions and 237 deletions

View File

@ -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', () => {

View File

@ -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']
}
}); });

View File

@ -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('⚠️ 收到空的视频帧数据')

View File

@ -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>

View File

@ -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,33 +171,65 @@ 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);
} }
renderer.render(scene, camera); if (renderer && 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);
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( watch(
() => [Number(props.rotation), Number(props.tilt), Number(props.pitch)], () => [Number(props.rotation), Number(props.tilt), Number(props.pitch)],
([rotation, tilt, pitch]) => { ([rotation, tilt, pitch]) => {
const toRad = (deg) => (deg || 0) * Math.PI / 180; const toRad = (deg) => (deg || 0) * Math.PI / 180;
// 'XYZ'
const euler = new THREE.Euler(toRad(pitch), toRad(rotation), toRad(tilt), 'XYZ'); const euler = new THREE.Euler(toRad(pitch), toRad(rotation), toRad(tilt), 'XYZ');
const q = new THREE.Quaternion().setFromEuler(euler); const q = new THREE.Quaternion().setFromEuler(euler);
targetQuaternion.copy(q); targetQuaternion.copy(q);
@ -179,61 +237,30 @@ watch(
{ immediate: true } { immediate: true }
) )
//
watch( watch(
() => props.gender, () => props.gender,
() => { (newVal, oldVal) => {
if (model) { if (newVal === oldVal) return;
try {
if (model && scene) {
scene.remove(model); scene.remove(model);
//
model.traverse((child) => { model.traverse((child) => {
if (child.isMesh) { if (child.isMesh) {
child.geometry && child.geometry.dispose(); child.geometry && child.geometry.dispose();
if (Array.isArray(child.material)) { if (Array.isArray(child.material)) {
child.material.forEach(m => m && m.dispose && m.dispose()); child.material.forEach(m => m && m.dispose && m.dispose());
} else { } else {
child.material && child.material.dispose && child.material.dispose(); 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> </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>

View File

@ -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