美化样式

This commit is contained in:
2026-03-16 08:54:03 +08:00
parent 85ea1396bb
commit b850830f3f
5 changed files with 222 additions and 95 deletions

View File

@ -291,10 +291,10 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
const validSocketIds: string[] = [];
for (const result of validationResults) {
if (result.isValid && result.sockets) {
this.server?.to(result.oldSocketId).emit('message', {
type: 'sev_device_conflict',
data: { fromRoomId: roomId, reason: '您已在其他设备进入相同入口', targetUid: shortUid },
} satisfies SucceedMessage);
// this.server?.to(result.oldSocketId).emit('message', {
// type: 'sev_device_conflict',
// data: { fromRoomId: roomId, reason: '您已在其他设备进入相同入口', targetUid: shortUid },
// } satisfies SucceedMessage);
result.sockets.forEach((s) => s.disconnect(true));
validSocketIds.push(result.oldSocketId);
}

View File

@ -1,12 +1,4 @@
/**
* 统一按钮样式规范
* 适用于项目中所有的按钮组件
*/
/* ========================================
会议室控制栏按钮 (.control-btn)
用于底部控制栏的功能按钮(上课/下课、静音、视频等)
======================================== */
// 会议室控制栏按钮 (.control-btn) 用于底部控制栏的功能按钮(上课/下课、静音、视频等)
.control-btn {
display: flex;
flex-direction: column;
@ -19,12 +11,11 @@
line-height: 1.5;
color: var(--el-color-primary);
cursor: pointer;
user-select: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 12px;
transition: all 0.3s ease;
user-select: none;
-webkit-user-select: none;
svg {
width: 24px;
@ -90,8 +81,8 @@
&:hover:not(:disabled) {
background: linear-gradient(135deg, #5568d3 0%, #63408a 100%);
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transform: translateY(-3px);
}
&:active:not(:disabled) {
@ -100,12 +91,10 @@
}
}
/* ========================================
视频用户控制按钮 (.control-icon-btn)
用于视频卡片右上角的图标按钮(设为主讲、禁麦、禁视频、踢人等)
======================================== */
// 视频用户控制按钮 (.control-icon-btn) 用于视频卡片右上角的图标按钮(设为主讲、禁麦、禁视频、踢人等)
.control-icon-btn {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32px;
@ -113,13 +102,11 @@
padding: 0;
color: #ffffff;
cursor: pointer;
user-select: none;
background: rgba(0, 0, 0, 0.6);
border: none;
border-radius: 8px;
transition: all 0.3s ease;
user-select: none;
-webkit-user-select: none;
flex-shrink: 0;
svg {
width: 18px;
@ -177,24 +164,20 @@
}
}
/* ========================================
窗口控制按钮 (.operating-button)
用于窗口标题栏的控制按钮(最小化、最大化、关闭、返回等)
======================================== */
// 窗口控制按钮 (.operating-button) 用于窗口标题栏的控制按钮(最小化、最大化、关闭、返回等)
.operating-button {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
user-select: none;
background-color: var(--button-bg);
border-radius: 50%;
transition: all 0.2s ease;
user-select: none;
-webkit-user-select: none;
flex-shrink: 0;
svg {
width: 12px;
@ -203,8 +186,8 @@
}
&:hover {
transform: scale(1.15);
filter: brightness(1.1);
transform: scale(1.15);
}
&:active {
@ -244,62 +227,53 @@
}
}
/* ========================================
Element Plus 按钮覆盖样式
用于登录页面等使用 el-button 的场景
======================================== */
.el-button {
&.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(93deg, #74e8ff 0%, #306fff 100%);
border: transparent;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
// Element Plus 按钮覆盖样式 用于登录页面等使用 el-button 的场景
.el-button.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(93deg, #74e8ff 0%, #306fff 100%);
border: transparent;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover:not(:disabled) {
background: linear-gradient(93deg, #5dd6ff 0%, #2560e8 100%);
transform: translateY(-2px);
box-shadow: 0 4px 16px 0 rgba(48, 111, 255, 0.3);
}
&:hover:not(:disabled) {
background: linear-gradient(93deg, #5dd6ff 0%, #2560e8 100%);
box-shadow: 0 4px 16px 0 rgba(48, 111, 255, 0.3);
transform: translateY(-2px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
/* ========================================
黑名单解除按钮 (.remove-btn)
用于黑名单列表中的解除按钮
======================================== */
// 黑名单解除按钮 (.remove-btn) 用于黑名单列表中的解除按钮
.remove-btn {
padding: 4px 12px;
font-size: 12px;
line-height: 1.5;
color: #ffffff;
cursor: pointer;
user-select: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 6px;
transition: all 0.3s ease;
user-select: none;
-webkit-user-select: none;
&:hover {
background: linear-gradient(135deg, #5568d3 0%, #63408a 100%);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
transform: translateY(-1px);
}
&:active {
@ -307,9 +281,7 @@
}
}
/* ========================================
响应式适配
======================================== */
// 响应式适配
@media (max-width: 768px) {
.control-btn {
min-width: 56px;

View File

@ -174,7 +174,7 @@
const studentUsers = computed(() => {
const users = meeting.remoteUsers.value as (IAgoraRTCRemoteUser & { userName?: string })[];
const filteredUsers = users.filter((u) => (teacherUid.value ? u.uid !== teacherUid.value : true));
// 按照 studentListForSort 中的顺序重新排列
if (studentListForSort.value.length > 0) {
// 创建 shortId -> student 的映射 (用于排序)
@ -182,7 +182,7 @@
studentListForSort.value.forEach((student, index) => {
studentOrderMap.set(student.shortId, index);
});
// 按照 studentListForSort 的顺序排序,但只包含实际存在的用户
return [...filteredUsers].sort((a, b) => {
const aIndex = studentOrderMap.get(Number(a.uid)) ?? Number.MAX_SAFE_INTEGER;
@ -190,7 +190,7 @@
return aIndex - bIndex;
});
}
return filteredUsers;
});
@ -365,7 +365,7 @@
if (classStatus.value === 'in_class') {
console.log('[监控端] 课程已开始,初始化 Agora...');
await meeting.initAndJoin(roomData);
// ✅ 获取学生信息列表
await fetchStudentList();
} else {
@ -428,7 +428,7 @@
shortUid.value = roomData.shortUid;
teacherUid.value = roomData.teacherUid;
await meeting.initAndJoin(roomData);
// ✅ 获取学生信息列表
await fetchStudentList();
console.log('[监控端] Agora 初始化完成');
@ -564,7 +564,7 @@
.loading-text {
margin-top: 20px;
font-size: 16px;
color: #999;
color: #999999;
}
}

View File

@ -149,14 +149,28 @@
<span class="btn-text">离开</span>
</button>
</div>
<el-drawer v-model="blacklistVisible" title="黑名单" size="30%">
<div v-if="blacklist.length === 0" class="empty">暂无黑名单</div>
<ul v-else class="blacklist">
<li v-for="item in blacklist" :key="item.shortUid" class="blacklist-item">
<span class="user-info">{{ item.userName }} (ID: {{ item.shortUid }})</span>
<button class="remove-btn" @click="removeFromBlacklist(item.shortUid)">解除</button>
</li>
</ul>
<el-drawer v-model="blacklistVisible" title="黑名单" size="30%" closeOnClickModal>
<div v-if="blacklist.length === 0" class="empty">
<icon-mdi-account-off class="empty-icon"></icon-mdi-account-off>
<span class="empty-text">暂无黑名单</span>
</div>
<el-scrollbar v-else class="blacklist-scrollbar">
<ul class="blacklist">
<li v-for="item in blacklist" :key="item.shortUid" class="blacklist-item">
<div class="user-avatar">
<icon-mdi-account class="avatar-icon"></icon-mdi-account>
</div>
<div class="user-details">
<span class="user-name">{{ item.userName }}</span>
<span class="user-id">ID: {{ item.shortUid }}</span>
</div>
<button class="remove-btn" @click="removeFromBlacklist(item.shortUid)">
<icon-mdi-delete class="remove-icon"></icon-mdi-delete>
<span>解除</span>
</button>
</li>
</ul>
</el-scrollbar>
</el-drawer>
<!-- 学生列表排序弹窗 -->
<student-sort-dialog v-model="studentSortDialogVisible" :roomId="courseRoomId" :students="studentListForSort"></student-sort-dialog>
@ -168,6 +182,8 @@
import { playPromptTone } from '@/utils/prompt-tone';
import { useResizeObserver } from '@vueuse/core';
import IconMdiAccount from '~icons/mdi/account';
import IconMdiAccountOff from '~icons/mdi/account-off';
import IconMdiDelete from '~icons/mdi/delete';
import IconMdiExitToApp from '~icons/mdi/exit-to-app';
import IconMdiLoading from '~icons/mdi/loading';
import IconMdiMicrophone from '~icons/mdi/microphone';
@ -1078,7 +1094,7 @@
.loading-text {
margin-top: 20px;
font-size: 16px;
color: #999;
color: #999999;
}
}
@ -1397,4 +1413,142 @@
// 使用统一按钮样式,移除重复的 .control-btn 定义
}
// 黑名单抽屉样式
:deep(.el-drawer__header) {
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.el-drawer__title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
:deep(.el-drawer__body) {
padding: 0;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
.empty-icon {
width: 80px;
height: 80px;
margin-bottom: 16px;
color: #c0c4cc;
}
.empty-text {
font-size: 14px;
}
}
.blacklist-scrollbar {
max-height: calc(100vh - 120px);
padding: 16px;
}
.blacklist {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0;
margin: 0;
list-style: none;
.blacklist-item {
display: flex;
gap: 12px;
align-items: center;
padding: 12px 16px;
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:hover {
background: #f5f7fa;
border-color: #667eea;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
transform: translateX(4px);
}
.user-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
.avatar-icon {
width: 24px;
height: 24px;
color: #ffffff;
}
}
.user-details {
display: flex;
flex: 1;
flex-direction: column;
gap: 4px;
min-width: 0;
.user-name {
overflow: hidden;
font-size: 14px;
font-weight: 500;
color: #303133;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-id {
font-size: 12px;
color: #909399;
}
}
.remove-btn {
display: flex;
gap: 4px;
align-items: center;
padding: 6px 12px;
font-size: 12px;
color: #ffffff;
white-space: nowrap;
cursor: pointer;
background: linear-gradient(135deg, #f56c6c 0%, #e74c3c 100%);
border: none;
border-radius: 6px;
transition: all 0.3s ease;
.remove-icon {
width: 14px;
height: 14px;
}
&:hover {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.4);
transform: scale(1.05);
}
&:active {
transform: scale(0.98);
}
}
}
}
</style>

View File

@ -109,8 +109,8 @@
<style scoped lang="scss">
.student-sort-container {
padding: 10px 0;
position: relative;
padding: 10px 0;
}
.slick-list {
@ -123,16 +123,17 @@
align-items: center;
padding: 12px 16px;
margin-bottom: 8px;
background: #f5f7fa;
border-radius: 8px;
cursor: move;
border: 2px solid transparent;
user-select: none;
background: #f5f7fa;
border: 1px solid #409eff;
border-radius: 8px;
box-shadow: 0 6px 16px rgba(54, 137, 246, 0.12);
.drag-handle {
margin-right: 12px;
font-size: 20px;
color: #909399;
margin-right: 12px;
cursor: grab;
}
@ -142,12 +143,12 @@
justify-content: center;
width: 28px;
height: 28px;
background: #409eff;
color: #fff;
border-radius: 50%;
font-weight: bold;
margin-right: 12px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
background: #409eff;
border-radius: 50%;
}
.student-name {
@ -164,7 +165,7 @@
.dialog-footer {
display: flex;
justify-content: center;
gap: 16px;
justify-content: center;
}
</style>