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:
44
src-tauri/src/window/commands.rs
Normal file
44
src-tauri/src/window/commands.rs
Normal 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())
|
||||
}
|
||||
241
src-tauri/src/window/create_float_list_window.rs
Normal file
241
src-tauri/src/window/create_float_list_window.rs
Normal 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(())
|
||||
}
|
||||
40
src-tauri/src/window/create_main_window.rs
Normal file
40
src-tauri/src/window/create_main_window.rs
Normal 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(())
|
||||
}
|
||||
59
src-tauri/src/window/create_meeting_window.rs
Normal file
59
src-tauri/src/window/create_meeting_window.rs
Normal 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(())
|
||||
}
|
||||
218
src-tauri/src/window/create_popup_window.rs
Normal file
218
src-tauri/src/window/create_popup_window.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
53
src-tauri/src/window/derive.rs
Normal file
53
src-tauri/src/window/derive.rs
Normal 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,
|
||||
}
|
||||
60
src-tauri/src/window/label.rs
Normal file
60
src-tauri/src/window/label.rs
Normal 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
|
||||
}
|
||||
}
|
||||
10
src-tauri/src/window/mod.rs
Normal file
10
src-tauri/src/window/mod.rs
Normal 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;
|
||||
76
src-tauri/src/window/util.rs
Normal file
76
src-tauri/src/window/util.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user