chore: 初始化项目基础结构和资源文件

- 添加项目图标文件(app-icon.png、各平台图标)
- 配置开发环境文件(.env、.nvmrc、.npmrc)
- 添加静态资源文件(背景图片、字体、音频)
- 初始化Tauri后端结构(build.rs、main.rs、模块文件)
- 配置前端项目结构(TypeScript、Vue组件、样式)
- 添加Node.js API服务基础结构
- 配置构建和开发工具(vite、prettier、gitignore)
This commit is contained in:
2026-03-13 10:03:05 +08:00
commit 78af453fe1
357 changed files with 70605 additions and 0 deletions

View File

@ -0,0 +1,44 @@
use crate::window::create_float_list_window::create_float_list_window_impl;
use crate::window::create_main_window::create_main_window_impl;
use crate::window::create_meeting_window::create_meeting_window_impl;
use crate::window::create_popup_window::create_popup_window_impl;
use crate::window::derive::TargetLocation;
use tauri::{AppHandle, Window};
/// 创建悬浮列表窗口
#[tauri::command]
pub async fn create_float_list_window(app: AppHandle, page_type: &str) -> Result<String, String> {
create_float_list_window_impl(std::sync::Arc::new(app), page_type).await.map(|_| "success".to_string())
}
/// 创建弹出框窗口
#[tauri::command]
pub async fn create_popup_window(app: AppHandle, window: Window) -> Result<String, String> {
let current_position_physical = window.outer_position().map_err(|e| format!("获取窗口位置失败: {}", e))?;
let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?;
// 获取缩放因子
let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?;
// 转换为逻辑位置和大小
let current_position = current_position_physical.to_logical::<i32>(scale_factor);
let current_size = current_size_physical.to_logical::<i32>(scale_factor);
let target = TargetLocation {
x: current_position.x,
y: current_position.y,
width: current_size.width as u32,
height: current_size.height as u32,
};
create_popup_window_impl(&app, &target, "auto").await.map(|_| "success".to_string())
}
#[tauri::command]
pub async fn create_main_window(app: AppHandle) -> Result<String, String> {
create_main_window_impl(&app).await.map(|_| "success".to_string())
}
/// 创建会议窗口Tauri命令
#[tauri::command]
pub async fn create_meeting_window(app: AppHandle, window_type: u8, course_id: Option<u64>) -> Result<String, String> {
create_meeting_window_impl(&app, window_type, course_id).await.map(|_| "success".to_string())
}

View File

