Browse Source

add FRONTEND

finalv1
NinjaKelly 2 months ago
parent
commit
1e43ceb2c8
26 changed files with 2182 additions and 0 deletions
  1. +13
    -0
      wavecontrol-test/src-tauri/.gitignore
  2. +32
    -0
      wavecontrol-test/src-tauri/Cargo.toml
  3. +8
    -0
      wavecontrol-test/src-tauri/Info.plist
  4. +3
    -0
      wavecontrol-test/src-tauri/build.rs
  5. +41
    -0
      wavecontrol-test/src-tauri/capabilities/default.json
  6. +17
    -0
      wavecontrol-test/src-tauri/capabilities/desktop.json
  7. +58
    -0
      wavecontrol-test/src-tauri/src/lib.rs
  8. +6
    -0
      wavecontrol-test/src-tauri/src/main.rs
  9. +61
    -0
      wavecontrol-test/src-tauri/tauri.conf.json
  10. +13
    -0
      wavecontrol-test/src-tauri/tauri.macos.conf.json
  11. +34
    -0
      wavecontrol-test/src/AppMediaPipe.vue
  12. +31
    -0
      wavecontrol-test/src/components/AutoStart.vue
  13. +77
    -0
      wavecontrol-test/src/components/CircleProgress.vue
  14. +82
    -0
      wavecontrol-test/src/components/GestureCard.vue
  15. +23
    -0
      wavecontrol-test/src/components/GestureIcon.vue
  16. +41
    -0
      wavecontrol-test/src/components/Menu.vue
  17. +49
    -0
      wavecontrol-test/src/locales/en.ts
  18. +18
    -0
      wavecontrol-test/src/locales/i18n.ts
  19. +47
    -0
      wavecontrol-test/src/locales/zh.ts
  20. +30
    -0
      wavecontrol-test/src/py_api.ts
  21. +38
    -0
      wavecontrol-test/src/router/index.ts
  22. +46
    -0
      wavecontrol-test/src/utils/subWindow.ts
  23. +348
    -0
      wavecontrol-test/src/view/mainWindow/Guide.vue
  24. +367
    -0
      wavecontrol-test/src/view/mainWindow/Home.vue
  25. +606
    -0
      wavecontrol-test/src/view/mainWindow/MainWindow.vue
  26. +93
    -0
      wavecontrol-test/src/view/subWindow/SubWindow.vue

+ 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 = "Lazyeat"
version = "0.3.11"
description = "Lazyeat 手势识别"
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("Lazyeat 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": "Lazyeat",
"version": "0.3.11",
"identifier": "com.Lazyeat.maplelost",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Lazyeat",
"width": 800,
"height": 600,
"devtools": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"externalBin": ["bin/backend-py/Lazyeat 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.Lazyeat.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>

+ 31
- 0
wavecontrol-test/src/components/AutoStart.vue View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { Power } from "@icon-park/vue-next";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { watch } from "vue";
import { use_app_store } from "@/store/app";
const app_store = use_app_store();
watch(
() => app_store.config.auto_start,
async (value) => {
if (value) {
await enable();
} else {
await disable();
}
}
);
</script>
<template>
<n-space align="center" style="display: flex; align-items: center">
<span style="display: flex; align-items: center">
<n-icon size="20" style="margin-right: 8px">
<Power />
</n-icon>
<span>{{ $t("开机自启动") }}</span>
</span>
<n-switch v-model:value="app_store.config.auto_start" />
</n-space>
</template>

+ 77
- 0
wavecontrol-test/src/components/CircleProgress.vue View File

@ -0,0 +1,77 @@
<template>
<div class="circle-progress">
<svg :width="size" :height="size" viewBox="0 0 100 100">
<!-- 背景圆环 -->
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke="backgroundColor"
:stroke-width="strokeWidth"
/>
<!-- 进度圆环 -->
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke="color"
:stroke-width="strokeWidth"
:stroke-dasharray="circumference"
:stroke-dashoffset="dashOffset"
class="progress"
/>
<!-- 中心文本 -->
<slot>
<text x="50" y="50" text-anchor="middle" dominant-baseline="middle" class="progress-text">
{{ text }}
{{ percentage }}%
</text>
</slot>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
percentage: number;
size?: number;
strokeWidth?: number;
color?: string;
backgroundColor?: string;
text?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: 100,
strokeWidth: 6,
color: '#409eff',
backgroundColor: '#e5e9f2',
text: ''
});
const radius = computed(() => 50 - props.strokeWidth / 2);
const circumference = computed(() => 2 * Math.PI * radius.value);
const dashOffset = computed(() =>
circumference.value * (1 - props.percentage / 100)
);
</script>
<style lang="scss" scoped>
.circle-progress {
display: inline-block;
.progress {
transform: rotate(-90deg);
transform-origin: center;
}
.progress-text {
font-size: 14px;
fill: #606266;
}
}
</style>

