From 5bf14aed888cd0e258e325c65f14022dad02985b Mon Sep 17 00:00:00 2001
From: baoshiwei <baoshiwei@shlanbao.cn>
Date: 星期四, 17 七月 2025 15:32:01 +0800
Subject: [PATCH] 更改为通过websocket获取数据

---
 src/components/ForceChart.vue         |   77 +---
 src/pipe_client.ts                    |   11 
 src-tauri/src/main.rs                 |    2 
 src/components/ThreeDDisplay.vue      |   55 --
 src/components/GaugeDisplay.vue       |   67 +---
 README.md                             |    4 
 src/components/ThreeDDisplay copy.vue |  525 ++++++++++++++++++++++++++++++++
 src/components/TableDisplay.vue       |   42 --
 src/utils/dataFetcher.ts              |   68 ++++
 src-tauri/src/ws_client.rs            |   45 ++
 src-tauri/src/lib.rs                  |   14 
 src/App.vue                           |   27 +
 src-tauri/Cargo.toml                  |    6 
 13 files changed, 751 insertions(+), 192 deletions(-)

diff --git a/README.md b/README.md
index 3d9a6a4..620ab6f 100644
--- a/README.md
+++ b/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)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 3d32d50..7cea56c 100644
--- a/src-tauri/Cargo.toml
+++ b/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"
+
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 193a629..8e0934c 100644
--- a/src-tauri/src/lib.rs
+++ b/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()));
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 9feb543..1da1ce2 100644
--- a/src-tauri/src/main.rs
+++ b/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();
 }
diff --git a/src-tauri/src/ws_client.rs b/src-tauri/src/ws_client.rs
new file mode 100644
index 0000000..0184b8c
--- /dev/null
+++ b/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(())
+}
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index bb69d54..f6a644e 100644
--- a/src/App.vue
+++ b/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>
diff --git a/src/components/ForceChart.vue b/src/components/ForceChart.vue
index 138a1c3..bc3c801 100644
--- a/src/components/ForceChart.vue
+++ b/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('绠¢亾閫氫俊宸叉仮澶�');
-    }
-    
-    // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁
-    try {
-      const forceData = JSON.parse(response);
-      if (Array.isArray(forceData) && forceData.length === 6) {
-        updateChart(forceData);
-      }
-    } catch (e) {
-      console.warn('瑙f瀽鏁版嵁澶辫触:', 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(() => {
diff --git a/src/components/GaugeDisplay.vue b/src/components/GaugeDisplay.vue
index a322a3f..9630d2c 100644
--- a/src/components/GaugeDisplay.vue
+++ b/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;
-    
-    // 瑙f瀽鏁版嵁
-    try {
-      const forceData = JSON.parse(response);
-      if (Array.isArray(forceData) && forceData.length === 6) {
-        updateChart(forceData);
-      }
-    } catch (parseErr) {
-      console.error('鏁版嵁瑙f瀽閿欒:', 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);
   
diff --git a/src/components/TableDisplay.vue b/src/components/TableDisplay.vue
index 1a11ada..9e6eb01 100644
--- a/src/components/TableDisplay.vue
+++ b/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;
-    
-    // 瑙f瀽鏁版嵁
-    try {
-      const forceData = JSON.parse(response);
-      if (Array.isArray(forceData) && forceData.length === 6) {
-        updateTable(forceData);
-      }
-    } catch (parseErr) {
-      console.error('鏁版嵁瑙f瀽閿欒:', 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);
 });
diff --git a/src/components/ThreeDDisplay copy.vue b/src/components/ThreeDDisplay copy.vue
new file mode 100644
index 0000000..239e4d5
--- /dev/null
+++ b/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;
+  }
+    
+  // 鍒涘缓骞舵坊鍔燲YZ杞存爣绛�
+  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杞村姏鐭╁姬褰� (缁昘杞存棆杞�)
+  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杞村姏鐭╁姬褰� (缁昚杞存棆杞�)
+  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杞村姏鐭╁姬褰� (缁昛杞存棆杞�)
+  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('绠¢亾閫氫俊宸叉仮澶�');
+    }
+    
+    // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁
+    try {
+      const forceData = JSON.parse(response);
+      if (Array.isArray(forceData) && forceData.length === 6) {
+        updateVisualization(forceData);
+      }
+    } catch (e) {
+      console.warn('瑙f瀽鏁版嵁澶辫触:', 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>
\ No newline at end of file
diff --git a/src/components/ThreeDDisplay.vue b/src/components/ThreeDDisplay.vue
index 239e4d5..10c71da 100644
--- a/src/components/ThreeDDisplay.vue
+++ b/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('绠¢亾閫氫俊宸叉仮澶�');
-    }
-    
-    // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁
-    try {
-      const forceData = JSON.parse(response);
-      if (Array.isArray(forceData) && forceData.length === 6) {
-        updateVisualization(forceData);
-      }
-    } catch (e) {
-      console.warn('瑙f瀽鏁版嵁澶辫触:', 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(() => {
diff --git a/src/pipe_client.ts b/src/pipe_client.ts
index a96e3cf..37b8c10 100644
--- a/src/pipe_client.ts
+++ b/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;
+  }
 }
\ No newline at end of file
diff --git a/src/utils/dataFetcher.ts b/src/utils/dataFetcher.ts
new file mode 100644
index 0000000..05bfdfe
--- /dev/null
+++ b/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);
+      }
+    }
+  };
+}
\ No newline at end of file

--
Gitblit v1.9.3