baoshiwei
6 天以前 5bf14aed888cd0e258e325c65f14022dad02985b
更改为通过websocket获取数据
已添加3个文件
已修改10个文件
943 ■■■■ 文件已修改
README.md 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src-tauri/Cargo.toml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src-tauri/src/lib.rs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src-tauri/src/main.rs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src-tauri/src/ws_client.rs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ForceChart.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/GaugeDisplay.vue 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TableDisplay.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ThreeDDisplay copy.vue 525 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ThreeDDisplay.vue 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pipe_client.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dataFetcher.ts 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md
@@ -2,6 +2,10 @@
这个模板旨在帮助您开始使用 Vue 3 å’Œ TypeScript åœ¨ Vite ä¸­è¿›è¡Œå¼€å‘。该模板使用 Vue 3 `<script setup>` SFCs,请查看 [script setup æ–‡æ¡£](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) äº†è§£æ›´å¤šä¿¡æ¯ã€‚
## é¡¹ç›®ä»‹ç»
本项目为"六维力传感器测试软件",基于 Tauri + Vue3 + TypeScript å¼€å‘。软件通过管道通信实时接收六维力(Fx, Fy, Fz, Mx, My, Mz)数据,支持曲线图、仪表盘、数据表格和 3D åæ ‡è½´ç­‰å¤šç§å¯è§†åŒ–方式,方便用户对六维力传感器进行调试、测试和数据分析。适用于科研、工业等多种应用场景。
## æŽ¨èçš„ IDE è®¾ç½®
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
src-tauri/Cargo.toml
@@ -25,3 +25,9 @@
tokio = { version = "1.45.0", features = ["rt", "net", "time"] }
rand = "0.8.5"
once_cell = "1.19"
tokio-tungstenite = "0.21"
futures-util = "0.3"
tungstenite = "0.21"
anyhow = "1.0"
src-tauri/src/lib.rs
@@ -1,5 +1,9 @@
mod tokio_utils;
mod pipe_server;
mod ws_client;
use crate::ws_client::LATEST_WS_DATA;
use std::sync::Mutex;
use tauri::async_runtime::spawn;
@@ -9,6 +13,10 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use crate::pipe_server::start_pipe_server;
#[tauri::command]
fn get_latest_ws_data() -> Option<String> {
    LATEST_WS_DATA.lock().unwrap().clone()
}
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
@@ -38,6 +46,8 @@
        .map_err(|e| e.to_string())
}
// #[cfg_attr(mobile, tauri::mobile_entry_point)]
// pub fn run() {
//     tauri::Builder::default()
@@ -60,10 +70,12 @@
            backend_task: false,
        }))
        // Add a command we can use to check
        .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message])
        .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message, get_latest_ws_data])
        // Use the setup hook to execute setup related tasks
        // Runs before the main loop, so no windows are yet created
        .setup(|app| {
            // å¯åЍ WebSocket å®¢æˆ·ç«¯ï¼ˆå¼‚步任务)
            ws_client::start_ws_client();
            // Spawn setup as a non-blocking task so the windows can be
            // created and ran while it executes
            spawn(setup(app.handle().clone()));
src-tauri/src/main.rs
@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
    six_axis_force_lib::run()
    six_axis_force_lib::run();
}
src-tauri/src/ws_client.rs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
use std::sync::{Arc, Mutex};
use once_cell::sync::Lazy;
use tokio::runtime::Runtime;
use tokio_tungstenite::connect_async;
use futures_util::{StreamExt};
// å…¨å±€ç¼“存最新六维力数据
pub static LATEST_WS_DATA: Lazy<Arc<Mutex<Option<String>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
// WebSocket æœåŠ¡ç«¯åœ°å€ï¼ˆå¯æ ¹æ®å®žé™…æƒ…å†µä¿®æ”¹ï¼‰
const WS_SERVER_URL: &str = "ws://192.168.21.25:5000";
pub fn start_ws_client() {
    // å¯åЍ tokio runtime
    std::thread::spawn(|| {
        let rt = Runtime::new().unwrap();
        rt.block_on(async move {
            if let Err(e) = ws_client_task().await {
                eprintln!("WebSocket å®¢æˆ·ç«¯å¯åŠ¨å¤±è´¥: {}", e);
            }
        });
    });
}
async fn ws_client_task() -> anyhow::Result<()> {
    println!("尝试连接 WebSocket æœåŠ¡ç«¯: {}", WS_SERVER_URL);
    let (ws_stream, _) = connect_async(WS_SERVER_URL).await?;
    println!("WebSocket å·²è¿žæŽ¥");
    let (_, mut read) = ws_stream.split();
    while let Some(msg) = read.next().await {
        match msg {
            Ok(tungstenite::Message::Text(text)) => {
                // å‡è®¾æœåŠ¡ç«¯æŽ¨é€çš„å°±æ˜¯å…­ç»´åŠ›æ•°æ®çš„ JSON å­—符串
                let mut data = LATEST_WS_DATA.lock().unwrap();
                *data = Some(text.clone());
            },
            Ok(_) => {},
            Err(e) => {
                eprintln!("WebSocket è¯»å–错误: {}", e);
                break;
            }
        }
    }
    Ok(())
}
src/App.vue
@@ -12,6 +12,7 @@
const showChart = ref(false);
const pipeName = 'tauri-pipe-server';
const currentDisplay = ref('line'); // é»˜è®¤æ˜¾ç¤ºæ›²çº¿å›¾
const dataSource = ref<'pipe' | 'ws'>('ws'); // é»˜è®¤æ•°æ®æºä¸ºç®¡é“
async function startPipeServer() {
  try {
@@ -43,6 +44,13 @@
<template>
  <TitleBar :currentDisplay="currentDisplay" @start-service="startPipeServer" @select-display="(mode) => currentDisplay = mode" />
  <main class="container">
    <!-- <div class="data-source-switch">
      <label>数据源:</label>
      <select v-model="dataSource">
        <option value="pipe">管道</option>
        <option value="ws">WebSocket</option>
      </select>
    </div> -->
    
      <div class="display-mode-icons">
      <button :class="['icon-button', { active: currentDisplay === 'line' }]" @click="currentDisplay = 'line'" title="曲线图">
@@ -54,9 +62,9 @@
      <button :class="['icon-button', { active: currentDisplay === 'table' }]" @click="currentDisplay = 'table'" title="数据表格">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="7" rx="2"/><path d="M10 12H6"/><path d="M18 12h-4"/><path d="M10 16H6"/><path d="M18 16h-4"/></svg>
      </button>
      <!-- <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3D坐标轴">
      <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3D坐标轴">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5l10-5l-10-5z"/><path d="M2 17l10 5l10-5"/><path d="M2 12l10 5l10-5"/></svg>
      </button> -->
      </button>
    </div>
    <div class="header-row">
      <h2>Force Sensor Data Visualization</h2>
@@ -66,10 +74,10 @@
    
    <!-- æ˜¾ç¤ºåŠ›ä¼ æ„Ÿå™¨æ•°æ® -->
    <div v-if="showChart" class="chart-wrapper">
      <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" />
      <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" />
      <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" />
      <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" />
      <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" :dataSource="dataSource" />
      <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" :dataSource="dataSource" />
      <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" :dataSource="dataSource" />
      <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" :dataSource="dataSource" />
    </div>
    
    <!-- <div class="input-group">
@@ -124,6 +132,13 @@
  color: #000;
  font-size: 0.9em;
}
.data-source-switch {
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>
<style>
src/components/ForceChart.vue
@@ -1,11 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted} from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { sendToPipe } from '../pipe_client';
const props = defineProps<{
  pipeName: string;
}>();
import { createDataReceiver } from '../utils/dataFetcher';
const chartContainer = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
@@ -64,37 +60,43 @@
        name: 'Fx',
        type: 'line',
        data: dataHistory.value[0],
        smooth: true
        smooth: true,
        showSymbol: false
      },
      {
        name: 'Fy',
        type: 'line',
        data: dataHistory.value[1],
        smooth: true
        smooth: true,
        showSymbol: false
      },
      {
        name: 'Fz',
        type: 'line',
        data: dataHistory.value[2],
        smooth: true
        smooth: true,
        showSymbol: false
      },
      {
        name: 'Mx',
        type: 'line',
        data: dataHistory.value[3],
        smooth: true
        smooth: true,
        showSymbol: false
      },
      {
        name: 'My',
        type: 'line',
        data: dataHistory.value[4],
        smooth: true
        smooth: true,
        showSymbol: false
      },
      {
        name: 'Mz',
        type: 'line',
        data: dataHistory.value[5],
        smooth: true
        smooth: true,
        showSymbol: false
      }
    ]
  };
