fix xem man hinh:
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m25s

This commit is contained in:
2026-07-13 15:09:12 +07:00
parent 5ab5ed1454
commit 58834c3255

View File

@@ -20,6 +20,17 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
const [searchQuery, setSearchQuery] = useState<string>('');
const [onlyOnline, setOnlyOnline] = useState<boolean>(false);
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [zoomedStudent, setZoomedStudent] = useState<ExamRoomStudent | null>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setZoomedStudent(null);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
const wsRef = useRef<WebSocket | null>(null);
const subscribedRef = useRef<Set<number>>(new Set());
@@ -31,9 +42,13 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
);
const onlineKey = useMemo(() => onlineIds.join(','), [onlineIds]);
const onlineIdsRef = useRef<number[]>(onlineIds);
onlineIdsRef.current = onlineIds;
const syncSubscriptions = (ws: WebSocket) => {
if (ws.readyState !== WebSocket.OPEN) return;
const target = new Set(onlineIds);
const currentOnlineIds = onlineIdsRef.current;
const target = new Set(currentOnlineIds);
const prev = subscribedRef.current;
prev.forEach((id) => {
@@ -42,7 +57,7 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
prev.delete(id);
}
});
onlineIds.forEach((id) => {
currentOnlineIds.forEach((id) => {
if (!prev.has(id)) {
ws.send(JSON.stringify({ event: 'teacher:subscribe', data: { studentId: id, mode: 'grid' } }));
prev.add(id);
@@ -321,7 +336,14 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
<div className="grid-proctor-card-body">
{isOnline ? (
<div className="proctor-frame-container">
<div
className="proctor-frame-container cursor-zoom-in"
onClick={(e) => {
e.stopPropagation();
setZoomedStudent(s);
}}
style={{ cursor: 'zoom-in' }}
>
{screenFrame ? (
<img
src={screenFrame}
@@ -389,6 +411,173 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
</div>
)}
</div>
{zoomedStudent && (
<div
className="zoomed-proctor-overlay"
onClick={() => setZoomedStudent(null)}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(10, 15, 30, 0.85)',
backdropFilter: 'blur(8px)',
zIndex: 9999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
animation: 'fadeIn 0.2s ease-out',
}}
>
<div
className="zoomed-proctor-modal"
onClick={(e) => e.stopPropagation()}
style={{
width: '100%',
maxWidth: '1200px',
backgroundColor: '#161d2a',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '16px',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)',
display: 'flex',
flexDirection: 'column',
maxHeight: '90vh',
overflow: 'hidden',
}}
>
{/* Modal Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem 1.5rem',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
backgroundColor: '#1c2436',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: onlineIds.includes(zoomedStudent.studentRkId) ? '#10b981' : '#ef4444',
boxShadow: onlineIds.includes(zoomedStudent.studentRkId) ? '0 0 8px #10b981' : 'none',
}}
/>
<h3 style={{ margin: 0, color: '#fff', fontSize: '1.1rem', fontWeight: 600 }}>
{zoomedStudent.fullName}
</h3>
<span
style={{
fontFamily: 'monospace',
fontSize: '0.85rem',
color: '#8f9cae',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
padding: '2px 8px',
borderRadius: '4px',
}}
>
{zoomedStudent.studentCode}
</span>
</div>
<button
onClick={() => setZoomedStudent(null)}
style={{
background: 'none',
border: 'none',
color: '#8f9cae',
fontSize: '1.5rem',
cursor: 'pointer',
padding: '4px 8px',
lineHeight: 1,
transition: 'color 0.2s',
}}
onMouseOver={(e) => (e.currentTarget.style.color = '#fff')}
onMouseOut={(e) => (e.currentTarget.style.color = '#8f9cae')}
>
&times;
</button>
</div>
{/* Modal Body */}
<div
style={{
flex: 1,
padding: '1.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0d131f',
position: 'relative',
minHeight: '400px',
overflow: 'hidden',
}}
>
{screenFrames[zoomedStudent.studentRkId] ? (
<div style={{ position: 'relative', width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img
src={screenFrames[zoomedStudent.studentRkId]}
alt="Zoomed Screen"
style={{
maxWidth: '100%',
maxHeight: '70vh',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid rgba(255, 255, 255, 0.08)',
}}
/>
{showWebcamOverlay && webcamFrames[zoomedStudent.studentRkId] && (
<div
style={{
position: 'absolute',
bottom: '20px',
right: '20px',
width: '240px',
aspectRatio: '4/3',
borderRadius: '8px',
border: '2px solid rgba(255, 255, 255, 0.2)',
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.4)',
overflow: 'hidden',
backgroundColor: '#000',
}}
>
<img
src={webcamFrames[zoomedStudent.studentRkId]}
alt="Zoomed Webcam"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
)}
</div>
) : (
<div style={{ color: '#8f9cae', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<div className="sync-spinner" style={{ width: '32px', height: '32px', borderWidth: '3px', marginBottom: '0.5rem' }} />
<span>Đang tải màn hình...</span>
</div>
)}
</div>
</div>
</div>
)}
<style>{`
.cursor-zoom-in {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.cursor-zoom-in:hover {
transform: scale(1.015);
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}</style>
</div>
);
};