合并冲突
This commit is contained in:
commit
d9b6141d54
@ -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', () => {
|
||||
|
||||
@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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('⚠️ 收到空的视频帧数据')
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
|
||||
// 应用SLERP平滑插值到模型
|
||||
if (!currentTime) currentTime = performance.now();
|
||||
|
||||
// 帧率节流
|
||||
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;
|
||||
}
|
||||
|
||||
#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;
|
||||
overflow: hidden; /* 防止 canvas 溢出 */
|
||||
}
|
||||
</style>
|
||||
@ -24,7 +24,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
host: '0.0.0.0',
|
||||
// 开发服务器配置
|
||||
cors: true,
|
||||
strictPort: false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user