Compare commits

...

6 Commits

Author SHA1 Message Date
  Backpack 580fb6fe4e fix finalv1 2 months ago
  NinjaKelly 0c4f3b89a1 delete 2 months ago
  NinjaKelly 1e43ceb2c8 add FRONTEND 2 months ago
  Backpack 27a882d29e mouse control and click 2 months ago
  Backpack cd75372433 other gestures 2 months ago
  Backpack f74228e552 THREE_FINGERS_UP 2 months ago
63 changed files with 3552 additions and 793 deletions
Split View
  1. +0
    -37
      README.md
  2. +0
    -43
      main.ts
  3. +0
    -6
      package-lock.json
  4. BIN
      public/mediapipe/gesture_recognizer.task
  5. +0
    -20
      public/mediapipe/wasm/vision_wasm_internal.js
  6. BIN
      public/mediapipe/wasm/vision_wasm_internal.wasm
  7. +0
    -20
      public/mediapipe/wasm/vision_wasm_nosimd_internal.js
  8. BIN
      public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm
  9. +0
    -81
      src/components/DevTool.vue
  10. +0
    -24
      src/hand_landmark/VideoDetector.vue
  11. +0
    -190
      src/hand_landmark/detector.ts
  12. +0
    -21
      src/hand_landmark/gesture_handler.ts
  13. +5
    -4
      wavecontrol-test/index.html
  14. +660
    -0
      wavecontrol-test/package-lock.json
  15. +4
    -0
      wavecontrol-test/package.json
  16. +43
    -0
      wavecontrol-test/src-py/main.py
  17. +0
    -0
      wavecontrol-test/src-py/router/__init__.py
  18. BIN
      wavecontrol-test/src-py/router/__pycache__/__init__.cpython-312.pyc
  19. BIN
      wavecontrol-test/src-py/router/__pycache__/__init__.cpython-38.pyc
  20. BIN
      wavecontrol-test/src-py/router/__pycache__/ws.cpython-312.pyc
  21. BIN
      wavecontrol-test/src-py/router/__pycache__/ws.cpython-38.pyc
  22. +143
    -0
      wavecontrol-test/src-py/router/ws.py
  23. +13
    -0
      wavecontrol-test/src-tauri/.gitignore
  24. +32
    -0
      wavecontrol-test/src-tauri/Cargo.toml
  25. +8
    -0
      wavecontrol-test/src-tauri/Info.plist
  26. +3
    -0
      wavecontrol-test/src-tauri/build.rs
  27. +41
    -0
      wavecontrol-test/src-tauri/capabilities/default.json
  28. +17
    -0
      wavecontrol-test/src-tauri/capabilities/desktop.json
  29. +58
    -0
      wavecontrol-test/src-tauri/src/lib.rs
  30. +6
    -0
      wavecontrol-test/src-tauri/src/main.rs
  31. +61
    -0
      wavecontrol-test/src-tauri/tauri.conf.json
  32. +13
    -0
      wavecontrol-test/src-tauri/tauri.macos.conf.json
  33. +34
    -0
      wavecontrol-test/src/AppMediaPipe.vue
  34. +0
    -0
      wavecontrol-test/src/components/AutoStart.vue
  35. +0
    -0
      wavecontrol-test/src/components/CircleProgress.vue
  36. +0
    -0
      wavecontrol-test/src/components/GestureCard.vue
  37. +0
    -0
      wavecontrol-test/src/components/GestureIcon.vue
  38. +0
    -0
      wavecontrol-test/src/components/Menu.vue
  39. +41
    -24
      wavecontrol-test/src/hand_landmark/VideoDetector.vue
  40. +104
    -22
      wavecontrol-test/src/hand_landmark/detector.ts
  41. +226
    -5
      wavecontrol-test/src/hand_landmark/gesture_handler.ts
  42. +49
    -0
      wavecontrol-test/src/locales/en.ts
  43. +18
    -0
      wavecontrol-test/src/locales/i18n.ts
  44. +47
    -0
      wavecontrol-test/src/locales/zh.ts
  45. +10
    -4
      wavecontrol-test/src/main.ts
  46. +30
    -0
      wavecontrol-test/src/py_api.ts
  47. +38
    -0
      wavecontrol-test/src/router/index.ts
  48. +87
    -0
      wavecontrol-test/src/store/app.ts
  49. +46
    -0
      wavecontrol-test/src/utils/subWindow.ts
  50. +348
    -0
      wavecontrol-test/src/view/mainWindow/Guide.vue
  51. +367
    -0
      wavecontrol-test/src/view/mainWindow/Home.vue
  52. +606
    -0
      wavecontrol-test/src/view/mainWindow/MainWindow.vue
  53. +93
    -0
      wavecontrol-test/src/view/subWindow/SubWindow.vue
  54. +7
    -0
      wavecontrol-test/src/vite-env.d copy.ts
  55. +9
    -0
      wavecontrol-test/test_mouse.py
  56. +0
    -15
      wavecontrol-test/tsconfig.app.json
  57. +28
    -5
      wavecontrol-test/tsconfig.json
  58. +3
    -18
      wavecontrol-test/tsconfig.node.json

+ 0
- 37
README.md View File

@ -1,37 +0,0 @@
# 隔空手势识别系统
### 项目简介
本项目是一套基于**摄像头手势识别 + 语音识别**的非接触式人机交互系统,旨在帮助用户**在无需触碰设备的情况下完成对电脑的基本操作**。
灵感源于日常生活中“手脏无法点击”、“操作不便”的场景,结合后疫情时代对无接触操作的广泛需求,系统通过**手势识别模块**感知用户动作,通过**语音识别模块**解析用户指令,并联动前端和后端的控制逻辑,完成**系统级的键盘/鼠标模拟操作**,打造自然、高效的交互体验。
### 项目周期
本项目为 2025 年《软件工程实践》课程的结课实践项目,采用**敏捷开发流程**,共分为四次迭代,逐步完成从原型设计到系统实现,最终交付一个可运行的完整系统,实现稳定、实用的**非接触式交互体验**。
### 项目成员
王云岱 朱子玥 杨嘉莉
### 核心功能
### 技术架构
### 应用场景
- 📺 追剧、吃饭时无需碰鼠标
- 👨‍🏫 教学演讲中通过手势控制幻灯片
- 👨‍🍳 厨房中翻菜谱、视频食谱不沾油手
- 🏥 医疗等无菌场景下控制系统界面
- 🤖 AR/智慧眼镜等未来设备交互前置尝试

+ 0
- 43
main.ts View File

