feat: 新增浮动列表窗口和弹出窗口功能
添加浮动列表窗口,支持拖拽、吸附和折叠功能,包含课程互动和辅导工具菜单 新增弹出窗口,提供主窗口、画板和退出等快捷操作 优化窗口创建逻辑,添加日志记录和焦点设置
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
use crate::constants::window::*;
|
use crate::constants::window::*;
|
||||||
use crate::window::{
|
use crate::window::{
|
||||||
|
WindowLabel,
|
||||||
derive::{PopupLocation, PopupSize, TargetLocation},
|
derive::{PopupLocation, PopupSize, TargetLocation},
|
||||||
util::get_monitor_for_window,
|
util::get_monitor_for_window,
|
||||||
WindowLabel,
|
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ pub async fn create_popup_window_impl<R: Runtime>(app: &AppHandle<R>, target: &T
|
|||||||
// 如果窗口已存在,直接关闭它
|
// 如果窗口已存在,直接关闭它
|
||||||
let _ = popup_window.close();
|
let _ = popup_window.close();
|
||||||
}
|
}
|
||||||
|
log::info!("创建弹出框窗口: {:?}", popup_location);
|
||||||
|
|
||||||
let url = format!("popup-window.html?placement={}", popup_location.placement); // 创建弹出框窗口
|
let url = format!("popup-window.html?placement={}", popup_location.placement); // 创建弹出框窗口
|
||||||
WebviewWindowBuilder::new(app, label, WebviewUrl::App(url.into()))
|
WebviewWindowBuilder::new(app, label, WebviewUrl::App(url.into()))
|
||||||
@ -34,6 +35,7 @@ pub async fn create_popup_window_impl<R: Runtime>(app: &AppHandle<R>, target: &T
|
|||||||
.decorations(false)
|
.decorations(false)
|
||||||
.shadow(false)
|
.shadow(false)
|
||||||
.focusable(true)
|
.focusable(true)
|
||||||
|
.focused(true)
|
||||||
.accept_first_mouse(true)
|
.accept_first_mouse(true)
|
||||||
.skip_taskbar(true)
|
.skip_taskbar(true)
|
||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
@ -60,12 +62,7 @@ pub fn calculate_popup_size_sync() -> PopupSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 计算弹出框位置
|
/// 计算弹出框位置
|
||||||
pub async fn calculate_placement(
|
pub async fn calculate_placement(target: &TargetLocation, popup_size: &PopupSize, placement: &str, page_size: &tauri::LogicalSize<i32>) -> Result<PopupLocation, String> {
|
||||||
target: &TargetLocation,
|
|
||||||
popup_size: &PopupSize,
|
|
||||||
placement: &str,
|
|
||||||
page_size: &tauri::LogicalSize<i32>,
|
|
||||||
) -> Result<PopupLocation, String> {
|
|
||||||
let margin = 12; // 箭头距离触发元素的距离
|
let margin = 12; // 箭头距离触发元素的距离
|
||||||
let mar = 12; // 距离屏幕边缘的最小距离
|
let mar = 12; // 距离屏幕边缘的最小距离
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<div class="loader" @click="toggleFold" @mousedown="handleMouseDown" @touchstart="handleTouchStart">
|
||||||
|
<div class="loader-inner">
|
||||||
|
<ul class="pages-list">
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<svg class="page-svg" fill="currentColor" viewBox="0 0 90 120">
|
||||||
|
<path
|
||||||
|
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { playPromptTone } from '@/utils/prompt-tone';
|
||||||
|
import { handleMouseDown, handleTouchStart, startWindowX, startWindowY } from '../move-window';
|
||||||
|
import { LogicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
|
const isFold = defineModel<boolean>();
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
// 使用Tauri API直接移动窗口,保持与outerPosition()返回类型一致
|
||||||
|
currentWindow.setSize(new LogicalSize(60, 50));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换折叠状态
|
||||||
|
*/
|
||||||
|
async function toggleFold() {
|
||||||
|
// 获取缩放因子和窗口物理位置
|
||||||
|
const [factor, windowPhysicalPosition] = await Promise.all([currentWindow.scaleFactor(), currentWindow.outerPosition()]);
|
||||||
|
// 转换为逻辑像素位置
|
||||||
|
const windowPosition = windowPhysicalPosition.toLogical(factor);
|
||||||
|
|
||||||
|
const newX = windowPosition.x - startWindowX.value;
|
||||||
|
const newY = windowPosition.y - startWindowY.value;
|
||||||
|
if (Math.abs(newX) < 4 && Math.abs(newY) < 4) {
|
||||||
|
playPromptTone();
|
||||||
|
isFold.value = !isFold.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.loader {
|
||||||
|
--background: linear-gradient(135deg, #23c4f8, #275efe);
|
||||||
|
--text: #6c7486;
|
||||||
|
--page: rgba(255, 255, 255, 0.36);
|
||||||
|
--page-fold: rgba(255, 255, 255, 0.52);
|
||||||
|
--duration: 3s;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image: var(--background);
|
||||||
|
border-radius: 16%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages-list {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item {
|
||||||
|
--r: 180deg;
|
||||||
|
--o: 0;
|
||||||
|
--c: var(--page);
|
||||||
|
position: absolute;
|
||||||
|
top: 10%;
|
||||||
|
left: 10%;
|
||||||
|
width: 40%;
|
||||||
|
height: 80%;
|
||||||
|
color: var(--c);
|
||||||
|
opacity: var(--o);
|
||||||
|
transform: rotateY(var(--r));
|
||||||
|
transform-origin: 100% 50%;
|
||||||
|
animation: var(--duration) ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:nth-child(2) {
|
||||||
|
--c: var(--page-fold);
|
||||||
|
animation-name: page-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:nth-child(3) {
|
||||||
|
--c: var(--page-fold);
|
||||||
|
animation-name: page-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:nth-child(4) {
|
||||||
|
--c: var(--page-fold);
|
||||||
|
animation-name: page-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:nth-child(5) {
|
||||||
|
--c: var(--page-fold);
|
||||||
|
animation-name: page-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:first-child {
|
||||||
|
--r: 0deg;
|
||||||
|
--o: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:last-child {
|
||||||
|
--o: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-2 {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
35%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes page-3 {
|
||||||
|
15% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
35% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
65%,
|
||||||
|
100% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes page-4 {
|
||||||
|
30% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
65%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes page-5 {
|
||||||
|
45% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
65% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
95%,
|
||||||
|
100% {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,385 @@
|
|||||||
|
<template>
|
||||||
|
<div class="menu-container">
|
||||||
|
<div
|
||||||
|
style="box-sizing: border-box; width: 100%; padding: 10px; cursor: pointer"
|
||||||
|
@click="toggleFold()"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@touchstart="handleTouchStart"
|
||||||
|
>
|
||||||
|
<div style="width: 100%; height: 6px; background: rgba(255, 255, 255, 0.6); border-radius: 4px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="time-container">
|
||||||
|
<div class="time-box">
|
||||||
|
<span class="time-text">{{ timeData.hour }}:{{ timeData.minute }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="isShowInteraction">
|
||||||
|
<div class="menu-item" @click="openChoiceTopic()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_01.png" />
|
||||||
|
<span class="menu-text">选择题</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="openRealTimeTopic()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_02.png" />
|
||||||
|
<span class="menu-text">实时题</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="openCompetitionTopic()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_03.png" />
|
||||||
|
<span class="menu-text">竞赛题</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="openJudgeTopic()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_04.png" />
|
||||||
|
<span class="menu-text">判断题</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="openCloudDisk()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_05.png" />
|
||||||
|
<span class="menu-text">云盘</span>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="menu-item" @click="openPk()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_01_06.png" />
|
||||||
|
<span class="menu-text">PK赛</span>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</template>
|
||||||
|
<template v-if="isShowTutorship">
|
||||||
|
<div style="width: calc(100% - 12px); height: 1px; margin: 9px 10px; background: rgba(180, 180, 180, 0.6)"></div>
|
||||||
|
<div class="menu-item tutorship-item" @click="openTutorship()">
|
||||||
|
<img class="menu-icon" style="width: 32px; height: 32px; margin-bottom: 2px" src="@/assets/images/floating-list-window/icon_02_01.png" />
|
||||||
|
<span class="menu-text">评讲</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item tutorship-item" @click="openMeeting()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_02.png" />
|
||||||
|
<span class="menu-text">会议控制</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item tutorship-item" @click="openSubtitle()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_03.png" />
|
||||||
|
<span class="menu-text">实时字幕</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div style="width: calc(100% - 12px); height: 1px; margin: 9px 10px; background: rgba(180, 180, 180, 0.6)"></div>
|
||||||
|
<div class="menu-item" @click="openDrawingBoard()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_03_01.png" />
|
||||||
|
<span class="menu-text">画板</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="switchCourses()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_03_03.png" />
|
||||||
|
<span class="menu-text">切换课程</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="logout()">
|
||||||
|
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_03_04.png" />
|
||||||
|
<span class="menu-text">退出登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { emitTo } from '@tauri-apps/api/event';
|
||||||
|
import { createDrawingBoardWindow, createMainWindow, createMeetingWindow, createSubtitleWindow } from '@/utils/tauri/windows-action';
|
||||||
|
import { windowLabel } from '@/utils/tauri/windows-util';
|
||||||
|
import { tauri_floating_list_page_action } from '@/utils/constant';
|
||||||
|
import { handleMouseDown, handleTouchStart, startWindowX, startWindowY } from '../move-window';
|
||||||
|
|
||||||
|
import { LogicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import { useTauriSessionStorage } from './use-tauri-session-storage';
|
||||||
|
import { playPromptTone } from '@/utils/prompt-tone';
|
||||||
|
import type { FloatingListPageAction } from '@/utils/tauri/types';
|
||||||
|
const isFold = defineModel<boolean>();
|
||||||
|
|
||||||
|
const timeData = ref({ hour: '00', minute: '00' });
|
||||||
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
const isShowInteraction = ref(false);
|
||||||
|
const isShowTutorship = ref(false);
|
||||||
|
|
||||||
|
const { userInfo, currTutorship, currCourse, updateBaseDate } = useTauriSessionStorage();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [currTutorship.value, currCourse.value, userInfo.value],
|
||||||
|
([_currTutorship, _currCourse, _userInfo]) => {
|
||||||
|
let item_num = 4;
|
||||||
|
if (_userInfo) {
|
||||||
|
if (currCourse.value) {
|
||||||
|
isShowInteraction.value = true;
|
||||||
|
item_num += 5;
|
||||||
|
} else {
|
||||||
|
isShowInteraction.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currTutorship) {
|
||||||
|
isShowTutorship.value = true;
|
||||||
|
item_num += 3;
|
||||||
|
} else {
|
||||||
|
isShowTutorship.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isShowInteraction.value = false;
|
||||||
|
isShowTutorship.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总高度 = item高度 * 项目数 + 额外用于移动按钮和时间部分的高度
|
||||||
|
const popup_height = 64 * item_num + 60;
|
||||||
|
const popup_width = 60;
|
||||||
|
currentWindow.setSize(new LogicalSize(popup_width, popup_height));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
updateBaseDate();
|
||||||
|
}, 2000);
|
||||||
|
updateTime(); // 初始化时立即更新
|
||||||
|
scheduleNextUpdate(); // 设置后续更新
|
||||||
|
updateBaseDate();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
timer && clearTimeout(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算到下一秒的毫秒数
|
||||||
|
*/
|
||||||
|
function getDelayToNextSecond(): number {
|
||||||
|
const now = new Date();
|
||||||
|
const delay = 1000 - now.getMilliseconds();
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下一个更新周期
|
||||||
|
*/
|
||||||
|
function scheduleNextUpdate() {
|
||||||
|
const delay = getDelayToNextSecond();
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
updateTime();
|
||||||
|
scheduleNextUpdate(); // 递归调用以继续更新
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前时间数据和格式化字符串
|
||||||
|
*/
|
||||||
|
function updateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
timeData.value = {
|
||||||
|
hour: now.getHours().toString().padStart(2, '0'),
|
||||||
|
minute: now.getMinutes().toString().padStart(2, '0'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换折叠状态
|
||||||
|
*/
|
||||||
|
async function toggleFold() {
|
||||||
|
// 获取缩放因子和窗口物理位置
|
||||||
|
const [factor, windowPhysicalPosition] = await Promise.all([currentWindow.scaleFactor(), currentWindow.outerPosition()]);
|
||||||
|
// 转换为逻辑像素位置
|
||||||
|
const windowPosition = windowPhysicalPosition.toLogical(factor);
|
||||||
|
|
||||||
|
const newX = windowPosition.x - startWindowX.value;
|
||||||
|
const newY = windowPosition.y - startWindowY.value;
|
||||||
|
if (Math.abs(newX) < 4 && Math.abs(newY) < 4) {
|
||||||
|
playPromptTone();
|
||||||
|
isFold.value = !isFold.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开选择题
|
||||||
|
*/
|
||||||
|
async function openChoiceTopic() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-choice-topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实时主观题
|
||||||
|
*/
|
||||||
|
async function openRealTimeTopic() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-real-time-topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 竞赛主观题
|
||||||
|
*/
|
||||||
|
async function openCompetitionTopic() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-competition-topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断题
|
||||||
|
*/
|
||||||
|
async function openJudgeTopic() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-judge-topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开辅导课程
|
||||||
|
*/
|
||||||
|
async function openTutorship() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-tutorship');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开云盘
|
||||||
|
*/
|
||||||
|
async function openCloudDisk() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('open-cloud-disk');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 打开PK赛
|
||||||
|
*/
|
||||||
|
// async function openPk() {
|
||||||
|
// playPromptTone();
|
||||||
|
// await createMainWindow();
|
||||||
|
// sendtPageActionEmit('open-pk');
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换课程
|
||||||
|
*/
|
||||||
|
async function switchCourses() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('switch-courses');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开会议
|
||||||
|
*/
|
||||||
|
async function openMeeting() {
|
||||||
|
playPromptTone();
|
||||||
|
|
||||||
|
await createMeetingWindow(2, currTutorship.value || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开字幕
|
||||||
|
*/
|
||||||
|
async function openSubtitle() {
|
||||||
|
playPromptTone();
|
||||||
|
await createSubtitleWindow(currTutorship.value || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开画板
|
||||||
|
*/
|
||||||
|
async function openDrawingBoard() {
|
||||||
|
playPromptTone();
|
||||||
|
await createDrawingBoardWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
async function logout() {
|
||||||
|
playPromptTone();
|
||||||
|
await createMainWindow();
|
||||||
|
sendtPageActionEmit('logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向某个窗口发送消息
|
||||||
|
*/
|
||||||
|
function sendtPageActionEmit(action: FloatingListPageAction) {
|
||||||
|
emitTo<FloatingListPageAction>(windowLabel.main, tauri_floating_list_page_action, action);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
img {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.time-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow:
|
||||||
|
1px 1px 2px #000000,
|
||||||
|
0 0 4px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-unit {
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow:
|
||||||
|
1px 1px 2px #000000,
|
||||||
|
0 0 4px #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 10px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container .menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container .menu-item .menu-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container .menu-item .menu-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 2px 2px 4px #000000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { isObject } from '@/utils/verify';
|
||||||
|
import { STORAGE_COMMANDS, tauriSessionStorage } from '@/utils/tauri/tauri-session';
|
||||||
|
import type { BriCourse, BriTutorship } from '@/api/select-classroom-type';
|
||||||
|
import type { BriUserInfo } from '@/api/login';
|
||||||
|
import { tauri_store_page_curr_course, tauri_store_page_curr_tutorship, tauri_store_page_user_info } from '@/utils/constant';
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useTauriSessionStorage
|
||||||
|
*/
|
||||||
|
export function useTauriSessionStorage() {
|
||||||
|
const userInfo = ref<BriUserInfo | null>(null);
|
||||||
|
const currTutorship = ref<BriTutorship | null>(null);
|
||||||
|
const currCourse = ref<BriCourse | null>(null);
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
updateBaseDate();
|
||||||
|
// 监听后端存储数据变更
|
||||||
|
currentWindow.listen<[string, string]>(STORAGE_COMMANDS.CHANGE, ({ payload: [key, value] }) => {
|
||||||
|
if (key === tauri_store_page_curr_tutorship) {
|
||||||
|
const _currTutorship = isObject(value) ? value : null;
|
||||||
|
if (_currTutorship && 'id' in _currTutorship) {
|
||||||
|
currTutorship.value = _currTutorship as unknown as BriTutorship;
|
||||||
|
}
|
||||||
|
} else if (key === tauri_store_page_curr_course) {
|
||||||
|
const _currCourse = isObject(value) ? value : null;
|
||||||
|
if (_currCourse && 'id' in _currCourse && 'classId' in _currCourse) {
|
||||||
|
currCourse.value = _currCourse as unknown as BriCourse;
|
||||||
|
}
|
||||||
|
} else if (key === tauri_store_page_user_info) {
|
||||||
|
const _userInfo = isObject(value) ? value : null;
|
||||||
|
if (_userInfo && 'id' in _userInfo && 'schoolId' in _userInfo) {
|
||||||
|
userInfo.value = _userInfo as unknown as BriUserInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听后端数据删除事件
|
||||||
|
currentWindow.listen<string>(STORAGE_COMMANDS.REMOVE, (event) => {
|
||||||
|
const key = event.payload;
|
||||||
|
if (key === tauri_store_page_curr_tutorship) {
|
||||||
|
currTutorship.value = null;
|
||||||
|
} else if (key === tauri_store_page_curr_course) {
|
||||||
|
currCourse.value = null;
|
||||||
|
} else if (key === tauri_store_page_user_info) {
|
||||||
|
userInfo.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听后端清空事件
|
||||||
|
currentWindow.listen(STORAGE_COMMANDS.CLEAR, () => {
|
||||||
|
currTutorship.value = null;
|
||||||
|
currCourse.value = null;
|
||||||
|
userInfo.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟新基础数据
|
||||||
|
*/
|
||||||
|
function updateBaseDate() {
|
||||||
|
tauriSessionStorage.get(tauri_store_page_user_info).then((value) => {
|
||||||
|
const obj = isObject(value) ? value : null;
|
||||||
|
if (obj && 'id' in obj && 'schoolId' in obj) {
|
||||||
|
userInfo.value = obj as unknown as BriUserInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tauriSessionStorage.get(tauri_store_page_curr_tutorship).then((value) => {
|
||||||
|
const obj = isObject(value) ? value : null;
|
||||||
|
if (obj && 'id') {
|
||||||
|
currTutorship.value = obj as unknown as BriTutorship;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tauriSessionStorage.get(tauri_store_page_curr_course).then((value) => {
|
||||||
|
const obj = isObject(value) ? value : null;
|
||||||
|
if (obj && 'id' in obj && 'classId' in obj) {
|
||||||
|
currCourse.value = obj as unknown as BriCourse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** 更新基础数据 */
|
||||||
|
updateBaseDate,
|
||||||
|
userInfo,
|
||||||
|
currTutorship,
|
||||||
|
currCourse,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div style="width: 100vw; height: 100vh; padding: 0; margin: 0">
|
||||||
|
<floating-ball v-if="isFold" v-model="isFold"></floating-ball>
|
||||||
|
<floating-list v-else v-model="isFold"></floating-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import FloatingBall from './aa-components/floating-ball.vue';
|
||||||
|
import FloatingList from './aa-components/floating-list.vue';
|
||||||
|
|
||||||
|
const isFold = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
// 阻止默认的右键菜单
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
img {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
src/other_pages/floating-list-window/main.ts
Normal file
7
src/other_pages/floating-list-window/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './floating-list-window.vue';
|
||||||
|
|
||||||
|
// 创建Vue应用实例
|
||||||
|
const app = createApp(App);
|
||||||
|
// 挂载应用到DOM元素#app上
|
||||||
|
app.mount('#app');
|
||||||
340
src/other_pages/floating-list-window/move-window.ts
Normal file
340
src/other_pages/floating-list-window/move-window.ts
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import { LogicalPosition, currentMonitor, getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 获取当前窗口实例
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
|
||||||
|
/** 当前触摸ID */
|
||||||
|
let activeTouchId: number | null = null;
|
||||||
|
/** 鼠标初始X坐标 */
|
||||||
|
let startMouseX = 0;
|
||||||
|
/** 鼠标初始Y坐标 */
|
||||||
|
let startMouseY = 0;
|
||||||
|
|
||||||
|
/** 触摸初始X坐标 */
|
||||||
|
let startTouchX = 0;
|
||||||
|
/** 触摸初始Y坐标 */
|
||||||
|
let startTouchY = 0;
|
||||||
|
|
||||||
|
/** 窗口初始X坐标 */
|
||||||
|
export const startWindowX = ref(0);
|
||||||
|
/** 窗口初始Y坐标 */
|
||||||
|
export const startWindowY = ref(0);
|
||||||
|
|
||||||
|
/** 屏幕宽度 */
|
||||||
|
let screenWidth = 0;
|
||||||
|
/** 屏幕高度 */
|
||||||
|
let screenHeight = 0;
|
||||||
|
/** 窗口宽度 */
|
||||||
|
let windowWidth = 0;
|
||||||
|
/** 窗口高度 */
|
||||||
|
let windowHeight = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口吸附动画
|
||||||
|
* @param startX - 起始X
|
||||||
|
* @param startY - 起始Y
|
||||||
|
* @param targetX - 目标X
|
||||||
|
* @param targetY - 目标Y
|
||||||
|
*/
|
||||||
|
function animateMove(startX: number, startY: number, targetX: number, targetY: number) {
|
||||||
|
const duration = 300; // 动画持续时间 ms
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤函数 用于移动
|
||||||
|
* @param currentTime - 当前时间
|
||||||
|
*/
|
||||||
|
function step(currentTime: number) {
|
||||||
|
const elapsed = currentTime - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// easeOutCubic 缓动效果
|
||||||
|
const ease = 1 - (1 - progress) ** 3;
|
||||||
|
|
||||||
|
const newX = startX + (targetX - startX) * ease;
|
||||||
|
const newY = startY + (targetY - startY) * ease;
|
||||||
|
|
||||||
|
currentWindow.setPosition(new LogicalPosition(newX, newY));
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动吸附到屏幕边缘
|
||||||
|
*/
|
||||||
|
async function snapToEdge() {
|
||||||
|
try {
|
||||||
|
const monitor = await currentMonitor();
|
||||||
|
if (!monitor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = await currentWindow.scaleFactor();
|
||||||
|
const windowPhysicalPos = await currentWindow.outerPosition();
|
||||||
|
const windowLogicalPos = windowPhysicalPos.toLogical(factor);
|
||||||
|
|
||||||
|
const windowPhysicalSize = await currentWindow.outerSize();
|
||||||
|
const windowLogicalSize = windowPhysicalSize.toLogical(factor);
|
||||||
|
|
||||||
|
const screenLogicalSize = monitor.size.toLogical(factor);
|
||||||
|
|
||||||
|
const currentX = windowLogicalPos.x;
|
||||||
|
const currentY = windowLogicalPos.y;
|
||||||
|
|
||||||
|
const _windowWidth = windowLogicalSize.width;
|
||||||
|
const _screenWidth = screenLogicalSize.width;
|
||||||
|
|
||||||
|
// 计算窗口中心点X坐标
|
||||||
|
const centerX = currentX + _windowWidth / 2;
|
||||||
|
|
||||||
|
let targetX = 0;
|
||||||
|
// 如果中心点在屏幕左半边,吸附到左边;否则吸附到右边
|
||||||
|
if (centerX < _screenWidth / 2) {
|
||||||
|
targetX = 0;
|
||||||
|
} else {
|
||||||
|
targetX = _screenWidth - _windowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行动画移动到目标位置(Y轴保持不变)
|
||||||
|
animateMove(currentX, currentY, targetX, currentY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自动吸附失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************* 鼠标 ********************************************************/
|
||||||
|
/******************************* 鼠标 ********************************************************/
|
||||||
|
/******************************* 鼠标 ********************************************************/
|
||||||
|
/******************************* 鼠标 ********************************************************/
|
||||||
|
/**
|
||||||
|
* 鼠标移动事件处理函数 - 更新窗口位置
|
||||||
|
* @param event - 鼠标事件
|
||||||
|
*/
|
||||||
|
function mouseMoveHandler(event: MouseEvent) {
|
||||||
|
// 阻止默认行为
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// 计算位置偏移
|
||||||
|
const deltaX = event.screenX - startMouseX;
|
||||||
|
const deltaY = event.screenY - startMouseY;
|
||||||
|
|
||||||
|
// 计算新位置
|
||||||
|
let newX = startWindowX.value + deltaX;
|
||||||
|
let newY = startWindowY.value + deltaY;
|
||||||
|
|
||||||
|
// 边界检查:限制在屏幕范围内
|
||||||
|
if (newX < 0) {
|
||||||
|
newX = 0;
|
||||||
|
}
|
||||||
|
if (newX > screenWidth - windowWidth) {
|
||||||
|
newX = screenWidth - windowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newY < 0) {
|
||||||
|
newY = 0;
|
||||||
|
}
|
||||||
|
if (newY > screenHeight - windowHeight) {
|
||||||
|
newY = screenHeight - windowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Tauri API直接移动窗口
|
||||||
|
currentWindow.setPosition(new LogicalPosition(newX, newY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标释放事件处理函数 - 结束拖动
|
||||||
|
* @param event - 鼠标事件
|
||||||
|
*/
|
||||||
|
function mouseUpHandler() {
|
||||||
|
// 移除事件监听器
|
||||||
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.removeEventListener('mouseup', mouseUpHandler);
|
||||||
|
// 触发吸附
|
||||||
|
snapToEdge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标按下事件处理函数 - 开始拖动窗口
|
||||||
|
* @param event - 鼠标事件
|
||||||
|
*/
|
||||||
|
export async function handleMouseDown(event: MouseEvent) {
|
||||||
|
// 确保只处理左键点击 且 目标元素包含 global-move-windows 类 除非 isAll 为 true
|
||||||
|
if (event.button === 0) {
|
||||||
|
// 阻止默认行为,防止事件冒泡
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
// 获取缩放因子、窗口物理位置、窗口物理大小、当前显示器信息
|
||||||
|
const [factor, windowPhysicalPosition, windowPhysicalSize, monitor] = await Promise.all([
|
||||||
|
currentWindow.scaleFactor(),
|
||||||
|
currentWindow.outerPosition(),
|
||||||
|
currentWindow.outerSize(),
|
||||||
|
currentMonitor(),
|
||||||
|
]);
|
||||||
|
// 转换为逻辑像素位置
|
||||||
|
const windowPosition = windowPhysicalPosition.toLogical(factor);
|
||||||
|
const windowSize = windowPhysicalSize.toLogical(factor);
|
||||||
|
|
||||||
|
// 保存窗口大小
|
||||||
|
windowWidth = windowSize.width;
|
||||||
|
windowHeight = windowSize.height;
|
||||||
|
|
||||||
|
// 保存屏幕大小
|
||||||
|
if (monitor) {
|
||||||
|
const screenSize = monitor.size.toLogical(factor);
|
||||||
|
screenWidth = screenSize.width;
|
||||||
|
screenHeight = screenSize.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录窗口初始位置
|
||||||
|
startWindowX.value = windowPosition.x;
|
||||||
|
startWindowY.value = windowPosition.y;
|
||||||
|
// 记录鼠标初始位置
|
||||||
|
startMouseX = event.screenX;
|
||||||
|
startMouseY = event.screenY;
|
||||||
|
|
||||||
|
// 添加鼠标移动和释放事件监听器
|
||||||
|
document.addEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.addEventListener('mouseup', mouseUpHandler);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取窗口位置失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************* 触摸 ********************************************************/
|
||||||
|
/******************************* 触摸 ********************************************************/
|
||||||
|
/******************************* 触摸 ********************************************************/
|
||||||
|
/******************************* 触摸 ********************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸移动事件处理函数 - 更新窗口位置
|
||||||
|
* @param event - 触摸事件
|
||||||
|
*/
|
||||||
|
function touchMoveHandler(event: TouchEvent) {
|
||||||
|
if (activeTouchId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止默认行为,避免页面滚动
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// 找到对应的触摸点,确保是同一个手指(通过触摸ID判断)
|
||||||
|
const touch = Array.from(event.touches).find((t) => t.identifier === activeTouchId);
|
||||||
|
|
||||||
|
// 如果找不到匹配的触摸点(手指已经离开或更换了手指),则不执行拖动操作
|
||||||
|
if (touch) {
|
||||||
|
// 计算位置偏移
|
||||||
|
const deltaX = touch.screenX - startTouchX;
|
||||||
|
const deltaY = touch.screenY - startTouchY;
|
||||||
|
|
||||||
|
// 计算新位置
|
||||||
|
let newX = startWindowX.value + deltaX;
|
||||||
|
let newY = startWindowY.value + deltaY;
|
||||||
|
|
||||||
|
// 边界检查:限制在屏幕范围内
|
||||||
|
if (newX < 0) {
|
||||||
|
newX = 0;
|
||||||
|
}
|
||||||
|
if (newX > screenWidth - windowWidth) {
|
||||||
|
newX = screenWidth - windowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newY < 0) {
|
||||||
|
newY = 0;
|
||||||
|
}
|
||||||
|
if (newY > screenHeight - windowHeight) {
|
||||||
|
newY = screenHeight - windowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Tauri API直接移动窗口
|
||||||
|
currentWindow.setPosition(new LogicalPosition(newX, newY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸结束事件处理函数 - 结束拖动
|
||||||
|
* @param event - 触摸事件
|
||||||
|
*/
|
||||||
|
function touchEndHandler(event: TouchEvent) {
|
||||||
|
if (activeTouchId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查结束的触摸点是否是我们正在跟踪的触摸点
|
||||||
|
const endedTouchId = event.changedTouches[0]?.identifier;
|
||||||
|
if (endedTouchId === activeTouchId) {
|
||||||
|
activeTouchId = null;
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
document.removeEventListener('touchmove', touchMoveHandler);
|
||||||
|
document.removeEventListener('touchend', touchEndHandler);
|
||||||
|
document.removeEventListener('touchcancel', touchEndHandler);
|
||||||
|
|
||||||
|
// 触发吸附
|
||||||
|
snapToEdge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触摸开始事件处理函数 - 开始拖动窗口
|
||||||
|
* @param event - 触摸事件
|
||||||
|
*/
|
||||||
|
export async function handleTouchStart(event: TouchEvent) {
|
||||||
|
// 确保只有一个触摸点 且 目标元素包含 global-move-windows 类 除非 isAll 为 true
|
||||||
|
if (event.touches.length === 1) {
|
||||||
|
// 阻止默认行为,防止页面滚动
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const touch = event.touches[0]!;
|
||||||
|
|
||||||
|
// 记录触摸点ID,用于后续判断是否是同一个手指
|
||||||
|
const touchId = touch.identifier;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取缩放因子、窗口物理位置、窗口物理大小、当前显示器信息
|
||||||
|
const [factor, windowPhysicalPosition, windowPhysicalSize, monitor] = await Promise.all([
|
||||||
|
currentWindow.scaleFactor(),
|
||||||
|
currentWindow.outerPosition(),
|
||||||
|
currentWindow.outerSize(),
|
||||||
|
currentMonitor(),
|
||||||
|
]);
|
||||||
|
// 转换为逻辑像素位置
|
||||||
|
const windowPosition = windowPhysicalPosition.toLogical(factor);
|
||||||
|
const windowSize = windowPhysicalSize.toLogical(factor);
|
||||||
|
|
||||||
|
// 保存窗口大小
|
||||||
|
windowWidth = windowSize.width;
|
||||||
|
windowHeight = windowSize.height;
|
||||||
|
|
||||||
|
// 保存屏幕大小
|
||||||
|
if (monitor) {
|
||||||
|
const screenSize = monitor.size.toLogical(factor);
|
||||||
|
screenWidth = screenSize.width;
|
||||||
|
screenHeight = screenSize.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录窗口初始位置
|
||||||
|
startWindowX.value = windowPosition.x;
|
||||||
|
startWindowY.value = windowPosition.y;
|
||||||
|
|
||||||
|
// 记录触摸初始位置和ID
|
||||||
|
startTouchX = touch.screenX;
|
||||||
|
startTouchY = touch.screenY;
|
||||||
|
activeTouchId = touchId;
|
||||||
|
|
||||||
|
// 添加触摸移动和结束事件监听器
|
||||||
|
document.addEventListener('touchmove', touchMoveHandler);
|
||||||
|
document.addEventListener('touchend', touchEndHandler);
|
||||||
|
document.addEventListener('touchcancel', touchEndHandler);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取窗口位置失败:', error);
|
||||||
|
activeTouchId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/other_pages/popup-window/main.ts
Normal file
7
src/other_pages/popup-window/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './popup-window.vue';
|
||||||
|
|
||||||
|
// 创建Vue应用实例
|
||||||
|
const app = createApp(App);
|
||||||
|
// 挂载应用到DOM元素#app上
|
||||||
|
app.mount('#app');
|
||||||
432
src/other_pages/popup-window/popup-window.vue
Normal file
432
src/other_pages/popup-window/popup-window.vue
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div ref="transitionBoxRef" class="transition-box">
|
||||||
|
<div class="blob"></div>
|
||||||
|
<div class="blob"></div>
|
||||||
|
<div class="blob"></div>
|
||||||
|
<div class="blob"></div>
|
||||||
|
<div class="subject-box">
|
||||||
|
<!-- 画板 -->
|
||||||
|
<div class="menu-item" @click="openMainWindow">
|
||||||
|
<fluent-window24-regular class="menu-icon"></fluent-window24-regular>
|
||||||
|
<span id="drawing-board-text">打开主窗口</span>
|
||||||
|
</div>
|
||||||
|
<!-- 画板 -->
|
||||||
|
<div class="menu-item" @click="openBoardWindow">
|
||||||
|
<fluent-hand-draw24-regular class="menu-icon"></fluent-hand-draw24-regular>
|
||||||
|
<span id="drawing-board-text">打开画板</span>
|
||||||
|
</div>
|
||||||
|
<!-- 退出应用程序 -->
|
||||||
|
<div class="menu-item" @click="exitApp">
|
||||||
|
<solar-power-linear class="menu-icon"> </solar-power-linear>
|
||||||
|
<span>退出应用程序</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||||
|
import FluentWindow24Regular from '~icons/fluent/window-24-regular';
|
||||||
|
import FluentHandDraw24Regular from '~icons/fluent/hand-draw-24-regular';
|
||||||
|
import SolarPowerLinear from '~icons/solar/power-linear';
|
||||||
|
import { playPromptTone } from '@/utils/prompt-tone';
|
||||||
|
import type { Placement } from '@/utils/tauri/windows-util';
|
||||||
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
|
import { exit } from '@tauri-apps/plugin-process';
|
||||||
|
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
import { createDrawingBoardWindow, createMainWindow } from '@/utils/tauri/windows-action';
|
||||||
|
|
||||||
|
const webviewWindow = WebviewWindow.getCurrent();
|
||||||
|
const placement = ref<Placement>('auto');
|
||||||
|
const transitionBoxRef = useTemplateRef('transitionBoxRef');
|
||||||
|
const isExitApp = ref(false);
|
||||||
|
let closeTimeout: NodeJS.Timeout | null = null;
|
||||||
|
const transitionName = computed(() => getTransitionName(placement.value));
|
||||||
|
let unlisten: UnlistenFn | undefined = undefined;
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
onMounted(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
placement.value = (params.get('placement') || 'auto') as Placement;
|
||||||
|
|
||||||
|
webviewWindow.setFocus();
|
||||||
|
webviewWindow
|
||||||
|
.listen('tauri://blur', () => {
|
||||||
|
hideWindow();
|
||||||
|
})
|
||||||
|
.then((_unlisten) => {
|
||||||
|
unlisten = _unlisten;
|
||||||
|
});
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
applyTransitionClass();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unlisten?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用过渡动画类
|
||||||
|
*/
|
||||||
|
function applyTransitionClass() {
|
||||||
|
// 设置过渡时间为0,确保初始状态立即应用
|
||||||
|
transitionBoxRef.value!.style.transitionDuration = `0ms`;
|
||||||
|
transitionBoxRef.value!.classList.add(`${transitionName.value}-enter-active`);
|
||||||
|
transitionBoxRef.value!.classList.add(`${transitionName.value}-enter-from`);
|
||||||
|
transitionBoxRef.value!.style.transitionDuration = `300ms`;
|
||||||
|
transitionBoxRef.value!.classList.remove(`${transitionName.value}-enter-from`);
|
||||||
|
transitionBoxRef.value!.classList.add(`${transitionName.value}-enter-to`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出应用程序
|
||||||
|
*/
|
||||||
|
function exitApp() {
|
||||||
|
playPromptTone();
|
||||||
|
isExitApp.value = true;
|
||||||
|
hideWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开画板窗口
|
||||||
|
*/
|
||||||
|
async function openBoardWindow() {
|
||||||
|
playPromptTone();
|
||||||
|
await createDrawingBoardWindow();
|
||||||
|
hideWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
function openMainWindow() {
|
||||||
|
playPromptTone();
|
||||||
|
createMainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏窗口
|
||||||
|
*/
|
||||||
|
function hideWindow() {
|
||||||
|
closeTimeout && clearTimeout(closeTimeout);
|
||||||
|
transitionBoxRef.value!.style.transitionDuration = `0ms`;
|
||||||
|
transitionBoxRef.value!.classList.remove(`${transitionName.value}-enter-to`);
|
||||||
|
transitionBoxRef.value!.classList.add(`${transitionName.value}-leave-from`);
|
||||||
|
nextTick(() => {
|
||||||
|
transitionBoxRef.value!.style.transitionDuration = `300ms`;
|
||||||
|
transitionBoxRef.value!.classList.remove(`${transitionName.value}-leave-from`);
|
||||||
|
transitionBoxRef.value!.classList.add(`${transitionName.value}-leave-to`);
|
||||||
|
// 动画结束后关闭窗口
|
||||||
|
closeTimeout = setTimeout(() => {
|
||||||
|
handleTransitionEnd();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理过渡动画结束事件
|
||||||
|
*/
|
||||||
|
function handleTransitionEnd() {
|
||||||
|
if (isExitApp.value) {
|
||||||
|
isExitApp.value = false;
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
webviewWindow.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过渡动画名称
|
||||||
|
*/
|
||||||
|
function getTransitionName(_placement: Placement) {
|
||||||
|
switch (_placement) {
|
||||||
|
case 'top':
|
||||||
|
return 'zoom-in-bottom';
|
||||||
|
case 'bottom':
|
||||||
|
return 'zoom-in-top';
|
||||||
|
case 'left':
|
||||||
|
return 'zoom-in-left';
|
||||||
|
case 'right':
|
||||||
|
return 'zoom-in-right';
|
||||||
|
default:
|
||||||
|
return 'fade-in';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@keyframes color-change1 {
|
||||||
|
0% {
|
||||||
|
background: rgb(250, 230, 115);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
background: rgb(250, 151, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: rgb(198, 139, 253);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
background: rgb(130, 214, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: rgb(250, 230, 115);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes color-change2 {
|
||||||
|
0% {
|
||||||
|
background: rgb(198, 139, 253);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
background: rgb(250, 151, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: rgb(250, 230, 115);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
background: rgb(130, 214, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: rgb(198, 139, 253);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes color-change3 {
|
||||||
|
0% {
|
||||||
|
background: rgb(130, 214, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
background: rgb(250, 230, 115);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: rgb(198, 139, 253);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
background: rgb(250, 151, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: rgb(130, 214, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes float-1 {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(70vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes float-2 {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-60vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-box {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #eeeeee;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
filter: blur(30px);
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
animation:
|
||||||
|
float-1 12s ease-in-out infinite,
|
||||||
|
color-change1 16s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: -1;
|
||||||
|
animation:
|
||||||
|
float-1 15s ease-in-out infinite,
|
||||||
|
color-change2 18s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
bottom: 30px;
|
||||||
|
left: 40px;
|
||||||
|
animation:
|
||||||
|
float-2 13s ease-in-out infinite,
|
||||||
|
color-change3 12s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
right: 40px;
|
||||||
|
bottom: 60px;
|
||||||
|
animation:
|
||||||
|
float-2 10s ease-in-out infinite,
|
||||||
|
color-change1 14s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #222222;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(207, 207, 207, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-enter-active,
|
||||||
|
.fade-in-leave-active {
|
||||||
|
transition: opacity 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-enter-from,
|
||||||
|
.fade-in-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-top-enter-active,
|
||||||
|
.zoom-in-top-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
transform-origin: center top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-top-enter-from,
|
||||||
|
.zoom-in-top-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-top-enter-to,
|
||||||
|
.zoom-in-top-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-bottom-enter-active,
|
||||||
|
.zoom-in-bottom-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
transform-origin: center bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-bottom-enter-from,
|
||||||
|
.zoom-in-bottom-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-bottom-enter-to,
|
||||||
|
.zoom-in-bottom-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-left-enter-active,
|
||||||
|
.zoom-in-left-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
transform-origin: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-left-enter-from,
|
||||||
|
.zoom-in-left-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-right-enter-active,
|
||||||
|
.zoom-in-right-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
transform-origin: center right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in-right-enter-from,
|
||||||
|
.zoom-in-right-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user