+ 82
- 0
wavecontrol-test/src/components/GestureCard.vue View File

@ -0,0 +1,82 @@
<template>
<n-card class="gesture-card" :bordered="false">
<n-space align="center" class="gesture-content">
<div class="gesture-icon" :class="{ 'double-hand': isDoubleHand }">
<slot name="icon"></slot>
</div>
<div class="gesture-info">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<slot name="extra"></slot>
</div>
</n-space>
</n-card>
</template>
<script setup lang="ts">
defineProps<{
title: string;
description: string;
isDoubleHand?: boolean;
}>();
</script>
<style scoped lang="scss">
.gesture-card {
transition: all 0.3s ease;
background: linear-gradient(145deg, #f8faff, #ffffff);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e9f2;
border-radius: 12px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(64, 152, 252, 0.15);
border-color: #4098fc;
}
}
.gesture-content {
padding: 12px;
}
.gesture-icon {
background: rgba(64, 152, 252, 0.1);
padding: 16px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(64, 152, 252, 0.2);
}
.gesture-info {
flex: 1;
h3 {
margin: 0 0 4px 0;
font-size: 1.1rem;
color: #2c3e50;
}
p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
}
.double-hand {
display: flex;
gap: 8px;
:deep(svg) {
width: 32px;
height: 32px;
}
}
.flipped {
transform: scaleX(-1);
}
</style>

+ 23
- 0
wavecontrol-test/src/components/GestureIcon.vue View File

@ -0,0 +1,23 @@
<template>
<component
:is="icon"
theme="outline"
size="40"
fill="#4098fc"
:stroke-width="3"
:class="{ flipped }"
/>
</template>
<script setup lang="ts">
defineProps<{
icon: any;
flipped?: boolean;
}>();
</script>
<style scoped>
.flipped {
transform: scaleX(-1);
}
</style>

+ 41
- 0
wavecontrol-test/src/components/Menu.vue View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { MenuOption } from "naive-ui";
import { NMenu } from "naive-ui";
import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const menuOptions: MenuOption[] = [
{
label: "首页",
key: "/",
},
{
label: "操作指南",
key: "/guide",
},
// {
// label: "",
// key: "/update",
// }
];
const activeKey = ref("/");
const handleUpdateValue = (key: string) => {
activeKey.value = key;
router.push(key);
};
</script>
<template>
<n-menu
:options="menuOptions"
v-model:value="activeKey"
mode="vertical"
@update:value="handleUpdateValue"
/>
</template>
<style scoped></style>

+ 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",
// 通知
"Lazyeat": "Lazyeat",
"提示": "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",
"可以通过右键->检查->控制台->捏合手势->查看当前距离": "可以通过右键->检查->控制台->捏合手势->查看当前距离",
// 通知
"Lazyeat": "Lazyeat",
"提示": "提示",
"停止语音识别": "停止语音识别",
"手势识别": "手势识别",
"继续手势识别": "继续手势识别",
"暂停手势识别": "暂停手势识别",
};

+ 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;

+ 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>

Loading…
Cancel
Save