@ -1,43 +0,0 @@
import { Hands } from '@mediapipe/hands';
import { Camera } from '@mediapipe/camera_utils';
const videoElement = document.createElement('video');
videoElement.style.display = 'none';
document.body.appendChild(videoElement);
const hands = new Hands({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`,
});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7,
});
hands.onResults((results) => {
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
// 检测食指是否伸出:tip高于pip(y坐标更小)
const tip = landmarks[8]; // 食指指尖
const pip = landmarks[6]; // 第二个关节
if (tip.y < pip.y) {
console.log('✅ 食指伸出');
} else {
console.log('❌ 食指未伸出');
}
}
});
const camera = new Camera(videoElement, {
onFrame: async () => {
await hands.send({ image: videoElement });
},
width: 640,
height: 480,
});
camera.start();

+ 0
- 6
package-lock.json View File

@ -1,6 +0,0 @@
{
"name": "wavecontrol",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

BIN
public/mediapipe/gesture_recognizer.task View File


+ 0
- 20
public/mediapipe/wasm/vision_wasm_internal.js
File diff suppressed because it is too large
View File


BIN
public/mediapipe/wasm/vision_wasm_internal.wasm View File


+ 0
- 20
public/mediapipe/wasm/vision_wasm_nosimd_internal.js
File diff suppressed because it is too large
View File


BIN
public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm View File


+ 0
- 81
src/components/DevTool.vue View File

@ -1,81 +0,0 @@
<template>
<div
v-if="is_dev"
class="dev-tool"
:class="{ 'dev-tool--expanded': isExpanded }"
>
<div class="dev-tool__toggle" @click="toggleToolbox">
<span class="dev-tool__icon">🔧</span>
</div>
<!-- <div class="dev-tool__content" v-if="isExpanded">
<div class="dev-tool__item" @click="createSubWindowClick">创建子窗口</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { createSubWindow } from "@/utils/subWindow";
import { ref } from "vue";
const is_dev = import.meta.env.DEV;
const isExpanded = ref(false);
const toggleToolbox = () => {
isExpanded.value = !isExpanded.value;
};
const createSubWindowClick = () => {
createSubWindow("/sub-window", "subWindow");
};
</script>
<style scoped lang="scss">
.dev-tool {
position: fixed;
top: 50%;
left: 0;
transform: translateY(-50%);
background-color: #fff;
border-radius: 0 8px 8px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
z-index: 9999;
&--expanded {
width: 200px;
}
&__toggle {
padding: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
}
&__icon {
font-size: 20px;
}
&__content {
padding: 10px;
}
&__item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 5px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f5f5;
}
&:last-child {
margin-bottom: 0;
}
}
}
</style>

+ 0
- 24
src/hand_landmark/VideoDetector.vue View File

@ -1,24 +0,0 @@
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize(); //
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
// 200ms
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});

+ 0
- 190
src/hand_landmark/detector.ts View File

@ -1,190 +0,0 @@
import {
FilesetResolver,
GestureRecognizer,
} from "@mediapipe/tasks-vision";
// 手势
export enum HandGesture {
// 食指举起,移动鼠标
ONLY_INDEX_UP = "only_index_up",
INDEX_AND_THUMB_UP = "index_and_thumb_up"
}
interface HandLandmark {
x: number;
y: number;
z: number;
}
export interface HandInfo {
landmarks: HandLandmark[];
handedness: "Left" | "Right";
score: number;
categoryName?: string;
}
interface DetectionResult {
leftHand?: HandInfo;
rightHand?: HandInfo;
// 原始检测结果,以防需要访问其他数据
rawResult: any;
}
/**
* -
* :
* 1.
* 2.
* 3. ()
* 4.
*/
export class Detector {
private detector: GestureRecognizer | null = null;
constructor(private modelPath = "/mediapipe/gesture_recognizer.task") {}
async initialize(useCanvas = false) {
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
try {
const params: any = {
baseOptions: {
modelAssetPath: this.modelPath,
delegate: "GPU",
},
runningMode: "VIDEO",
numHands: 1,
};
if (useCanvas) {
params.canvas = document.createElement("canvas");
}
this.detector = await GestureRecognizer.createFromOptions(vision, params);
} catch (error: any) {
if (error.toString().includes("kGpuService")) {
await this.initialize(true);
} else {
throw error;
}
}
}
// 检测视频帧中的手部(保持独立)
async detect(video: HTMLVideoElement): Promise<DetectionResult> {
if (!this.detector) {
throw new Error("检测器未初始化");
}
const now = performance.now();
const result = await this.detector.recognizeForVideo(video, now);
const detection: DetectionResult = {
rawResult: result,
};
if (result.landmarks && result.handedness) {
for (let i = 0; i < result.landmarks.length; i++) {
const hand: HandInfo = {
landmarks: result.landmarks[i],
handedness: result.handedness[i][0].categoryName as "Left" | "Right",
score: result.handedness[i][0].score,
};
if (result.gestures.length > 0) {
hand.categoryName = result.gestures[0][0].categoryName;
}
if (hand.handedness === "Left") {
detection.leftHand = hand;
} else {
detection.rightHand = hand;
}
}
}
return detection;
}
/**
*
*/
static _fingersUp(hand: HandInfo): number[] {
const fingers: number[] = [];
const tipIds = [4, 8, 12, 16, 20]; // 从大拇指开始,依次为每个手指指尖
// 检测大拇指
if (hand.handedness === "Right") {
if (hand.landmarks[tipIds[0]].x < hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
} else {
if (hand.landmarks[tipIds[0]].x > hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
}
// 检测其他四个手指
for (let id = 1; id < 5; id++) {
if (hand.landmarks[tipIds[id]].y < hand.landmarks[tipIds[id] - 2].y) {
fingers.push(1);
} else {
fingers.push(0);
}
}
return fingers;
}
/**
*
*/
public static getSingleHandGesture(hand: HandInfo): HandGesture {
const fingers = this._fingersUp(hand);
const fingerState = fingers.join(",");
// 定义手势映射表
const gestureMap = new Map<string, HandGesture>([
// 食指举起,移动鼠标
["0,1,0,0,0", HandGesture.ONLY_INDEX_UP],
// 鼠标左键点击手势
["1,1,0,0,0", HandGesture.INDEX_AND_THUMB_UP],
]);
if (gestureMap.has(fingerState)) {
return gestureMap.get(fingerState) as HandGesture;
}
// 返回默认值
return HandGesture.OTHER;
}
/**
*
*/
async process(detection: DetectionResult): Promise<HandGesture> {
const hand = detection.rightHand ?? detection.leftHand;
if (!hand) {
console.log("没检测到手");
return HandGesture.OTHER;
}
const gesture = Detector.getSingleHandGesture(hand);
console.log("当前手势状态是:", gesture);
if (gesture === HandGesture.ONLY_INDEX_UP) {
console.log("识别到食指竖起!");
}
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
console.log("识别到食指和大拇指竖起!");
}
return gesture;
}
}

+ 0
- 21
src/hand_landmark/gesture_handler.ts View File

@ -1,21 +0,0 @@
// gesture_handler.ts
import { HandGesture, HandInfo } from "./detector";
export class GestureHandler {
private previousGesture: HandGesture | null = null;
private previousGestureCount = 0;
private minGestureCount = 3;
handleGesture(gesture: HandGesture, hand: HandInfo) {
if (gesture === this.previousGesture) {
this.previousGestureCount++;
} else {
this.previousGesture = gesture;
this.previousGestureCount = 1;
}
if (this.previousGestureCount >= this.minGestureCount) {
console.log("识别手势类型:", gesture);
}
}
}

+ 5
- 4
wavecontrol-test/index.html View File

@ -1,13 +1,14 @@
<!doctype html>
<html lang="en">
<html lang="en" style="height: 100%;width: 100%;">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>Tauri + Vue + Typescript App</title>
</head>
<body>
<div id="app"></div>
<body style="height: 100%;width: 100%;overflow: hidden;">
<div id="app" style="height: 100%;width: 100%"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

+ 660
- 0
wavecontrol-test/package-lock.json View File

@ -12,7 +12,11 @@
"@tensorflow-models/handpose": "^0.1.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0",
"element-plus": "^2.10.4",
"fingerpose": "^0.1.0",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-shared-state": "^1.0.1",
"vue": "^3.5.17"
},
"devDependencies": {
@ -57,6 +61,18 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.0.tgz",
@ -70,6 +86,48 @@
"node": ">=6.9.0"
}
},
"node_modules/@css-render/plugin-bem": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
"integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
"license": "MIT",
"peerDependencies": {
"css-render": "~0.15.14"
}
},
"node_modules/@css-render/vue3-ssr": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
"integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@ -512,18 +570,60 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"license": "MIT"
},
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.22-rc.20250304",
"resolved": "https://registry.npmmirror.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.22-rc.20250304.tgz",
"integrity": "sha512-dElxVXMFGthshfIj+qAVm8KE2jmNo2p8oXFib8WzEjb7GNaX/ClWBc8UJfoSZwjEMVrdHJ4YUfa7P3ifl6MIWw==",
"license": "Apache-2.0"
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@ -901,6 +1001,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/katex": {
"version": "0.16.7",
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz",
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz",
@ -929,6 +1050,12 @@
"integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
@ -1036,6 +1163,39 @@
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
@ -1130,6 +1290,94 @@
}
}
},
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"license": "MIT",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@webgpu/types": {
"version": "0.1.38",
"resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.38.tgz",
@ -1143,12 +1391,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.4.0.tgz",
"integrity": "sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
@ -1159,18 +1422,89 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/broadcast-channel": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/broadcast-channel/-/broadcast-channel-7.1.0.tgz",
"integrity": "sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "7.27.0",
"oblivious-set": "1.4.0",
"p-queue": "6.6.2",
"unload": "2.4.1"
},
"funding": {
"url": "https://github.com/sponsors/pubkey"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/css-render": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "~0.8.0",
"csstype": "~3.0.5"
}
},
"node_modules/css-render/node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
@ -1178,6 +1512,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/element-plus": {
"version": "2.10.4",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.4.tgz",
"integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@ -1232,12 +1592,30 @@
"@esbuild/win32-x64": "0.25.6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/evtd": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz",
@ -1333,6 +1711,21 @@
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
@ -1350,6 +1743,41 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz",
@ -1365,6 +1793,12 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
@ -1381,6 +1815,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
@ -1388,6 +1828,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/naive-ui": {
"version": "2.42.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.42.0.tgz",
"integrity": "sha512-c7cXR2YgOjgtBadXHwiWL4Y0tpGLAI5W5QzzHksOi22iuHXoSGMAzdkVTGVPE/PM0MSGQ/JtUIzCx2Y0hU0vTQ==",
"license": "MIT",
"dependencies": {
"@css-render/plugin-bem": "^0.15.14",
"@css-render/vue3-ssr": "^0.15.14",
"@types/katex": "^0.16.2",
"@types/lodash": "^4.14.198",
"@types/lodash-es": "^4.17.9",
"async-validator": "^4.2.5",
"css-render": "^0.15.14",
"csstype": "^3.1.3",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"evtd": "^0.2.4",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.8",
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.63"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
@ -1426,6 +1896,21 @@
}
}
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/oblivious-set": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/oblivious-set/-/oblivious-set-1.4.0.tgz",
"integrity": "sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
@ -1435,6 +1920,43 @@
"wrappy": "1"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmmirror.com/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@ -1451,6 +1973,12 @@
"node": ">=0.10.0"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@ -1470,6 +1998,42 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia-shared-state": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/pinia-shared-state/-/pinia-shared-state-1.0.1.tgz",
"integrity": "sha512-S7JGPsXuV5+cp+juoVaoszdH5fEFVtagQkw4X8ivNITSN+57axloXfdECcigWq1OOWoVOjdmdanuJS8xJc+VAg==",
"license": "MIT",
"dependencies": {
"broadcast-channel": "^7.0.0"
},
"funding": {
"url": "https://github.com/sponsors/wobsoriano"
},
"peerDependencies": {
"pinia": "^3.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
@ -1498,6 +2062,18 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
@ -1560,6 +2136,12 @@
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
},
"node_modules/seemly": {
"version": "0.3.10",
"resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz",
"integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@ -1569,6 +2151,27 @@
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"license": "MIT",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -1592,6 +2195,12 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/treemate": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
@ -1619,6 +2228,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/unload": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/unload/-/unload-2.4.1.tgz",
"integrity": "sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==",
"license": "Apache-2.0",
"funding": {
"url": "https://github.com/sponsors/pubkey"
}
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/vite": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.3.tgz",
@ -1694,6 +2324,18 @@
}
}
},
"node_modules/vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
@ -1739,6 +2381,24 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vueuc": {
"version": "0.4.64",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz",
"integrity": "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==",
"license": "MIT",
"dependencies": {
"@css-render/vue3-ssr": "^0.15.10",
"@juggle/resize-observer": "^3.3.1",
"css-render": "^0.15.10",
"evtd": "^0.2.4",
"seemly": "^0.3.6",
"vdirs": "^0.1.4",
"vooks": "^0.2.4"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

+ 4
- 0
wavecontrol-test/package.json View File

@ -13,7 +13,11 @@
"@tensorflow-models/handpose": "^0.1.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0",
"element-plus": "^2.10.4",
"fingerpose": "^0.1.0",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-shared-state": "^1.0.1",
"vue": "^3.5.17"
},
"devDependencies": {

+ 43
- 0
wavecontrol-test/src-py/main.py View File

@ -0,0 +1,43 @@
import os
import sys
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from router.ws import router as ws_router
if hasattr(sys, 'frozen'):
os.chdir(os.path.dirname(sys.argv[0]))
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 添加 WebSocket 路由
app.include_router(ws_router)
@app.get("/")
def read_root():
return "ready"
@app.get("/shutdown")
def shutdown():
import signal
import os
os.kill(os.getpid(), signal.SIGINT)
if __name__ == '__main__':
port = 62334
print(f"Starting server at http://127.0.0.1:{port}/docs")
uvicorn.run(app, host="127.0.0.1", port=port)

+ 0
- 0
wavecontrol-test/src-py/router/__init__.py View File


BIN
wavecontrol-test/src-py/router/__pycache__/__init__.cpython-312.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/__init__.cpython-38.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/ws.cpython-312.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/ws.cpython-38.pyc View File


+ 143
- 0
wavecontrol-test/src-py/router/ws.py View File

@ -0,0 +1,143 @@
import json
from dataclasses import dataclass
from typing import Dict, List, Optional, Union
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from pynput.keyboard import Controller as KeyboardController
from pynput.keyboard import Key
from pynput.mouse import Button, Controller
router = APIRouter()
# 存储活跃的WebSocket连接
active_connection: Optional[WebSocket] = None
@dataclass
class WebSocketMessage:
"""WebSocket消息数据类"""
type: str
msg: str = ""
title: str = "提示"
duration: int = 1
data: Dict = None
def __post_init__(self):
if self.data is None:
self.data = {"x": 0, "y": 0, "key_str": ""}
def to_dict(self) -> Dict:
"""将消息转换为字典格式"""
return {
"type": self.type,
"msg": self.msg,
"title": self.title,
"duration": self.duration,
"data": self.data,
}
class WebSocketMessageType:
"""WebSocket消息类型常量"""
# 系统消息类型
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
# 鼠标操作类型
MOUSE_MOVE = "mouse_move"
MOUSE_CLICK = "mouse_click"
class MessageSender:
"""消息发送器,发送给前端"""
@staticmethod
async def send_message(
ws_data_type: str, msg: str, title: str = "提示", duration: int = 1
) -> None:
"""
WebSocket客户端
Args:
ws_data_type:
msg:
title:
duration:
"""
if not active_connection:
return
try:
message = WebSocketMessage(
type=ws_data_type, msg=msg, title=title, duration=duration
)
await active_connection.send_json(message.to_dict())
except Exception as e:
print(f"发送消息失败: {e}")
class GestureHandler:
"""手势控制器"""
def __init__(self):
self.keyboard = KeyboardController()
self.mouse = Controller()
def move_mouse(self, x: int, y: int) -> None:
"""移动鼠标到指定位置"""
print(f"👉 正在移动鼠标到位置: ({x}, {y})")
self.mouse.position = (x, y)
def click_mouse(self) -> None:
"""点击鼠标左键"""
print("👉 正在点击鼠标")
self.mouse.click(Button.left)
@router.websocket("/ws_wavecontrol")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket端点处理函数"""
global active_connection
await websocket.accept()
active_connection = websocket
gesture_handler = GestureHandler()
try:
while True:
data_str = await websocket.receive_text()
await _handle_message(data_str, websocket, gesture_handler)
except WebSocketDisconnect:
active_connection = None
except Exception as e:
print(f"WebSocket处理错误: {e}")
active_connection = None
async def _handle_message(
data_str: str,
websocket: WebSocket,
gesture_handler: GestureHandler,
) -> None:
"""处理WebSocket消息"""
try:
message = WebSocketMessage(**json.loads(data_str))
data = message.data
print(f"[收到指令] type={message.type}, data={data}") # 调试输出
# 处理鼠标操作
if message.type == WebSocketMessageType.MOUSE_MOVE:
gesture_handler.move_mouse(data["x"], data["y"])
elif message.type == WebSocketMessageType.MOUSE_CLICK:
gesture_handler.click_mouse()
except json.JSONDecodeError:
print("无效的JSON数据")
except Exception as e:
print(f"处理消息失败: {e}")

+ 13
- 0
wavecontrol-test/src-tauri/.gitignore View File

@ -0,0 +1,13 @@
2
Cargo.lock
bin/
icons/
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

+ 32
- 0
wavecontrol-test/src-tauri/Cargo.toml View File

@ -0,0 +1,32 @@
[package]
name = "WaveControl"
version = "0.3.11"
description = "WaveControl 手势识别"
authors = ["https://github.com/maplelost"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["devtools"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-shell = "2"
tauri-plugin-store = "2"
tauri-plugin-notification = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2"
tauri-plugin-window-state = "2"

+ 8
- 0
wavecontrol-test/src-tauri/Info.plist View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>请允许本程序访问您的摄像头</string>
</dict>
</plist>

+ 3
- 0
wavecontrol-test/src-tauri/build.rs View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

+ 41
- 0
wavecontrol-test/src-tauri/capabilities/default.json View File

@ -0,0 +1,41 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main", "NewWindow_2"],
"permissions": [
"core:default",
"shell:default",
"shell:allow-execute",
"shell:allow-spawn",
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default",
"core:window:allow-destroy",
"core:window:allow-set-title",
"store:default",
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-notify",
"notification:allow-show",
"notification:allow-request-permission",
"opener:default",
"opener:allow-open-path",
"opener:allow-reveal-item-in-dir",
"opener:allow-default-urls",
"opener:allow-open-url",
"core:webview:default",
"core:window:allow-show",
"core:window:allow-hide",
"core:webview:allow-create-webview-window",
"core:window:allow-set-position",
"core:window:allow-set-size"
]
}

+ 17
- 0
wavecontrol-test/src-tauri/capabilities/desktop.json View File

@ -0,0 +1,17 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"window-state:default"
]
}

+ 58
- 0
wavecontrol-test/src-tauri/src/lib.rs View File

@ -0,0 +1,58 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
// 提取sidecar启动逻辑到单独的函数
async fn start_sidecar(app: tauri::AppHandle) -> Result<String, String> {
let sidecar = app
.shell()
.sidecar("WaveControl Backend")
.map_err(|e| format!("无法找到sidecar: {}", e))?;
let (_rx, _child) = sidecar
.spawn()
.map_err(|e| format!("无法启动sidecar: {}", e))?;
Ok("Sidecar已启动".to_string())
}
// 保留命令供可能的手动调用
#[tauri::command]
async fn run_sidecar(app: tauri::AppHandle) -> Result<String, String> {
start_sidecar(app).await
}
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_autostart::ManagerExt;
use tauri_plugin_shell::process::CommandEvent;
use tauri_plugin_shell::ShellExt;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
// .plugin(tauri_plugin_window_state::Builder::new().build()) // 窗口状态管理,启用了导致 sub-window 无法设置decorations
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec!["--flag1", "--flag2"]),
))
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
// 在应用启动时自动启动sidecar
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match start_sidecar(app_handle).await {
Ok(msg) => println!("{}", msg),
Err(e) => eprintln!("启动sidecar失败: {}", e),
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![greet, run_sidecar])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

+ 6
- 0
wavecontrol-test/src-tauri/src/main.rs View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri_app_lib::run()
}

+ 61
- 0
wavecontrol-test/src-tauri/tauri.conf.json View File

@ -0,0 +1,61 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "WaveControl",
"version": "0.3.11",
"identifier": "com.WaveControl.maplelost",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "WaveControl",
"width": 800,
"height": 600,
"devtools": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"externalBin": ["bin/backend-py/WaveControl Backend"],
"resources": {
"bin/backend-py/_internal": "_internal/",
"../model": "model/",
"../debug backend.bat": "/"
},
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
}
},
"files": {},
"hardenedRuntime": true,
"minimumSystemVersion": "10.13"
},
"targets": ["dmg", "msi"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/mac/icon.icns",
"icons/icon.ico"
]
}
}

+ 13
- 0
wavecontrol-test/src-tauri/tauri.macos.conf.json View File

@ -0,0 +1,13 @@
{
"$schema": "https://schema.tauri.app/config/2",
"identifier": "com.WaveControl.maplelost",
"bundle": {
"resources": [],
"macOS": {
"files": {
"Resources/model": "../model",
"Frameworks": "./bin/backend-py/_internal"
}
}
}
}

+ 34
- 0
wavecontrol-test/src/AppMediaPipe.vue View File

@ -0,0 +1,34 @@
<template>
<n-message-provider>
<div class="app-container">
<h1>MediaPipe</h1>
<div class="detection-container">
<hand-landmark-detection />
</div>
</div>
</n-message-provider>
</template>
<script setup lang="ts">
import HandLandmarkDetection from "@/hand_landmark/VideoDetector.vue";
</script>
<style scoped>
.app-container {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #333;
margin-bottom: 20px;
}
.detection-container {
width: 100%;
max-width: 640px;
margin: 0 auto;
}
</style>

src/components/AutoStart.vue → wavecontrol-test/src/components/AutoStart.vue View File


src/components/CircleProgress.vue → wavecontrol-test/src/components/CircleProgress.vue View File


src/components/GestureCard.vue → wavecontrol-test/src/components/GestureCard.vue View File


src/components/GestureIcon.vue → wavecontrol-test/src/components/GestureIcon.vue View File


src/components/Menu.vue → wavecontrol-test/src/components/Menu.vue View File


+ 41
- 24
wavecontrol-test/src/hand_landmark/VideoDetector.vue View File

@ -1,24 +1,41 @@
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize(); //
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
// 200ms
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});
// UI +
import { createApp, ref, onMounted } from "vue";
import { Detector } from "@/hand_landmark/detector";
import { GestureHandler } from "@/hand_landmark/gesture_handler"; //
const App = {
setup() {
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
const handler = new GestureHandler();
onMounted(async () => {
await detector.initialize();
const video = videoRef.value;
if (!video) return;
//
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await video.play();
//
const loop = async () => {
const result = await detector.detect(video);
const hand = result.rightHand ?? result.leftHand;
if (hand) {
const gesture = Detector.getSingleHandGesture(hand);
handler.handleGesture(gesture, hand);
}
requestAnimationFrame(loop);
};
loop();
});
return () => <video ref={videoRef} width="640" height="480" autoplay />;
},
};
createApp(App).mount("#app");

+ 104
- 22
wavecontrol-test/src/hand_landmark/detector.ts View File

@ -1,13 +1,34 @@
import { GestureHandler } from "@/hand_landmark/gesture_handler";
import {
FilesetResolver,
GestureRecognizer,
GestureRecognizerOptions,
} from "@mediapipe/tasks-vision";
// 手势
export enum HandGesture {
// 食指举起,移动鼠标
ONLY_INDEX_UP = "only_index_up",
INDEX_AND_THUMB_UP = "index_and_thumb_up"
// 食指和拇指举起,移动鼠标
INDEX_AND_THUMB_UP = "index_and_thumb_up",
// ok手势 - 滚动屏幕
THREE_FINGERS_UP = "three_fingers_up",
// 四根手指同时竖起 - enter
FOUR_FINGERS_UP = "four_fingers_up",
// 五根手指同时竖起 - 暂停/开始 识别
STOP_GESTURE = "stop_gesture",
// 6手势 - 语音识别
VOICE_GESTURE_START = "voice_gesture_start",
VOICE_GESTURE_STOP = "voice_gesture_stop",
// 其他手势
OTHER = "other",
}
interface HandLandmark {
@ -40,26 +61,25 @@ interface DetectionResult {
*/
export class Detector {
private detector: GestureRecognizer | null = null;
private gestureHandler: GestureHandler | null = null;
constructor(private modelPath = "/mediapipe/gesture_recognizer.task") {}
async initialize(useCanvas = false) {
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
try {
const params: any = {
const params = {
baseOptions: {
modelAssetPath: this.modelPath,
modelAssetPath: "/mediapipe/gesture_recognizer.task",
delegate: "GPU",
},
runningMode: "VIDEO",
numHands: 1,
};
} as GestureRecognizerOptions;
if (useCanvas) {
params.canvas = document.createElement("canvas");
}
this.detector = await GestureRecognizer.createFromOptions(vision, params);
this.gestureHandler = new GestureHandler();
} catch (error: any) {
if (error.toString().includes("kGpuService")) {
await this.initialize(true);
@ -149,8 +169,26 @@ export class Detector {
const gestureMap = new Map<string, HandGesture>([
// 食指举起,移动鼠标
["0,1,0,0,0", HandGesture.ONLY_INDEX_UP],
// 鼠标左键点击手势
["1,1,0,0,0", HandGesture.INDEX_AND_THUMB_UP],
// 滚动屏幕手势 ok
["0,0,1,1,1", HandGesture.THREE_FINGERS_UP],
// 四根手指同时竖起
["0,1,1,1,1", HandGesture.FOUR_FINGERS_UP],
// 五根手指同时竖起 - 暂停/开始 识别
["1,1,1,1,1", HandGesture.STOP_GESTURE],
// 6手势- 语音识别
["1,0,0,0,1", HandGesture.VOICE_GESTURE_START],
// 结束语音识别
["0,0,0,0,0", HandGesture.VOICE_GESTURE_STOP],
]);
if (gestureMap.has(fingerState)) {
@ -165,26 +203,70 @@ export class Detector {
/**
*
*/
async process(detection: DetectionResult): Promise<HandGesture> {
const hand = detection.rightHand ?? detection.leftHand;
//async process(detection: DetectionResult): Promise<HandGesture> {
// const hand = detection.rightHand ?? detection.leftHand;
if (!hand) {
console.log("没检测到手");
return HandGesture.OTHER;
}
// if (!hand) {
// console.log("没检测到手");
// return HandGesture.OTHER;
// }
const gesture = Detector.getSingleHandGesture(hand);
console.log("当前手势状态是:", gesture);
// const gesture = Detector.getSingleHandGesture(hand);
// console.log("当前手势状态是:", gesture);
if (gesture === HandGesture.ONLY_INDEX_UP) {
console.log("识别到食指竖起!");
}
// if (gesture === HandGesture.ONLY_INDEX_UP) {
// console.log("识别到食指竖起!");
// }
// if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
// console.log("识别到食指和大拇指竖起!");
// }
// if (gesture === HandGesture.THREE_FINGERS_UP) {
// console.log("识别到ok手势!");
// }
// if (gesture === HandGesture.FOUR_FINGERS_UP) {
// console.log("识别到四指竖起!");
// }
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
console.log("识别到食指和大拇指竖起!");
// if (gesture === HandGesture.STOP_GESTURE) {
// console.log("识别到五指竖起!");
// }
// if (gesture === HandGesture.VOICE_GESTURE_START) {
// console.log("识别到6手势!");
// }
// if (gesture === HandGesture.VOICE_GESTURE_STOP) {
// console.log("识别到拳头手势!");
// }
// return gesture;
async process(detection: DetectionResult): Promise<void> {
const rightHandGesture = detection.rightHand
? Detector.getSingleHandGesture(detection.rightHand)
: HandGesture.OTHER;
const leftHandGesture = detection.leftHand
? Detector.getSingleHandGesture(detection.leftHand)
: HandGesture.OTHER;
// 优先使用右手
let effectiveGesture = rightHandGesture;
if (detection.rightHand) {
effectiveGesture = rightHandGesture;
} else if (detection.leftHand) {
effectiveGesture = leftHandGesture;
}
return gesture;
// 将手势处理交给GestureHandler
if (detection.rightHand) {
this.gestureHandler?.handleGesture(effectiveGesture, detection.rightHand);
} else if (detection.leftHand) {
this.gestureHandler?.handleGesture(effectiveGesture, detection.leftHand);
}
}
}

+ 226
- 5
wavecontrol-test/src/hand_landmark/gesture_handler.ts View File

@ -1,12 +1,208 @@
// gesture_handler.ts
import { HandGesture, HandInfo } from "./detector";
import { HandGesture, HandInfo } from "@/hand_landmark/detector";
import use_app_store from "@/store/app";
// WebSocket数据类型定义
enum WsDataType {
// 系统消息类型
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
// 鼠标操作类型
MOUSE_MOVE = "mouse_move",
MOUSE_CLICK = "mouse_click",
// 键盘操作类型
SEND_KEYS = "send_keys",
}
interface WsData {
type: WsDataType;
msg?: string;
duration?: number;
title?: string;
data?: {
x?: number;
y?: number;
key_str?: string;
};
}
/**
* -
*
* 1. WebSocket连接
* 2.
* 3.
*/
export class TriggerAction {
private ws: WebSocket | null = null;
constructor() {
this.connectWebSocket();
}
private connectWebSocket() {
try {
this.ws = new WebSocket("ws://127.0.0.1:62334/ws_wavecontrol");
this.ws.onmessage = (event: MessageEvent) => {
const response: WsData = JSON.parse(event.data);
const app_store = use_app_store();
app_store.sub_window_info(response.msg || "");
};
this.ws.onopen = () => {
console.log("ws_wavecontrol connected");
};
this.ws.onclose = () => {
console.log("ws_wavecontrol closed, retrying...");
this.ws = null;
setTimeout(() => this.connectWebSocket(), 3000);
};
this.ws.onerror = (error) => {
console.error("ws_wavecontrol error:", error);
this.ws?.close();
};
} catch (error) {
console.error("Failed to create WebSocket instance:", error);
this.ws = null;
setTimeout(() => this.connectWebSocket(), 1000);
}
}
private send(data: { type: WsDataType } & Partial<Omit<WsData, "type">>) {
const message: WsData = {
type: data.type,
msg: data.msg || "",
title: data.title || "wavecontrol",
duration: data.duration || 1,
data: data.data || {},
};
console.log("👉 正在发送指令给后端:", message);
this.ws?.send(JSON.stringify(message));
}
moveMouse(x: number, y: number) {
this.send({
type: WsDataType.MOUSE_MOVE,
data: { x, y },
});
}
clickMouse() {
this.send({
type: WsDataType.MOUSE_CLICK,
});
}
}
// 手势处理
export class GestureHandler {
private previousGesture: HandGesture | null = null;
private previousGestureCount = 0;
private minGestureCount = 3;
private triggerAction: TriggerAction;
private previousGesture: HandGesture | null = null; // 上一个识别到的手势
private previousGestureCount: number = 0; // 连续识别到相同手势的次数
private minGestureCount: number = 5; // 最小连续识别次数,达到后才触发操作
// 屏幕参数,用于将摄像头图像坐标映射到实际屏幕坐标
private screen_width: number = window.screen.width;
private screen_height: number = window.screen.height;
private smoothening = 7; // 平滑系数,用于减少抖动
private prev_loc_x: number = 0; // 上一帧鼠标X坐标
private prev_loc_y: number = 0; // 上一帧鼠标Y坐标
private lastClickTime: number = 0; // 上次点击时间戳(用于点击节流)
private readonly CLICK_INTERVAL = 500; // 点击最小间隔(ms)
private app_store: any; // 存储应用状态,比如视频宽高、边界设置等
constructor() {
this.triggerAction = new TriggerAction(); // 初始化动作触发器,用于发指令到后端
this.app_store = use_app_store(); // 获取状态管理对象
}
/**
*
*/
private handleIndexFingerUp(hand: HandInfo) {
const indexTip = this.getFingerTip(hand, 1); // 获取食指尖坐标
if (!indexTip) return;
try {
// 将 landmark 坐标转为视频画面坐标(x 轴方向翻转)
const video_x =
this.app_store.VIDEO_WIDTH - indexTip.x * this.app_store.VIDEO_WIDTH;
const video_y = indexTip.y * this.app_store.VIDEO_HEIGHT;
/**
*
*/
function mapRange(
value: number,
fromMin: number,
fromMax: number,
toMin: number,
toMax: number
): number {
return (
((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin
);
}
// 将视频坐标映射为屏幕坐标
let screenX = mapRange(
video_x,
this.app_store.config.boundary_left,
this.app_store.config.boundary_left + this.app_store.config.boundary_width,
0,
this.screen_width
);
let screenY = mapRange(
video_y,
this.app_store.config.boundary_top,
this.app_store.config.boundary_top + this.app_store.config.boundary_height,
0,
this.screen_height
);
// 加入平滑处理,缓解抖动
screenX = this.prev_loc_x + (screenX - this.prev_loc_x) / this.smoothening;
screenY = this.prev_loc_y + (screenY - this.prev_loc_y) / this.smoothening;
// 更新上一帧坐标
this.prev_loc_x = screenX;
this.prev_loc_y = screenY;
// 更新状态,触发鼠标移动指令
this.app_store.sub_windows.x = screenX + 10;
this.app_store.sub_windows.y = screenY;
this.triggerAction.moveMouse(screenX, screenY);
} catch (error) {
console.error("处理鼠标移动失败:", error);
}
}
/**
* +
*/
private handleMouseClick() {
const now = Date.now();
if (now - this.lastClickTime < this.CLICK_INTERVAL) {
return; // 距离上次点击时间太短,忽略(节流)
}
this.lastClickTime = now;
// 发送鼠标点击指令
this.triggerAction.clickMouse();
}
/**
*
*/
handleGesture(gesture: HandGesture, hand: HandInfo) {
// 判断手势是否重复,连续计数
if (gesture === this.previousGesture) {
this.previousGestureCount++;
} else {
@ -14,8 +210,33 @@ export class GestureHandler {
this.previousGestureCount = 1;
}
// 鼠标移动(不需要连续确认)
if (gesture === HandGesture.ONLY_INDEX_UP) {
this.handleIndexFingerUp(hand);
return;
}
// 连续识别足够多次 → 触发操作
if (this.previousGestureCount >= this.minGestureCount) {
console.log("识别手势类型:", gesture);
switch (gesture) {
case HandGesture.INDEX_AND_THUMB_UP: // 自定义为点击手势
this.handleMouseClick();
break;
}
}
}
/**
* landmark
* fingerIndex: 0=, 1=, 2=, ...
*/
private getFingerTip(hand: HandInfo, fingerIndex: number) {
if (!hand) return null;
const tipIndices = [4, 8, 12, 16, 20]; // 手指指尖 landmark 的索引
return hand.landmarks[tipIndices[fingerIndex]];
}
}

+ 49
- 0
wavecontrol-test/src/locales/en.ts View File

@ -0,0 +1,49 @@
export default {
"手势识别控制": "Gesture Recognition Control",
"运行中": "Running",
"已停止": "Stopped",
"开机自启动": "Auto Start",
"显示识别窗口": "Show Recognition Window",
"摄像头选择": "Camera Selection",
"手势操作指南": "Gesture Guide",
"光标控制": "Cursor Control",
"竖起食指滑动控制光标位置": "Slide with index finger to control cursor position",
"单击操作": "Click Operation",
"双指举起执行鼠标单击": "Raise two fingers to perform mouse click",
"Rock手势执行鼠标单击": "Rock gesture to perform mouse click",
"滚动控制": "Scroll Control",
"三指上下滑动控制页面滚动": "Slide three fingers up/down to control page scrolling",
"(okay手势)食指和拇指捏合滚动页面": "(okay gesture)Pinch with index and thumb to scroll page",
"食指和拇指距离小于": "Index and thumb distance less than",
"触发捏合": "Trigger pinch",
"默认值0.02": "Default value 0.02",
"可以通过右键->检查->控制台->捏合手势->查看当前距离": "Can check current distance by right-click -> inspect -> console -> pinch gesture",
"全屏控制": "Full Screen Control",
"四指并拢发送按键": "Four fingers together to send key",
"点击设置快捷键": "Click to set shortcut",
"请按下按键...": "Please press keys...",
"点击设置": "Click to set",
"退格": "Backspace",
"发送退格键": "Send backspace key",
"开始语音识别": "Start Voice Recognition",
"六指手势开始语音识别": "Six fingers gesture to start voice recognition",
"结束语音识别": "End Voice Recognition",
"拳头手势结束语音识别": "Fist gesture to end voice recognition",
"暂停/继续": "Pause/Resume",
"单手张开1.5秒 暂停/继续 手势识别": "Open one hand for 1.5 seconds to pause/resume gesture recognition",
"识别框x": "Recognition box x",
"识别框y": "Recognition box y",
"识别框宽": "Recognition box width",
"识别框高": "Recognition box height",
// 通知
"WaveControl": "WaveControl",
"提示": "Tip",
"停止语音识别": "Stop Voice Recognition",
"手势识别": "Gesture Recognition",
"继续手势识别": "Continue Gesture Recognition",
"暂停手势识别": "Pause Gesture Recognition",
};

+ 18
- 0
wavecontrol-test/src/locales/i18n.ts View File

@ -0,0 +1,18 @@
import { createI18n } from "vue-i18n";
// 导入语言包
import en from "./en";
import zh from "./zh";
// 创建 i18n 实例
const i18n = createI18n({
locale: navigator.language.split("-")[0], // 使用系统语言作为默认语言
// locale: "en", // 使用系统语言作为默认语言
fallbackLocale: "zh", // 回退语言
messages: {
en,
zh,
},
});
export default i18n;

+ 47
- 0
wavecontrol-test/src/locales/zh.ts View File

@ -0,0 +1,47 @@
export default {
"手势识别控制": "手势识别控制",
"运行中": "运行中",
"已停止": "已停止",
"显示识别窗口": "显示识别窗口",
"摄像头选择": "摄像头选择",
"手势操作指南": "手势操作指南",
"开机自启动": "开机自启动",
"光标控制": "光标控制",
"竖起食指滑动控制光标位置": "竖起食指滑动控制光标位置",
"单击操作": "单击操作",
"双指举起执行鼠标单击": "双指举起执行鼠标单击",
"Rock手势执行鼠标单击": "Rock手势执行鼠标单击",
"滚动控制": "滚动控制",
"三指上下滑动控制页面滚动": "三指上下滑动控制页面滚动",
"(okay手势)食指和拇指捏合滚动页面": "(okay手势)食指和拇指捏合滚动页面",
"食指和拇指距离小于": "食指和拇指距离小于",
"触发捏合": "触发捏合",
"全屏控制": "全屏控制",
"四指并拢发送按键": "四指并拢发送按键",
"点击设置快捷键": "点击设置快捷键",
"请按下按键...": "请按下按键...",
"点击设置": "点击设置",
"退格": "退格",
"发送退格键": "发送退格键",
"开始语音识别": "开始语音识别",
"六指手势开始语音识别": "六指手势开始语音识别",
"结束语音识别": "结束语音识别",
"拳头手势结束语音识别": "拳头手势结束语音识别",
"暂停/继续": "暂停/继续",
"单手张开1.5秒 暂停/继续 手势识别": "单手张开1.5秒 暂停/继续 手势识别",
"识别框x": "识别框x",
"识别框y": "识别框y",
"识别框宽": "识别框宽",
"识别框高": "识别框高",
"默认值0.02": "默认值0.02",
"可以通过右键->检查->控制台->捏合手势->查看当前距离": "可以通过右键->检查->控制台->捏合手势->查看当前距离",
// 通知
"WaveControl": "WaveControl",
"提示": "提示",
"停止语音识别": "停止语音识别",
"手势识别": "手势识别",
"继续手势识别": "继续手势识别",
"暂停手势识别": "暂停手势识别",
};

+ 10
- 4
wavecontrol-test/src/main.ts View File

@ -1,5 +1,11 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createApp } from "vue";
import App from "@/App.vue";
createApp(App).mount('#app')
import { createPinia } from "pinia";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");

+ 30
- 0
wavecontrol-test/src/py_api.ts View File

@ -0,0 +1,30 @@
const port = 62334;
const base_url = `http://localhost:${port}`;
class PyApi {
async ready(): Promise<boolean> {
try {
await fetch(`${base_url}/`, {
signal: AbortSignal.timeout(1000),
});
return true;
} catch (error) {
return false;
}
}
async shutdown() {
try {
await fetch(`${base_url}/shutdown`, {
method: "GET",
signal: AbortSignal.timeout(500),
});
} catch (error) {
console.error("关闭服务失败:", error);
}
}
}
const pyApi = new PyApi();
export default pyApi;

+ 38
- 0
wavecontrol-test/src/router/index.ts View File

@ -0,0 +1,38 @@
import MainWindow from "@/view/mainWindow/MainWindow.vue";
import SubWindow from "@/view/subWindow/SubWindow.vue";
import Home from "@/view/mainWindow/Home.vue";
import Guide from "@/view/mainWindow/Guide.vue";
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
name: "mainWindow",
component: MainWindow,
children: [
{
path: "",
name: "home",
component: Home,
},
{
path: "guide",
name: "guide",
component: Guide,
}
]
},
{
path: "/sub-window",
name: "subWindow",
component: SubWindow,
},
];
const router = createRouter({
// 设置成 html5 模式,subWindow 才能正常工作
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;

+ 87
- 0
wavecontrol-test/src/store/app.ts View File

@ -0,0 +1,87 @@
import { defineStore } from "pinia";
interface Camera {
deviceId: string;
label: string;
kind: string;
}
enum NotiType {
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
}
export const use_app_store = defineStore("app-store", {
state: () => ({
config: {
auto_start: false,
show_window: false,
four_fingers_up_send: "f",
selected_camera_id: "",
// 识别框
boundary_left: 100,
boundary_top: 100,
boundary_width: 100,
boundary_height: 100,
// 手势识别
scroll_gesture_2_thumb_and_index_threshold: 0.02, // 食指和拇指距离阈值
},
sub_windows: {
x: 0,
y: 0,
progress: 0,
notification: "",
noti_type: NotiType.INFO,
},
mission_running: false,
cameras: [] as Camera[],
VIDEO_WIDTH: 640,
VIDEO_HEIGHT: 480,
flag_detecting: false,
}),
// PiniaSharedState 来共享不同 tauri 窗口之间的状态
share: {
// Override global config for this store.
enable: true,
initialize: true,
},
actions: {
is_macos() {
return navigator.userAgent.includes("Mac");
},
is_windows() {
return navigator.userAgent.includes("Windows");
},
is_linux() {
return navigator.userAgent.includes("Linux");
},
async sub_window_info(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.INFO;
},
async sub_window_success(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.SUCCESS;
},
async sub_window_warning(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.WARNING;
},
async sub_window_error(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.ERROR;
},
},
});
export default use_app_store;

+ 46
- 0
wavecontrol-test/src/utils/subWindow.ts View File

@ -0,0 +1,46 @@
import {
WebviewWindow,
getAllWebviewWindows,
} from "@tauri-apps/api/webviewWindow";
export const SUB_WINDOW_WIDTH = 130;
export const SUB_WINDOW_HEIGHT = 130;
export async function createSubWindow(url: string, title: string) {
let message = "";
let success = true;
try {
const allWindows = await getAllWebviewWindows();
const windownsLen = allWindows.length;
const label = `NewWindow_${windownsLen + 1}`;
const openUrl = url || "index.html";
const newTitle = title || "新窗口";
const openTitle = `${newTitle}-${windownsLen + 1}`;
const webview_window = new WebviewWindow(label, {
url: openUrl,
title: openTitle,
parent: "main",
zoomHotkeysEnabled: false,
width: SUB_WINDOW_WIDTH,
height: SUB_WINDOW_HEIGHT,
minWidth: SUB_WINDOW_WIDTH,
minHeight: SUB_WINDOW_HEIGHT,
alwaysOnTop: true,
decorations: false, // 隐藏窗口边框
visible: false,
resizable: false,
});
webview_window.once("tauri://created", async () => {
message = "打开成功";
});
webview_window.once("tauri://error", function (e) {
message = `打开${openTitle}报错: ${e}`;
success = false;
});
return { success: success, message: message, webview: webview_window };
} catch (error) {
return { success: false, message: error };
}
}

+ 348
- 0
wavecontrol-test/src/view/mainWindow/Guide.vue View File

@ -0,0 +1,348 @@
<template>
<div class="guide-container">
<div class="header-bar">
<div class="title-container">
<h1 class="view-title">{{ $t('手势操作指南') }}</h1>
<div class="view-description">{{ $t('学习使用各种手势控制您的设备') }}</div>
</div>
</div>
<div class="main-content">
<n-card class="guide-card">
<div class="gesture-grid">
<!-- 卡片1: 光标控制 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">👆</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('光标控制') }}</h3>
<p class="card-description">{{ $t('竖起食指滑动控制光标位置') }}</p>
</div>
</div>
<!-- 卡片2: 单击操作双指 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol"></span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('单击操作') }}</h3>
<p class="card-description">{{ $t('双指举起执行鼠标单击') }}</p>
</div>
</div>
<!-- 卡片3: 单击操作Rock手势 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">🤘</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('单击操作') }}</h3>
<p class="card-description">{{ $t('Rock手势执行鼠标单击') }}</p>
<div class="card-extra">
</div>
</div>
</div>
<!-- 卡片4: 滚动控制 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">👌</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('滚动控制') }}</h3>
<p class="card-description">{{ $t('(okay手势)食指和拇指捏合滚动页面') }}</p>
<div class="card-extra">
<div class="setting-control">
<span>{{ $t("食指和拇指距离小于") }}</span>
<n-input-number
v-model:value="app_store.config.scroll_gesture_2_thumb_and_index_threshold"
:min="0"
:step="0.01"
size="small"
/>
<span>{{ $t("触发捏合") }}</span>
</div>
<div class="hint-tags">
<n-tag size="small" type="info">{{ $t("默认值0.02") }}</n-tag>
<n-tag size="small" type="info">{{ $t("右键->检查->控制台->查看当前距离") }}</n-tag>
</div>
</div>
</div>
</div>
<!-- 卡片5: 全屏控制 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">🖐</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('全屏控制') }}</h3>
<p class="card-description">{{ $t('四指并拢发送按键') }}</p>
<div class="card-extra">
<div class="keyboard-input">
<n-input
:value="app_store.config.four_fingers_up_send || 'f'"
readonly
:placeholder="$t('点击设置快捷键')"
@click="listenForKey"
:status="isListening ? 'warning' : undefined"
size="small"
>
<template #suffix>
<span>{{ isListening ? $t("请按下按键...") : $t("点击设置") }}</span>
</template>
</n-input>
</div>
</div>
</div>
</div>
<!-- 卡片6: 退格 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">🔙</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('退格') }}</h3>
<p class="card-description">{{ $t('发送退格键') }}</p>
</div>
</div>
<!-- 卡片7: 开始语音识别 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">🎤</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('开始语音识别') }}</h3>
<p class="card-description">{{ $t('六指手势开始语音识别') }}</p>
</div>
</div>
<!-- 卡片8: 结束语音识别 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol">👊</span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('结束语音识别') }}</h3>
<p class="card-description">{{ $t('拳头手势结束语音识别') }}</p>
</div>
</div>
<!-- 卡片9: 暂停/继续 -->
<div class="card">
<div class="card-icon">
<div class="icon-circle">
<span class="icon-symbol"></span>
</div>
</div>
<div class="card-content">
<h3 class="card-title">{{ $t('暂停/继续') }}</h3>
<p class="card-description">{{ $t('单手张开1.5秒 暂停/继续 手势识别') }}</p>
</div>
</div>
</div>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import { use_app_store } from "@/store/app";
import { ref } from "vue";
const app_store = use_app_store();
const isListening = ref(false);
const listenForKey = () => {
isListening.value = true;
// ...
};
</script>
<style scoped lang="scss">
.guide-container {
padding: 16px;
box-sizing: border-box;
}
.header-bar {
margin-bottom: 24px;
}
.title-container {
padding: 0 16px;
}
.view-title {
font-size: 24px;
font-weight: 700;
margin: 0;
color: #333;
}
.view-description {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.main-content {
display: flex;
flex-direction: column;
}
.guide-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.gesture-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 16px;
}
.card {
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
}
.card-icon {
padding: 16px;
display: flex;
justify-content: center;
}
.icon-circle {
width: 64px;
height: 64px;
border-radius: 50%;
background: #e6f7ff;
display: flex;
align-items: center;
justify-content: center;
}
.icon-symbol {
font-size: 32px;
color: #1890ff;
}
.card-content {
padding: 0 16px 16px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 8px 0 4px;
}
.card-description {
font-size: 14px;
color: #666;
margin: 4px 0 8px;
line-height: 1.4;
}
.card-extra {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
margin-top: 12px;
a {
color: #1890ff;
text-decoration: none;
margin-right: 12px;
&:hover {
text-decoration: underline;
}
}
}
.setting-control {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
.n-input-number {
width: 100px;
}
}
.hint-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
.n-tag {
padding: 4px 8px;
background: #f0f9ff;
border-color: #91d5ff;
color: #1890ff;
}
}
.keyboard-input {
width: 100%;
.n-input {
width: 100%;
}
}
@media (max-width: 992px) {
.gesture-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.gesture-grid {
grid-template-columns: 1fr;
}
.setting-control {
flex-direction: column;
align-items: flex-start;
}
}
</style>

+ 367
- 0
wavecontrol-test/src/view/mainWindow/Home.vue View File

@ -0,0 +1,367 @@
<template>
<div class="home-container">
<!-- 顶部统计数据卡片 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon">👋</div>
<div class="stat-data">
<div class="value">92%</div>
<div class="label">识别准确率</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-data">
<div class="value">38ms</div>
<div class="label">平均响应时间</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🔍</div>
<div class="stat-data">
<div class="value">312</div>
<div class="label">今日识别次数</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧摄像头预览区域 -->
<div class="preview-card">
<div class="preview-header">
<h3>手势识别预览</h3>
<!-- 运行开关移到预览区域顶部 -->
<n-switch
v-model:value="app_store.mission_running"
size="large"
:rail-style="railStyle"
>
<template #checked>{{ $t("运行中") }}</template>
<template #unchecked>{{ $t("已停止") }}</template>
</n-switch>
</div>
<!-- 摄像头预览组件 -->
<VideoDetector />
</div>
<!-- 右侧控制面板 -->
<div class="control-panel">
<!-- 系统设置 -->
<div class="settings-section">
<h3 class="section-title">手势识别控制</h3>
<div class="setting-item">
<div class="setting-label">
<n-icon size="20">
<Browser />
</n-icon>
<span>{{ $t("显示识别窗口") }}</span>
</div>
<n-switch v-model:value="app_store.config.show_window" />
</div>
<div class="setting-item">
<div class="setting-label">
<AutoStart />
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<n-icon size="20">
<Camera />
</n-icon>
<span>{{ $t("摄像头选择") }}</span>
</div>
<n-select
v-model:value="app_store.config.selected_camera_id"
:options="camera_options"
:disabled="app_store.mission_running"
style="width: 100%"
/>
</div>
</div>
<!-- 手势库展示 -->
<div class="gesture-gallery">
<div class="gallery-header">
<div>手势库</div>
<button class="add-btn">+ 添加手势</button>
</div>
<div class="gesture-grid">
<div class="gesture-item" v-for="i in 6" :key="i">
<div class="gesture-icon">👌</div>
<div class="gesture-name">OK手势</div>
<div class="gesture-action">暂停媒体</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AutoStart from "@/components/AutoStart.vue";
import VideoDetector from "@/hand_landmark/VideoDetector.vue";
import { use_app_store } from "@/store/app";
import { Browser, Camera } from "@icon-park/vue-next";
import { computed, onMounted } from "vue";
const is_dev = computed(() => import.meta.env.DEV);
const app_store = use_app_store();
//
const camera_options = computed(() => {
return app_store.cameras.map((camera) => ({
label: camera.label || `摄像头 ${camera.deviceId.slice(0, 4)}`,
value: camera.deviceId,
}));
});
const getCameras = async () => {
try {
//
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
});
stream.getTracks().forEach((track) => track.stop());
const devices = await navigator.mediaDevices.enumerateDevices();
app_store.cameras = devices.filter(
(device) => device.kind === "videoinput"
);
} catch (error) {
console.error("获取摄像头列表失败:", error);
}
};
onMounted(async () => {
await getCameras();
});
</script>
<style scoped lang="scss">
.home-container {
display: flex;
flex-direction: column;
gap: 24px;
height: 100%;
}
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.stat-card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px var(--shadow-color);
}
}
.stat-icon {
width: 56px;
height: 56px;
background: rgba(14, 165, 233, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--accent-primary);
}
.stat-data {
flex: 1;
}
.value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.label {
font-size: 14px;
color: var(--text-secondary);
margin-top: 4px;
}
.main-content {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 24px;
height: calc(100% - 120px);
}
.preview-card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 16px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
}
.control-panel {
display: flex;
flex-direction: column;
gap: 24px;
}
.settings-section {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-top: 0;
margin-bottom: 20px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
&:last-child {
border-bottom: none;
}
}
.setting-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-primary);
}
.gesture-gallery {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--border-color);
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
div {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
}
.add-btn {
background: rgba(14, 165, 233, 0.15);
border: none;
border-radius: 8px;
padding: 8px 15px;
color: var(--accent-primary);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(14, 165, 233, 0.25);
}
}
.gesture-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-top: 10px;
}
.gesture-item {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.2s;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 10px var(--shadow-color);
}
}
.gesture-icon {
font-size: 32px;
margin-bottom: 10px;
}
.gesture-name {
font-weight: 500;
color: var(--text-primary);
}
.gesture-action {
font-size: 13px;
color: var(--text-secondary);
text-align: center;
margin-top: 5px;
}
.ad-container {
height: 200px;
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
overflow: hidden;
iframe {
border: none;
background-color: var(--bg-secondary);
border-radius: 16px;
}
}
</style>