@@ -150,53 +152,22 @@
  chart?.resize();
}
// çŠ¶æ€å˜é‡ï¼Œç”¨äºŽæŽ§åˆ¶é”™è¯¯æ˜¾ç¤ºé¢‘çŽ‡
const errorCount = ref(0);
const maxConsecutiveErrors = 5;
const showingError = ref(false);
// æŽ¥æ”¶ç®¡é“数据的函数
async function receiveForceData() {
  try {
    // ä»Žç®¡é“接收数据
    const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA');
    // æˆåŠŸæŽ¥æ”¶æ•°æ®ï¼Œé‡ç½®é”™è¯¯è®¡æ•°
    errorCount.value = 0;
    if (showingError.value) {
      showingError.value = false;
      console.log('管道通信已恢复');
    }
    // è§£æžæŽ¥æ”¶åˆ°çš„æ•°æ®
    try {
      const forceData = JSON.parse(response);
      if (Array.isArray(forceData) && forceData.length === 6) {
        updateChart(forceData);
      }
    } catch (e) {
      console.warn('解析数据失败:', e);
    }
  } catch (err) {
    // å¢žåŠ é”™è¯¯è®¡æ•°
    errorCount.value++;
    // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼æ—¶æ˜¾ç¤ºé”™è¯¯ä¿¡æ¯ï¼Œé¿å…æ—¥å¿—刷屏
    if (errorCount.value >= maxConsecutiveErrors && !showingError.value) {
      showingError.value = true;
      console.error('管道通信持续失败,请检查服务端状态:', err);
    }
  }
}
// å®šæ—¶èŽ·å–æ•°æ®
let dataTimer: number | null = null;
onMounted(() => {
  initChart();
  
  // æ¯ç§’获取一次数据
  dataTimer = window.setInterval(receiveForceData, 1000);
  const receiveData = createDataReceiver( (forceData) => {
    updateChart(forceData);
  });
  // æ¯100ms获取一次数据
  dataTimer = window.setInterval(receiveData, 100);
  // ç›‘听窗口大小变化,调整图表大小
  window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
src/components/GaugeDisplay.vue
@@ -1,11 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { sendToPipe } from '../pipe_client';
const props = defineProps<{
  pipeName: string;
}>();
import { createDataReceiver } from '../utils/dataFetcher';
const chartContainer = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
@@ -38,8 +34,8 @@
      {
        name: 'Fx',
        type: 'gauge',
        min: -100,
        max: 100,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['16.7%', '30%'],
@@ -61,8 +57,8 @@
      {
        name: 'Fy',
        type: 'gauge',
        min: -100,
        max: 100,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['50%', '30%'],
@@ -84,8 +80,8 @@
      {
        name: 'Fz',
        type: 'gauge',
        min: -100,
        max: 100,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['83.3%', '30%'],
@@ -107,8 +103,8 @@
      {
        name: 'Mx',
        type: 'gauge',
        min: -10,
        max: 10,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['16.7%', '75%'],
@@ -130,8 +126,8 @@
      {
        name: 'My',
        type: 'gauge',
        min: -10,
        max: 10,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['50%', '75%'],
@@ -153,8 +149,8 @@
      {
        name: 'Mz',
        type: 'gauge',
        min: -10,
        max: 10,
        min: 1000,
        max: 4000,
        splitNumber: 10,
        radius: '30%',
        center: ['83.3%', '75%'],
@@ -209,46 +205,17 @@
  });
}
// çŠ¶æ€å˜é‡ï¼Œç”¨äºŽæŽ§åˆ¶é”™è¯¯æ˜¾ç¤ºé¢‘çŽ‡
const errorCount = ref(0);
const maxConsecutiveErrors = 5;
const showingError = ref(false);
// æŽ¥æ”¶ç®¡é“数据的函数
async function receiveData() {
  try {
    const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA');
    // é‡ç½®é”™è¯¯è®¡æ•°
    errorCount.value = 0;
    showingError.value = false;
    // è§£æžæ•°æ®
    try {
      const forceData = JSON.parse(response);
      if (Array.isArray(forceData) && forceData.length === 6) {
        updateChart(forceData);
      }
    } catch (parseErr) {
      console.error('数据解析错误:', parseErr);
    }
  } catch (err) {
    // å¢žåŠ é”™è¯¯è®¡æ•°
    errorCount.value++;
    // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼ä¸”尚未显示错误时显示
    if (errorCount.value >= maxConsecutiveErrors && !showingError.value) {
      console.error('管道通信失败:', err);
      showingError.value = true;
    }
  }
}
let dataInterval: number | null = null;
onMounted(() => {
  initChart();
  
  const receiveData = createDataReceiver((forceData) => {
    updateChart(forceData);
  });
  // æ¯200ms获取一次数据
  dataInterval = window.setInterval(receiveData, 200);
  
src/components/TableDisplay.vue
@@ -1,10 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { sendToPipe } from '../pipe_client';
const props = defineProps<{
  pipeName: string;
}>();
import { createDataReceiver } from '../utils/dataFetcher';
interface ForceData {
  timestamp: string;
@@ -42,44 +38,14 @@
  }
}
// çŠ¶æ€å˜é‡ï¼Œç”¨äºŽæŽ§åˆ¶é”™è¯¯æ˜¾ç¤ºé¢‘çŽ‡
const errorCount = ref(0);
const maxConsecutiveErrors = 5;
const showingError = ref(false);
// æŽ¥æ”¶ç®¡é“数据的函数
async function receiveData() {
  try {
    const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA');
    // é‡ç½®é”™è¯¯è®¡æ•°
    errorCount.value = 0;
    showingError.value = false;
    // è§£æžæ•°æ®
    try {
      const forceData = JSON.parse(response);
      if (Array.isArray(forceData) && forceData.length === 6) {
        updateTable(forceData);
      }
    } catch (parseErr) {
      console.error('数据解析错误:', parseErr);
    }
  } catch (err) {
    // å¢žåŠ é”™è¯¯è®¡æ•°
    errorCount.value++;
    // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼ä¸”尚未显示错误时显示
    if (errorCount.value >= maxConsecutiveErrors && !showingError.value) {
      console.error('管道通信失败:', err);
      showingError.value = true;
    }
  }
}
let dataInterval: number | null = null;
onMounted(() => {
  const receiveData = createDataReceiver( (forceData) => {
    updateTable(forceData);
  });
  // æ¯500ms获取一次数据
  dataInterval = window.setInterval(receiveData, 500);
});
src/components/ThreeDDisplay copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,525 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { sendToPipe } from '../pipe_client';
const props = defineProps<{
  pipeName: string;
}>();
const containerRef = ref<HTMLElement | null>(null);
let scene: THREE.Scene | null = null;
let camera: THREE.PerspectiveCamera | null = null;
let renderer: THREE.WebGLRenderer | null = null;
let controls: OrbitControls | null = null;
// åŠ›å’ŒåŠ›çŸ©çš„ç®­å¤´
let forceArrows: THREE.ArrowHelper[] = [];
let torqueArcs: THREE.Line[] = [];
// å½“前力和力矩数据
const currentData = ref<number[]>([0, 0, 0, 0, 0, 0]);
// é¢œè‰²å®šä¹‰
const colors = {
  fx: 0xff0000, // çº¢è‰²
  fy: 0x00ff00, // ç»¿è‰²
  fz: 0x0000ff, // è“è‰²
  mx: 0xff00ff, // ç´«è‰²
  my: 0xffff00, // é»„色
  mz: 0x00ffff  // é’色
};
// åˆå§‹åŒ–3D场景
function initScene() {
  if (!containerRef.value) return;
  // åˆ›å»ºåœºæ™¯
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000000);
  // åˆ›å»ºç›¸æœº
  const width = containerRef.value.clientWidth;
  const height = containerRef.value.clientHeight;
  camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
  camera.position.set(5, 5, 5);
  camera.lookAt(0, 0, 0);
  // åˆ›å»ºæ¸²æŸ“器
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(width, height);
  containerRef.value.appendChild(renderer.domElement);
  // æ·»åŠ è½¨é“æŽ§åˆ¶å™¨
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.25;
  // æ·»åŠ åæ ‡è½´
  const axesHelper = new THREE.AxesHelper(4);
  axesHelper.setColors(
    new THREE.Color(0x707070), // X轴颜色
    new THREE.Color(0x707070), // Y轴颜色
    new THREE.Color(0x707070)  // Z轴颜色
  );
  // è®¾ç½®åæ ‡è½´çš„æ¸²æŸ“顺序为较低值
  axesHelper.renderOrder = -1;
  if (axesHelper instanceof THREE.Object3D) {
    axesHelper.traverse((child) => {
      if (child instanceof THREE.Line) {
        if (child.material instanceof THREE.LineBasicMaterial) {
          child.material.depthTest = false;
        }
      }
    });
  }
  scene.add(axesHelper);
  // æ·»åŠ åæ ‡è½´æ ‡ç­¾
  const textDistance = 4.2; // åæ ‡è½´é•¿åº¦ + ä¸€ç‚¹é—´è·
  // åˆ›å»ºæ ‡ç­¾ç²¾çµ
  function createAxisLabel(text: string, position: THREE.Vector3) {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    if (!context) return null;
    canvas.width = 64;
    canvas.height = 64;
    context.fillStyle = '#ffffff';
    context.font = 'bold 48px Arial';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillText(text, 32, 32);
    const texture = new THREE.CanvasTexture(canvas);
    const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
    const sprite = new THREE.Sprite(spriteMaterial);
    sprite.position.copy(position);
    sprite.scale.set(0.5, 0.5, 1);
    return sprite;
  }
  // åˆ›å»ºå¹¶æ·»åŠ XYZ轴标签
  const labelX = createAxisLabel('X', new THREE.Vector3(textDistance, 0, 0));
  const labelY = createAxisLabel('Y', new THREE.Vector3(0, textDistance, 0));
  const labelZ = createAxisLabel('Z', new THREE.Vector3(0, 0, textDistance));
  if (labelX) scene.add(labelX);
  if (labelY) scene.add(labelY);
  if (labelZ) scene.add(labelZ);
  // æ·»åŠ ç½‘æ ¼
  const gridHelper = new THREE.GridHelper(20, 20, 0x404040, 0x404040);
  gridHelper.material.opacity = 0.5;
  gridHelper.material.transparent = true;
  scene.add(gridHelper);
  // æ·»åŠ çŽ¯å¢ƒå…‰
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
  scene.add(ambientLight);
  // æ·»åŠ æ–¹å‘å…‰
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  directionalLight.position.set(10, 10, 10);
  scene.add(directionalLight);
  // åˆå§‹åŒ–力和力矩的箭头
  initForceArrows();
  initTorqueArcs();
  // å¼€å§‹åŠ¨ç”»å¾ªçŽ¯
  animate();
  // å“åº”窗口大小变化
  window.addEventListener('resize', handleResize);
}
// åˆå§‹åŒ–力的箭头
function initForceArrows() {
  if (!scene) return;
  // æ¸…除现有的箭头
  forceArrows.forEach(arrow => scene?.remove(arrow));
  forceArrows = [];
  // åˆ›å»ºä¸‰ä¸ªåŠ›çš„ç®­å¤´ (Fx, Fy, Fz)
  const headLength = 0.5;
  const headWidth = 0.3;
  // åˆ›å»ºåŠ›çš„ç®­å¤´ (初始长度为0)
  const fxArrow = new THREE.ArrowHelper(
    new THREE.Vector3(1, 0, 0),
    new THREE.Vector3(0, 0, 0),
    0.001,
    colors.fx,
    headLength,
    headWidth
  );
  fxArrow.renderOrder = 1;
  const fyArrow = new THREE.ArrowHelper(
    new THREE.Vector3(0, 1, 0),
    new THREE.Vector3(0, 0, 0),
    0.001,
    colors.fy,
    headLength,
    headWidth
  );
  fyArrow.renderOrder = 1;
  const fzArrow = new THREE.ArrowHelper(
    new THREE.Vector3(0, 0, 1),
    new THREE.Vector3(0, 0, 0),
    0.001,
    colors.fz,
    headLength,
    headWidth
  );
  fzArrow.renderOrder = 1;
  // æ·»åŠ åˆ°åœºæ™¯
  scene.add(fxArrow);
  scene.add(fyArrow);
  scene.add(fzArrow);
  // ä¿å­˜å¼•用
  forceArrows = [fxArrow, fyArrow, fzArrow];
}
// åˆå§‹åŒ–力矩的弧形表示
function initTorqueArcs() {
  if (!scene) return;
  // æ¸…除现有的弧形
  torqueArcs.forEach(arc => scene?.remove(arc));
  torqueArcs = [];
  // åˆ›å»ºä¸‰ä¸ªåŠ›çŸ©çš„å¼§å½¢ (Mx, My, Mz)
  const radius = 2;
  const segments = 32;
  // åˆ›å»ºX轴力矩弧形 (绕X轴旋转)
  const mxGeometry = new THREE.BufferGeometry();
  const mxPoints = [];
  for (let i = 0; i <= segments; i++) {
    const theta = (i / segments) * Math.PI;
    mxPoints.push(new THREE.Vector3(0, radius * Math.cos(theta), radius * Math.sin(theta)));
  }
  mxGeometry.setFromPoints(mxPoints);
  const mxArc = new THREE.Line(mxGeometry, new THREE.LineBasicMaterial({ color: colors.mx, linewidth: 2 }));
  // åˆ›å»ºY轴力矩弧形 (绕Y轴旋转)
  const myGeometry = new THREE.BufferGeometry();
  const myPoints = [];
  for (let i = 0; i <= segments; i++) {
    const theta = (i / segments) * Math.PI;
    myPoints.push(new THREE.Vector3(radius * Math.cos(theta), 0, radius * Math.sin(theta)));
  }
  myGeometry.setFromPoints(myPoints);
  const myArc = new THREE.Line(myGeometry, new THREE.LineBasicMaterial({ color: colors.my, linewidth: 2 }));
  // åˆ›å»ºZ轴力矩弧形 (绕Z轴旋转)
  const mzGeometry = new THREE.BufferGeometry();
  const mzPoints = [];
  for (let i = 0; i <= segments; i++) {
    const theta = (i / segments) * Math.PI;
    mzPoints.push(new THREE.Vector3(radius * Math.cos(theta), radius * Math.sin(theta), 0));
  }
  mzGeometry.setFromPoints(mzPoints);
  const mzArc = new THREE.Line(mzGeometry, new THREE.LineBasicMaterial({ color: colors.mz, linewidth: 2 }));
  // æ·»åŠ åˆ°åœºæ™¯
  scene.add(mxArc);
  scene.add(myArc);
  scene.add(mzArc);
  // ä¿å­˜å¼•用
  torqueArcs = [mxArc, myArc, mzArc];
  // åˆå§‹çŠ¶æ€ä¸‹éšè—å¼§å½¢
  torqueArcs.forEach(arc => {
    arc.visible = false;
  });
}
// æ›´æ–°åŠ›å’ŒåŠ›çŸ©çš„å¯è§†åŒ–
function updateVisualization(forceData: number[]) {
  if (!scene) return;
  // æ›´æ–°å½“前数据
  currentData.value = forceData;
  // è®¡ç®—力的最大值,用于归一化
  const maxForce = Math.max(
    Math.abs(forceData[0]),
    Math.abs(forceData[1]),
    Math.abs(forceData[2])
  ) || 1; // é¿å…é™¤ä»¥é›¶
  // è®¡ç®—力矩的最大值,用于归一化
  const maxTorque = Math.max(
    Math.abs(forceData[3]),
    Math.abs(forceData[4]),
    Math.abs(forceData[5])
  ) || 1; // é¿å…é™¤ä»¥é›¶
  // æ›´æ–°åŠ›çš„ç®­å¤´
  const forceScale = 3; // æœ€å¤§é•¿åº¦ä¸º3个单位
  for (let i = 0; i < 3; i++) {
    const force = forceData[i];
    const length = Math.abs(force) / maxForce * forceScale;
    // æ›´æ–°ç®­å¤´é•¿åº¦å’Œæ–¹å‘
    if (forceArrows[i]) {
      // å¦‚果力为零或非常小,则几乎不可见
      if (Math.abs(force) < 0.1) {
        forceArrows[i].setLength(0.001);
      } else {
        forceArrows[i].setLength(length);
        // å¦‚果力为负,则反转方向
        if (force < 0) {
          const dir = new THREE.Vector3();
          if (i === 0) dir.set(-1, 0, 0);
          if (i === 1) dir.set(0, -1, 0);
          if (i === 2) dir.set(0, 0, -1);
          forceArrows[i].setDirection(dir);
        } else {
          const dir = new THREE.Vector3();
          if (i === 0) dir.set(1, 0, 0);
          if (i === 1) dir.set(0, 1, 0);
          if (i === 2) dir.set(0, 0, 1);
          forceArrows[i].setDirection(dir);
        }
      }
    }
  }
  // æ›´æ–°åŠ›çŸ©çš„å¼§å½¢
  for (let i = 0; i < 3; i++) {
    const torque = forceData[i + 3];
    // æ˜¾ç¤ºæˆ–隐藏力矩弧形
    if (torqueArcs[i]) {
      if (Math.abs(torque) < 0.1) {
        torqueArcs[i].visible = false;
      } else {
        torqueArcs[i].visible = true;
        // æ ¹æ®åŠ›çŸ©æ–¹å‘æ—‹è½¬å¼§å½¢
        const arc = torqueArcs[i];
        arc.rotation.set(0, 0, 0); // é‡ç½®æ—‹è½¬
        // æ ¹æ®åŠ›çŸ©å¤§å°å’Œæ–¹å‘è®¾ç½®æ—‹è½¬
        const rotationAngle = (Math.abs(torque) / maxTorque) * Math.PI / 2;
        if (i === 0) { // Mx
          arc.rotation.x = torque > 0 ? rotationAngle : -rotationAngle;
        } else if (i === 1) { // My
          arc.rotation.y = torque > 0 ? rotationAngle : -rotationAngle;
        } else if (i === 2) { // Mz
          arc.rotation.z = torque > 0 ? rotationAngle : -rotationAngle;
        }
      }
    }
  }
}
// åŠ¨ç”»å¾ªçŽ¯
function animate() {
  if (!scene || !camera || !renderer || !controls) return;
  requestAnimationFrame(animate);
  // æ›´æ–°æŽ§åˆ¶å™¨
  controls.update();
  // æ¸²æŸ“场景
  renderer.render(scene, camera);
}
// å¤„理窗口大小变化
function handleResize() {
  if (!containerRef.value || !camera || !renderer) return;
  const width = containerRef.value.clientWidth;
  const height = containerRef.value.clientHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
}
// çŠ¶æ€å˜é‡ï¼Œç”¨äºŽæŽ§åˆ¶é”™è¯¯æ˜¾ç¤ºé¢‘çŽ‡
const errorCount = ref(0);
const maxConsecutiveErrors = 5;
const showingError = ref(false);
// æŽ¥æ”¶ç®¡é“数据的函数
async function receiveForceData() {
  try {
    // ä»Žç®¡é“接收数据
    const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA');
    // æˆåŠŸæŽ¥æ”¶æ•°æ®ï¼Œé‡ç½®é”™è¯¯è®¡æ•°
    errorCount.value = 0;
    if (showingError.value) {
      showingError.value = false;
      console.log('管道通信已恢复');
    }
    // è§£æžæŽ¥æ”¶åˆ°çš„æ•°æ®
    try {
      const forceData = JSON.parse(response);
      if (Array.isArray(forceData) && forceData.length === 6) {
        updateVisualization(forceData);
      }
    } catch (e) {
      console.warn('解析数据失败:', e);
    }
  } catch (err) {
    // å¢žåŠ é”™è¯¯è®¡æ•°
    errorCount.value++;
    // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼æ—¶æ˜¾ç¤ºé”™è¯¯ä¿¡æ¯ï¼Œé¿å…æ—¥å¿—刷屏
    if (errorCount.value >= maxConsecutiveErrors && !showingError.value) {
      showingError.value = true;
      console.error('管道通信持续失败,请检查服务端状态:', err);
    }
  }
}
// å®šæ—¶èŽ·å–æ•°æ®
let dataTimer: number | null = null;
onMounted(() => {
  initScene();
  // æ¯ç§’获取一次数据
  dataTimer = window.setInterval(receiveForceData, 3000);
});
onUnmounted(() => {
  // æ¸…理定时器和事件监听
  if (dataTimer !== null) {
    clearInterval(dataTimer);
  }
  window.removeEventListener('resize', handleResize);
  // æ¸…理Three.js资源
  if (renderer) {
    renderer.dispose();
    containerRef.value?.removeChild(renderer.domElement);
  }
  // æ¸…除场景中的所有对象
  if (scene) {
    scene.clear();
  }
  // æ¸…空引用
  scene = null;
  camera = null;
  renderer = null;
  controls = null;
  forceArrows = [];
  torqueArcs = [];
});
</script>
<template>
  <div class="three-container">
    <div class="three-scene" ref="containerRef"></div>
    <div class="data-panel">
      <h3 style="color: #ffffff;">六维力数据</h3>
      <div class="data-row">
        <div class="data-item" style="color: #ff0000">
          <span class="label">Fx:</span>
          <span class="value">{{ currentData[0].toFixed(2) }} N</span>
        </div>
        <div class="data-item" style="color: #00ff00">
          <span class="label">Fy:</span>
          <span class="value">{{ currentData[1].toFixed(2) }} N</span>
        </div>
        <div class="data-item" style="color: #0000ff">
          <span class="label">Fz:</span>
          <span class="value">{{ currentData[2].toFixed(2) }} N</span>
        </div>
      </div>
      <div class="data-row">
        <div class="data-item" style="color: #ff00ff">
          <span class="label">Mx:</span>
          <span class="value">{{ currentData[3].toFixed(2) }} Nm</span>
        </div>
        <div class="data-item" style="color: #ffff00">
          <span class="label">My:</span>
          <span class="value">{{ currentData[4].toFixed(2) }} Nm</span>
        </div>
        <div class="data-item" style="color: #00ffff">
          <span class="label">Mz:</span>
          <span class="value">{{ currentData[5].toFixed(2) }} Nm</span>
        </div>
      </div>
    </div>
  </div>
</template>
<style scoped>
.three-container {
  width: 100%;
  height: 540px;
  margin: 20px 0;
  display: flex;
  flex-direction: column;
  background-color: #ffffff;
  border-radius: 8px;
  overflow: hidden;
}
.three-scene {
  width: 100%;
  height: 450px;
  position: relative;
}
.data-panel {
  padding: 10px 20px;
  background-color: #000000;
  border-top: 1px solid #535353;
}
.data-panel h3 {
  margin: 0 0 10px 0;
  font-size: 16px;
  text-align: center;
}
.data-row {
  display: flex;
  justify-content: space-around;
  margin-bottom: 5px;
}
.data-item {
  display: flex;
  align-items: center;
  font-weight: bold;
}
.label {
  margin-right: 5px;
}
.value {
  font-family: monospace;
}
</style>
src/components/ThreeDDisplay.vue
@@ -2,11 +2,8 @@
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { sendToPipe } from '../pipe_client';
import { createDataReceiver } from '../utils/dataFetcher';
const props = defineProps<{
  pipeName: string;
}>();
const containerRef = ref<HTMLElement | null>(null);
let scene: THREE.Scene | null = null;
@@ -358,53 +355,25 @@
  renderer.setSize(width, height);
}
// çŠ¶æ€å˜é‡ï¼Œç”¨äºŽæŽ§åˆ¶é”™è¯¯æ˜¾ç¤ºé¢‘çŽ‡
const errorCount = ref(0);
const maxConsecutiveErrors = 5;
const showingError = ref(false);
// æŽ¥æ”¶ç®¡é“数据的函数
async function receiveForceData() {
  try {
    // ä»Žç®¡é“接收数据
    const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA');
    // æˆåŠŸæŽ¥æ”¶æ•°æ®ï¼Œé‡ç½®é”™è¯¯è®¡æ•°
    errorCount.value = 0;
    if (showingError.value) {
      showingError.value = false;
      console.log('管道通信已恢复');
    }
    // è§£æžæŽ¥æ”¶åˆ°çš„æ•°æ®
    try {
      const forceData = JSON.parse(response);
      if (Array.isArray(forceData) && forceData.length === 6) {
        updateVisualization(forceData);
      }
    } catch (e) {
      console.warn('解析数据失败:', e);
    }
  } catch (err) {
    // å¢žåŠ é”™è¯¯è®¡æ•°
    errorCount.value++;
    // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼æ—¶æ˜¾ç¤ºé”™è¯¯ä¿¡æ¯ï¼Œé¿å…æ—¥å¿—刷屏
    if (errorCount.value >= maxConsecutiveErrors && !showingError.value) {
      showingError.value = true;
      console.error('管道通信持续失败,请检查服务端状态:', err);
    }
  }
}
// å®šæ—¶èŽ·å–æ•°æ®
let dataTimer: number | null = null;
onMounted(() => {
  initScene();
  animate();
  
  // æ¯ç§’获取一次数据
  dataTimer = window.setInterval(receiveForceData, 3000);
  const receiveData = createDataReceiver( (forceData) => {
    updateVisualization(forceData);
  });
  // æ¯100ms获取一次数据
  dataTimer = window.setInterval(receiveData, 100);
  // ç›‘听窗口大小变化
  window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
src/pipe_client.ts
@@ -32,4 +32,15 @@
  
  // æ‰€æœ‰é‡è¯•都失败,抛出最后一个错误
  throw lastError;
}
export async function getLatestWsData(): Promise<string | null> {
  try {
    const result = await invoke<string | null>('get_latest_ws_data');
    console.log("getlatestwsData", result);
    return result;
  } catch (err) {
    console.error('获取 WebSocket æ•°æ®å¤±è´¥:', err);
    return null;
  }
}
src/utils/dataFetcher.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
import {  getLatestWsData } from '../pipe_client';
// const pipeName = 'tauri-pipe-server';
// const dataSource = 'ws';  // pipe|ws
export async function fetchForceData() {
  try {
    let response: string | null = null;
    // if (dataSource === 'pipe') {
    //   if (!pipeName) throw new Error('Pipe name is required for pipe data source');
    //   response = await sendToPipe(pipeName, 'GET_FORCE_DATA');
    // } else if (dataSource === 'ws') {
      response = await getLatestWsData();
      if (!response) return null;
    // }
    if (!response) return null;
    const jsonData = JSON.parse(response);
    const forceData = jsonData[0];
    if (Array.isArray(forceData) && forceData.length === 6) {
      // å°†æ•°ç»„中的每个数分别*4000/4194303
      for (let i = 0; i < forceData.length; i++) {
        forceData[i] = forceData[i] * 4000 / 4194303;
      }
      return forceData;
    }
    return null;
  } catch (error) {
    console.error('Error fetching force data:', error);
    return null;
  }
}
export function createDataReceiver( callback: (data: number[]) => void) {
  let errorCount = 0;
  const maxConsecutiveErrors = 5;
  let showingError = false;
  return async () => {
    try {
      const forceData = await fetchForceData();
      console.log('forceData111', forceData);
      if (forceData) {
        // é‡ç½®é”™è¯¯è®¡æ•°
        errorCount = 0;
        if (showingError) {
          showingError = false;
          console.log('数据通信已恢复');
        }
        console.log('forceData222', forceData);
        callback(forceData);
      }
    } catch (err) {
      // å¢žåŠ é”™è¯¯è®¡æ•°
      errorCount++;
      // åªåœ¨è¿žç»­é”™è¯¯è¾¾åˆ°é˜ˆå€¼æ—¶æ˜¾ç¤ºé”™è¯¯ä¿¡æ¯ï¼Œé¿å…æ—¥å¿—刷屏
      if (errorCount >= maxConsecutiveErrors && !showingError) {
        showingError = true;
        console.error('数据通信持续失败,请检查服务端状态:', err);
      }
    }
  };
}