core v1
This commit is contained in:
368
client/frontend/src/main.js
Normal file
368
client/frontend/src/main.js
Normal 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();
|
||||
Reference in New Issue
Block a user