添加三维模型(未完成)

This commit is contained in:
limengnan 2025-11-28 16:14:15 +08:00
parent 8f1eb56a47
commit 900ca4dd2c
7 changed files with 622 additions and 7 deletions

Binary file not shown.

Binary file not shown.

View File

@ -231,7 +231,6 @@ import { useAuthStore } from '../stores/index.js'
import Header from '@/views/Header.vue' import Header from '@/views/Header.vue'
import PatientCreate from '@/views/PatientCreate.vue' import PatientCreate from '@/views/PatientCreate.vue'
import Detection from '@/views/Detection.vue' import Detection from '@/views/Detection.vue'
import { color } from 'echarts'
import PatientProfile from '@/views/PatientProfile.vue' import PatientProfile from '@/views/PatientProfile.vue'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()

View File

@ -85,7 +85,6 @@
</el-button> </el-button>
</div> </div>
<div class="body-son-display"> <div class="body-son-display">
{{ }}
<img src="@/assets/new/refresh.svg" alt="" style="margin-right: 8px;cursor: pointer;" title="重启IMU" <img src="@/assets/new/refresh.svg" alt="" style="margin-right: 8px;cursor: pointer;" title="重启IMU"
@click="refreshClick('imu')"> @click="refreshClick('imu')">
<div class="connecttext" :style="{ color: imuStatus == '已连接' ? '#00CC00' : '#808080' }"> <div class="connecttext" :style="{ color: imuStatus == '已连接' ? '#00CC00' : '#808080' }">
@ -128,7 +127,12 @@
<span class="currencytext1">旋转角</span> <span class="currencytext1">旋转角</span>
<span class="currencytext3">{{ headlist.rotation }}°</span> <span class="currencytext3">{{ headlist.rotation }}°</span>
</div> </div>
<img src="@/assets/new/testheader.png" style="width: 100%;height: 80%;" alt=""> <div style="width: 100%;height: 80%;" alt="">
<!-- <img src="@/assets/new/testheader.png" > -->
<Model />
</div>
</div> </div>
<div class="body-header-bottombox-right"> <div class="body-header-bottombox-right">
<div class="body-header-bottombox-righttext" style="height: 20%;"></div> <div class="body-header-bottombox-righttext" style="height: 20%;"></div>
@ -433,6 +437,7 @@ import noImageSvg from '@/assets/no-image.svg'
import DiagnosticMessage from '@/views/DiagnosticMessage.vue' import DiagnosticMessage from '@/views/DiagnosticMessage.vue'
import PatientCreate from '@/views/PatientCreate.vue' import PatientCreate from '@/views/PatientCreate.vue'
import HistoryDashboard from '@/views/PatientProfile.vue' import HistoryDashboard from '@/views/PatientProfile.vue'
import Model from './model.vue'
const emit = defineEmits([ 'endChange']); const emit = defineEmits([ 'endChange']);
const asd = ref(0) const asd = ref(0)

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="login-page" :style="{ backgroundImage: `url(${bg})` }"> <div class="login-page" :style="{ backgroundImage: `url(${bg})` }">
<!-- <Model /> -->
<!-- 页面主内容 --> <!-- 页面主内容 -->
<div class="login-content"> <div class="login-content">
<!-- 系统标题 --> <!-- 系统标题 -->
@ -223,6 +224,7 @@ import { User, Lock, View, Hide, Phone } from '@element-plus/icons-vue'
import bg from '@/assets/new/newbg.jpg' import bg from '@/assets/new/newbg.jpg'
import { getBackendUrl,systemAPI } from '../services/api.js' import { getBackendUrl,systemAPI } from '../services/api.js'
// import Model from './model.vue'
const BACKEND_URL = getBackendUrl() const BACKEND_URL = getBackendUrl()
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()

View File

