This commit is contained in:
2026-06-30 09:31:33 +07:00
parent c736326162
commit bbf8336664
77 changed files with 12601 additions and 556 deletions

368
client/frontend/src/main.js Normal file
View File

@@ -0,0 +1,368 @@
import './style.css';
import './app.css';
// Trạng thái cục bộ
let loggedIn = false;
let studentInfo = null;
let stats = {
wifiSSID: '',
onlineSecs: 0,
offlineSecs: 0,
wsConnected: false,
serverReachable: false,
allowedApps: '',
monitorMode: '',
monitorLabel: '',
className: '',
classCode: '',
currentPeriod: 0,
currentCourseName: '',
shifts: []
};
// Quản lý webcam
let webcamStream = null;
let webcamInterval = null;
const webcamVideo = document.createElement('video');
webcamVideo.autoplay = true;
webcamVideo.playsInline = true;
webcamVideo.style.display = 'none';
document.body.appendChild(webcamVideo);
const webcamCanvas = document.createElement('canvas');
webcamCanvas.width = 320;
webcamCanvas.height = 240;
webcamCanvas.style.display = 'none';
document.body.appendChild(webcamCanvas);
function init() {
if (typeof window.go === 'undefined' || typeof window.go.main === 'undefined') {
setTimeout(init, 200);
return;
}
window.runtime.EventsOn('start_webcam_stream', startWebcam);
window.runtime.EventsOn('stop_webcam_stream', stopWebcam);
checkLogin();
}
async function checkLogin() {
try {
const isLogged = await window.go.main.App.CheckLoginStatus();
if (isLogged) {
loggedIn = true;
studentInfo = await window.go.main.App.GetStudentInfo();
renderDashboard();
startStatsTicker();
} else {
loggedIn = false;
renderLoginPrompt();
}
} catch (err) {
console.error('Error checking login status:', err);
}
}
function renderLoginPrompt() {
document.querySelector('#app').innerHTML = `
<div class="login-prompt-container">
<div class="card">
<div class="login-brand-row">
<div class="login-logo">RE</div>
<div class="login-brand-text">
<div class="login-brand-name">Simple Care</div>
<div class="login-brand-sub">Rikkei Education</div>
</div>
</div>
<h2 class="login-title">Đăng nhập sinh viên</h2>
<p class="login-desc">
Ứng dụng sẽ chuyển hướng bạn đến trang Rikkei Portal. Sau khi đăng nhập thành công, hệ thống sẽ tự động đồng bộ tài khoản học tập của bạn.
</p>
<button class="btn btn-primary" id="btn-goto-login" style="width: 100%; padding: 0.75rem;">
Đi đến trang đăng nhập
</button>
</div>
</div>
`;
document.getElementById('btn-goto-login').addEventListener('click', () => {
window.go.main.App.NavigateToLogin();
});
}
function monitorModeMeta(mode) {
switch (mode) {
case 'learning':
return { icon: '🛡️', label: 'Đang giám sát', cls: 'mode-learning' };
case 'exam':
return { icon: '📝', label: 'Phòng thi', cls: 'mode-exam' };
case 'outside_schedule':
return { icon: '😴', label: 'Ngoài giờ học', cls: 'mode-outside' };
case 'not_configured':
return { icon: '⚙️', label: 'Chưa sẵn sàng', cls: 'mode-config' };
default:
return { icon: '⏳', label: 'Đang kết nối', cls: 'mode-pending' };
}
}
function attendanceClass(status) {
if (status === 4) return 'att-ok';
if (status === 3) return 'att-late';
if (status === 1 || status === 2) return 'att-leave';
if (status === 0) return 'att-absent';
return 'att-pending';
}
function renderShiftsTable(shifts) {
if (!shifts || shifts.length === 0) {
return '<div class="shifts-empty">Hôm nay không có ca học theo lịch lớp.</div>';
}
return `
<table class="shifts-table">
<thead>
<tr>
<th>Ca</th>
<th>Môn học</th>
<th>Giờ</th>
<th>Trực tuyến</th>
<th>Ngoại tuyến</th>
<th>Điểm danh</th>
</tr>
</thead>
<tbody>
${shifts.map(s => `
<tr class="${s.isActiveNow ? 'shift-row-active' : ''}">
<td><strong>Ca ${s.period}</strong>${s.isActiveNow ? ' <span class="shift-now">đang học</span>' : ''}</td>
<td>${s.courseName || '—'}</td>
<td class="shift-time">${s.startTime}${s.endTime}</td>
<td class="time-online">${formatDuration(s.onlineSeconds || 0)}</td>
<td class="time-offline">${formatDuration(s.offlineSeconds || 0)}</td>
<td><span class="att-badge ${attendanceClass(s.attendanceStatus)}">${s.attendanceLabel || 'Chưa tính'}</span></td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function renderDashboard() {
if (!studentInfo) return;
const mode = monitorModeMeta(stats.monitorMode);
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
document.querySelector('#app').innerHTML = `
<div class="dashboard-wrapper">
<header class="header">
<div class="brand">
<div class="brand-logo">RE</div>
<div class="brand-text">
<span class="brand-name">Simple Care</span>
<span class="brand-sub">Rikkei Education</span>
</div>
</div>
<button class="btn btn-logout" id="btn-logout">Đăng xuất</button>
</header>
<div class="main-grid">
<div class="card profile-card">
<div class="avatar-container">
<img src="${studentInfo.avatar || 'https://via.placeholder.com/150'}" alt="Avatar" class="avatar" />
<div class="status-indicator ${stats.serverReachable ? 'online' : 'offline'}"></div>
</div>
<h2 class="student-name">${studentInfo.fullName}</h2>
<div class="student-code">${studentInfo.studentCode}</div>
<p class="student-email">${studentInfo.email}</p>
<div class="divider"></div>
<div class="profile-info-row">
<span>Lớp</span>
<strong id="profile-class">${classLine}</strong>
</div>
<div class="profile-info-row">
<span>Điện thoại</span>
<strong>${studentInfo.phone || 'Chưa cung cấp'}</strong>
</div>
</div>
<div class="right-column">
<div class="card status-banner ${mode.cls}">
<div class="status-banner-icon" id="status-icon">${mode.icon}</div>
<div class="status-banner-main">
<div class="status-banner-top">
<span class="monitor-mode-tag" id="monitor-mode-tag">${mode.label}</span>
<span class="status-class-line" id="status-class-line">${classLine}</span>
</div>
<div class="status-banner-value" id="status-message">${stats.monitorLabel || stats.statusMsg || 'Đang kết nối...'}</div>
<div class="status-banner-sub" id="status-sub">
${stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp'}
</div>
</div>
</div>
<div class="stats-grid stats-grid--two">
<div class="card stat-card">
<div class="stat-icon-wrap">📡</div>
<div class="stat-info">
<div class="stat-title">WiFi</div>
<div class="stat-value" id="wifi-ssid">${stats.wifiSSID || 'Đang quét...'}</div>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon-wrap">🖥️</div>
<div class="stat-info">
<div class="stat-title">Máy chủ</div>
<div class="stat-value" id="server-status" style="color: ${stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)'}">
${stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối'}
</div>
</div>
</div>
</div>
<div class="clocks-card card">
<div class="card-title-row">
<div class="card-title">Hôm nay — theo ca</div>
<div class="card-title-sub" id="session-date">${stats.sessionDate || ''}</div>
</div>
<div class="clock-display clock-display-summary">
<div class="clock-item">
<div class="clock-label">Tổng trực tuyến</div>
<div class="clock-value online" id="clock-online">00:00:00</div>
</div>
<div class="clock-item">
<div class="clock-label">Tổng ngoại tuyến</div>
<div class="clock-value offline" id="clock-offline">00:00:00</div>
</div>
</div>
<div class="shifts-wrap" id="shifts-wrap">
${renderShiftsTable(stats.shifts)}
</div>
</div>
</div>
</div>
</div>
`;
document.getElementById('btn-logout').addEventListener('click', async () => {
if (confirm('Bạn có chắc chắn muốn đăng xuất khỏi ứng dụng giám sát?')) {
loggedIn = false;
studentInfo = null;
stopWebcam();
await window.go.main.App.Logout();
}
});
}
function formatDuration(totalSeconds) {
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const pad = (num) => String(num).padStart(2, '0');
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
}
function startStatsTicker() {
setInterval(async () => {
if (!loggedIn) return;
try {
const freshStats = await window.go.main.App.GetStats();
stats = { ...stats, ...freshStats };
if (!Array.isArray(stats.shifts)) stats.shifts = [];
const wifiEl = document.getElementById('wifi-ssid');
if (wifiEl) wifiEl.innerText = stats.wifiSSID || 'Không có WiFi';
const serverEl = document.getElementById('server-status');
if (serverEl) {
serverEl.innerText = stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối';
serverEl.style.color = stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)';
}
const statusMsgEl = document.getElementById('status-message');
if (statusMsgEl) {
statusMsgEl.innerText = stats.monitorLabel || stats.statusMsg || 'Đang kết nối...';
}
const mode = monitorModeMeta(stats.monitorMode);
const statusIconEl = document.getElementById('status-icon');
if (statusIconEl) statusIconEl.innerText = mode.icon;
const modeTagEl = document.getElementById('monitor-mode-tag');
if (modeTagEl) modeTagEl.innerText = mode.label;
const banner = document.querySelector('.status-banner');
if (banner) banner.className = `card status-banner ${mode.cls}`;
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
const classLineEl = document.getElementById('status-class-line');
if (classLineEl) classLineEl.innerText = classLine;
const profileClassEl = document.getElementById('profile-class');
if (profileClassEl) profileClassEl.innerText = classLine;
const statusSubEl = document.getElementById('status-sub');
if (statusSubEl) {
statusSubEl.innerText = stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp';
}
const onlineEl = document.getElementById('clock-online');
if (onlineEl) onlineEl.innerText = formatDuration(stats.onlineSecs || 0);
const offlineEl = document.getElementById('clock-offline');
if (offlineEl) offlineEl.innerText = formatDuration(stats.offlineSecs || 0);
const shiftsWrap = document.getElementById('shifts-wrap');
if (shiftsWrap) shiftsWrap.innerHTML = renderShiftsTable(stats.shifts);
const sessionDateEl = document.getElementById('session-date');
if (sessionDateEl && stats.sessionDate) sessionDateEl.innerText = stats.sessionDate;
const indicator = document.querySelector('.status-indicator');
if (indicator) {
indicator.className = `status-indicator ${stats.serverReachable ? 'online' : 'offline'}`;
}
} catch (err) {
console.error('Error fetching stats:', err);
}
}, 1000);
}
async function startWebcam() {
if (webcamStream) return;
try {
webcamStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, frameRate: { max: 10 } }
});
webcamVideo.srcObject = webcamStream;
webcamInterval = setInterval(() => {
const ctx = webcamCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(webcamVideo, 0, 0, webcamCanvas.width, webcamCanvas.height);
const dataUrl = webcamCanvas.toDataURL('image/jpeg', 0.4);
window.go.main.App.SendWebcamFrame(dataUrl);
}
}, 250);
} catch (err) {
console.error('Failed to open webcam:', err);
}
}
function stopWebcam() {
if (webcamInterval) {
clearInterval(webcamInterval);
webcamInterval = null;
}
if (webcamStream) {
webcamStream.getTracks().forEach(track => track.stop());
webcamStream = null;
}
webcamVideo.srcObject = null;
}
init();