@ -0,0 +1,241 @@
use std::sync::Arc;
use crate::{
constants::{common::*, window::*},
storage::SessionStorage,
window::{
WindowLabel,
derive::FloatListInfo,
util::{get_monitor_for_window, show_windows_by_label},
},
};
use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, utils::config::BackgroundThrottlingPolicy};
/// 创建悬浮列表窗口(内部实现)
pub async fn create_float_list_window_impl<R: Runtime>(app: Arc<AppHandle<R>>, page_type: &str) -> Result<(), String> {
let app_handle = Arc::clone(&app);
let page_type_owned = page_type.to_string();
if show_windows_by_label(app.as_ref(), WindowLabel::FLOAT_LIST) {
return Ok(());
}
// 悬浮列表尺寸
let float_info = calculate_float_info_sync(app.as_ref(), page_type);
// 获取屏幕尺寸优先为main窗口所在屏幕主窗口没有打开时才找主屏幕
let monitor = get_monitor_for_window(app.as_ref())?;
// 获取屏幕缩放因子
let scale_factor = monitor.scale_factor();
let page_size: LogicalSize<f64> = monitor.work_area().size.to_logical(scale_factor);
let screen_width = page_size.width;
let screen_height = page_size.height;
let url = format!(
"floating-list-window.html?isShowInteraction={}&isShowTutorship={}",
float_info.is_show_interaction, float_info.is_show_tutorship
);
let label = WindowLabel::FLOAT_LIST.as_ref();
// 创建悬浮球窗口,直接在初始化时设置好位置和尺寸
let window = WebviewWindowBuilder::new(app.as_ref(), label, WebviewUrl::App(url.into()))
.title("悬浮列表")
.inner_size(float_info.width as f64, float_info.height as f64)
.max_inner_size(float_info.width as f64, float_info.height as f64)
.min_inner_size(float_info.width as f64, float_info.height as f64)
.position(screen_width - float_info.width as f64, 0.0)
.transparent(true)
.visible_on_all_workspaces(true)
.background_throttling(BackgroundThrottlingPolicy::Disabled)
.decorations(false)
.shadow(false)
.focusable(true)
.focused(false)
.accept_first_mouse(true)
.skip_taskbar(true)
.always_on_top(true)
.devtools(true)
.build();
let window = match window {
Ok(w) => w,
Err(e) => {
log::error!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e);
return Err(format!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e));
}
};
#[cfg(not(target_os = "linux"))]
{
let _ = window.set_resizable(false);
}
#[cfg(target_os = "windows")]
{
let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64));
}
// 窗口显示后等待200毫秒再执行动画
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let _ = animate_to_bottom_right(&app_handle, window, screen_width as i32, screen_height as i32, &page_type_owned).await;
});
Ok(())
}
/// 根据用户登录状态和课程选择情况动态计算弹出窗口高度
pub fn calculate_float_info_sync<R: Runtime>(app: &AppHandle<R>, page_type: &str) -> FloatListInfo {
// 默认基础项目数
let mut item_num = 4;
let mut is_show_interaction = false;
let mut is_show_tutorship = false;
// 获取会话存储实例
let storage = match app.try_state::<SessionStorage>() {
Some(s) => s,
None => {
log::error!("获取会话存储实例失败,使用默认值");
let item_height = 64;
let popup_height = item_height * item_num + 40;
let popup_width = 60;
return FloatListInfo {
width: popup_width as u32,
height: popup_height as u32,
is_show_interaction,
is_show_tutorship,
};
}
};
// 从会话存储中获取用户信息和课程信息来判断是否增加项目数
let user_info_exists = match storage.get::<serde_json::Value>(STORAGE_KEY_USER_INFO) {
Some(Ok(value)) => {
// 检查必要的字段是否存在
value.get("id").is_some()
}
_ => false,
};
let tutorship_info_exists = match storage.get::<serde_json::Value>(STORAGE_KEY_TUTORSHIP) {
Some(Ok(value)) => {
// 检查必要的字段是否存在
value.get("id").is_some()
}
_ => false,
};
let course_info_exists = match storage.get::<serde_json::Value>(STORAGE_KEY_COURSE) {
Some(Ok(value)) => {
// 检查必要的字段是否存在
value.get("id").is_some()
}
_ => false,
};
if user_info_exists {
if page_type == "interaction" {
is_show_interaction = true;
// 如果能获取到用户信息和课程信息,则增加项目数
if course_info_exists {
item_num += 5;
}
} else if page_type == "tutorship" {
// 如果能获取到用户信息和课程信息,则增加项目数
if course_info_exists {
is_show_interaction = true;
item_num += 5;
}
if tutorship_info_exists {
is_show_tutorship = true;
item_num += 3;
}
}
}
// 每个item高度
let item_height = 64;
// 总高度 = item高度 * 项目数 + 额外用于移动按钮和时间部分的高度
let popup_height = item_height * item_num + 60;
let popup_width = 60;
// 返回计算得到的弹出窗口尺寸
FloatListInfo {
width: popup_width as u32,
height: popup_height as u32,
is_show_tutorship,
is_show_interaction,
}
}
/// 使用抛物线动画将窗口移动到右下角
pub async fn animate_to_bottom_right<R: Runtime>(
app: &AppHandle<R>,
window: tauri::WebviewWindow<R>,
screen_width: i32,
screen_height: i32,
page_type: &str,
) -> Result<(), String> {
// 获取当前窗口位置和大小
let current_position_physical = window.outer_position().map_err(|e| format!("获取窗口位置失败: {}", e))?;
let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?;
let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?;
// 转换为逻辑位置和大小
let current_position = current_position_physical.to_logical::<i32>(scale_factor);
let current_size = current_size_physical.to_logical::<i32>(scale_factor);
let start_x = current_position.x;
let start_y = current_position.y;
// 计算目标位置(右下角)
// 注意screen_width 和 screen_height 已经在调用方考虑了缩放因子
let end_x = screen_width - current_size.width;
let end_y = screen_height - current_size.height - 120;
// 计算移动距离
let distance_x = (end_x - start_x) as f64;
let distance_y = (end_y - start_y) as f64;
// 动画参数
let duration = 800; // 总动画时间(毫秒)
let start_time = std::time::Instant::now();
// 抛物线控制参数
// 抛物线顶点高度偏移量(负值表示向下偏移,形成开口向上的抛物线)
let apex_offset = -(distance_y.abs() * 0.3).min(200.0);
// 执行动画
loop {
let elapsed = start_time.elapsed().as_millis() as u64;
if elapsed >= duration {
break;
}
let progress = (elapsed as f64) / (duration as f64);
// 使用抛物线轨迹计算位置
// 水平方向匀速运动
let current_x = (start_x as f64 + distance_x * progress).round() as i32;
// 垂直方向抛物线运动(开口向上,波峰向下)
// y = apex_offset * (4*x^2 - 4*x) 形成开口向上的抛物线,谷点在 x=0.5 处
let parabolic_factor = apex_offset * (4.0 * progress * progress - 4.0 * progress);
let current_y = (start_y as f64 + distance_y * progress + parabolic_factor).round() as i32;
// 更新窗口位置,使用 LogicalPosition
if let Err(_e) = window.set_position(LogicalPosition::new(current_x as f64, current_y as f64)) {
break;
}
// 短暂休眠以控制动画帧率
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
// 确保最终位置准确, 再次获取窗口大小和缩放因子
let float_info = calculate_float_info_sync(app, page_type);
let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?;
let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?;
let current_size = current_size_physical.to_logical::<i32>(scale_factor);
let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64));
let end_x = screen_width - current_size.width;
let end_y = screen_height - current_size.height - 120;
if let Err(e) = window.set_position(LogicalPosition::new(end_x as f64, end_y as f64)) {
return Err(format!("设置最终窗口位置失败: {}", e));
}
Ok(())
}

