/
klg
/
mytest
Обзор
Документация
Войти
/
klg
/
mytest
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
genus.html
737 строк
26 KB
pgdrozdov
Первый коммит
11 июн 2026, 12:46
11 июн 2026, 12:46
505f2cd
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> <title>Тест: род существительных + таймер и сохранение</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; } body { background: linear-gradient(145deg, #f6f9fc 0%, #eef2f5 100%); margin: 0; padding: 1.5rem; display: flex; justify-content: center; align-items: center; min-height: 100vh; } /* ГЛАВНЫЙ КОНТЕЙНЕР — FLEX с двумя колонками */ .test-container { max-width: 1400px; width: 100%; background: rgba(255,255,255,0.92); backdrop-filter: blur(0px); border-radius: 2.5rem; box-shadow: 0 20px 40px rgba(0,0,0,0.08), 0 8px 20px rgba(0,0,0,0.05); overflow: hidden; transition: all 0.2s; display: flex; flex-wrap: wrap; gap: 1.5rem; padding: 1.5rem; } /* ЛЕВАЯ КОЛОНКА — основная часть теста */ .left-col { flex: 2.5; min-width: 260px; background: transparent; display: flex; flex-direction: column; gap: 0; } /* ПРАВАЯ КОЛОНКА — теория в рамке */ .right-col { flex: 1.4; min-width: 260px; } .theory-card { background: #ffffffea; border-radius: 1.8rem; border: 1px solid #dce5f0; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05); padding: 1.5rem 1.3rem; height: auto; transition: 0.2s; backdrop-filter: blur(2px); } .theory-card h3 { font-size: 1.5rem; font-weight: 700; color: #1e293b; margin-bottom: 1rem; border-left: 5px solid #2c3e66; padding-left: 0.8rem; display: flex; align-items: center; gap: 0.5rem; } .theory-card h3::before { content: "📘"; font-size: 1.4rem; } .theory-content { font-size: 0.96rem; line-height: 1.5; color: #1e2f3e; } .theory-content p { margin-bottom: 0.9rem; } .theory-content .rule-list { background: #f8fafd; padding: 0.6rem 1rem; border-radius: 1rem; margin: 0.8rem 0; border-left: 3px solid #2c3e66; } .theory-content strong { color: #1f3a5f; font-weight: 600; } .theory-content hr { margin: 1rem 0; border: none; height: 1px; background: #e2edf2; } /* Стили для таймера, формы, ошибок */ .top-bar { background: #ffffffcc; backdrop-filter: blur(8px); border-radius: 2rem; padding: 0.8rem 1.2rem; margin-bottom: 1.2rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.8rem; border: 1px solid rgba(255,255,255,0.6); } #timer { font-size: 1.7rem; font-weight: 800; background: #0b2b3f; color: #f0fcff; padding: 0.3rem 1rem; border-radius: 60px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); font-family: monospace; letter-spacing: 1px; text-align: center; min-width: 180px; } .fio-block { flex: 2; min-width: 200px; } .fio-block label { font-weight: 700; color: #144e6b; margin-right: 0.5rem; } .fio-block input { padding: 0.5rem 1rem; border-radius: 40px; border: 1.5px solid #bdd4e8; background: white; font-size: 0.9rem; width: 220px; transition: 0.2s; } .fio-block input:focus { border-color: #2c7da0; outline: none; box-shadow: 0 0 0 3px rgba(44,125,160,0.2); } .error, .server-response { border-radius: 60px; padding: 10px 20px; font-weight: 500; text-align: center; margin-bottom: 1rem; } .error { background: #ffe8e6; color: #b12a1f; border-left: 6px solid #c0392b; } .server-response { background: #eaf6ff; border-left: 6px solid #2c7da0; color: #155f7c; } .test-header { background: #2c3e66; padding: 1.2rem 1.8rem; color: white; border-radius: 1.5rem; margin-bottom: 1.2rem; } .test-header h1 { margin: 0 0 0.35rem 0; font-weight: 600; font-size: 1.7rem; letter-spacing: -0.3px; } .test-header p { margin: 0; opacity: 0.85; font-size: 0.9rem; } .question-card { background: white; border-radius: 1.2rem; margin-bottom: 1rem; padding: 0.9rem 1.2rem; box-shadow: 0 2px 6px rgba(0,0,0,0.04); border: 1px solid #e9edf2; } .word-title { font-size: 1.4rem; font-weight: 700; margin-bottom: 0.6rem; color: #1e293b; display: inline-block; background: #f1f5f9; padding: 0.2rem 1rem; border-radius: 40px; } .options { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 0.3rem; align-items: center; } .option-label { display: inline-flex; align-items: center; gap: 0.5rem; background: #f8fafc; padding: 0.4rem 1rem; border-radius: 60px; cursor: pointer; transition: 0.2s; border: 1px solid #e2e8f0; font-weight: 500; font-size: 0.85rem; } .option-label:hover { background: #eef2ff; border-color: #b9c8f0; } input[type="radio"] { accent-color: #2c3e66; width: 1rem; height: 1rem; } .feedback { margin-top: 0.6rem; font-size: 0.8rem; font-weight: 500; padding-left: 0.3rem; } .feedback.correct { color: #15803d; } .feedback.wrong { color: #b91c1c; } .score-area { background: #f1f5f9; border-radius: 1.8rem; padding: 0.8rem 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; } .score-text { font-size: 1.3rem; font-weight: 700; background: #ffffffcc; padding: 0.3rem 1rem; border-radius: 60px; } .button-group { display: flex; gap: 0.8rem; } button { border: none; background: #2c3e66; color: white; padding: 0.6rem 1.4rem; border-radius: 2rem; font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } button:hover { background: #1e2f4f; transform: scale(0.97); } button.reset-btn { background: #475569; } button.reset-btn:hover { background: #334155; } .menu-button { background: #3a6b47; color: white; padding: 0.6rem 1.4rem; border-radius: 2rem; text-decoration: none; font-weight: 600; font-size: 0.9rem; display: inline-block; text-align: center; transition: 0.2s; cursor: pointer; } .menu-button:hover { background: #2c5539; transform: scale(0.97); } .footer-note { padding: 0.8rem 0 0.2rem; font-size: 0.75rem; color: #5b6e8c; border-top: 1px solid #e2edf2; margin-top: 0.6rem; } #results { margin-top: 1.5rem; background: #ffffffec; border-radius: 2rem; padding: 1rem; box-shadow: 0 8px 20px rgba(0,0,0,0.1); border: 1px solid #d4e2f0; overflow-x: auto; } table { width: 100%; border-collapse: collapse; border-radius: 1rem; font-size: 0.85rem; } th { background: #e3f0fa; color: #1a4c74; padding: 8px; } td, th { border: 1px solid #cde0ef; padding: 6px 8px; } @media (max-width: 860px) { body { padding: 1rem; } .test-container { flex-direction: column; } .top-bar { flex-direction: column; align-items: stretch; } .fio-block input { width: 100%; } } </style> </head> <body> <div style="width:100%; max-width:1400px; margin:0 auto;"> <!-- Таймер и блок ФИО --> <div class="top-bar"> <div id="timer">⏱️ Время: 0 сек</div> <div class="fio-block"> <label for="fio">📝 ФИО / Имя:</label> <input type="text" id="fio" name="fio" list="lift" autocomplete="off" placeholder="Выберите или введите имя" required> <datalist id="lift"> <option>Егор</option><option>Ксюша</option><option>Анюта</option><option>Даня</option> </datalist> </div> </div> <div id="errorMessage" class="error" style="display: none;"></div> <div id="serverResponse" class="server-response" style="display: none;"></div> <form id="myForm" method="POST" action="save.php"> <input type="hidden" name="predmet" value="русский язык"> <input type="hidden" name="tdate" id="tdate"> <input type="hidden" name="nazv" value="Род существительных"> <input type="hidden" name="time" id="time_input" value="0"> <input type="hidden" name="grade" id="grade_input" value=""> <!-- Скрытые поля для ответов будут добавлены динамически --> </form> <div class="test-container" id="quizRoot"> <div class="left-col"> <div class="test-header"> <h1>📖 Род существительных</h1> <p>Выберите правильный род для каждого слова • таймер + оценка • результат сохраняется</p> </div> <div id="questionsArea" class="test-words"></div> <div class="score-area"> <div class="score-text" id="scoreDisplay">✅ 0 / 0</div> <div class="button-group"> <button id="checkAndFinishBtn">✅ ЗАВЕРШИТЬ ТЕСТ ✅</button> <button id="resetAnswersBtn" class="reset-btn">⟳ СБРОСИТЬ ВСЕ</button> <a href="javascript:void(0);" class="menu-button" id="viewDataLink">🔍 Мои данные</a> </div> </div> <div class="footer-note"> ★ Для слов, которые употребляются ТОЛЬКО во множественном числе (щипцы, клещи, ножницы, грабли, вилы) — выбирайте «Множественное число».<br> Кенгуру, кофе — мужской род; метро, кино, шоссе, радио — средний; меч — мужской род. </div> </div> <div class="right-col"> <div class="theory-card"> <h3>Теория: род имён существительных</h3> <div class="theory-content"> <p>Каждое имя существительное (в ед.ч.) принадлежит <strong>мужскому, женскому или среднему</strong> роду.</p> <div class="rule-list"><p><strong>🏷 Мужской род</strong><br>замена местоимением <strong>он, мой</strong>: <em>он (мой) дядя, он (мой) кофе, он (мой) меч</em>.</p></div> <div class="rule-list"><p><strong>🏷 Женский род</strong><br>замена местоимением <strong>она, моя</strong>: <em>она (моя) лапша, она (моя) мука</em>.</p></div> <div class="rule-list"><p><strong>🏷 Средний род</strong><br>замена местоимением <strong>оно, моё</strong>: <em>оно (моё) масло, оно (моё) метро</em>.</p></div> <p><strong>📌 Родовые окончания (И.п., ед.ч.):</strong> м.р. – нулевое, -я, -а; ж.р. – нулевое, -а, -я; ср.р. – -е, -о.</p> <p><strong>⚡ ВАЖНО:</strong> Для определения рода ставим слово в начальную форму (И.п., ед.ч.). Слова только мн.ч. (pluralia tantum) не имеют рода.</p> </div> </div> </div> </div> <div id="results" style="display: none;"></div> </div> <script> // ======================= ДАННЫЕ ТЕСТА (18 слов, дубликат "шоссе" заменён на "меч") ======================= const group1 = ["масло", "лапша", "кефир", "мука", "зефир", "печенье"]; const group2 = ["щипцы", "клещи", "ножницы", "грабли", "вилы"]; // ВНИМАНИЕ: раньше было два "шоссе", одно заменили на "меч" const group3 = ["метро", "кино", "шоссе", "радио", "меч", "кенгуру", "кофе"]; const allWordsRaw = [...group1, ...group2, ...group3]; const TOTAL_QUESTIONS = allWordsRaw.length; // 6 + 5 + 7 = 18 // Функция определения правильного рода для каждого слова function getCorrectGender(word) { const w = word.toLowerCase(); if (w === "масло") return "n"; if (w === "лапша") return "f"; if (w === "кефир") return "m"; if (w === "мука") return "f"; if (w === "зефир") return "m"; if (w === "печенье") return "n"; if (w === "щипцы" || w === "клещи" || w === "ножницы" || w === "грабли" || w === "вилы") return "p"; if (w === "метро" || w === "кино" || w === "шоссе" || w === "радио") return "n"; if (w === "меч") return "m"; if (w === "кенгуру") return "m"; if (w === "кофе") return "m"; return "m"; // fallback } const questions = allWordsRaw.map((word, idx) => ({ id: idx, word: word, correctGender: getCorrectGender(word) })); const genderLabels = { m: "Мужской", f: "Женский", n: "Средний", p: "Множественное число" }; // ---------- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ТАЙМЕРА ---------- let startTime = null; let timerInterval = null; let isTestFinished = false; function startTimer() { if (timerInterval) clearInterval(timerInterval); startTime = Date.now(); timerInterval = setInterval(() => { if (isTestFinished) return; const elapsed = Math.floor((Date.now() - startTime) / 1000); const timerDiv = document.getElementById('timer'); if (timerDiv) timerDiv.textContent = `⏱️ Время: ${elapsed} сек`; }, 200); } function stopTimerAndGetSeconds() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } if (startTime) return Math.floor((Date.now() - startTime) / 1000); return 0; } function getCurrentDateTime() { const now = new Date(); const Y = now.getFullYear(); const M = String(now.getMonth() + 1).padStart(2,'0'); const D = String(now.getDate()).padStart(2,'0'); const h = String(now.getHours()).padStart(2,'0'); const m = String(now.getMinutes()).padStart(2,'0'); const s = String(now.getSeconds()).padStart(2,'0'); return `${Y}-${M}-${D} ${h}:${m}:${s}`; } function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } // Оценка всех ответов (собираем выбранные значения) function evaluateAllAnswers() { let wrongCount = 0; let detailed = {}; for (let i = 0; i < TOTAL_QUESTIONS; i++) { const radios = document.getElementsByName(`q${i}`); let selectedVal = null; for (let r of radios) if (r.checked) { selectedVal = r.value; break; } const correct = questions[i].correctGender; const isCorrect = (selectedVal === correct); if (!isCorrect) wrongCount++; detailed[questions[i].word] = { userAnswer: selectedVal ? genderLabels[selectedVal] : "не выбран", correctAnswer: genderLabels[correct], isCorrect: isCorrect }; } const correctCount = TOTAL_QUESTIONS - wrongCount; const percent = (correctCount / TOTAL_QUESTIONS) * 100; let grade = 2; if (percent >= 90) grade = 5; else if (percent >= 70) grade = 4; else if (percent >= 50) grade = 3; else grade = 2; return { wrongCount, correctCount, percent, grade, detailed }; } // Отрисовка таблицы результатов function renderResultsTable(fio, timeSec, correct, wrong, percent, grade, detailedMap) { let html = `<div><h3>📊 РЕЗУЛЬТАТЫ ТЕСТА (род существительных)</h3> <p><strong>👤 Участник:</strong> ${escapeHtml(fio)}</p> <p><strong>⏱️ Время:</strong> ${timeSec} секунд</p> <p><strong>✅ Правильно:</strong> ${correct} из ${TOTAL_QUESTIONS}</p> <p><strong>❌ Ошибок:</strong> ${wrong}</p> <p><strong>📈 Процент:</strong> ${percent.toFixed(1)}%</p> <p><strong>🎓 ОЦЕНКА:</strong> <span style="font-size:1.5rem;">${grade}</span></p> <h4>Детализация:</h4><div style="overflow-x:auto;">`; html += `<table><thead><tr><th>Слово</th><th>Ваш ответ</th><th>Правильный род</th><th>Результат</th></tr></thead><tbody>`; for (const [word, data] of Object.entries(detailedMap)) { const color = data.isCorrect ? '#1f7a3b' : '#c0392b'; const status = data.isCorrect ? '✔ Верно' : '✘ Ошибка'; html += `<tr> <td>${escapeHtml(word)}</td> <td>${escapeHtml(data.userAnswer)}</td> <td>${escapeHtml(data.correctAnswer)}</td> <td style="color:${color};">${status}</td> </tr>`; } html += `</tbody></table></div></div>`; const resultsDiv = document.getElementById('results'); resultsDiv.innerHTML = html; resultsDiv.style.display = 'block'; resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Отправка данных на save.php async function sendToServer(formData) { try { const response = await fetch('save.php', { method: 'POST', body: formData }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const serverDiv = document.getElementById('serverResponse'); serverDiv.innerHTML = `<strong>✅ Результаты успешно сохранены на сервере.</strong>`; serverDiv.style.display = 'block'; setTimeout(() => { if(serverDiv) serverDiv.style.display = 'none'; }, 5000); } catch (err) { console.warn(err); const errorDiv = document.getElementById('errorMessage'); errorDiv.innerHTML = `⚠️ Ошибка отправки: ${escapeHtml(err.message)}. Данные сохранены локально.`; errorDiv.style.display = 'block'; setTimeout(() => { if(errorDiv) errorDiv.style.display = 'none'; }, 6000); } } // ГЛАВНАЯ ФУНКЦИЯ ЗАВЕРШЕНИЯ ТЕСТА let isProcessing = false; async function finishTest() { if (isTestFinished || isProcessing) return; document.getElementById('errorMessage').style.display = 'none'; document.getElementById('serverResponse').style.display = 'none'; document.getElementById('results').style.display = 'none'; const fioInput = document.getElementById('fio'); const userFio = fioInput.value.trim(); if (!userFio) { const errDiv = document.getElementById('errorMessage'); errDiv.textContent = '❗ Введите ваше ФИО / имя перед завершением теста!'; errDiv.style.display = 'block'; setTimeout(() => errDiv.style.display = 'none', 4000); return; } // останавливаем таймер const spentSeconds = stopTimerAndGetSeconds(); isTestFinished = true; // заполняем скрытые поля формы document.getElementById('time_input').value = spentSeconds; document.getElementById('tdate').value = getCurrentDateTime(); const { wrongCount, correctCount, percent, grade, detailed } = evaluateAllAnswers(); document.getElementById('grade_input').value = grade; // показываем детальную таблицу renderResultsTable(userFio, spentSeconds, correctCount, wrongCount, percent, grade, detailed); // подготовка FormData для отправки: берём стандартные поля формы + добавляем ответы const formElement = document.getElementById('myForm'); const formData = new FormData(formElement); formData.set('fio', userFio); // обновляем ФИО // Добавляем все ответы как поля answer_0, answer_1... for (let i = 0; i < TOTAL_QUESTIONS; i++) { const radios = document.getElementsByName(`q${i}`); let selected = null; for (let r of radios) if (r.checked) { selected = r.value; break; } formData.append(`answer_${i}`, selected !== null ? selected : ''); formData.append(`word_${i}`, questions[i].word); } formData.append('total_correct', correctCount); formData.append('total_questions', TOTAL_QUESTIONS); formData.append('percent', percent.toFixed(2)); isProcessing = true; await sendToServer(formData); isProcessing = false; } // Сброс всех радиокнопок и обратной связи function resetAllSelections() { if (isTestFinished) return; for (let i = 0; i < TOTAL_QUESTIONS; i++) { const radios = document.getElementsByName(`q${i}`); radios.forEach(radio => radio.checked = false); const fb = document.getElementById(`fb_${i}`); if (fb) { fb.innerHTML = ""; fb.className = "feedback"; } } updateScoreDisplay(0); } function updateScoreDisplay(correctCount) { const scoreSpan = document.getElementById("scoreDisplay"); if (scoreSpan) scoreSpan.innerHTML = `🎯 ${correctCount} / ${TOTAL_QUESTIONS}`; } // Вспомогательная: обновление счётчика в реальном времени (опционально) function refreshLiveScore() { let corr = 0; for (let i = 0; i < TOTAL_QUESTIONS; i++) { const radios = document.getElementsByName(`q${i}`); let selected = null; for (let r of radios) if (r.checked) { selected = r.value; break; } if (selected === questions[i].correctGender) corr++; } updateScoreDisplay(corr); } // Отрисовка всех вопросов function renderTest() { const container = document.getElementById("questionsArea"); if (!container) return; container.innerHTML = ""; questions.forEach((q, idx) => { const card = document.createElement("div"); card.className = "question-card"; const wordSpan = document.createElement("div"); wordSpan.className = "word-title"; wordSpan.textContent = q.word; const optionsDiv = document.createElement("div"); optionsDiv.className = "options"; const genders = [ { code: "m", label: "Мужской" }, { code: "f", label: "Женский" }, { code: "n", label: "Средний" }, { code: "p", label: "Множественное число" } ]; genders.forEach(gender => { const radioId = `q${idx}_${gender.code}`; const label = document.createElement("label"); label.htmlFor = radioId; label.className = "option-label"; const radio = document.createElement("input"); radio.type = "radio"; radio.name = `q${idx}`; radio.value = gender.code; radio.id = radioId; radio.addEventListener('change', () => refreshLiveScore()); label.appendChild(radio); label.appendChild(document.createTextNode(gender.label)); optionsDiv.appendChild(label); }); const feedbackDiv = document.createElement("div"); feedbackDiv.className = "feedback"; feedbackDiv.id = `fb_${idx}`; card.appendChild(wordSpan); card.appendChild(optionsDiv); card.appendChild(feedbackDiv); container.appendChild(card); }); refreshLiveScore(); } // Инициализация всех обработчиков function initEventListeners() { const finishBtn = document.getElementById("checkAndFinishBtn"); const resetBtn = document.getElementById("resetAnswersBtn"); const viewLink = document.getElementById("viewDataLink"); if (finishBtn) finishBtn.addEventListener("click", finishTest); if (resetBtn) resetBtn.addEventListener("click", resetAllSelections); if (viewLink) { viewLink.addEventListener("click", (e) => { e.preventDefault(); const fioVal = document.getElementById('fio').value.trim(); if (!fioVal) { const errDiv = document.getElementById('errorMessage'); errDiv.textContent = '❗ Введите имя, чтобы посмотреть свои результаты.'; errDiv.style.display = 'block'; setTimeout(() => errDiv.style.display = 'none', 4000); return; } window.location.href = `show.php?fio=${encodeURIComponent(fioVal)}`; }); } // Блокировка отправки формы через Enter document.getElementById('myForm')?.addEventListener('keypress', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') e.preventDefault(); }); } function initPage() { renderTest(); initEventListeners(); startTimer(); isTestFinished = false; if (!document.getElementById('tdate').value) { document.getElementById('tdate').value = getCurrentDateTime(); } refreshLiveScore(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initPage); else initPage(); </script> </body> </html>