+ 606
- 0
wavecontrol-test/src/view/mainWindow/MainWindow.vue View File

@ -0,0 +1,606 @@
<script setup lang="ts">
import pyApi from "@/py_api";
import use_app_store from "@/store/app";
import { getVersion } from "@tauri-apps/api/app";
import {
getCurrentWindow,
LogicalPosition,
LogicalSize,
} from "@tauri-apps/api/window";
import { LazyStore } from "@tauri-apps/plugin-store";
import { onMounted, ref, watch, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const is_dev = import.meta.env.DEV;
const appVersion = ref("");
const app_store = use_app_store();
const ready = ref(false);
const darkTheme = ref(false);
const randomIconPath = ref("");
// UI
const settings = ref({
autostart: true,
showWindow: true
});
//
const currentView = computed(() => route.name || 'home');
//
const viewTitle = computed(() => {
switch (currentView.value) {
case 'home': return '控制面板';
case 'gestures': return '手势管理';
case 'settings': return '设置中心';
case 'help': return '帮助文档';
default: return 'WaveControl';
}
});
//
onMounted(() => {
const randomIndex = Math.floor(Math.random() * 9) + 1;
randomIconPath.value = `/wavecontrol${randomIndex}.jpg`;
});
//
onMounted(async () => {
ready.value = await pyApi.ready();
const timer = setInterval(async () => {
ready.value = await pyApi.ready();
if (ready.value) {
clearInterval(timer);
}
}, 5000);
await getCurrentWindow().onCloseRequested(async () => {
const factor = await getCurrentWindow().scaleFactor();
const position = (await getCurrentWindow().innerPosition()).toLogical(factor);
const size = (await getCurrentWindow().innerSize()).toLogical(factor);
await window_store_json.set("window_state", {
x: position.x,
y: position.y,
width: size.width,
height: size.height,
});
if (!is_dev) {
await pyApi.shutdown();
}
});
});
//
const window_store_json = new LazyStore("window_state.json");
onMounted(async () => {
appVersion.value = await getVersion();
const window_state = await window_store_json.get("window_state");
if (window_state) {
let new_x = window_state.x;
let new_y = window_state.y;
const screen_width = window.screen.width;
const screen_height = window.screen.height;
// 100100
if (new_x <= 0) new_x = 100;
if (new_x >= screen_width) new_x = 100
if (new_y <= 0) new_y = 100;
if (new_y >= screen_height) new_y = 100;
getCurrentWindow().setPosition(new LogicalPosition(new_x, new_y));
getCurrentWindow().setSize(
new LogicalSize(window_state.width, window_state.height)
);
}
});
// app_store
const app_store_json = new LazyStore("settings.json");
onMounted(async () => {
const config_data = await app_store_json.get("config");
console.log("config_data", config_data);
if (config_data) {
Object.assign(app_store.config, JSON.parse(JSON.stringify(config_data)));
}
});
watch(
() => app_store.config,
async (value) => {
await app_store_json.set("config", value);
app_store_json.save();
},
{ deep: true }
);
//
import {
isPermissionGranted,
requestPermission,
} from "@tauri-apps/plugin-notification";
onMounted(async () => {
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === "granted";
}
});
// 使 iframe <a>
import { openUrl } from "@tauri-apps/plugin-opener";
window.addEventListener("message", async function (e) {
const url = e.data;
if (url) {
await openUrl(url);
}
});
//
import { createSubWindow } from "@/utils/subWindow";
const subWindow = ref(null);
onMounted(async () => {
if (!subWindow.value) {
subWindow.value = await createSubWindow("/sub-window", "subWindow");
}
});
//
const toggleTheme = () => {
darkTheme.value = !darkTheme.value;
};
const switchView = (viewName: string) => {
router.push({ name: viewName });
};
const toggleSetting = (setting: string) => {
settings.value[setting] = !settings.value[setting];
};
//
const statusClass = computed(() => ({
'status-active': ready.value,
'status-inactive': !ready.value
}));
const statusText = computed(() => {
return ready.value ? '运行中' : '已停止';
});
</script>
<template>
<div class="app-container" :class="{'dark-theme': darkTheme}">
<!-- 加载状态 -->
<div v-if="!ready" class="loading-overlay">
<div class="loader-container">
<div class="spinner"></div>
<div class="loading-text">手势识别模块加载中...</div>
</div>
</div>
<!-- 主界面 -->
<div v-else class="main-layout">
<!-- 左侧导航 -->
<div class="sidebar">
<div class="branding">
<!-- 使用随机图标路径 -->
<img :src="randomIconPath" alt="WaveControl Logo" class="logo" />
<div class="app-info">
<div class="app-name">WaveControl</div>
</div>
</div>
<div class="nav-items">
<div class="nav-item" :class="{active: currentView === 'home'}" @click="switchView('home')">
<i class="icon">🏠</i>
<span>主控台</span>
</div>
<div class="nav-item" :class="{active: currentView === 'guide'}" @click="switchView('guide')">
<i class="icon">👋</i>
<span>手势管理</span>
</div>
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="switchView('settings')">
<i class="icon"></i>
<span>设置</span>
</div>
<div class="nav-item" :class="{active: currentView === 'help'}" @click="switchView('help')">
<i class="icon"></i>
<span>帮助</span>
</div>
</div>
<div class="status-card">
<div class="status-indicator" :class="statusClass">
{{ statusText }}
</div>
<div class="fps-counter">
<span>FPS</span>
<span class="value">60</span>
</div>
</div>
<div class="contributors">
<div v-if="app_store.is_macos()">
<a
class="contributor-link"
href="https://gitee.com/wydhhh/software-engineering"
target="_blank"
>
@backpack</a
>
</div>
<div v-else class="contributor">@ninjakelly</div>
</div>
</div>
<!-- 主内容区 -->
<div class="content-area">
<div class="header-bar">
<div class="title-container">
<h1 class="view-title">{{ viewTitle }}</h1>
<div class="view-description">隔空手势控制系统</div>
</div>
<div class="controls">
<button class="theme-toggle" @click="toggleTheme">
<i class="icon">{{ darkTheme ? '☀️' : '🌙' }}</i>
</button>
</div>
</div>
<div class="main-content">
<router-view />
</div>
</div>
</div>
<DevTool v-if="is_dev" />
</div>
</template>
<style scoped lang="scss">
.app-container {
font-family: 'Segoe UI', system-ui, sans-serif;
height: 100vh;
width: 100vw;
overflow: hidden;
transition: background-color 0.3s, color 0.3s;
&.dark-theme {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent-primary: #0ea5e9;
--accent-secondary: #7dd3fc;
--hover-bg: rgba(148, 163, 184, 0.12);
--border-color: #334155;
--card-bg: rgba(30, 41, 59, 0.7);
--shadow-color: rgba(0, 0, 0, 0.4);
--status-active: #0ea5e9;
--status-inactive: #475569;
}
&:not(.dark-theme) {
--bg-primary: #f8fafc;
--bg-secondary: #f1f5f9;
--bg-tertiary: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--accent-primary: #0284c7;
--accent-secondary: #0ea5e9;
--hover-bg: rgba(15, 23, 42, 0.05);
--border-color: #cbd5e1;
--card-bg: rgba(241, 245, 249, 0.7);
--shadow-color: rgba(0, 0, 0, 0.08);
--status-active: #0284c7;
--status-inactive: #94a3b8;
}
background-color: var(--bg-primary);
color: var(--text-primary);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-primary);
z-index: 100;
}
.loader-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(14, 165, 233, 0.2);
border-radius: 50%;
border-top: 4px solid var(--accent-primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 18px;
font-weight: 500;
color: var(--text-primary);
}
.main-layout {
display: flex;
height: 100vh;
}
/* 左侧导航样式 */
.sidebar {
width: 260px;
background-color: var(--bg-secondary);
display: flex;
flex-direction: column;
padding: 25px 0;
border-right: 1px solid var(--border-color);
z-index: 10;
}
.branding {
display: flex;
align-items: center;
padding: 0 20px 20px;
}
.logo {
width: 42px;
height: 42px;
border-radius: 10px;
margin-right: 12px;
background: linear-gradient(135deg, var(--accent-primary), #7dd3fc);
padding: 5px;
}
.app-info {
display: flex;
flex-direction: column;
}
.app-name {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.5px;
color: var(--text-primary);
}
.app-version {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
opacity: 0.8;
}
.nav-items {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 15px;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
gap: 14px;
font-size: 16px;
color: var(--text-secondary);
&:hover {
background-color: var(--hover-bg);
color: var(--text-primary);
}
&.active {
background: rgba(14, 165, 233, 0.15);
color: var(--accent-primary);
font-weight: 500;
.icon {
color: var(--accent-primary);
}
}
}
.icon {
font-size: 18px;
}
.status-card {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
margin: 20px;
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
box-shadow: 0 4px 6px var(--shadow-color);
}
.status-indicator {
display: inline-flex;
align-items: center;
font-size: 14px;
font-weight: 500;
padding: 6px 12px;
border-radius: 20px;
&::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
&.status-active {
background: rgba(14, 165, 233, 0.2);
color: var(--accent-primary);
&::before {
background-color: var(--accent-primary);
}
}
&.status-inactive {
background: rgba(148, 163, 184, 0.2);
color: var(--text-secondary);
&::before {
background-color: var(--text-secondary);
}
}
}
.fps-counter {
margin-top: 15px;
display: flex;
align-items: baseline;
gap: 8px;
span {
font-size: 14px;
color: var(--text-secondary);
}
.value {
font-size: 24px;
font-weight: 600;
color: var(--accent-primary);
}
}
.contributors {
padding: 20px;
color: var(--text-secondary);
font-size: 14px;
display: flex;
flex-direction: column;
gap: 15px;
.contributor {
padding: 5px 12px;
border-radius: 6px;
background-color: var(--bg-tertiary);
}
.contributor-link {
color: var(--accent-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
/* 主内容区域样式 */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px 30px 15px;
border-bottom: 1px solid var(--border-color);
}
.title-container {
display: flex;
flex-direction: column;
}
.view-title {
font-size: 28px;
font-weight: 700;
margin: 0;
color: var(--text-primary);
line-height: 1.2;
}
.view-description {
font-size: 15px;
color: var(--text-secondary);
margin-top: 6px;
}
.controls {
display: flex;
gap: 15px;
}
.theme-toggle {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-secondary);
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
color: var(--text-primary);
transition: all 0.2s;
&:hover {
background: var(--hover-bg);
transform: scale(1.05);
}
}
.main-content {
flex: 1;
overflow: auto;
padding: 30px;
}
/* 添加路由视图过渡效果 */
.router-view-container {
transition: opacity 0.3s ease;
> * {
animation: fadeIn 0.5s ease;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

+ 93
- 0
wavecontrol-test/src/view/subWindow/SubWindow.vue View File

@ -0,0 +1,93 @@
<template>
<div class="container-sub-window">
<CircleProgress
:percentage="app_store.sub_windows.progress"
:size="100"
:text="app_store.flag_detecting ? '暂停检测' : '继续检测'"
:color="app_store.flag_detecting ? '#F56C6C' : '#67C23A'"
/>
<div style="height: 30px">
<span>{{ app_store.sub_windows.notification }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import CircleProgress from "@/components/CircleProgress.vue";
import use_app_store from "@/store/app";
import { getCurrentWindow, LogicalPosition } from "@tauri-apps/api/window";
import { computed, ref, watch } from "vue";
const app_store = use_app_store();
const display_progress = ref(false);
const display_notification = computed(() => {
return !display_progress.value;
});
let hideTimer: number | null = null;
async function show_window() {
await getCurrentWindow().show();
}
async function hide_window() {
await getCurrentWindow().hide();
}
watch(
() => app_store.sub_windows.x,
(newVal) => {
getCurrentWindow().setPosition(
new LogicalPosition(newVal, app_store.sub_windows.y)
);
}
);
// sub-window
watch(
() => app_store.sub_windows.progress,
(newVal) => {
if (newVal) {
display_progress.value = true;
show_window();
//
if (hideTimer) {
clearTimeout(hideTimer);
}
//
hideTimer = setTimeout(() => {
hide_window();
}, 300);
}
}
);
watch(
() => app_store.sub_windows.notification,
(newVal) => {
if (newVal) {
display_progress.value = false;
show_window();
//
if (hideTimer) {
clearTimeout(hideTimer);
}
//
hideTimer = setTimeout(() => {
hide_window();
app_store.sub_windows.notification = "";
}, 1000);
}
}
);
</script>
<style lang="scss" scoped>
.container-sub-window {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
</style>

+ 7
- 0
wavecontrol-test/src/vite-env.d copy.ts View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

+ 9
- 0
wavecontrol-test/test_mouse.py View File

@ -0,0 +1,9 @@
from pynput.mouse import Controller
import time
mouse = Controller()
print("3秒后移动鼠标到位置 (300, 300)...")
time.sleep(3)
mouse.position = (300, 300)
print("鼠标已移动 ✅")

+ 0
- 15
wavecontrol-test/tsconfig.app.json View File

@ -1,15 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

+ 28
- 5
wavecontrol-test/tsconfig.json View File

@ -1,7 +1,30 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"] // @ src
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

+ 3
- 18
wavecontrol-test/tsconfig.node.json View File

@ -1,25 +1,10 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"composite": true,
"skipLibCheck": true,
/* Bundler mode */
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

Loading…
Cancel
Save