View File

@ -0,0 +1,40 @@
use crate::constants::{common::APP_NAME, window::*};
use crate::window::{util::show_windows_by_label, WindowLabel};
use tauri::{AppHandle, Runtime, WebviewUrl, WebviewWindowBuilder};
/// 根据tauri.conf.json配置创建主窗口
pub async fn create_main_window_impl<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
// 检查是否已经存在主窗口
if show_windows_by_label(app, WindowLabel::MAIN) {
return Ok(());
}
let label = WindowLabel::MAIN.as_ref();
// 根据tauri.conf.json配置创建主窗口
let _window = WebviewWindowBuilder::new(app, label, WebviewUrl::App("index.html".into()))
.title(APP_NAME)
.inner_size(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT)
.resizable(true)
.fullscreen(false)
.decorations(false)
.transparent(true)
.always_on_top(false)
.center()
.shadow(true)
.devtools(true)
.zoom_hotkeys_enabled(true)
.focusable(true)
.accept_first_mouse(true)
.visible(true)
.build()
.map_err(|e| format!("创建主窗口失败: {}", e))?;
// 设置窗口圆角效果
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
use tauri::window::{Color, EffectsBuilder};
let _ = _window.set_effects(Some(EffectsBuilder::new().radius(WINDOW_CORNER_RADIUS).color(Color(0, 0, 0, 255)).build()));
}
Ok(())
}

View File

@ -0,0 +1,59 @@
use crate::constants::{common::*, window::*};
use crate::env::TEACHERCONTROL_JK_URL;
use crate::window::WindowLabel;
use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
/// 创建会议窗口(内部实现)
pub async fn create_meeting_window_impl<R: Runtime>(app: &AppHandle<R>, window_type: u8, course_id: Option<u64>) -> Result<(), String> {
// 根据窗口类型定义label和url
let (label, url, title, width, height, decorations, transparent) = match window_type {
1 => {
let label = WindowLabel::MEETING.as_ref();
(
label,
WebviewUrl::App("/#/meeting".into()),
"会议",
MEETING_TYPE_1_WIDTH,
MEETING_TYPE_1_HEIGHT,
false,
true,
)
}
2 => {
if let Some(id) = course_id {
println!("创建会议窗口2: {}", TEACHERCONTROL_JK_URL);
let url = format!("{}?CourseID={}", TEACHERCONTROL_JK_URL, id);
let label = WindowLabel::MEETING2.as_ref();
(
label,
WebviewUrl::External(url.parse().map_err(|_| "URL格式错误".to_string())?),
"会议2",
MEETING_TYPE_2_WIDTH,
MEETING_TYPE_2_HEIGHT,
true,
false,
)
} else {
return Err(MSG_MEETING_TYPE_2_NEEDS_COURSE_ID.to_string());
}
}
_ => return Err(MSG_UNSUPPORTED_MEETING_WINDOW_TYPE.to_string()),
};
// 如果窗口已存在,先关闭它
if let Some(win) = app.get_webview_window(label) {
let _ = win.close();
}
// 创建窗口
WebviewWindowBuilder::new(app, label, url)
.title(title)
.inner_size(width, height)
.decorations(decorations)
.transparent(transparent)
.devtools(true)
.build()
.map_err(|e| format!("{} {}", MSG_FAILED_CREATE_MEETING_WINDOW, e))?;
Ok(())
}

