fix xem man hinh:
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m25s
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m25s
This commit is contained in:
@@ -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')}
|
||||
>
|
||||
×
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user