chore: 初始化项目基础结构和资源文件
- 添加项目图标文件(app-icon.png、各平台图标) - 配置开发环境文件(.env、.nvmrc、.npmrc) - 添加静态资源文件(背景图片、字体、音频) - 初始化Tauri后端结构(build.rs、main.rs、模块文件) - 配置前端项目结构(TypeScript、Vue组件、样式) - 添加Node.js API服务基础结构 - 配置构建和开发工具(vite、prettier、gitignore)
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
7102
src-tauri/Cargo.lock
generated
Normal file
114
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,114 @@
|
||||
[package]
|
||||
name = "zhipinghuiyi"
|
||||
version = "0.1.0"
|
||||
description = "树人教育研究院旗下智评会议APP"
|
||||
authors = ["mxp131011@qq.com"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.94.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "zhipinghuiyi_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.5", features = [] }
|
||||
|
||||
[features]
|
||||
default = ["production"]
|
||||
development = []
|
||||
production = []
|
||||
prod_150_8080 = []
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.149", features = ["raw_value"] }
|
||||
tauri-plugin-persisted-scope = "2.3.5"
|
||||
log = "0.4.29"
|
||||
tauri = { version = "2.10.2", features = [
|
||||
"macos-private-api",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
"devtools",
|
||||
] }
|
||||
tauri-plugin-os = "2.3.2"
|
||||
tauri-plugin-dialog = "2.6.0"
|
||||
tauri-plugin-fs = "2.4.5"
|
||||
tauri-plugin-http = "2.5.7"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-websocket = "2.4.2"
|
||||
rodio = "0.22.1"
|
||||
lazy_static = "1.5.0"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
parking_lot = "0.12.5"
|
||||
once_cell = "1.21.3"
|
||||
tokio-cron-scheduler = "0.15.1"
|
||||
tracing = "0.1.44"
|
||||
thiserror = "2.0.18"
|
||||
bytes = "1.11.1"
|
||||
|
||||
# Linux 平台依赖:获取工作区尺寸
|
||||
# [target.'cfg(target_os = "linux")'.dependencies]
|
||||
# x11-dl = "2.21"
|
||||
|
||||
# macOS 平台依赖
|
||||
# [target.'cfg(target_os = "macos")'.dependencies]
|
||||
# objc2 = "0.6"
|
||||
# objc2-foundation = "0.3"
|
||||
# objc2-app-kit = "0.3"
|
||||
|
||||
# Windows 平台依赖
|
||||
# [target.'cfg(target_os = "windows")'.dependencies]
|
||||
# windows = { version = "0.58", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
tauri-plugin-single-instance = "2.4.0"
|
||||
|
||||
|
||||
# ==================== 开发环境配置 ====================
|
||||
# 优化策略:保持 opt-level = 3 的同时加快编译速度
|
||||
[profile.dev]
|
||||
opt-level = 3 # 必须保持为 3
|
||||
debug = 1 # 保留最小调试信息(line tables only),加速链接
|
||||
strip = "none" # 开发时不剥离任何信息
|
||||
debug-assertions = true # 开启调试断言,帮助发现bug
|
||||
overflow-checks = true # 开启溢出检查
|
||||
lto = false # 关闭 LTO,大幅加快编译
|
||||
codegen-units = 256 # 增加并行编译单元,加快编译(牺牲一些运行性能)
|
||||
panic = 'unwind' # 使用 unwind 以便调试
|
||||
incremental = true # 启用增量编译,大幅加快重复编译
|
||||
rpath = false
|
||||
|
||||
# 构建依赖优化(proc-macro, build-script)
|
||||
[profile.dev.build-override]
|
||||
opt-level = 0 # 构建脚本不需要优化,加快编译
|
||||
debug = 0 # 构建脚本不需要调试信息
|
||||
codegen-units = 256 # 最大化并行
|
||||
incremental = true
|
||||
|
||||
# 为特定慢编译的 crate 启用优化
|
||||
# 这些 crate 编译很慢,启用优化可以显著加速编译
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3 # 依赖库全优化
|
||||
|
||||
|
||||
# ==================== 生产环境配置 ====================
|
||||
# 性能优先 + 安全优先
|
||||
[profile.release]
|
||||
opt-level = 3 # 最高优化级别(性能优先)
|
||||
debug = false # 不包含调试信息
|
||||
strip = "symbols" # 剥离所有符号(包括调试符号和函数名),减小体积,提升安全性
|
||||
debug-assertions = false # 关闭调试断言
|
||||
overflow-checks = false # 关闭溢出检查(性能优先)
|
||||
lto = "fat" # 完整 LTO(链接时优化),最佳性能
|
||||
codegen-units = 1 # 单个代码生成单元,最佳优化
|
||||
panic = 'abort' # panic 时直接中止,减小体积和提升性能
|
||||
rpath = false # 不使用 rpath(安全考虑)
|
||||
incremental = false # 禁用增量编译
|
||||
|
||||
36
src-tauri/Entitlements.plist
Normal file
@ -0,0 +1,36 @@
|
||||
<?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>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
<!-- 音频输入权限 -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<!-- 相机权限 -->
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<!-- 文件读写权限 -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<!-- 网络客户端权限 -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<!-- 网络服务端权限 -->
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<!-- 蓝牙权限 -->
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<!-- 本地通知权限 -->
|
||||
<key>com.apple.security.notifications</key>
|
||||
<true/>
|
||||
<!-- 自动化苹果事件权限
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/> -->
|
||||
<!-- 应用沙盒 -->
|
||||
<!-- <key>com.apple.security.app-sandbox</key>
|
||||
<true/> -->
|
||||
</dict>
|
||||
</plist>
|
||||
56
src-tauri/Info.plist
Normal file
@ -0,0 +1,56 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>zh_CN</string>
|
||||
|
||||
|
||||
<!-- 屏幕捕获使用描述,告知用户为何需要此权限 -->
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>智评会议需要访问您的屏幕以进行屏幕共享</string>
|
||||
|
||||
<!-- 本地化语言支持列表 -->
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
|
||||
<!-- Apple语言设置 -->
|
||||
<key>NSAppleLanguages</key>
|
||||
<array>
|
||||
<string>zh-Hans-CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
|
||||
<!-- 麦克风权限说明 -->
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问您的麦克风,用于语音输入和通话功能。</string>
|
||||
|
||||
<!-- 相机权限说明 -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问您的相机,用于拍照和视频通话功能。</string>
|
||||
|
||||
<!-- 照片库权限说明 -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问您的照片库,用于上传和保存图片。</string>
|
||||
|
||||
<!-- 文档文件夹访问权限说明 -->
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string>需要访问您的文档文件夹,用于文件的读取和保存。</string>
|
||||
|
||||
<!-- 蓝牙权限说明 -->
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>需要访问蓝牙,用于连接蓝牙设备。</string>
|
||||
|
||||
<!-- 本地网络权限说明 -->
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>需要访问本地网络,用于设备间通信。</string>
|
||||
|
||||
<!-- 用户通知权限说明 -->
|
||||
<key>NSUserNotificationUsageDescription</key>
|
||||
<string>需要发送通知,用于消息提醒。</string>
|
||||
</dict>
|
||||
</plist>
|
||||
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
30
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "../gen/schemas-zh/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": ["main", "meeting", "meeting2", "popup", "float-list"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-create",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-is-resizable",
|
||||
"core:window:allow-set-position",
|
||||
"core:webview:allow-create-webview",
|
||||
"core:webview:allow-create-webview-window",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:window:allow-set-focus"
|
||||
]
|
||||
}
|
||||
27
src-tauri/capabilities/desktop.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "../gen/schemas-zh/desktop-schema.json",
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main", "meeting", "meeting2", "popup", "float-list"],
|
||||
"permissions": [
|
||||
"dialog:default",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"process:allow-exit",
|
||||
"fs:default",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-download-write",
|
||||
"websocket:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "http://*:*"
|
||||
},
|
||||
{
|
||||
"url": "https://*:*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
6044
src-tauri/gen/schemas-zh/acl-manifests.json
Normal file
61
src-tauri/gen/schemas-zh/capabilities.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"default": {
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"local": true,
|
||||
"windows": ["main", "meeting", "drawing-board", "popup", "float-list", "draw-board-tools"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-create",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-is-resizable",
|
||||
"core:window:allow-set-position",
|
||||
"core:webview:allow-create-webview",
|
||||
"core:webview:allow-create-webview-window",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:window:allow-set-focus"
|
||||
]
|
||||
},
|
||||
"desktop-capability": {
|
||||
"identifier": "desktop-capability",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": ["main", "meeting", "drawing-board", "popup", "float-list", "draw-board-tools"],
|
||||
"permissions": [
|
||||
"dialog:default",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"window-state:default",
|
||||
"process:allow-exit",
|
||||
"fs:default",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-download-write",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "http://*:*"
|
||||
},
|
||||
{
|
||||
"url": "https://*:*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"websocket:default"
|
||||
],
|
||||
"platforms": ["macOS", "windows", "linux"]
|
||||
}
|
||||
}
|
||||
3295
src-tauri/gen/schemas-zh/config.json
Normal file
4902
src-tauri/gen/schemas-zh/desktop-schema.json
Normal file
6324
src-tauri/gen/schemas-zh/windows-schema.json
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 95 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/resources/audio/click_prompt_tone.wav
Normal file
5
src-tauri/resources/icons/btn_close.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#795548" stroke="#5D4037" stroke-width="2"/>
|
||||
<line x1="10" y1="10" x2="22" y2="22" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="22" y1="10" x2="10" y2="22" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
8
src-tauri/resources/icons/btn_color.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#2196F3" stroke="#1976D2" stroke-width="2"/>
|
||||
<circle cx="12" cy="12" r="4" fill="#F44336"/>
|
||||
<circle cx="20" cy="12" r="4" fill="#4CAF50"/>
|
||||
<circle cx="16" cy="20" r="4" fill="#FFEB3B"/>
|
||||
<circle cx="12" cy="16" r="2.5" fill="#9C27B0"/>
|
||||
<circle cx="20" cy="16" r="2.5" fill="#00BCD4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
6
src-tauri/resources/icons/btn_eraser.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#F44336" stroke="#C62828" stroke-width="2"/>
|
||||
<rect x="8" y="10" width="16" height="12" rx="2" fill="white" stroke="#333" stroke-width="1.5"/>
|
||||
<rect x="10" y="22" width="12" height="3" fill="#DDD" stroke="#999" stroke-width="0.5"/>
|
||||
<line x1="11" y1="13" x2="21" y2="19" stroke="#999" stroke-width="1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 461 B |
5
src-tauri/resources/icons/btn_pen.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#4CAF50" stroke="#2E7D32" stroke-width="2"/>
|
||||
<path d="M8 24 L14 10 L18 18 L24 8 L26 12 L18 26 Z" fill="white" stroke="#333" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<circle cx="10" cy="22" r="1.5" fill="#333"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
7
src-tauri/resources/icons/btn_shape.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#FF9800" stroke="#F57C00" stroke-width="2"/>
|
||||
<rect x="8" y="12" width="6" height="6" fill="white" stroke="#333" stroke-width="1"/>
|
||||
<line x1="17" y1="15" x2="23" y2="15" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="20" r="3" fill="none" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M12 22 L14 26 L18 26" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
6
src-tauri/resources/icons/btn_width.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#9C27B0" stroke="#7B1FA2" stroke-width="2"/>
|
||||
<line x1="8" y1="16" x2="24" y2="16" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="10" y1="16" x2="22" y2="16" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="12" y1="16" x2="20" y2="16" stroke="white" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 465 B |
31
src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,31 @@
|
||||
# 最大行宽,超过此宽度将换行
|
||||
max_width = 160
|
||||
# 是否使用硬制表符,false表示使用空格
|
||||
hard_tabs = false
|
||||
# 制表符的空格数
|
||||
tab_spaces = 4
|
||||
# 换行符风格,"Auto"表示自动检测
|
||||
newline_style = "Auto"
|
||||
# 启发式换行策略,"Default"表示使用默认策略
|
||||
use_small_heuristics = "Default"
|
||||
# 是否重新排序导入语句
|
||||
reorder_imports = true
|
||||
# 是否重新排序模块
|
||||
reorder_modules = true
|
||||
# 是否删除嵌套括号
|
||||
remove_nested_parens = true
|
||||
# Rust版本,影响格式化规则
|
||||
edition = "2024"
|
||||
# 是否合并派生宏
|
||||
merge_derives = true
|
||||
# 是否将 Result<T, E> 转换为 ? 操作符
|
||||
use_try_shorthand = true
|
||||
# 是否使用字段初始化简写
|
||||
use_field_init_shorthand = false
|
||||
# 是否强制显式 ABI
|
||||
force_explicit_abi = true
|
||||
# 导入粒度,"Crate"表示按 crate 分组
|
||||
imports_granularity = "Crate"
|
||||
# 删除行尾的多余空格
|
||||
remove_trailing_whitespace = true
|
||||
|
||||
27
src-tauri/src/app/commands.rs
Normal file
@ -0,0 +1,27 @@
|
||||
//! 应用相关的 Tauri 命令
|
||||
|
||||
use tauri::{command, AppHandle, Runtime};
|
||||
|
||||
/// 获取应用版本
|
||||
#[command]
|
||||
pub fn get_app_version<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.package_info().version.to_string()
|
||||
}
|
||||
|
||||
/// 获取 Tauri 版本
|
||||
#[command]
|
||||
pub fn get_tauri_version() -> &'static str {
|
||||
tauri::VERSION
|
||||
}
|
||||
|
||||
/// 获取应用名称
|
||||
#[command]
|
||||
pub fn get_app_name<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.package_info().name.clone()
|
||||
}
|
||||
|
||||
/// 重启应用程序
|
||||
#[command]
|
||||
pub fn restart_application<R: Runtime>(app: AppHandle<R>) {
|
||||
app.restart();
|
||||
}
|
||||
18
src-tauri/src/app/initializer.rs
Normal file
@ -0,0 +1,18 @@
|
||||
/// 初始化应用
|
||||
///
|
||||
/// 这个函数在应用启动时被调用,负责设置应用的初始状态
|
||||
///
|
||||
/// 注意: SessionStorage 和托盘图标已经在 lib.rs 中通过 .manage() 和 setup() 初始化了,
|
||||
/// 所以这里不需要重复注册
|
||||
pub fn initialize_app<R: tauri::Runtime>(_app: &tauri::AppHandle<R>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 创建并设置应用菜单
|
||||
// 只在 macOS 上有意义,因为 macOS 的应用菜单是独立的
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::menu::create_app_menu;
|
||||
let app_menu = create_app_menu(_app)?;
|
||||
_app.set_menu(app_menu)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
2
src-tauri/src/app/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod commands;
|
||||
pub mod initializer;
|
||||
15
src-tauri/src/audio/commands.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use crate::audio::{play_prompt_tone_imp, stop_prompt_tone_imp};
|
||||
use tauri::command;
|
||||
|
||||
/// 播放提示音的Tauri命令
|
||||
#[command]
|
||||
pub fn play_prompt_tone() {
|
||||
play_prompt_tone_imp();
|
||||
}
|
||||
|
||||
/// 停止音频播放的Tauri命令
|
||||
#[command]
|
||||
#[allow(dead_code)] // 允许未使用函数,保留以备将来使用
|
||||
pub fn stop_prompt_tone() {
|
||||
stop_prompt_tone_imp();
|
||||
}
|
||||
138
src-tauri/src/audio/mod.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::constants::common::*;
|
||||
use lazy_static::lazy_static;
|
||||
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
// 导出commands子模块
|
||||
pub mod commands;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AudioCommand {
|
||||
/// ⚡ 使用 Arc 共享音频数据,避免克隆
|
||||
PlayPromptTone(Arc<Vec<u8>>),
|
||||
Stop,
|
||||
}
|
||||
|
||||
// 预加载的提示音数据 - 使用 Arc 避免克隆
|
||||
lazy_static! {
|
||||
static ref PROMPT_TONE_DATA: Arc<Vec<u8>> = {
|
||||
// 读取提示音文件
|
||||
let resource_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join(AUDIO_PROMPT_FILE_PATH);
|
||||
Arc::new(
|
||||
File::open(&resource_path)
|
||||
.ok()
|
||||
.and_then(|mut file| {
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data).ok().map(|_| data)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::error!("无法读取提示音文件 {:?}", resource_path);
|
||||
Vec::new()
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
static ref AUDIO_SYSTEM: AudioSystem = AudioSystem::new();
|
||||
}
|
||||
|
||||
struct AudioResources {
|
||||
stream: MixerDeviceSink,
|
||||
}
|
||||
|
||||
impl AudioResources {
|
||||
fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// 使用 rodio 0.22 API - DeviceSinkBuilder::open_default_sink()
|
||||
let stream = DeviceSinkBuilder::open_default_sink()?;
|
||||
|
||||
Ok(Self { stream })
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioSystem {
|
||||
tx: mpsc::Sender<AudioCommand>,
|
||||
}
|
||||
|
||||
impl AudioSystem {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
thread::spawn(move || Self::audio_worker(rx));
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
fn audio_worker(rx: mpsc::Receiver<AudioCommand>) {
|
||||
let audio_resources = match AudioResources::new() {
|
||||
Ok(resources) => resources,
|
||||
Err(e) => {
|
||||
log::error!("音频系统初始化失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 通过 mixer 创建 Player - rodio 0.22 API
|
||||
let sink = Player::connect_new(audio_resources.stream.mixer());
|
||||
|
||||
while let Ok(cmd) = rx.recv() {
|
||||
match cmd {
|
||||
AudioCommand::PlayPromptTone(data) => {
|
||||
// 检查音频数据是否为空
|
||||
if data.is_empty() {
|
||||
log::warn!("提示音数据为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 停止当前播放并清空队列
|
||||
sink.stop();
|
||||
sink.clear();
|
||||
|
||||
// ⚡ 在 worker 线程中克隆数据(只在播放时克隆一次,避免在主线程克隆)
|
||||
let data_vec = (*data).clone();
|
||||
|
||||
// 解码音频数据
|
||||
match Decoder::new(Cursor::new(data_vec)) {
|
||||
Ok(source) => {
|
||||
// 设置音量并播放
|
||||
let source = source.amplify(AUDIO_VOLUME_AMPLIFICATION);
|
||||
sink.append(source);
|
||||
sink.play();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("音频解码失败: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
AudioCommand::Stop => {
|
||||
sink.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 线程结束时确保停止播放
|
||||
sink.stop();
|
||||
}
|
||||
|
||||
fn play_prompt_tone(&self) {
|
||||
// ⚡ 直接克隆 Arc 指针,不再拷贝音频数据
|
||||
let _ = self.tx.send(AudioCommand::PlayPromptTone(PROMPT_TONE_DATA.clone()));
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // 允许未使用的方法,保留以备将来使用
|
||||
fn stop(&self) {
|
||||
let _ = self.tx.send(AudioCommand::Stop);
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放提示音
|
||||
pub fn play_prompt_tone_imp() {
|
||||
AUDIO_SYSTEM.play_prompt_tone();
|
||||
}
|
||||
|
||||
/// 停止音频播放
|
||||
pub fn stop_prompt_tone_imp() {
|
||||
AUDIO_SYSTEM.stop();
|
||||
}
|
||||
55
src-tauri/src/constants/common.rs
Normal file
@ -0,0 +1,55 @@
|
||||
// ============ 应用程序基本信息常量 ============
|
||||
|
||||
/// 应用程序名称
|
||||
pub const APP_NAME: &str = "智评会议";
|
||||
|
||||
// ============ 音频常量 ============
|
||||
|
||||
/// 音频音量放大倍数
|
||||
pub const AUDIO_VOLUME_AMPLIFICATION: f32 = 0.2;
|
||||
|
||||
/// 提示音文件路径
|
||||
pub const AUDIO_PROMPT_FILE_PATH: &str = "resources/audio/click_prompt_tone.wav";
|
||||
|
||||
// ============ 缩放常量 ============
|
||||
|
||||
/// 默认缩放因子(100%)
|
||||
pub const DEFAULT_SCALE_FACTOR: f64 = 1.0;
|
||||
|
||||
/// 放大倍数(+5%)
|
||||
pub const ZOOM_IN_MULTIPLIER: f64 = 1.05;
|
||||
|
||||
/// 缩小倍数(-5%)
|
||||
pub const ZOOM_OUT_MULTIPLIER: f64 = 0.95;
|
||||
|
||||
/// 最大缩放级别(200%)
|
||||
pub const MAX_ZOOM_LEVEL: f64 = 2.0;
|
||||
|
||||
/// 最小缩放级别(80%)
|
||||
pub const MIN_ZOOM_LEVEL: f64 = 0.8;
|
||||
|
||||
// ============ 应用程序消息常量 ============
|
||||
|
||||
/// 主窗口不存在的警告消息
|
||||
pub const MSG_MAIN_WINDOW_NOT_EXIST: &str = "主窗口不存在,无法设置焦点";
|
||||
|
||||
/// 启动定时重启任务失败消息
|
||||
pub const MSG_FAILED_START_RESTART_TASK: &str = "启动定时重启任务失败: {}";
|
||||
|
||||
/// 定时重启任务已启动消息
|
||||
pub const MSG_RESTART_TASK_STARTED: &str = "定时重启任务(凌晨1点)已启动";
|
||||
|
||||
/// 构建Tauri应用失败消息
|
||||
pub const MSG_FAILED_BUILD_TAURI: &str = "构建 Tauri 应用程序时出现错误";
|
||||
|
||||
/// 创建悬浮球窗口失败消息
|
||||
pub const MSG_FAILED_CREATE_FLOATING_LIST: &str = "创建悬浮球窗口失败: {}";
|
||||
|
||||
/// 创建会议窗口失败消息
|
||||
pub const MSG_FAILED_CREATE_MEETING_WINDOW: &str = "创建会议窗口失败: {}";
|
||||
|
||||
/// 类型2会议窗口需要CourseID的错误消息
|
||||
pub const MSG_MEETING_TYPE_2_NEEDS_COURSE_ID: &str = "类型2的会议窗口需要提供CourseID";
|
||||
|
||||
/// 不支持的会议窗口类型错误消息
|
||||
pub const MSG_UNSUPPORTED_MEETING_WINDOW_TYPE: &str = "不支持的会议窗口类型";
|
||||
3
src-tauri/src/constants/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
// 常量模块
|
||||
pub mod common;
|
||||
pub mod window;
|
||||
56
src-tauri/src/constants/window.rs
Normal file
@ -0,0 +1,56 @@
|
||||
// ============ 窗口尺寸常量 ============
|
||||
|
||||
/// 主窗口默认宽度(像素)
|
||||
pub const MAIN_WINDOW_WIDTH: f64 = 1200.0;
|
||||
|
||||
/// 主窗口默认高度(像素)
|
||||
pub const MAIN_WINDOW_HEIGHT: f64 = 700.0;
|
||||
|
||||
/// 类型1会议窗口宽度(像素)
|
||||
pub const MEETING_TYPE_1_WIDTH: f64 = 800.0;
|
||||
|
||||
/// 类型1会议窗口高度(像素)
|
||||
pub const MEETING_TYPE_1_HEIGHT: f64 = 600.0;
|
||||
|
||||
/// 类型2会议窗口宽度(像素)
|
||||
pub const MEETING_TYPE_2_WIDTH: f64 = 1400.0;
|
||||
|
||||
/// 类型2会议窗口高度(像素)
|
||||
pub const MEETING_TYPE_2_HEIGHT: f64 = 800.0;
|
||||
|
||||
// ============ 弹出窗口常量 ============
|
||||
|
||||
/// 窗口圆角半径
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
pub const WINDOW_CORNER_RADIUS: f64 = 8.0;
|
||||
|
||||
// ============ 放置位置常量 ============
|
||||
|
||||
/// 居中放置
|
||||
pub const PLACEMENT_CENTER: &str = "center";
|
||||
|
||||
/// 自动放置
|
||||
pub const PLACEMENT_AUTO: &str = "auto";
|
||||
|
||||
/// 顶部放置
|
||||
pub const PLACEMENT_TOP: &str = "top";
|
||||
|
||||
/// 底部放置
|
||||
pub const PLACEMENT_BOTTOM: &str = "bottom";
|
||||
|
||||
/// 左侧放置
|
||||
pub const PLACEMENT_LEFT: &str = "left";
|
||||
|
||||
/// 右侧放置
|
||||
pub const PLACEMENT_RIGHT: &str = "right";
|
||||
|
||||
// ============ 存储键常量 ============
|
||||
|
||||
/// 用户信息存储键
|
||||
pub const STORAGE_KEY_USER_INFO: &str = "tauri_store_page_user_info";
|
||||
|
||||
/// 辅导信息存储键
|
||||
pub const STORAGE_KEY_TUTORSHIP: &str = "tauri_store_page_curr_tutorship";
|
||||
|
||||
/// 课程信息存储键
|
||||
pub const STORAGE_KEY_COURSE: &str = "tauri_store_page_curr_course";
|
||||
11
src-tauri/src/env/mod.rs
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
//! 环境配置模块
|
||||
// const TEACHERCONTROL_DEV_URL: &str = "https://jk.qyzhjy.com/TeacherControl_bak.shtml";
|
||||
const TEACHERCONTROL_DEV_URL: &str = "https://socket.qyzhjy.com/JKList/TeacherControl_bak.shtml";
|
||||
const TEACHERCONTROL_PROD_URL: &str = "https://jk.qyzhjy.com/TeacherControl_bak.shtml";
|
||||
|
||||
/// 实时字幕地址
|
||||
pub const TEACHERCONTROL_JK_URL: &str = if cfg!(feature = "development") || cfg!(feature = "prod_150_8080") {
|
||||
TEACHERCONTROL_DEV_URL
|
||||
} else {
|
||||
TEACHERCONTROL_PROD_URL
|
||||
};
|
||||
227
src-tauri/src/error/mod.rs
Normal file
@ -0,0 +1,227 @@
|
||||
//! 错误处理模块
|
||||
//!
|
||||
//! 定义了应用中用到的所有错误类型,支持错误链追踪和上下文信息
|
||||
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
/// 应用统一错误类型
|
||||
///
|
||||
/// 这个枚举涵盖了应用可能遇到的各种错误情况
|
||||
/// 使用 thiserror 提供结构化的错误处理和错误链追踪功能
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
/// WebSocket相关错误
|
||||
#[error("WebSocket错误: {message}")]
|
||||
WebSocket {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
/// 序列化错误
|
||||
#[error("序列化错误: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
/// IO错误
|
||||
#[error("IO错误: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// 网络错误
|
||||
#[error("网络错误: {message}")]
|
||||
Network {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
/// 窗口错误
|
||||
#[error("窗口错误: {message}")]
|
||||
Window {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
/// 音频错误
|
||||
#[error("音频错误: {message}")]
|
||||
Audio {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
/// 存储错误
|
||||
#[error("存储错误: {message}")]
|
||||
Storage {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
/// 验证错误
|
||||
#[error("验证错误: {0}")]
|
||||
Validation(String),
|
||||
|
||||
/// 未找到错误
|
||||
#[error("未找到: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// 状态错误
|
||||
#[error("状态错误: {0}")]
|
||||
InvalidState(String),
|
||||
|
||||
/// 超时错误
|
||||
#[error("操作超时: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
/// 取消错误
|
||||
#[error("操作已取消: {0}")]
|
||||
Canceled(String),
|
||||
|
||||
/// 其他错误
|
||||
#[error("错误: {message}")]
|
||||
Other {
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
/// 创建一个 WebSocket 错误
|
||||
pub fn websocket<M: Into<String>>(message: M) -> Self {
|
||||
AppError::WebSocket {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个带原始错误的 WebSocket 错误
|
||||
pub fn websocket_with_source<M, E>(message: M, source: E) -> Self
|
||||
where
|
||||
M: Into<String>,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
AppError::WebSocket {
|
||||
message: message.into(),
|
||||
source: Some(Arc::new(source)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个网络错误
|
||||
pub fn network<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Network {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个带原始错误的网络错误
|
||||
pub fn network_with_source<M, E>(message: M, source: E) -> Self
|
||||
where
|
||||
M: Into<String>,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
AppError::Network {
|
||||
message: message.into(),
|
||||
source: Some(Arc::new(source)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个窗口错误
|
||||
pub fn window<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Window {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个音频错误
|
||||
pub fn audio<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Audio {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个存储错误
|
||||
pub fn storage<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Storage {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建一个验证错误
|
||||
pub fn validation<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Validation(message.into())
|
||||
}
|
||||
|
||||
/// 创建一个未找到错误
|
||||
pub fn not_found<M: Into<String>>(message: M) -> Self {
|
||||
AppError::NotFound(message.into())
|
||||
}
|
||||
|
||||
/// 创建一个状态错误
|
||||
pub fn invalid_state<M: Into<String>>(message: M) -> Self {
|
||||
AppError::InvalidState(message.into())
|
||||
}
|
||||
|
||||
/// 创建一个超时错误
|
||||
pub fn timeout<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Timeout(message.into())
|
||||
}
|
||||
|
||||
/// 创建一个取消错误
|
||||
pub fn canceled<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Canceled(message.into())
|
||||
}
|
||||
|
||||
/// 创建一个通用错误
|
||||
pub fn other<M: Into<String>>(message: M) -> Self {
|
||||
AppError::Other {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 给错误添加额外的上下文信息
|
||||
pub fn context<M: Into<String>>(self, message: M) -> Self {
|
||||
let msg = format!("{}: {}", message.into(), &self);
|
||||
match self {
|
||||
AppError::WebSocket { .. } => AppError::websocket(msg),
|
||||
AppError::Network { .. } => AppError::network(msg),
|
||||
AppError::Window { .. } => AppError::window(msg),
|
||||
AppError::Audio { .. } => AppError::audio(msg),
|
||||
AppError::Storage { .. } => AppError::storage(msg),
|
||||
_ => AppError::other(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 AppError 转换成字符串
|
||||
/// 主要用于 Tauri 命令返回值
|
||||
impl From<AppError> for String {
|
||||
fn from(error: AppError) -> Self {
|
||||
error.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// 从字符串创建 AppError
|
||||
/// 为了兼容旧代码保留这个转换
|
||||
impl From<String> for AppError {
|
||||
fn from(message: String) -> Self {
|
||||
AppError::other(message)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从字符串切片创建 AppError
|
||||
/// 为了兼容旧代码保留这个转换
|
||||
impl From<&str> for AppError {
|
||||
fn from(message: &str) -> Self {
|
||||
AppError::other(message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
src-tauri/src/invoke/handlers.rs
Normal file
@ -0,0 +1,46 @@
|
||||
//! Tauri 命令处理器
|
||||
//! 集中管理所有 Tauri 命令的注册和处理
|
||||
//!
|
||||
use crate::storage::session_storage::*;
|
||||
|
||||
use crate::app::commands::*;
|
||||
use crate::audio::commands::*;
|
||||
use crate::tray::commands::*;
|
||||
use crate::window::commands::*;
|
||||
use tauri::Builder;
|
||||
|
||||
/// 注册所有 Tauri 命令
|
||||
///
|
||||
/// 将所有可用的 Tauri 命令注册到应用构建器中
|
||||
///
|
||||
/// # 参数
|
||||
/// * `builder` - Tauri 应用构建器
|
||||
///
|
||||
/// # 返回值
|
||||
/// 返回配置了所有命令的应用构建器
|
||||
pub fn register_commands(builder: Builder<tauri::Wry>) -> Builder<tauri::Wry> {
|
||||
builder.invoke_handler(tauri::generate_handler![
|
||||
// 会话存储相关命令
|
||||
session_storage_get,
|
||||
session_storage_set,
|
||||
session_storage_remove,
|
||||
session_storage_clear,
|
||||
session_storage_keys,
|
||||
session_storage_has,
|
||||
// 应用相关命令
|
||||
get_app_version,
|
||||
get_tauri_version,
|
||||
get_app_name,
|
||||
restart_application,
|
||||
// 音频相关命令
|
||||
play_prompt_tone,
|
||||
stop_prompt_tone,
|
||||
// 窗口相关命令
|
||||
create_float_list_window,
|
||||
create_popup_window,
|
||||
create_main_window,
|
||||
create_meeting_window,
|
||||
// 系统托盘相关命令
|
||||
update_tray_menu,
|
||||
])
|
||||
}
|
||||
1
src-tauri/src/invoke/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod handlers;
|
||||
144
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,144 @@
|
||||
// 下面引入了应用的各个功能模块
|
||||
// app - 应用的初始化和启动相关逻辑
|
||||
// audio - 音频播放和声音处理
|
||||
// constants - 常量定义
|
||||
// env - 环境变量和配置读取
|
||||
// error - 错误类型和错误处理
|
||||
// invoke - Tauri 命令处理器,处理前端调用
|
||||
// menu - 应用菜单配置和事件处理
|
||||
// models - 公共数据结构定义
|
||||
// storage - 数据持久化存储
|
||||
// task - 定时任务和计划任务
|
||||
// tray - 系统托盘图标和菜单
|
||||
// utils - 通用工具函数
|
||||
// window - 窗口创建和管理
|
||||
mod app;
|
||||
mod audio;
|
||||
mod constants;
|
||||
mod env;
|
||||
mod error;
|
||||
mod invoke;
|
||||
mod menu;
|
||||
mod storage;
|
||||
mod task;
|
||||
mod tray;
|
||||
mod utils;
|
||||
pub mod window;
|
||||
|
||||
// 引入各个模块的类型和功能
|
||||
use crate::constants::common::*;
|
||||
use crate::window::{WindowLabel, util::show_windows_by_label};
|
||||
use app::initializer::initialize_app;
|
||||
use invoke::handlers::register_commands;
|
||||
use menu::menu_handler::handle_menu_event;
|
||||
use storage::session_storage::SessionStorage;
|
||||
use task::scheduled_restart::start_scheduled_restart_task;
|
||||
use tauri::Manager;
|
||||
use tauri::RunEvent;
|
||||
use tray::setup_tray;
|
||||
/// 移动平台入口点
|
||||
/// 在移动设备上运行时需要这个属性标记
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
// 下面加载各种 Tauri 插件
|
||||
// os 插件 - 获取操作系统信息
|
||||
.plugin(tauri_plugin_os::init())
|
||||
// dialog 插件 - 显示文件选择对话框等
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
// fs 插件 - 文件系统访问
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
// persisted_scope 插件 - 记住文件访问权限
|
||||
.plugin(tauri_plugin_persisted_scope::init())
|
||||
// process 插件 - 进程相关信息
|
||||
.plugin(tauri_plugin_process::init())
|
||||
// updater 插件 - 应用自动更新
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
// http 插件 - HTTP 客户端请求
|
||||
.plugin(tauri_plugin_http::init())
|
||||
// single_instance 插件 - 保证只启动一个应用实例
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
let app_handle = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let main_label = WindowLabel::MAIN.as_ref();
|
||||
if app_handle.get_webview_window(main_label).is_some() {
|
||||
show_windows_by_label(&app_handle, WindowLabel::MAIN);
|
||||
} else {
|
||||
log::warn!("{}", MSG_MAIN_WINDOW_NOT_EXIST);
|
||||
}
|
||||
});
|
||||
}))
|
||||
// 注册全局状态管理
|
||||
.manage(SessionStorage::new())
|
||||
// 下面是应用初始化和事件处理的设置
|
||||
// setup 函数会在应用构建完成后执行一次
|
||||
.setup(|app| {
|
||||
// 初始化应用,包括创建窗口、设置初始状态等
|
||||
initialize_app(app.handle())?;
|
||||
|
||||
// 创建系统托盘图标
|
||||
setup_tray(app.handle())?;
|
||||
|
||||
// 启动每日定时重启任务
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = start_scheduled_restart_task(app_handle).await {
|
||||
log::error!("{} {}", MSG_FAILED_START_RESTART_TASK, e);
|
||||
} else {
|
||||
log::info!("{}", MSG_RESTART_TASK_STARTED);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
// 处理菜单点击事件
|
||||
// 当用户点击菜单项时会触发这个回调
|
||||
.on_menu_event(|app, event| {
|
||||
handle_menu_event(app, event.id.as_ref());
|
||||
});
|
||||
|
||||
// 注册 Tauri 命令处理器
|
||||
// 这些命令会被前端通过 invoke 调用
|
||||
let builder = register_commands(builder);
|
||||
|
||||
let app = builder
|
||||
// 构建应用上下文
|
||||
.build(tauri::generate_context!())
|
||||
// 如果构建失败,输出错误信息并退出
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("{}: {:?}", MSG_FAILED_BUILD_TAURI, e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// 启动应用并处理系统事件
|
||||
app.run(|_app_handle: &tauri::AppHandle, event| {
|
||||
match event {
|
||||
// 应用退出时触发(无法阻止,确保一定执行)
|
||||
RunEvent::Exit => {
|
||||
log::info!("应用正在退出,执行资源清理...");
|
||||
}
|
||||
// 处理 macOS 上的 Reopen 事件
|
||||
// 当用户点击 Dock 图标时触发
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::Reopen { .. } => {
|
||||
// 获取主窗口
|
||||
if let Some(main_window) = _app_handle.get_webview_window(WindowLabel::MAIN.as_ref()) {
|
||||
// 如果窗口被最小化了,就恢复它
|
||||
if main_window.is_minimized().unwrap_or(false) && main_window.unminimize().is_err() {
|
||||
log::warn!("恢复主窗口失败");
|
||||
}
|
||||
|
||||
// 显示窗口并放到前台
|
||||
if let Err(e) = main_window.show() {
|
||||
log::warn!("显示主窗口失败: {:?}", e);
|
||||
}
|
||||
if let Err(e) = main_window.set_focus() {
|
||||
log::warn!("设置主窗口焦点失败: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal 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() {
|
||||
zhipinghuiyi_lib::run();
|
||||
}
|
||||
47
src-tauri/src/menu/app_menu.rs
Normal file
@ -0,0 +1,47 @@
|
||||
/// 创建应用菜单(中文)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn create_app_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> {
|
||||
use tauri::{
|
||||
image::Image,
|
||||
menu::{AboutMetadataBuilder, MenuBuilder, SubmenuBuilder},
|
||||
};
|
||||
|
||||
let default_name = "智评会议";
|
||||
let app_name = app.config().product_name.as_deref().unwrap_or(default_name);
|
||||
|
||||
// 创建视图菜单
|
||||
let view_submenu = SubmenuBuilder::new(app, "视图")
|
||||
.text("zoom_in", "放大")
|
||||
.text("zoom_out", "缩小")
|
||||
.text("zoom_reset", "实际大小")
|
||||
.separator()
|
||||
.text("refresh", "刷新页面")
|
||||
.build()?;
|
||||
|
||||
// 创建窗口菜单
|
||||
let window_submenu = SubmenuBuilder::new(app, "窗口")
|
||||
.minimize_with_text("最小化")
|
||||
.maximize_with_text("最大化")
|
||||
.separator()
|
||||
.close_window_with_text("关闭")
|
||||
.build()?;
|
||||
|
||||
// 应用菜单(跨平台)
|
||||
let about_metadata = AboutMetadataBuilder::new()
|
||||
.copyright(Some("Copyright © 2025 智评会议".to_string()))
|
||||
.authors(Some(vec!["重庆树人教育研究院".to_string()]))
|
||||
.website_label(Some("https://www.zpingketang.com".to_string()))
|
||||
.icon(Image::from_bytes(include_bytes!("../../icons/icon.png")).ok())
|
||||
.build();
|
||||
|
||||
let app_submenu = SubmenuBuilder::new(app, app_name)
|
||||
.about_with_text("关于应用", Some(about_metadata))
|
||||
.separator()
|
||||
.quit_with_text("退出应用程序")
|
||||
.build()?;
|
||||
|
||||
// 构建完整菜单
|
||||
let menu = MenuBuilder::new(app).items(&[&app_submenu, &view_submenu, &window_submenu]).build()?;
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
259
src-tauri/src/menu/menu_event.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use crate::constants::common::*;
|
||||
use crate::window::WindowLabel;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
// 使用lazy_static宏创建全局静态变量,用于存储各个窗口的缩放因子
|
||||
// parking_lot::Mutex 确保线程安全,性能更佳,不会返回 Result
|
||||
// HashMap存储窗口标签和对应的缩放因子
|
||||
lazy_static::lazy_static! {
|
||||
/// 全局窗口缩放因子映射表
|
||||
/// key: 窗口标签(WindowLabel)
|
||||
/// value: 缩放因子(f64)
|
||||
static ref WINDOW_SCALE_FACTORS: Mutex<HashMap<WindowLabel, f64>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// 获取指定窗口的当前缩放级别
|
||||
///
|
||||
/// # 参数
|
||||
/// * `window_label` - 窗口标签
|
||||
///
|
||||
/// # 返回值
|
||||
/// 返回窗口当前的缩放因子,默认为1.0(100%)
|
||||
fn get_window_scale(window_label: WindowLabel) -> f64 {
|
||||
WINDOW_SCALE_FACTORS.lock().get(&window_label).copied().unwrap_or(DEFAULT_SCALE_FACTOR)
|
||||
}
|
||||
|
||||
/// 设置指定窗口的缩放级别
|
||||
///
|
||||
/// # 参数
|
||||
/// * `window_label` - 窗口标签
|
||||
/// * `scale` - 新的缩放因子
|
||||
fn set_window_scale(window_label: WindowLabel, scale: f64) {
|
||||
let mut scales = WINDOW_SCALE_FACTORS.lock();
|
||||
scales.insert(window_label, scale);
|
||||
}
|
||||
|
||||
/// 处理窗口放大事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 1. 获取主窗口对象
|
||||
/// 2. 获取当前窗口缩放级别
|
||||
/// 3. 计算新的缩放级别(增加5%)
|
||||
/// 4. 限制最大缩放为2倍(200%)
|
||||
/// 5. 更新窗口缩放级别并应用到窗口
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_zoom_in<R: Runtime>(app: &AppHandle<R>) {
|
||||
// 获取标签为"main"的窗口对象
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
let window_label = WindowLabel::MAIN;
|
||||
|
||||
// 在异步运行时中执行缩放逻辑,避免阻塞主线程
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 获取当前窗口缩放级别
|
||||
let current_scale = get_window_scale(window_label);
|
||||
|
||||
// 计算新的缩放级别 (每次放大5%)
|
||||
let new_scale = current_scale * ZOOM_IN_MULTIPLIER;
|
||||
|
||||
// 限制最大缩放为2倍
|
||||
let clamped_scale = new_scale.min(MAX_ZOOM_LEVEL);
|
||||
|
||||
// 设置新的缩放级别
|
||||
set_window_scale(window_label, clamped_scale);
|
||||
|
||||
// 应用缩放到窗口
|
||||
if let Err(e) = window.set_zoom(clamped_scale) {
|
||||
log::error!("设置窗口缩放失败: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口缩小事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 1. 获取主窗口对象
|
||||
/// 2. 获取当前窗口缩放级别
|
||||
/// 3. 计算新的缩放级别(减少5%)
|
||||
/// 4. 限制最小缩放为0.8倍(80%)
|
||||
/// 5. 更新窗口缩放级别并应用到窗口
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_zoom_out<R: Runtime>(app: &AppHandle<R>) {
|
||||
// 获取标签为"main"的窗口对象
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
let window_label = WindowLabel::MAIN;
|
||||
|
||||
// 在异步运行时中执行缩放逻辑,避免阻塞主线程
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 获取当前窗口缩放级别
|
||||
let current_scale = get_window_scale(window_label);
|
||||
|
||||
// 计算新的缩放级别 (每次缩小5%)
|
||||
let new_scale = current_scale * ZOOM_OUT_MULTIPLIER;
|
||||
|
||||
// 限制最小缩放为0.8倍
|
||||
let clamped_scale = new_scale.max(MIN_ZOOM_LEVEL);
|
||||
|
||||
// 设置新的缩放级别
|
||||
set_window_scale(window_label, clamped_scale);
|
||||
|
||||
// 应用缩放到窗口
|
||||
if let Err(e) = window.set_zoom(clamped_scale) {
|
||||
log::error!("设置窗口缩放失败: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口重置缩放事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 1. 获取主窗口对象
|
||||
/// 2. 将窗口缩放级别重置为默认值(1.0)
|
||||
/// 3. 更新窗口缩放级别并应用到窗口
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_zoom_reset<R: Runtime>(app: &AppHandle<R>) {
|
||||
// 获取标签为"main"的窗口对象
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
let window_label = WindowLabel::MAIN;
|
||||
|
||||
// 在异步运行时中执行缩放逻辑,避免阻塞主线程
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 重置缩放为1.0 (默认大小)
|
||||
set_window_scale(window_label, DEFAULT_SCALE_FACTOR);
|
||||
|
||||
if let Err(e) = window.set_zoom(DEFAULT_SCALE_FACTOR) {
|
||||
log::error!("重置窗口缩放失败: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口最小化事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 将主窗口最小化到任务栏或dock
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_minimize<R: Runtime>(app: &AppHandle<R>) {
|
||||
// 获取标签为"main"的窗口对象
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
// 在异步运行时中执行最小化操作,避免阻塞主线程
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = window.minimize() {
|
||||
log::error!("最小化窗口失败: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口还原事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 将主窗口从最小化状态还原到正常状态
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_restore<R: Runtime>(app: &AppHandle<R>) {
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = window.unminimize() {
|
||||
log::error!("还原窗口失败: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口最大化事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 切换主窗口的最大化状态
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_maximize<R: Runtime>(app: &AppHandle<R>) {
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
// 切换最大化状态
|
||||
match window.is_maximized() {
|
||||
Ok(is_maximized) => {
|
||||
if let Err(e) = if is_maximized { window.unmaximize() } else { window.maximize() } {
|
||||
log::error!("切换窗口最大化状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("获取窗口最大化状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口关闭事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 关闭主窗口
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_close<R: Runtime>(app: &AppHandle<R>) {
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label)
|
||||
&& let Err(e) = window.close()
|
||||
{
|
||||
log::error!("关闭窗口失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口全屏事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 切换主窗口的全屏状态
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_fullscreen<R: Runtime>(app: &AppHandle<R>) {
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
// 切换全屏状态
|
||||
match window.is_fullscreen() {
|
||||
Ok(is_fullscreen) => {
|
||||
if let Err(e) = window.set_fullscreen(!is_fullscreen) {
|
||||
log::error!("切换窗口全屏状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("获取窗口全屏状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理窗口刷新事件
|
||||
///
|
||||
/// # 功能说明
|
||||
/// 刷新主窗口的页面内容
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app` - Tauri应用句柄
|
||||
pub fn handle_refresh<R: Runtime>(app: &AppHandle<R>) {
|
||||
let label = WindowLabel::MAIN.as_ref();
|
||||
if let Some(window) = app.get_webview_window(label)
|
||||
&& let Err(e) = window.eval("window.location.reload()")
|
||||
{
|
||||
log::error!("刷新页面失败: {}", e);
|
||||
}
|
||||
}
|
||||
19
src-tauri/src/menu/menu_handler.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::menu::menu_event::{
|
||||
handle_close, handle_fullscreen, handle_maximize, handle_minimize, handle_refresh, handle_restore, handle_zoom_in, handle_zoom_out, handle_zoom_reset,
|
||||
};
|
||||
|
||||
/// 处理菜单事件
|
||||
pub fn handle_menu_event<R: tauri::Runtime>(app: &tauri::AppHandle<R>, event_id: &str) {
|
||||
match event_id {
|
||||
"zoom_in" => handle_zoom_in(app),
|
||||
"zoom_out" => handle_zoom_out(app),
|
||||
"zoom_reset" => handle_zoom_reset(app),
|
||||
"minimize" => handle_minimize(app),
|
||||
"restore" => handle_restore(app),
|
||||
"maximize" => handle_maximize(app),
|
||||
"close" => handle_close(app),
|
||||
"fullscreen" => handle_fullscreen(app),
|
||||
"refresh" => handle_refresh(app),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
6
src-tauri/src/menu/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod app_menu;
|
||||
pub mod menu_event;
|
||||
pub mod menu_handler;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use app_menu::create_app_menu;
|
||||
4
src-tauri/src/storage/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod session_storage;
|
||||
|
||||
// 重新导出存储结构和命令
|
||||
pub use session_storage::SessionStorage;
|
||||
73
src-tauri/src/storage/session_storage/commands.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use super::SessionStorage;
|
||||
use tauri::{AppHandle, Emitter, EventTarget, State, Window};
|
||||
|
||||
// 获取会话存储的值
|
||||
#[tauri::command]
|
||||
pub fn session_storage_get(key: String, storage: State<SessionStorage>) -> Option<serde_json::Value> {
|
||||
storage.get(&key).and_then(|result| result.ok())
|
||||
}
|
||||
|
||||
// 设置会话存储的值
|
||||
#[tauri::command]
|
||||
pub fn session_storage_set(key: String, value: serde_json::Value, storage: State<SessionStorage>, app: AppHandle, window: Window) -> bool {
|
||||
let result = storage.set(&key, &value).is_ok();
|
||||
if result
|
||||
&& let Err(e) = app.emit_filter("session_storage_changed", (key, &value), |target| {
|
||||
match target {
|
||||
// 只有当窗口的 label 不是调用窗口的 label 时,才发送事件
|
||||
EventTarget::WebviewWindow { label } => label != window.label(),
|
||||
_ => false,
|
||||
}
|
||||
}) {
|
||||
log::warn!("发送存储变更事件失败: {}", e);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// 从会话存储中移除值
|
||||
#[tauri::command]
|
||||
pub fn session_storage_remove(key: String, storage: State<SessionStorage>, app: AppHandle, window: Window) -> bool {
|
||||
let result = storage.remove(&key);
|
||||
if result {
|
||||
// 向除调用窗口外的所有窗口发送数据删除事件
|
||||
if let Err(e) = app.emit_filter("session_storage_removed", key, |target| {
|
||||
match target {
|
||||
// 只有当窗口的 label 不是调用窗口的 label 时,才发送事件
|
||||
EventTarget::WebviewWindow { label } => label != window.label(),
|
||||
_ => false,
|
||||
}
|
||||
}) {
|
||||
log::warn!("发送存储删除事件失败: {}", e);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// 清空会话存储
|
||||
#[tauri::command]
|
||||
pub fn session_storage_clear(storage: State<SessionStorage>, app: AppHandle, window: Window) -> bool {
|
||||
storage.clear();
|
||||
// 向除调用窗口外的所有窗口发送清空事件
|
||||
if let Err(e) = app.emit_filter("session_storage_cleared", (), |target| {
|
||||
match target {
|
||||
// 只有当窗口的 label 不是调用窗口的 label 时,才发送事件
|
||||
EventTarget::WebviewWindow { label } => label != window.label(),
|
||||
_ => false,
|
||||
}
|
||||
}) {
|
||||
log::warn!("发送存储清空事件失败: {}", e);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// 获取所有会话存储键
|
||||
#[tauri::command]
|
||||
pub fn session_storage_keys(storage: State<SessionStorage>) -> Vec<String> {
|
||||
storage.keys()
|
||||
}
|
||||
|
||||
// 检查会话存储中是否存在指定键
|
||||
#[tauri::command]
|
||||
pub fn session_storage_has(key: String, storage: State<SessionStorage>) -> bool {
|
||||
storage.has(&key)
|
||||
}
|
||||
4
src-tauri/src/storage/session_storage/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod storage;
|
||||
pub use storage::SessionStorage;
|
||||
mod commands;
|
||||
pub use commands::*;
|
||||
123
src-tauri/src/storage/session_storage/storage.rs
Normal file
@ -0,0 +1,123 @@
|
||||
//! 会话存储模块
|
||||
//!
|
||||
//! 提供高性能的会话数据存储实现,使用零拷贝操作来提升性能
|
||||
|
||||
use crate::utils::zero_copy::ZeroCopyBytes;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 会话存储错误类型
|
||||
#[derive(Debug)]
|
||||
pub enum SessionStorageError {
|
||||
/// 序列化时出错
|
||||
SerializationError(serde_json::Error),
|
||||
/// 反序列化时出错
|
||||
DeserializationError(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionStorageError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SessionStorageError::SerializationError(e) => write!(f, "序列化时出错: {}", e),
|
||||
SessionStorageError::DeserializationError(e) => write!(f, "反序列化时出错: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SessionStorageError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
SessionStorageError::SerializationError(e) => Some(e),
|
||||
SessionStorageError::DeserializationError(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SessionStorageError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
SessionStorageError::SerializationError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// 会话存储数据结构
|
||||
///
|
||||
/// 使用 ZeroCopyBytes 存储序列化后的数据,避免不必要的内存拷贝
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionStorage {
|
||||
/// 使用 ZeroCopyBytes 代替 String,减少内存分配
|
||||
data: Arc<RwLock<HashMap<String, ZeroCopyBytes>>>,
|
||||
}
|
||||
|
||||
impl SessionStorage {
|
||||
/// 创建一个新的会话存储实例
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储任意类型的值
|
||||
///
|
||||
/// 值会被序列化成 JSON 格式存储
|
||||
///
|
||||
/// 性能优化: 使用 serde_json::to_vec 而不是 to_string,可以减少一次 UTF-8 验证
|
||||
pub fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), SessionStorageError> {
|
||||
// 序列化为 JSON 字节,比 to_string 稍快一点
|
||||
let json_bytes = serde_json::to_vec(value)?;
|
||||
// 使用 ZeroCopyBytes 包装,避免额外的字符串分配
|
||||
let zero_copy_bytes = ZeroCopyBytes::new(json_bytes);
|
||||
// 写入数据
|
||||
self.data.write().insert(key.to_owned(), zero_copy_bytes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取存储的值并反序列化成指定类型
|
||||
pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<Result<T, SessionStorageError>> {
|
||||
// 优化:在锁内只进行克隆操作(ZeroCopyBytes 是 cheap clone),将耗时的反序列化移出锁外
|
||||
let zero_copy_bytes = {
|
||||
let data_guard = self.data.read();
|
||||
data_guard.get(key).cloned()
|
||||
};
|
||||
|
||||
if let Some(zero_copy_bytes) = zero_copy_bytes {
|
||||
// 在锁外进行反序列化
|
||||
match serde_json::from_slice(zero_copy_bytes.as_slice()) {
|
||||
Ok(value) => Some(Ok(value)),
|
||||
Err(err) => Some(Err(SessionStorageError::DeserializationError(err))),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除指定键的值
|
||||
pub fn remove(&self, key: &str) -> bool {
|
||||
self.data.write().remove(key).is_some()
|
||||
}
|
||||
|
||||
/// 清空所有存储的数据
|
||||
pub fn clear(&self) {
|
||||
self.data.write().clear();
|
||||
}
|
||||
|
||||
/// 获取所有存储的键
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.data.read().keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// 检查键是否存在
|
||||
pub fn has(&self, key: &str) -> bool {
|
||||
self.data.read().contains_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SessionStorage {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
1
src-tauri/src/task/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod scheduled_restart;
|
||||
51
src-tauri/src/task/scheduled_restart.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tauri::AppHandle;
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use tracing::info;
|
||||
|
||||
/// 全局调度器实例
|
||||
/// 使用 OnceCell 确保只创建一次,并在应用生命周期内保持活动
|
||||
static SCHEDULER: OnceCell<JobScheduler> = OnceCell::new();
|
||||
|
||||
/// 启动每日凌晨1点定时重启任务
|
||||
///
|
||||
/// 注意:此函数会启动一个后台任务,调度器会在后台持续运行
|
||||
/// 调度器的生命周期与应用程序相同
|
||||
pub async fn start_scheduled_restart_task(app_handle: AppHandle) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 如果调度器已经存在,直接返回
|
||||
if SCHEDULER.get().is_some() {
|
||||
info!("定时重启任务已经启动,跳过重复初始化");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 创建调度器
|
||||
let scheduler = JobScheduler::new().await?;
|
||||
|
||||
// 创建每天凌晨1点执行的定时任务
|
||||
// cron表达式: "0 0 1 * * *" 表示每天1点0分0秒执行
|
||||
// 格式: sec min hour day_of_month month day_of_week
|
||||
let job = Job::new_async("0 0 1 * * *", move |_uuid, _l| {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
Box::pin(async move {
|
||||
info!("定时重启任务已触发,正在重启应用程序...");
|
||||
|
||||
// 重启应用
|
||||
app_handle_clone.restart();
|
||||
|
||||
// 注意:由于重启操作会终止程序,下面的代码不会执行
|
||||
})
|
||||
})?;
|
||||
|
||||
// 添加任务到调度器
|
||||
scheduler.add(job).await?;
|
||||
|
||||
// 启动调度器
|
||||
scheduler.start().await?;
|
||||
|
||||
// 将调度器保存到全局静态变量中,确保其不会被清理
|
||||
SCHEDULER.set(scheduler).map_err(|_| "定时任务调度器已初始化")?;
|
||||
|
||||
info!("定时重启任务已启动 - 应用程序将在每天凌晨1点重启");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
18
src-tauri/src/tray/commands.rs
Normal file
@ -0,0 +1,18 @@
|
||||
// 系统托盘相关命令
|
||||
use tauri::AppHandle;
|
||||
|
||||
// 更新托盘菜单命令
|
||||
// Linux平台会实际更新托盘菜单,其他平台为空实现
|
||||
#[tauri::command]
|
||||
pub async fn update_tray_menu(_app: AppHandle) -> Result<(), String> {
|
||||
// 只在Linux平台上实际更新托盘菜单
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Err(e) = crate::tray::linux_tray::rebuild_tray_menu(&_app).await {
|
||||
return Err(format!("更新托盘菜单失败: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 非Linux平台不支持动态更新托盘菜单,直接返回成功
|
||||
Ok(())
|
||||
}
|
||||
83
src-tauri/src/tray/common_tray.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use crate::audio::play_prompt_tone_imp;
|
||||
use crate::window::{WindowLabel, create_popup_window::create_popup_window_impl, derive::TargetLocation};
|
||||
use tauri::{AppHandle, Runtime, tray::TrayIconBuilder};
|
||||
|
||||
/// 创建并设置系统托盘图标
|
||||
///
|
||||
/// 该函数会检查是否已存在相同ID的托盘图标以避免重复添加,
|
||||
/// 并为托盘图标绑定点击事件处理函数,点击时直接打开弹出框窗口。
|
||||
pub fn setup_tray<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
|
||||
let tray_id = WindowLabel::MAIN_TRAY.as_ref();
|
||||
// 检查是否已经存在托盘图标,避免重复添加
|
||||
if app.tray_by_id(tray_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 获取默认窗口图标
|
||||
let icon = app.default_window_icon().ok_or("获取默认窗口图标失败")?.clone();
|
||||
|
||||
// 创建托盘图标(不关联菜单)
|
||||
use std::sync::Arc;
|
||||
let app_shared = Arc::new(app.clone());
|
||||
TrayIconBuilder::with_id(tray_id)
|
||||
.icon(icon)
|
||||
.tooltip("智评会议")
|
||||
.on_tray_icon_event(move |_tray, event| {
|
||||
// 处理托盘图标点击事件
|
||||
if let tauri::tray::TrayIconEvent::Click {
|
||||
button_state: tauri::tray::MouseButtonState::Up,
|
||||
rect,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app_handle = Arc::clone(&app_shared);
|
||||
// 如果popup窗口不存在,则创建并显示弹出框窗口
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 获取主显示器的缩放因子
|
||||
let scale_factor = (*app_handle).primary_monitor().ok().flatten().map(|monitor| monitor.scale_factor()).unwrap_or(1.0);
|
||||
|
||||
let target = get_target_location(&rect, scale_factor);
|
||||
|
||||
// 直接调用后端函数创建弹出框窗口
|
||||
let _ = create_popup_window_impl(&app_handle, &target, "auto").await;
|
||||
play_prompt_tone_imp();
|
||||
});
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.map_err(|e| format!("创建托盘图标失败: {}", e))?;
|
||||
|
||||
// 托盘图标创建成功
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标位置信息
|
||||
*/
|
||||
fn get_target_location(rect: &tauri::Rect, scale_factor: f64) -> TargetLocation {
|
||||
// 使用scale_factor将物理坐标转换为逻辑坐标
|
||||
// 在托盘图标的上下文中,默认使用1.0作为scale_factor
|
||||
// 这样可以确保返回的坐标始终是逻辑坐标
|
||||
|
||||
let x = match rect.position {
|
||||
tauri::Position::Physical(physical_position) => (physical_position.x as f64 / scale_factor) as i32,
|
||||
tauri::Position::Logical(logical_position) => logical_position.x as i32,
|
||||
};
|
||||
|
||||
let y = match rect.position {
|
||||
tauri::Position::Physical(physical_position) => (physical_position.y as f64 / scale_factor) as i32,
|
||||
tauri::Position::Logical(logical_position) => logical_position.y as i32,
|
||||
};
|
||||
|
||||
let width = match rect.size {
|
||||
tauri::Size::Physical(physical_size) => (physical_size.width as f64 / scale_factor) as u32,
|
||||
tauri::Size::Logical(logical_size) => logical_size.width as u32,
|
||||
};
|
||||
|
||||
let height = match rect.size {
|
||||
tauri::Size::Physical(physical_size) => (physical_size.height as f64 / scale_factor) as u32,
|
||||
tauri::Size::Logical(logical_size) => logical_size.height as u32,
|
||||
};
|
||||
|
||||
TargetLocation { x, y, width, height }
|
||||
}
|
||||
134
src-tauri/src/tray/linux_tray.rs
Normal file
@ -0,0 +1,134 @@
|
||||
//! Linux系统托盘菜单实现
|
||||
//!
|
||||
//! Linux系统托盘的特殊实现,因为Linux系统托盘的行为与其他平台不同,
|
||||
//! 需要专门处理以确保托盘图标和菜单能正常显示。
|
||||
|
||||
use crate::audio::play_prompt_tone_imp;
|
||||
use crate::window::{WindowLabel, create_drawing_board_window::create_drawing_board_window_impl, create_main_window::create_main_window_impl};
|
||||
use tauri::menu::{ContextMenu, MenuBuilder, MenuItemBuilder};
|
||||
use tauri::{AppHandle, Runtime, tray::TrayIconBuilder};
|
||||
|
||||
/// 创建并设置Linux系统托盘图标和菜单
|
||||
///
|
||||
/// Linux系统托盘需要特殊处理,因为Linux系统托盘的行为与其他平台不同
|
||||
pub fn setup_linux_tray<R: Runtime>(app: &AppHandle<R>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tray_id = WindowLabel::MAIN_TRAY.as_ref();
|
||||
// 检查是否已经存在托盘图标,避免重复添加
|
||||
if app.tray_by_id(tray_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 创建托盘菜单
|
||||
let tray_menu = create_tray_menu(app)?;
|
||||
|
||||
// 获取默认窗口图标
|
||||
let icon = app.default_window_icon().ok_or_else(|| "获取默认窗口图标失败".to_string())?.clone();
|
||||
|
||||
// 创建托盘图标(关联菜单)
|
||||
let tray_icon = TrayIconBuilder::with_id(tray_id)
|
||||
.icon(icon)
|
||||
.tooltip("智评会议")
|
||||
.menu(&tray_menu)
|
||||
.build(app)
|
||||
.map_err(|e| format!("创建托盘图标失败: {}", e))?;
|
||||
|
||||
// 使用 on_menu_event 处理所有菜单事件
|
||||
tray_icon.on_menu_event(move |app, event| {
|
||||
let app_handle = app.clone();
|
||||
match event.id.as_ref() {
|
||||
"main_window" => {
|
||||
play_prompt_tone_imp();
|
||||
// 切换主窗口显示/隐藏状态
|
||||
tauri::async_runtime::spawn(async move {
|
||||
toggle_window_visibility(&app_handle, WindowLabel::MAIN).await;
|
||||
});
|
||||
}
|
||||
"drawing_board" => {
|
||||
play_prompt_tone_imp();
|
||||
// 切换画板窗口显示/隐藏状态
|
||||
tauri::async_runtime::spawn(async move {
|
||||
toggle_window_visibility(&app_handle, WindowLabel::DRAWING_BOARD).await;
|
||||
});
|
||||
}
|
||||
"quit" => {
|
||||
play_prompt_tone_imp();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 直接退出,RunEvent::ExitRequested 会处理清理
|
||||
app_handle.exit(0);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
// 托盘图标创建成功
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 创建托盘上下文菜单
|
||||
fn create_tray_menu<R: Runtime>(app: &AppHandle<R>) -> Result<impl ContextMenu, tauri::Error> {
|
||||
// 创建菜单项
|
||||
let main_window_item = MenuItemBuilder::with_id("main_window", "显示主窗口").build(app)?;
|
||||
let drawing_board_item = MenuItemBuilder::with_id("drawing_board", "显示画板").build(app)?;
|
||||
let quit_item = MenuItemBuilder::with_id("quit", "退出应用程序").build(app)?;
|
||||
// 构建菜单
|
||||
let menu_builder = MenuBuilder::new(app).item(&main_window_item).item(&drawing_board_item);
|
||||
// 添加退出项
|
||||
let menu = menu_builder.separator().item(&quit_item).build()?;
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
/// 切换窗口显示/隐藏状态
|
||||
async fn toggle_window_visibility<R: Runtime>(app: &AppHandle<R>, window_label: WindowLabel) {
|
||||
// 窗口不存在,创建窗口
|
||||
create_window(app, window_label).await;
|
||||
// 重建菜单以更新菜单项文字
|
||||
// let _ = rebuild_tray_menu(&app);
|
||||
}
|
||||
|
||||
/// 创建窗口
|
||||
async fn create_window<R: Runtime>(app: &AppHandle<R>, window_label: WindowLabel) {
|
||||
match window_label {
|
||||
WindowLabel::MAIN => {
|
||||
if let Err(e) = create_main_window_impl(app).await {
|
||||
log::error!("创建主窗口失败: {}", e);
|
||||
// 可以考虑显示错误对话框或通知用户
|
||||
}
|
||||
}
|
||||
WindowLabel::DRAWING_BOARD => {
|
||||
if let Err(e) = create_drawing_board_window_impl(app).await {
|
||||
log::error!("创建画板窗口失败: {}", e);
|
||||
// 可以考虑显示错误对话框或通知用户
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("未知窗口标签: {}", window_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重建托盘菜单
|
||||
pub async fn rebuild_tray_menu<R: Runtime>(app: &AppHandle<R>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 克隆 app handle 以解决生命周期问题
|
||||
let app_handle = app.clone();
|
||||
|
||||
// 获取托盘图标并更新菜单
|
||||
if let Some(tray_icon) = app.tray_by_id(WindowLabel::MAIN_TRAY.as_ref()) {
|
||||
// 创建新的托盘菜单
|
||||
let main_window_item = MenuItemBuilder::with_id("main_window", "显示主窗口").build(&app_handle)?;
|
||||
let drawing_board_item = MenuItemBuilder::with_id("drawing_board", "显示画板").build(&app_handle)?;
|
||||
let quit_item = MenuItemBuilder::with_id("quit", "退出应用程序").build(&app_handle)?;
|
||||
// 构建菜单
|
||||
let menu = MenuBuilder::new(&app_handle)
|
||||
.item(&main_window_item)
|
||||
.item(&drawing_board_item)
|
||||
.separator()
|
||||
.item(&quit_item)
|
||||
.build()?;
|
||||
|
||||
tray_icon.set_menu(Some(menu)).map_err(|e| format!("设置托盘菜单失败: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||