feat(桌面与监控): 完善工作台导航和登录活动定位
- 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验 - 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移 - 更新桌面发布检查、运维文档和前后端测试覆盖
This commit is contained in:
+290
-31
@@ -1,15 +1,90 @@
|
||||
mod credentials;
|
||||
mod updates;
|
||||
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu};
|
||||
use tauri::{Emitter, Manager, Runtime};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::menu::{
|
||||
AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::RunEvent;
|
||||
use tauri::{Emitter, Manager, Runtime, Theme, WebviewWindow, WebviewWindowBuilder};
|
||||
|
||||
const DESKTOP_MENU_COMMAND_EVENT: &str = "ctms:desktop-menu-command";
|
||||
const VIEW_MENU_ID: &str = "ctms.menu.view";
|
||||
const NAVIGATION_MENU_ID: &str = "ctms.menu.navigation";
|
||||
const REFRESH_COMMAND_ID: &str = "ctms.desktop.refresh";
|
||||
const TOGGLE_SIDEBAR_COMMAND_ID: &str = "ctms.desktop.toggleSidebar";
|
||||
const BACK_COMMAND_ID: &str = "ctms.desktop.back";
|
||||
const FORWARD_COMMAND_ID: &str = "ctms.desktop.forward";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DesktopMenuShortcuts {
|
||||
back: String,
|
||||
forward: String,
|
||||
refresh: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum DesktopThemePreference {
|
||||
System,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let package = handle.package_info();
|
||||
let config = handle.config();
|
||||
let about = AboutMetadata {
|
||||
name: Some(package.name.clone()),
|
||||
version: Some(package.version.to_string()),
|
||||
// macOS falls back to CFBundleVersion when this value is omitted, which
|
||||
// renders duplicated text such as `Version 0.1.0 (0.1.0)`.
|
||||
#[cfg(target_os = "macos")]
|
||||
short_version: Some(String::new()),
|
||||
copyright: config.bundle.copyright.clone(),
|
||||
authors: config
|
||||
.bundle
|
||||
.publisher
|
||||
.clone()
|
||||
.map(|publisher| vec![publisher]),
|
||||
// Supplying the icon explicitly keeps the native About panel from
|
||||
// falling back to the generic macOS application/folder icon.
|
||||
icon: handle.default_window_icon().cloned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Menu::with_items(
|
||||
handle,
|
||||
&[
|
||||
#[cfg(target_os = "macos")]
|
||||
&Submenu::with_items(
|
||||
handle,
|
||||
package.name.clone(),
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::about(handle, Some("关于 CTMS"), Some(about.clone()))?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.preferences",
|
||||
"设置…",
|
||||
true,
|
||||
Some("CmdOrCtrl+,"),
|
||||
)?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&PredefinedMenuItem::services(handle, None)?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&PredefinedMenuItem::hide(handle, None)?,
|
||||
&PredefinedMenuItem::hide_others(handle, None)?,
|
||||
&PredefinedMenuItem::show_all(handle, None)?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&PredefinedMenuItem::quit(handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
handle,
|
||||
"文件",
|
||||
@@ -22,15 +97,17 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
|
||||
true,
|
||||
Some("CmdOrCtrl+K"),
|
||||
)?,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.serverSettings",
|
||||
"服务器设置",
|
||||
"ctms.desktop.preferences",
|
||||
"设置",
|
||||
true,
|
||||
None::<&str>,
|
||||
Some("CmdOrCtrl+,"),
|
||||
)?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&PredefinedMenuItem::close_window(handle, None)?,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
&PredefinedMenuItem::quit(handle, None)?,
|
||||
],
|
||||
)?,
|
||||
@@ -48,44 +125,49 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
|
||||
&PredefinedMenuItem::select_all(handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
&Submenu::with_id_and_items(
|
||||
handle,
|
||||
"视图",
|
||||
VIEW_MENU_ID,
|
||||
"显示",
|
||||
true,
|
||||
&[
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.refresh",
|
||||
REFRESH_COMMAND_ID,
|
||||
"刷新当前视图",
|
||||
true,
|
||||
Some("CmdOrCtrl+R"),
|
||||
)?,
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
TOGGLE_SIDEBAR_COMMAND_ID,
|
||||
"显示/隐藏导航栏",
|
||||
true,
|
||||
None::<&str>,
|
||||
)?,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&PredefinedMenuItem::fullscreen(handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
&Submenu::with_id_and_items(
|
||||
handle,
|
||||
NAVIGATION_MENU_ID,
|
||||
"导航",
|
||||
true,
|
||||
&[
|
||||
&MenuItem::with_id(handle, BACK_COMMAND_ID, "后退", true, Some("CmdOrCtrl+["))?,
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.back",
|
||||
"返回",
|
||||
true,
|
||||
None::<&str>,
|
||||
)?,
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.forward",
|
||||
FORWARD_COMMAND_ID,
|
||||
"前进",
|
||||
true,
|
||||
None::<&str>,
|
||||
Some("CmdOrCtrl+]"),
|
||||
)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
&Submenu::with_id_and_items(
|
||||
handle,
|
||||
WINDOW_SUBMENU_ID,
|
||||
"窗口",
|
||||
true,
|
||||
&[
|
||||
@@ -94,17 +176,20 @@ fn desktop_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<
|
||||
&PredefinedMenuItem::close_window(handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
&Submenu::with_id_and_items(
|
||||
handle,
|
||||
HELP_SUBMENU_ID,
|
||||
"帮助",
|
||||
true,
|
||||
&[
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
&PredefinedMenuItem::about(handle, Some("关于 CTMS"), Some(about))?,
|
||||
&MenuItem::with_id(
|
||||
handle,
|
||||
"ctms.desktop.preferences",
|
||||
"桌面偏好",
|
||||
"ctms.desktop.help",
|
||||
"CTMS 命令与导航",
|
||||
true,
|
||||
Some("CmdOrCtrl+,"),
|
||||
None::<&str>,
|
||||
)?,
|
||||
],
|
||||
)?,
|
||||
@@ -118,8 +203,148 @@ fn emit_menu_command<R: Runtime>(app: &tauri::AppHandle<R>, command: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_main_window<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = app
|
||||
.config()
|
||||
.app
|
||||
.windows
|
||||
.iter()
|
||||
.find(|config| config.label == "main")
|
||||
.cloned()
|
||||
.ok_or_else(|| "未找到主窗口配置".to_string())?;
|
||||
let window = WebviewWindowBuilder::from_config(app, &config)
|
||||
.map_err(|error| format!("读取主窗口配置失败:{error}"))?
|
||||
.build()
|
||||
.map_err(|error| format!("重建主窗口失败:{error}"))?;
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_allowed_shortcut_key(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"[" | "]"
|
||||
| ","
|
||||
| "."
|
||||
| "/"
|
||||
| ";"
|
||||
| "="
|
||||
| "-"
|
||||
| "ArrowLeft"
|
||||
| "ArrowRight"
|
||||
| "ArrowUp"
|
||||
| "ArrowDown"
|
||||
| "Home"
|
||||
| "End"
|
||||
| "PageUp"
|
||||
| "PageDown"
|
||||
) || (value.len() == 1
|
||||
&& value
|
||||
.as_bytes()
|
||||
.first()
|
||||
.is_some_and(|value| value.is_ascii_uppercase() || value.is_ascii_digit()))
|
||||
}
|
||||
|
||||
fn normalize_menu_shortcut(value: &str) -> Result<String, String> {
|
||||
const RESERVED: &[&str] = &[
|
||||
"Mod+K",
|
||||
"Mod+,",
|
||||
"Mod+W",
|
||||
"Mod+Q",
|
||||
"Mod+C",
|
||||
"Mod+V",
|
||||
"Mod+X",
|
||||
"Mod+A",
|
||||
"Mod+Z",
|
||||
"Mod+Shift+Z",
|
||||
];
|
||||
|
||||
if RESERVED.contains(&value) {
|
||||
return Err("快捷键与系统或应用保留快捷键冲突".to_string());
|
||||
}
|
||||
|
||||
let parts = value.split('+').collect::<Vec<_>>();
|
||||
if parts.len() < 2 || parts[0] != "Mod" {
|
||||
return Err("快捷键必须以 Mod 开头".to_string());
|
||||
}
|
||||
|
||||
let key = parts.last().copied().unwrap_or_default();
|
||||
if !is_allowed_shortcut_key(key) {
|
||||
return Err("快捷键按键不受支持".to_string());
|
||||
}
|
||||
|
||||
let mut modifiers = HashSet::new();
|
||||
for modifier in &parts[1..parts.len() - 1] {
|
||||
if !matches!(*modifier, "Shift" | "Alt") || !modifiers.insert(*modifier) {
|
||||
return Err("快捷键修饰键不受支持或重复".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!("CmdOrCtrl+{}", parts[1..].join("+")))
|
||||
}
|
||||
|
||||
fn set_menu_accelerator<R: Runtime>(
|
||||
app: &tauri::AppHandle<R>,
|
||||
submenu_id: &str,
|
||||
item_id: &str,
|
||||
accelerator: &str,
|
||||
) -> Result<(), String> {
|
||||
let menu = app.menu().ok_or_else(|| "桌面菜单尚未初始化".to_string())?;
|
||||
let submenu = menu
|
||||
.get(submenu_id)
|
||||
.and_then(|item| item.as_submenu().cloned())
|
||||
.ok_or_else(|| format!("未找到桌面菜单:{submenu_id}"))?;
|
||||
let item = submenu
|
||||
.get(item_id)
|
||||
.and_then(|item| item.as_menuitem().cloned())
|
||||
.ok_or_else(|| format!("未找到桌面菜单项:{item_id}"))?;
|
||||
item.set_accelerator(Some(accelerator))
|
||||
.map_err(|error| format!("更新桌面菜单快捷键失败:{error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn desktop_menu_set_shortcuts<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
shortcuts: DesktopMenuShortcuts,
|
||||
) -> Result<(), String> {
|
||||
let back = normalize_menu_shortcut(&shortcuts.back)?;
|
||||
let forward = normalize_menu_shortcut(&shortcuts.forward)?;
|
||||
let refresh = normalize_menu_shortcut(&shortcuts.refresh)?;
|
||||
if HashSet::from([back.as_str(), forward.as_str(), refresh.as_str()]).len() != 3 {
|
||||
return Err("桌面快捷键不能重复".to_string());
|
||||
}
|
||||
|
||||
set_menu_accelerator(&app, NAVIGATION_MENU_ID, BACK_COMMAND_ID, &back)?;
|
||||
set_menu_accelerator(&app, NAVIGATION_MENU_ID, FORWARD_COMMAND_ID, &forward)?;
|
||||
set_menu_accelerator(&app, VIEW_MENU_ID, REFRESH_COMMAND_ID, &refresh)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn desktop_window_set_theme<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
theme: DesktopThemePreference,
|
||||
) -> Result<(), String> {
|
||||
let theme = match theme {
|
||||
DesktopThemePreference::System => None,
|
||||
DesktopThemePreference::Light => Some(Theme::Light),
|
||||
DesktopThemePreference::Dark => Some(Theme::Dark),
|
||||
};
|
||||
window
|
||||
.set_theme(theme)
|
||||
.map_err(|error| format!("更新桌面窗口主题失败:{error}"))
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let app = tauri::Builder::default()
|
||||
.menu(desktop_menu)
|
||||
.on_menu_event(|app, event| {
|
||||
let command = event.id().as_ref();
|
||||
@@ -128,11 +353,7 @@ pub fn run() {
|
||||
}
|
||||
})
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
let _ = restore_main_window(app);
|
||||
}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
@@ -149,7 +370,45 @@ pub fn run() {
|
||||
credentials::login_credential_delete,
|
||||
updates::desktop_update_check,
|
||||
updates::desktop_update_install,
|
||||
desktop_menu_set_shortcuts,
|
||||
desktop_window_set_theme,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running CTMS desktop application");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building CTMS desktop application");
|
||||
|
||||
app.run(|app, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let RunEvent::Reopen { .. } = event {
|
||||
if let Err(error) = restore_main_window(app) {
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = (app, event);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_menu_shortcut;
|
||||
|
||||
#[test]
|
||||
fn converts_runtime_shortcuts_to_native_accelerators() {
|
||||
assert_eq!(
|
||||
normalize_menu_shortcut("Mod+Shift+[").unwrap(),
|
||||
"CmdOrCtrl+Shift+["
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_menu_shortcut("Mod+Alt+ArrowLeft").unwrap(),
|
||||
"CmdOrCtrl+Alt+ArrowLeft"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_reserved_or_malformed_shortcuts() {
|
||||
assert!(normalize_menu_shortcut("Mod+Q").is_err());
|
||||
assert!(normalize_menu_shortcut("Ctrl+R").is_err());
|
||||
assert!(normalize_menu_shortcut("Mod+Shift+Shift+R").is_err());
|
||||
assert!(normalize_menu_shortcut("Mod+F12").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user