View File

@ -0,0 +1,218 @@
use crate::constants::window::*;
use crate::window::{
derive::{PopupLocation, PopupSize, TargetLocation},
util::get_monitor_for_window,
WindowLabel,
};
use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
/// 创建弹出框窗口(内部实现)
pub async fn create_popup_window_impl<R: Runtime>(app: &AppHandle<R>, target: &TargetLocation, placement: &str) -> Result<(), String> {
// 获取屏幕尺寸优先为main窗口所在屏幕主窗口没有打开时才找主屏幕
let monitor: tauri::Monitor = get_monitor_for_window(app)?;
// 使用动态计算的弹出窗口尺寸
let popup_size = calculate_popup_size_sync();
let page_size = monitor.size().to_logical(monitor.scale_factor());
// 计算弹出框位置和大小
let popup_location = calculate_placement(target, &popup_size, placement, &page_size).await?;
let label = WindowLabel::POPUP.as_ref();
// 检查是否已经存在名为"popup"的窗口
if let Some(popup_window) = app.get_webview_window(label) {
// 如果窗口已存在,直接关闭它
let _ = popup_window.close();
}
let url = format!("popup-window.html?placement={}", popup_location.placement); // 创建弹出框窗口
WebviewWindowBuilder::new(app, label, WebviewUrl::App(url.into()))
.title("弹出框")
.inner_size(popup_location.width as f64, popup_location.height as f64)
.position(popup_location.x as f64, popup_location.y as f64)
.resizable(false)
.transparent(true)
.decorations(false)
.shadow(false)
.focusable(true)
.accept_first_mouse(true)
.skip_taskbar(true)
.always_on_top(true)
.visible(true)
.build()
.map_err(|e| format!("创建弹出框窗口失败: {}", e))?;
Ok(())
}
/// 根据用户登录状态和课程选择情况动态计算弹出窗口高度
pub fn calculate_popup_size_sync() -> PopupSize {
// 默认基础项目数
let item_num = 3;
// 根据项目数计算高度每个项目40像素加上边距2像素
let popup_height = 40 * item_num + 2;
let popup_width = 180;
// 返回计算得到的弹出窗口尺寸
PopupSize {
width: popup_width,
height: popup_height,
}
}
/// 计算弹出框位置
pub async fn calculate_placement(
target: &TargetLocation,
popup_size: &PopupSize,
placement: &str,
page_size: &tauri::LogicalSize<i32>,
) -> Result<PopupLocation, String> {
let margin = 12; // 箭头距离触发元素的距离
let mar = 12; // 距离屏幕边缘的最小距离
let page_width = page_size.width;
let page_height = page_size.height;
// 点击dom宽高
let target_width = target.width;
let target_height = target.height;
// 点击dom右上角的坐标
let target_x = target.x;
let target_y = target.y;
// 点击dom中心点坐标
let center_x = target_x + (target_width / 2) as i32;
let center_y = target_y + (target_height / 2) as i32;
// 弹出框的宽高
let bubble_width = popup_size.width;
let bubble_height = popup_size.height;
// 如果placement为"center",则直接返回屏幕中央位置
if placement == PLACEMENT_CENTER {
return Ok(calculate_center_position(popup_size, page_size));
}
// 上面足够
let top_enough = bubble_height as i32 + margin + mar < target_y;
// 下面足够
let bottom_enough = (target_y + target_height as i32 + bubble_height as i32 + margin + mar) < page_height;
// 左面足够
let left_enough = target_x - bubble_width as i32 - margin - mar >= 0;
// 右面足够
let right_enough = target_x + target_width as i32 + bubble_width as i32 + margin + mar < page_width;
// 屏幕宽度足够
let width_enough = page_width > bubble_width as i32 + mar * 2;
// 屏幕高度足够
let height_enough = page_height > bubble_height as i32 + mar * 2;
if (placement == PLACEMENT_AUTO || placement == PLACEMENT_TOP) && top_enough && width_enough {
get_top_bottom_placement(PLACEMENT_TOP, target, popup_size, margin, mar, center_x, page_width)
} else if (placement == PLACEMENT_AUTO || placement == PLACEMENT_BOTTOM) && bottom_enough && width_enough {
get_top_bottom_placement(PLACEMENT_BOTTOM, target, popup_size, margin, mar, center_x, page_width)
} else if (placement == PLACEMENT_AUTO || placement == PLACEMENT_LEFT) && left_enough && height_enough {
get_left_right_placement(PLACEMENT_LEFT, target, popup_size, margin, mar, center_y, page_height)
} else if (placement == PLACEMENT_AUTO || placement == PLACEMENT_RIGHT) && right_enough && height_enough {
get_left_right_placement(PLACEMENT_RIGHT, target, popup_size, margin, mar, center_y, page_height)
} else {
let popup_y = center_y + (target_height as i32) / 2 + margin;
Ok(PopupLocation {
enough_show: false,
placement: PLACEMENT_CENTER.to_string(),
x: 0,
y: popup_y,
width: popup_size.width,
height: popup_size.height,
})
}
}
/// 计算屏幕中央位置
pub fn calculate_center_position(popup_size: &PopupSize, page_size: &tauri::LogicalSize<i32>) -> PopupLocation {
let page_width = page_size.width;
let page_height = page_size.height;
let popup_x = (page_width - popup_size.width as i32) / 2;
let popup_y = (page_height - popup_size.height as i32) / 2;
PopupLocation {
enough_show: true,
placement: PLACEMENT_CENTER.to_string(),
x: popup_x,
y: popup_y,
width: popup_size.width,
height: popup_size.height,
}
}
/// 上或者下足够并且宽度也足够时的位置计算
fn get_top_bottom_placement(
placement: &str,
target: &TargetLocation,
popup_size: &PopupSize,
margin: i32,
mar: i32,
center_x: i32,
page_width: i32,
) -> Result<PopupLocation, String> {
let popup_y = if placement == "bottom" {
target.y + target.height as i32 + margin
} else {
target.y - popup_size.height as i32 - margin
};
let right_x = center_x - (popup_size.width as i32) / 2;
let left_x = center_x + (popup_size.width as i32) / 2;
let mut popup_x = mar;
if right_x >= mar && page_width - left_x >= mar {
popup_x = right_x;
} else if page_width - left_x >= mar {
popup_x = mar;
} else if right_x >= mar {
popup_x = page_width - popup_size.width as i32 - mar;
}
Ok(PopupLocation {
enough_show: true,
placement: placement.to_string(),
y: popup_y,
x: popup_x,
width: popup_size.width,
height: popup_size.height,
})
}
/// 左或者右足够并且高度也足够时的位置计算
fn get_left_right_placement(
placement: &str,
target: &TargetLocation,
popup_size: &PopupSize,
margin: i32,
mar: i32,
center_y: i32,
page_height: i32,
) -> Result<PopupLocation, String> {
let popup_x = if placement == "left" {
target.x - popup_size.width as i32 - margin
} else {
target.x + target.width as i32 + margin
};
let top_y = center_y - (popup_size.height as i32) / 2;
let bottom_y = center_y + (popup_size.height as i32) / 2;
let popup_y = if top_y >= mar && page_height - bottom_y >= mar {
// 水平居中
top_y
} else if page_height - bottom_y >= mar {
mar
} else {
page_height - popup_size.height as i32 - mar
};
Ok(PopupLocation {
enough_show: true,
placement: placement.to_string(),
y: popup_y,
x: popup_x,
width: popup_size.width,
height: popup_size.height,
})
}