@ -73,15 +73,15 @@
<el-button v-if="selectedPatient" type="primary" <el-button v-if="selectedPatient" type="primary"
:class="selectedData.length == 2? 'patientprofile-selectedbutotn':'patientprofile-butotn'" :class="selectedData.length == 2? 'patientprofile-selectedbutotn':'patientprofile-butotn'"
@click="viewPatientProfile"> @click="viewPatientProfile">
<img v-if="selectedData.length == 2" src="@/assets/new/bi2.svg" alt="" style="margin-right: 8px;"> <img v-if="selectedData.length == 2" src="@/assets/new/bi2.png" alt="" style="margin-right: 8px;">
<img v-else src="@/assets/new/bi.svg" alt="" style="margin-right: 8px;"> <img v-else src="@/assets/new/bi.png" alt="" style="margin-right: 8px;">
报告对比 报告对比
</el-button> </el-button>
<el-button v-if="selectedPatient" type="primary" <el-button v-if="selectedPatient" type="primary"
:class="selectedData.length>0? 'patientprofile-selectedbutotn':'patientprofile-butotn'" :class="selectedData.length>0? 'patientprofile-selectedbutotn':'patientprofile-butotn'"
@click="viewPatientProfile"> @click="viewPatientProfile">
<img v-if="selectedData.length>0" src="@/assets/new/del2.svg" alt="" style="margin-right: 8px;"> <img v-if="selectedData.length>0" src="@/assets/new/del2.png" alt="" style="margin-right: 8px;">
<img v-else src="@/assets/new/del.svg" alt="" style="margin-right: 8px;"> <img v-else src="@/assets/new/del.png" alt="" style="margin-right: 8px;">
删除记录 删除记录
</el-button> </el-button>
</div> </div>

View File

