refactor(meeting): 优化悬浮窗交互和会议控制功能

- 移除废弃的环境配置模块文件
- 简化悬浮球组件的折叠逻辑,移除复杂的窗口位置计算
- 将静态图片图标替换为动态图标组件,提升视觉效果
- 重构悬浮列表窗口的大小调整逻辑,实现折叠展开平滑过渡
- 优化教师端全员静音功能,使用displayUsers替代studentUsers
- 调整会议控制事件处理函数的调用方式,提升响应性能
- 添加窗口边界检测,确保折叠展开时窗口始终可见
```
This commit is contained in:
2026-03-15 13:45:56 +08:00
parent a3b31f0ff9
commit b262cfac14
5 changed files with 158 additions and 92 deletions

View File

@ -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
};

View File

@ -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>

View File

@ -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>

View File

@ -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) => {

View File

@ -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);
}
}
/** /**
* 组件挂载生命周期钩子 * 组件挂载生命周期钩子
*/ */