View File

@ -0,0 +1,53 @@
use serde::{Deserialize, Serialize};
/// 目标位置信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetLocation {
/// 触发元素右上角的X轴坐标
pub x: i32,
/// 触发元素右上角的Y轴坐标
pub y: i32,
/// 触发元素宽高
pub width: u32,
/// 触发元素宽高
pub height: u32,
}
/// 弹出框尺寸信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupSize {
/// 弹出框的宽度
pub width: u32,
/// 弹出框的高度
pub height: u32,
}
/// 悬浮列表信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FloatListInfo {
/// 弹出框的宽度
pub width: u32,
/// 弹出框的高度
pub height: u32,
/** 是否显示互动课堂相关按钮 */
pub is_show_interaction: bool,
/** 是否显示智评会议相关按钮 */
pub is_show_tutorship: bool,
}
/// 弹出框位置信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupLocation {
/// 弹出bubble的位置
pub placement: String,
/// 是浏览器窗口否足够bubble显示, true:足够显示; false:不足够(改用ElDialog方式显示)
pub enough_show: bool,
/// bubble弹出框右上角的Y轴坐标
pub y: i32,
/// bubble弹出框右上角的X轴坐标
pub x: i32,
/// 弹出框的宽度
pub width: u32,
/// 弹出框的高度
pub height: u32,
}

