```
refactor(meeting): 优化悬浮窗交互和会议控制功能 - 移除废弃的环境配置模块文件 - 简化悬浮球组件的折叠逻辑,移除复杂的窗口位置计算 - 将静态图片图标替换为动态图标组件,提升视觉效果 - 重构悬浮列表窗口的大小调整逻辑,实现折叠展开平滑过渡 - 优化教师端全员静音功能,使用displayUsers替代studentUsers - 调整会议控制事件处理函数的调用方式,提升响应性能 - 添加窗口边界检测,确保折叠展开时窗口始终可见 ```
This commit is contained in:
11
src-tauri/src/env/mod.rs
vendored
11
src-tauri/src/env/mod.rs
vendored
@ -1,11 +0,0 @@
|
|||||||
//! 环境配置模块
|
|
||||||
// const TEACHERCONTROL_DEV_URL: &str = "https://jk.qyzhjy.com/TeacherControl_bak.shtml";
|
|
||||||
const TEACHERCONTROL_DEV_URL: &str = "https://socket.qyzhjy.com/JKList/TeacherControl_bak.shtml";
|
|
||||||
const TEACHERCONTROL_PROD_URL: &str = "https://jk.qyzhjy.com/TeacherControl_bak.shtml";
|
|
||||||
|
|
||||||
/// 实时字幕地址
|
|
||||||
pub const TEACHERCONTROL_JK_URL: &str = if cfg!(feature = "development") || cfg!(feature = "prod_150_8080") {
|
|
||||||
TEACHERCONTROL_DEV_URL
|
|
||||||
} else {
|
|
||||||
TEACHERCONTROL_PROD_URL
|
|
||||||
};
|
|
||||||
@ -51,29 +51,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { playPromptTone } from '@/utils/prompt-tone';
|
import { playPromptTone } from '@/utils/prompt-tone';
|
||||||
import { handleMouseDown, handleTouchStart, startWindowX, startWindowY } from '../move-window';
|
import { handleMouseDown, handleTouchStart } from '../move-window';
|
||||||
import { LogicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
|
||||||
|
|
||||||
const isFold = defineModel<boolean>();
|
const isFold = defineModel<boolean>();
|
||||||
const currentWindow = getCurrentWindow();
|
|
||||||
// 使用Tauri API直接移动窗口,保持与outerPosition()返回类型一致
|
|
||||||
currentWindow.setSize(new LogicalSize(60, 50));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换折叠状态
|
* 切换折叠状态
|
||||||
*/
|
*/
|
||||||
async function toggleFold() {
|
function toggleFold() {
|
||||||
// 获取缩放因子和窗口物理位置
|
playPromptTone();
|
||||||
const [factor, windowPhysicalPosition] = await Promise.all([currentWindow.scaleFactor(), currentWindow.outerPosition()]);
|
isFold.value = !isFold.value;
|
||||||
// 转换为逻辑像素位置
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -11,19 +11,19 @@
|
|||||||
<div style="width: calc(100% - 12px); height: 1px; margin: 9px 10px; background: rgba(180, 180, 180, 0.6)"></div>
|
<div style="width: calc(100% - 12px); height: 1px; margin: 9px 10px; background: rgba(180, 180, 180, 0.6)"></div>
|
||||||
<!-- 会议控制按钮 -->
|
<!-- 会议控制按钮 -->
|
||||||
<div class="menu-item" @click="muteAllStudents()">
|
<div class="menu-item" @click="muteAllStudents()">
|
||||||
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_04.png" />
|
<icon-mdi-volume-high class="menu-icon"></icon-mdi-volume-high>
|
||||||
<span class="menu-text">全员静音</span>
|
<span class="menu-text">全员静音</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item" @click="muteTeacher()">
|
<div class="menu-item" @click="muteTeacher()">
|
||||||
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_05.png" />
|
<icon-mdi-microphone-off class="menu-icon"></icon-mdi-microphone-off>
|
||||||
<span class="menu-text">老师静音</span>
|
<span class="menu-text">老师静音</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item" @click="endClass()">
|
<div class="menu-item" @click="endClass()">
|
||||||
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_06.png" />
|
<icon-mdi-school class="menu-icon"></icon-mdi-school>
|
||||||
<span class="menu-text">下课</span>
|
<span class="menu-text">下课</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item" @click="openConsole()">
|
<div class="menu-item" @click="openConsole()">
|
||||||
<img class="menu-icon" src="@/assets/images/floating-list-window/icon_02_07.png" />
|
<icon-mdi-console class="menu-icon"></icon-mdi-console>
|
||||||
<span class="menu-text">控制台</span>
|
<span class="menu-text">控制台</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -32,31 +32,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { emitTo } from '@tauri-apps/api/event';
|
import { emitTo } from '@tauri-apps/api/event';
|
||||||
import { windowLabel } from '@/utils/tauri/windows-util';
|
import { windowLabel } from '@/utils/tauri/windows-util';
|
||||||
import { tauri_floating_list_page_action } from '@/utils/constant';
|
import { handleMouseDown, handleTouchStart } from '../move-window';
|
||||||
import { handleMouseDown, handleTouchStart, startWindowX, startWindowY } from '../move-window';
|
|
||||||
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
||||||
import { playPromptTone } from '@/utils/prompt-tone';
|
import { playPromptTone } from '@/utils/prompt-tone';
|
||||||
import type { FloatingListPageAction } from '@/utils/tauri/types';
|
import { createMainWindow } from '@/utils/tauri/windows-action';
|
||||||
const isFold = defineModel<boolean>();
|
|
||||||
|
|
||||||
const currentWindow = getCurrentWindow();
|
// 导入图标组件
|
||||||
|
import IconMdiVolumeHigh from '~icons/mdi/volume-high';
|
||||||
|
import IconMdiMicrophoneOff from '~icons/mdi/microphone-off';
|
||||||
|
import IconMdiSchool from '~icons/mdi/school';
|
||||||
|
import IconMdiConsole from '~icons/mdi/console';
|
||||||
|
const isFold = defineModel<boolean>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换折叠状态
|
* 切换折叠状态
|
||||||
*/
|
*/
|
||||||
async function toggleFold() {
|
function toggleFold() {
|
||||||
// 获取缩放因子和窗口物理位置
|
isFold.value = !isFold.value;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 全员静音 (不包括老师)
|
* 全员静音 (不包括老师)
|
||||||
@ -98,13 +90,6 @@
|
|||||||
playPromptTone();
|
playPromptTone();
|
||||||
await createMainWindow();
|
await createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 向某个窗口发送消息
|
|
||||||
*/
|
|
||||||
function sendtPageActionEmit(action: FloatingListPageAction) {
|
|
||||||
emitTo<FloatingListPageAction>(windowLabel.main, tauri_floating_list_page_action, action);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -183,11 +168,12 @@
|
|||||||
.menu-container .menu-item .menu-icon {
|
.menu-container .menu-item .menu-icon {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
color: #ffffff; // 白色图标,与深色背景形成对比
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-container .menu-item .menu-text {
|
.menu-container .menu-item .menu-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #ffffff;
|
color: #ffffff; // 白色文字
|
||||||
text-shadow: 2px 2px 4px #000000;
|
text-shadow: 2px 2px 4px #000000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -6,11 +6,115 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { LogicalSize, PhysicalPosition, currentMonitor, getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import FloatingBall from './aa-components/floating-ball.vue';
|
import FloatingBall from './aa-components/floating-ball.vue';
|
||||||
import FloatingList from './aa-components/floating-list.vue';
|
import FloatingList from './aa-components/floating-list.vue';
|
||||||
|
|
||||||
const isFold = ref(false);
|
const isFold = ref(false);
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置折叠状态下的窗口大小
|
||||||
|
*/
|
||||||
|
async function setFoldedWindowSize() {
|
||||||
|
const factor = await currentWindow.scaleFactor();
|
||||||
|
const windowPhysicalPos = await currentWindow.outerPosition();
|
||||||
|
const windowLogicalPos = windowPhysicalPos.toLogical(factor);
|
||||||
|
|
||||||
|
// 折叠时设置为较小的固定大小
|
||||||
|
const foldedWidth = 60;
|
||||||
|
const foldedHeight = 50;
|
||||||
|
|
||||||
|
// 设置窗口大小
|
||||||
|
currentWindow.setSize(new LogicalSize(foldedWidth, foldedHeight));
|
||||||
|
|
||||||
|
// 获取显示器信息以确保窗口可见
|
||||||
|
const monitor = await currentMonitor();
|
||||||
|
if (monitor) {
|
||||||
|
const screenLogicalSize = monitor.size.toLogical(factor);
|
||||||
|
|
||||||
|
// 确保窗口在屏幕范围内
|
||||||
|
const newY = Math.min(windowLogicalPos.y, screenLogicalSize.height - foldedHeight);
|
||||||
|
const finalY = Math.max(0, newY);
|
||||||
|
|
||||||
|
if (finalY !== windowLogicalPos.y) {
|
||||||
|
await currentWindow.setPosition(new PhysicalPosition(windowLogicalPos.x * factor, finalY * factor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新窗口大小并确保完全可见 (从折叠状态展开时使用)
|
||||||
|
*/
|
||||||
|
async function expandWindowSizeFromFolded() {
|
||||||
|
// 获取当前显示器信息
|
||||||
|
const monitor = await currentMonitor();
|
||||||
|
if (!monitor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = await currentWindow.scaleFactor();
|
||||||
|
|
||||||
|
// 关键:先获取折叠状态下的窗口位置 (此时还未改变大小)
|
||||||
|
const windowPhysicalPos = await currentWindow.outerPosition();
|
||||||
|
const windowLogicalPos = windowPhysicalPos.toLogical(factor);
|
||||||
|
|
||||||
|
// 计算展开后的窗口大小:4 个按钮 (全员静音、老师静音、下课、控制台)
|
||||||
|
const item_num = 4;
|
||||||
|
const popup_height = 64 * item_num + 60; // 316px
|
||||||
|
const popup_width = 60;
|
||||||
|
|
||||||
|
// 关键修复:增加额外余量,避免因为 padding/border 导致内容显示不全
|
||||||
|
const extraPadding = 8; // 上下各留 4px 余量
|
||||||
|
const actualHeight = popup_height + extraPadding; // 324px
|
||||||
|
|
||||||
|
// 获取屏幕尺寸 (逻辑像素)
|
||||||
|
const screenLogicalSize = monitor.size.toLogical(factor);
|
||||||
|
|
||||||
|
// 关键:使用展开后的高度重新计算位置,确保窗口完全可见
|
||||||
|
// 如果当前 Y 坐标 + 展开后的高度 > 屏幕高度,则需要向上移动
|
||||||
|
const maxY = screenLogicalSize.height - actualHeight;
|
||||||
|
let finalY = windowLogicalPos.y;
|
||||||
|
|
||||||
|
if (finalY > maxY) {
|
||||||
|
// 窗口底部超出屏幕,向上移动到刚好显示的位置
|
||||||
|
finalY = maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保不小于 0
|
||||||
|
finalY = Math.max(0, finalY);
|
||||||
|
|
||||||
|
// 关键修复:先移动到安全位置,再改变窗口大小
|
||||||
|
// 转换为物理像素
|
||||||
|
const physicalX = Math.round(windowLogicalPos.x * factor);
|
||||||
|
const physicalY = Math.round(finalY * factor);
|
||||||
|
console.log(' 最终 physicalY:', physicalY, factor);
|
||||||
|
|
||||||
|
// 第二步:再设置展开后的窗口大小 (使用实际高度)
|
||||||
|
await currentWindow.setSize(new LogicalSize(popup_width, actualHeight));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 第一步:先移动到目标位置 (此时窗口还是折叠状态的小尺寸)
|
||||||
|
currentWindow.setPosition(new PhysicalPosition(physicalX, physicalY));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听折叠状态变化,调整窗口大小并确保完全可见
|
||||||
|
watch(
|
||||||
|
() => isFold.value,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
// 折叠成悬浮球:设置小窗口并确保可见
|
||||||
|
await setFoldedWindowSize();
|
||||||
|
} else {
|
||||||
|
// 展开成列表:设置完整窗口大小并确保完全可见
|
||||||
|
// 从折叠状态展开时,使用专门的方法处理位置调整
|
||||||
|
await expandWindowSizeFromFolded();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('contextmenu', (e) => {
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
|||||||
@ -896,46 +896,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理来自悬浮窗的会议控制事件
|
|
||||||
* @param action - 控制动作
|
|
||||||
*/
|
|
||||||
async function handleMeetingControl(action: string): Promise<void> {
|
|
||||||
console.log('[教师端] 处理会议控制动作:', action);
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'mute_all_students': {
|
|
||||||
// 全员静音 (不包括老师)
|
|
||||||
await muteAllStudents();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'mute_teacher': {
|
|
||||||
// 老师静音
|
|
||||||
await muteTeacher();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'end_class': {
|
|
||||||
// 下课
|
|
||||||
await endClassAndLeave();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
console.warn('[教师端] 未知的控制动作:', action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全员静音 (不包括老师)
|
* 全员静音 (不包括老师)
|
||||||
*/
|
*/
|
||||||
async function muteAllStudents(): Promise<void> {
|
function muteAllStudents(): void {
|
||||||
if (!roomId.value) {
|
if (!roomId.value) {
|
||||||
Toast.warning('房间未初始化');
|
Toast.warning('房间未初始化');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 遍历所有学生用户,发送禁麦消息
|
// 遍历所有学生用户,发送禁麦消息
|
||||||
const studentUids = studentUsers.value.map((u) => u.uid);
|
const studentUids = displayUsers.value.map((u: DisplayUser) => u.uid);
|
||||||
for (const uid of studentUids) {
|
for (const uid of studentUids) {
|
||||||
socket.emit('client_mute_audio', {
|
socket.emit('client_mute_audio', {
|
||||||
roomId: roomId.value,
|
roomId: roomId.value,
|
||||||
@ -952,12 +924,12 @@
|
|||||||
/**
|
/**
|
||||||
* 老师静音
|
* 老师静音
|
||||||
*/
|
*/
|
||||||
async function muteTeacher(): Promise<void> {
|
function muteTeacher(): void {
|
||||||
if (!roomId.value || !shortUid.value) {
|
if (!roomId.value || !shortUid.value) {
|
||||||
Toast.warning('房间未初始化');
|
Toast.warning('房间未初始化');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 给自己发送禁麦消息
|
// 给自己发送禁麦消息
|
||||||
socket.emit('client_mute_audio', {
|
socket.emit('client_mute_audio', {
|
||||||
@ -971,6 +943,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理来自悬浮窗的会议控制事件
|
||||||
|
* @param action - 控制动作
|
||||||
|
*/
|
||||||
|
async function handleMeetingControl(action: string): Promise<void> {
|
||||||
|
console.log('[教师端] 处理会议控制动作:', action);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'mute_all_students': {
|
||||||
|
// 全员静音 (不包括老师)
|
||||||
|
muteAllStudents();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mute_teacher': {
|
||||||
|
// 老师静音
|
||||||
|
muteTeacher();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'end_class': {
|
||||||
|
// 下课
|
||||||
|
await endClassAndLeave();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.warn('[教师端] 未知的控制动作:', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件挂载生命周期钩子
|
* 组件挂载生命周期钩子
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user