feat(classroom): открытие любого учебника в онлайн-уроке
Учитель может выбрать любой активный учебник из каталога /api/textbooks и открыть его в общем iframe для всех участников. По аналогии с симуляциями: - Backend: контроллер classroom/textbook.js + 4 роута (POST/DELETE /:id/textbook, /:id/textbook/nav, /:id/textbook/mode) с SSE-событиями classroom_textbook_open|close|nav|mode - Embed-режим /textbook/:slug?embed=1: сервер injectит CSS+JS-bridge перед </head>, скрывая хедер/сайдбар и пересылая клики/скролл наверх через postMessage (без правки 40+ HTML-учебников) - Frontend (classroom.html): кнопка «Учебник» в header, пикер с фильтрами по предмету, iframe-панель с режимами демо/свободно, relay nav-событий учителя → всем студентам в demo-режиме
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
const db = require('../../db/db');
|
||||
const { emitToSession } = require('./_shared');
|
||||
|
||||
const _stmtTextbookBySlug = db.prepare(
|
||||
'SELECT slug, title, subject, grade FROM textbooks WHERE slug=? AND is_active=1'
|
||||
);
|
||||
|
||||
function _requireTeacher(req, res) {
|
||||
const sessionId = Number(req.params.id);
|
||||
const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId);
|
||||
if (!session) { res.status(404).json({ error: 'Сессия не активна' }); return null; }
|
||||
if (session.teacher_id !== req.user.id && req.user.role !== 'admin') {
|
||||
res.status(403).json({ error: 'Нет доступа' }); return null;
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
function textbookOpen(req, res) {
|
||||
const sessionId = _requireTeacher(req, res);
|
||||
if (!sessionId) return;
|
||||
|
||||
const { slug, hash } = req.body || {};
|
||||
if (!slug || typeof slug !== 'string' || !/^[a-z0-9_-]{1,80}$/i.test(slug))
|
||||
return res.status(400).json({ error: 'Неверный slug' });
|
||||
|
||||
const tb = _stmtTextbookBySlug.get(slug);
|
||||
if (!tb) return res.status(404).json({ error: 'Учебник не найден' });
|
||||
|
||||
const safeHash = (typeof hash === 'string' && /^[a-z0-9_-]{1,40}$/i.test(hash)) ? hash : null;
|
||||
|
||||
emitToSession(sessionId, {
|
||||
type: 'classroom_textbook_open',
|
||||
sessionId,
|
||||
slug: tb.slug,
|
||||
title: tb.title,
|
||||
subject: tb.subject,
|
||||
grade: tb.grade,
|
||||
hash: safeHash,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function textbookNav(req, res) {
|
||||
const sessionId = _requireTeacher(req, res);
|
||||
if (!sessionId) return;
|
||||
|
||||
const { slug, hash, scrollY } = req.body || {};
|
||||
if (!slug || typeof slug !== 'string' || !/^[a-z0-9_-]{1,80}$/i.test(slug))
|
||||
return res.status(400).json({ error: 'Неверный slug' });
|
||||
if (!_stmtTextbookBySlug.get(slug))
|
||||
return res.status(404).json({ error: 'Учебник не найден' });
|
||||
|
||||
const safeHash = (typeof hash === 'string' && /^[a-z0-9_-]{1,40}$/i.test(hash)) ? hash : null;
|
||||
const sy = (typeof scrollY === 'number' && isFinite(scrollY)) ? Math.max(0, Math.min(scrollY, 1e6)) : null;
|
||||
|
||||
emitToSession(sessionId, {
|
||||
type: 'classroom_textbook_nav',
|
||||
sessionId, slug, hash: safeHash, scrollY: sy,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function textbookMode(req, res) {
|
||||
const sessionId = _requireTeacher(req, res);
|
||||
if (!sessionId) return;
|
||||
const { mode } = req.body || {};
|
||||
if (mode !== 'demo' && mode !== 'free') return res.status(400).json({ error: 'mode must be demo|free' });
|
||||
emitToSession(sessionId, { type: 'classroom_textbook_mode', sessionId, mode });
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
function textbookClose(req, res) {
|
||||
const sessionId = _requireTeacher(req, res);
|
||||
if (!sessionId) return;
|
||||
emitToSession(sessionId, { type: 'classroom_textbook_close', sessionId });
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = { textbookOpen, textbookNav, textbookMode, textbookClose };
|
||||
@@ -8,5 +8,6 @@ module.exports = {
|
||||
...require('./classroom/chat'),
|
||||
...require('./classroom/permissions'),
|
||||
...require('./classroom/sim'),
|
||||
...require('./classroom/textbook'),
|
||||
...require('./classroom/admin'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user