View File

@ -0,0 +1,60 @@
//! 窗口标签定义模块
//!
//! 提供窗口标签的类型安全常量,避免硬编码字符串。
use std::fmt::{Display, Formatter};
/// 窗口标签类型
///
/// 提供类型安全的窗口标签常量,避免使用魔法字符串
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WindowLabel(&'static str);
impl WindowLabel {
/// 主窗口
pub const MAIN: Self = Self("main");
/// 悬浮列表窗口
pub const FLOAT_LIST: Self = Self("float-list");
/// 弹出框窗口
pub const POPUP: Self = Self("popup");
/// 画板窗口
pub const DRAWING_BOARD: Self = Self("drawing-board");
/// 实时字幕窗口
pub const SUBTITLE: Self = Self("subtitle");
/// 会议窗口
pub const MEETING: Self = Self("meeting");
/// 会议窗口2
pub const MEETING2: Self = Self("meeting2");
/// 系统托盘
pub const MAIN_TRAY: Self = Self("main");
}
impl Display for WindowLabel {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for WindowLabel {
fn as_ref(&self) -> &str {
self.0
}
}
impl From<WindowLabel> for String {
fn from(label: WindowLabel) -> Self {
label.0.to_string()
}
}
impl From<WindowLabel> for &'static str {
fn from(label: WindowLabel) -> Self {
label.0
}
}
impl<'a> From<&'a WindowLabel> for &'a str {
fn from(label: &'a WindowLabel) -> Self {
label.0
}
}

View File

@ -0,0 +1,10 @@
pub mod commands;
pub mod create_float_list_window;
pub mod create_main_window;
pub mod create_meeting_window;
pub mod create_popup_window;
pub mod derive;
pub mod label;
pub mod util;
pub use label::WindowLabel;

View File

@ -0,0 +1,76 @@
//! 窗口工具模块
//!
//! 提供窗口相关的实用函数,包括动画和位置计算等功能。
use super::WindowLabel;
use tauri::{AppHandle, Manager, Runtime};
/**
* 显示指定标签的窗口,如果窗口已存在则将其打开。
*/
pub fn show_windows_by_label<R: Runtime>(app: &AppHandle<R>, window_label: WindowLabel) -> bool {
// 检查是否已存,如果存在则直接打开
let label = window_label.as_ref();
let window = app.get_webview_window(label);
if let Some(existing_window) = window {
if let Ok(visible) = existing_window.is_visible()
&& !visible
{
let _ = existing_window.show();
}
if let Ok(minimized) = existing_window.is_minimized()
&& minimized
{
let _ = existing_window.unminimize();
}
if let Ok(focused) = existing_window.is_focused()
&& !focused
{
let _ = existing_window.set_focus();
}
return true;
}
false
}
/// 检查窗口是否可见且未最小化
#[allow(dead_code)]
pub fn check_window_visible<R: Runtime>(app: &AppHandle<R>, window_label: WindowLabel) -> bool {
let label = window_label.as_ref();
if let Some(window) = app.get_webview_window(label) {
// 检查窗口是否可见
let is_visible = window.is_visible().unwrap_or(false);
// 如果窗口不可见直接返回false
if !is_visible {
return false;
}
// 检查窗口是否最小化
let is_minimized = window.is_minimized().unwrap_or(false);
// 窗口可见且未最小化才认为是真正可见
is_visible && !is_minimized
} else {
false
}
}
/// 获取显示器优先使用main窗口所在显示器回退到主显示器
pub fn get_monitor_for_window<R: Runtime>(app: &AppHandle<R>) -> Result<tauri::Monitor, String> {
if let Some(main_window) = app.get_webview_window(WindowLabel::MAIN.as_ref()) {
// 尝试获取主窗口所在的显示器
let Ok(Some(monitor)) = main_window.current_monitor() else {
return Err("未找到主窗口显示器".to_string());
};
Ok(monitor)
} else {
// 主窗口未打开,获取主显示器
let Ok(Some(monitor)) = app.primary_monitor() else {
return Err("未找到主显示器".to_string());
};
Ok(monitor)
}
}