@ -0,0 +1,609 @@
<template>
<div id="containermodel" >
<div @click="asd" style="position: absolute;top: 0;">开始动画</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// const { ipcRenderer } = require('electron');
// Three.js
let scene, camera, renderer, model;
let targetQuaternion = new THREE.Quaternion();
let currentQuaternion = new THREE.Quaternion();
//
let axisMapping = {
roll: 'z', // RollZ
pitch: 'x', // PitchX
yaw: 'y' // YawY
};
let manualOffsets = {
x: 0,
y: 0,
z: 0
};
onMounted(() => {
initThreeJS()
// initDebugControls();
})
function initThreeJS() {
//
scene = new THREE.Scene();
scene.background = new THREE.Color(0x282828); //
let containermodel = document.getElementById('containermodel');
//
camera = new THREE.PerspectiveCamera(75, containermodel.offsetWidth / containermodel.offsetHeight, 0.1, 1000);
camera.position.set(3, 3, 2);
camera.lookAt(0, 0, 0);
//
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(containermodel.offsetWidth, containermodel.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);
//
// -
const ambientLight = new THREE.AmbientLight(0x606060, 2.0);
scene.add(ambientLight);
// -
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(10, 10, 5);
directionalLight1.castShadow = true;
directionalLight1.shadow.mapSize.width = 2048;
directionalLight1.shadow.mapSize.height = 2048;
directionalLight1.shadow.camera.near = 0.1;
directionalLight1.shadow.camera.far = 50;
directionalLight1.shadow.camera.left = -10;
directionalLight1.shadow.camera.right = 10;
directionalLight1.shadow.camera.top = 10;
directionalLight1.shadow.camera.bottom = -10;
scene.add(directionalLight1);
// -
const directionalLight2 = new THREE.DirectionalLight(0xbbddff, 1.0);
directionalLight2.position.set(-8, 6, 4);
scene.add(directionalLight2);
//
const directionalLight3 = new THREE.DirectionalLight(0xffddbb, 0.8);
directionalLight3.position.set(8, 6, -4);
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, -5, 0);
scene.add(bottomLight);
// 线
createCoordinateAxes();
// 3D
loadModel();
//
animate();
}
function loadModel() {
const loader = new GLTFLoader();
// Model.glb
loader.load('Model.glb',
(gltf) => {
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 = 2 / maxDim; //
model.scale.set(scale, scale, scale);
//
const center = box.getCenter(new THREE.Vector3());
model.position.set(-center.x * scale, -center.y * scale + 1, -center.z * scale);
//
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
console.log('3D模型加载成功');
},
(progress) => {
console.log('加载进度:', (progress.loaded / progress.total * 100) + '%');
},
(error) => {
console.error('模型加载失败:', error);
//
createFallbackModel();
}
);
}
function createFallbackModel() {
console.log('使用默认立方体模型');
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshLambertMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8
});
model = new THREE.Mesh(geometry, material);
model.position.set(0, 1, 0);
model.castShadow = true;
model.receiveShadow = true;
scene.add(model);
}
function animate() {
requestAnimationFrame(animate);
// SLERP
if (model) {
currentQuaternion.slerp(targetQuaternion, 0.1);
model.quaternion.copy(currentQuaternion);
}
renderer.render(scene, camera);
}
//
window.addEventListener('resize', () => {
let containermodel = document.getElementById('containermodel');
camera.aspect = containermodel.offsetWidth / containermodel.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize(containermodel.offsetWidth, containermodel.offsetHeight);
});
function asd(){
setInterval(()=>{
targetQuaternion.set(
Math.random(),
Math.random(),
Math.random(),
Math.random()
);
},500)
}
// // IPC
// ipcRenderer.on('orientation-data', (event, data) => {
// if (data.quaternion && model) {
// //
// updateDebugDisplay('raw-quaternion', data.quaternion);
// //
// const mappedQuaternion = applyAxisMapping(data.quaternion);
// updateDebugDisplay('mapped-quaternion', mappedQuaternion);
// //
// const finalQuaternion = applyManualOffsets(mappedQuaternion);
// updateDebugDisplay('final-quaternion', finalQuaternion);
// //
// targetQuaternion.set(
// finalQuaternion.x,
// finalQuaternion.y,
// finalQuaternion.z,
// finalQuaternion.w
// );
// }
// });
// ipcRenderer.on('device-connected', () => {
// updateConnectionStatus('', true);
// });
// ipcRenderer.on('device-disconnected', () => {
// updateConnectionStatus('', false);
// });
// ipcRenderer.on('server-info', (event, info) => {
// updateServerInfo(info);
// });
function updateConnectionStatus(status, connected) {
const statusElement = document.getElementById('connection-status');
statusElement.textContent = `设备状态: ${status}`;
statusElement.className = connected ? 'status connected' : 'status disconnected';
}
function updateServerInfo(info) {
const serverInfoElement = document.getElementById('server-info');
serverInfoElement.innerHTML = `
服务器: ${info.ip}:${info.port}<br>
<strong>手机访问:</strong><br>
<a href="${info.mobileUrl}" target="_blank" style="color: #4CAF50;">${info.mobileUrl}</a><br>
<small>: <a href="${info.alternativeUrl}" target="_blank" style="color: #4CAF50;">${info.alternativeUrl}</a></small>
`;
//
generateQRCode(info.mobileUrl);
}
async function generateQRCode(url) {
const canvas = document.getElementById('qr-canvas');
try {
// 使qrcode
const QRCode = require('qrcode');
//
const options = {
width: 120,
height: 120,
margin: 1,
color: {
dark: '#000000',
light: '#FFFFFF'
},
errorCorrectionLevel: 'M'
};
// canvas
await QRCode.toCanvas(canvas, url, options);
console.log('二维码生成成功:', url);
} catch (error) {
console.error('二维码生成失败:', error);
//
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 120, 120);
ctx.fillStyle = '#000000';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText('QR生成失败', 60, 50);
ctx.font = '10px Arial';
ctx.fillText('请手动输入URL', 60, 70);
}
}
//
function applyAxisMapping(quaternion) {
//
const euler = new THREE.Euler().setFromQuaternion(
new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w)
);
//
const mappedEuler = {
x: getAxisValue(euler, axisMapping.pitch),
y: getAxisValue(euler, axisMapping.yaw),
z: getAxisValue(euler, axisMapping.roll)
};
//
const resultQuaternion = new THREE.Quaternion().setFromEuler(
new THREE.Euler(mappedEuler.x, mappedEuler.y, mappedEuler.z)
);
return {
w: resultQuaternion.w,
x: resultQuaternion.x,
y: resultQuaternion.y,
z: resultQuaternion.z
};
}
function getAxisValue(euler, mapping) {
const value = {
'x': euler.x,
'-x': -euler.x,
'y': euler.y,
'-y': -euler.y,
'z': euler.z,
'-z': -euler.z
}[mapping];
return value || 0;
}
//
function applyManualOffsets(quaternion) {
//
const offsetQuaternion = new THREE.Quaternion().setFromEuler(
new THREE.Euler(
manualOffsets.x * Math.PI / 180,
manualOffsets.y * Math.PI / 180,
manualOffsets.z * Math.PI / 180
)
);
//
const originalQuaternion = new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
const resultQuaternion = offsetQuaternion.multiply(originalQuaternion);
return {
w: resultQuaternion.w,
x: resultQuaternion.x,
y: resultQuaternion.y,
z: resultQuaternion.z
};
}
//
function updateDebugDisplay(elementId, quaternion) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = `w:${quaternion.w.toFixed(3)}, x:${quaternion.x.toFixed(3)}, y:${quaternion.y.toFixed(3)}, z:${quaternion.z.toFixed(3)}`;
}
}
// 线
function createCoordinateAxes() {
const axesGroup = new THREE.Group();
// 线
const axisLength = 3;
const axisRadius = 0.02;
// X - 使
const xGeometry = new THREE.CylinderGeometry(axisRadius, axisRadius, axisLength, 8);
const xMaterial = new THREE.MeshBasicMaterial({
color: 0xff6666,
transparent: true,
opacity: 0.9
});
const xAxis = new THREE.Mesh(xGeometry, xMaterial);
xAxis.rotation.z = -Math.PI / 2;
xAxis.position.x = axisLength / 2;
axesGroup.add(xAxis);
// X
const xArrowGeometry = new THREE.ConeGeometry(axisRadius * 3, axisRadius * 8, 8);
const xArrow = new THREE.Mesh(xArrowGeometry, xMaterial);
xArrow.rotation.z = -Math.PI / 2;
xArrow.position.x = axisLength + axisRadius * 4;
axesGroup.add(xArrow);
// Y - 绿使
const yGeometry = new THREE.CylinderGeometry(axisRadius, axisRadius, axisLength, 8);
const yMaterial = new THREE.MeshBasicMaterial({
color: 0x66ff66,
transparent: true,
opacity: 0.9
});
const yAxis = new THREE.Mesh(yGeometry, yMaterial);
yAxis.position.y = axisLength / 2;
axesGroup.add(yAxis);
// Y
const yArrowGeometry = new THREE.ConeGeometry(axisRadius * 3, axisRadius * 8, 8);
const yArrow = new THREE.Mesh(yArrowGeometry, yMaterial);
yArrow.position.y = axisLength + axisRadius * 4;
axesGroup.add(yArrow);
// Z - 使
const zGeometry = new THREE.CylinderGeometry(axisRadius, axisRadius, axisLength, 8);
const zMaterial = new THREE.MeshBasicMaterial({
color: 0x6666ff,
transparent: true,
opacity: 0.9
});
const zAxis = new THREE.Mesh(zGeometry, zMaterial);
zAxis.rotation.x = Math.PI / 2;
zAxis.position.z = axisLength / 2;
axesGroup.add(zAxis);
// Z
const zArrowGeometry = new THREE.ConeGeometry(axisRadius * 3, axisRadius * 8, 8);
const zArrow = new THREE.Mesh(zArrowGeometry, zMaterial);
zArrow.rotation.x = Math.PI / 2;
zArrow.position.z = axisLength + axisRadius * 4;
axesGroup.add(zArrow);
// -
const originGeometry = new THREE.SphereGeometry(axisRadius * 2, 16, 16);
const originMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8
});
const origin = new THREE.Mesh(originGeometry, originMaterial);
axesGroup.add(origin);
scene.add(axesGroup);
}
//
function initDebugControls() {
//
document.getElementById('roll-mapping').addEventListener('change', (e) => {
axisMapping.roll = e.target.value;
});
document.getElementById('pitch-mapping').addEventListener('change', (e) => {
axisMapping.pitch = e.target.value;
});
document.getElementById('yaw-mapping').addEventListener('change', (e) => {
axisMapping.yaw = e.target.value;
});
//
['x', 'y', 'z'].forEach(axis => {
const slider = document.getElementById(`offset-${axis}`);
const valueDisplay = document.getElementById(`offset-${axis}-value`);
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
manualOffsets[axis] = value;
valueDisplay.textContent = `${value}°`;
});
});
//
document.getElementById('reset-offsets').addEventListener('click', () => {
['x', 'y', 'z'].forEach(axis => {
manualOffsets[axis] = 0;
document.getElementById(`offset-${axis}`).value = 0;
document.getElementById(`offset-${axis}-value`).textContent = '0°';
});
});
}
//
document.addEventListener('DOMContentLoaded', () => {
initThreeJS();
initDebugControls();
//
ipcRenderer.invoke('get-server-info').then(info => {
updateServerInfo(info);
});
});
//
document.addEventListener('keydown', (event) => {
if (!model) return;
switch(event.key) {
case 'r':
case 'R':
//
targetQuaternion.set(0, 0, 0, 1);
currentQuaternion.set(0, 0, 0, 1);
break;
}
});
</script>
<style scoped>
#containermodel {
width: 100%;
height: 100%;
position: relative;
}
#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>