This commit is contained in:
2026-05-08 15:23:29 +03:00
parent 951f4c5c60
commit f828068dd7
5 changed files with 1229 additions and 127 deletions
+4
View File
@@ -125,3 +125,7 @@ temp/
# Session files # Session files
flask_session/ flask_session/
sessions/ sessions/
# Добавить в существующий .gitignore
questions_custom.json
*.export.json
+4 -2
View File
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$">
<orderEntry type="inheritedJdk" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (dr_mp_quiz)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>
+365 -37
View File
@@ -2,24 +2,248 @@ from flask import Flask, render_template, request, jsonify, session, send_file
from questions import QUESTION_POOL from questions import QUESTION_POOL
import random import random
import uuid import uuid
import qrcode
from io import BytesIO
import os import os
import json import json
from datetime import datetime from datetime import datetime
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter
# Пытаемся импортировать qrcode с обработкой ошибок
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L
QRCODE_AVAILABLE = True
except ImportError:
QRCODE_AVAILABLE = False
print("Warning: qrcode library not installed. Install with: pip install qrcode[pil]")
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'dnr_youth_parliament_secret_key_2024' app.secret_key = 'dnr_youth_parliament_secretary_key_2024'
# Хранилище сессий игроков и результатов # Хранилище сессий игроков и результатов
active_games = {} active_games = {}
saved_results = {} # Сохраняем результаты для QR-кодов saved_results = {}
# Файл для сохранения вопросов
QUESTIONS_FILE = 'questions_custom.json'
def load_questions():
"""Загружает вопросы из файла или использует стандартный пул"""
if os.path.exists(QUESTIONS_FILE):
with open(QUESTIONS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return QUESTION_POOL.copy()
def save_questions(questions):
"""Сохраняет вопросы в файл"""
with open(QUESTIONS_FILE, 'w', encoding='utf-8') as f:
json.dump(questions, f, ensure_ascii=False, indent=2)
def create_result_image(result_data):
"""Создаёт изображение с результатами викторины в формате 16:9"""
# Формат 16:9 (1920x1080)
width = 1920
height = 1080
# Создаём изображение с градиентным фоном
img = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(img)
# Новый градиентный фон (тёмный градиент)
for i in range(height):
r = int(18 + (i / height) * 35) # RGB(18, 25, 45) -> RGB(53, 66, 89)
g = int(25 + (i / height) * 41)
b = int(45 + (i / height) * 44)
draw.line([(0, i), (width, i)], fill=(r, g, b))
# Декоративные элементы
# Большой круг в правом верхнем углу
circle1_radius = 300
draw.ellipse([width - circle1_radius - 50, -circle1_radius // 2,
width + 50, circle1_radius + 50],
fill=(255, 100, 100, 30), outline=None)
# Круг в левом нижнем углу
circle2_radius = 250
draw.ellipse([-circle2_radius // 2, height - circle2_radius - 50,
circle2_radius + 50, height + 50],
fill=(100, 150, 255, 30), outline=None)
# Золотая рамка с двойным контуром
border_margin = 15
border_color = (218, 165, 32) # Золотой
# Внешняя рамка
draw.rectangle([border_margin, border_margin,
width - border_margin, height - border_margin],
outline=border_color, width=3)
# Внутренняя рамка
draw.rectangle([border_margin + 10, border_margin + 10,
width - border_margin - 10, height - border_margin - 10],
outline=border_color, width=1)
# Угловые украшения
corner_length = 60
corner_width = 5
# Верхний левый угол
draw.line([border_margin, border_margin + corner_length,
border_margin, border_margin], fill=border_color, width=corner_width)
draw.line([border_margin + corner_length, border_margin,
border_margin, border_margin], fill=border_color, width=corner_width)
# Верхний правый угол
draw.line([width - border_margin, border_margin + corner_length,
width - border_margin, border_margin], fill=border_color, width=corner_width)
draw.line([width - border_margin - corner_length, border_margin,
width - border_margin, border_margin], fill=border_color, width=corner_width)
# Нижний левый угол
draw.line([border_margin, height - border_margin - corner_length,
border_margin, height - border_margin], fill=border_color, width=corner_width)
draw.line([border_margin + corner_length, height - border_margin,
border_margin, height - border_margin], fill=border_color, width=corner_width)
# Нижний правый угол
draw.line([width - border_margin, height - border_margin - corner_length,
width - border_margin, height - border_margin], fill=border_color, width=corner_width)
draw.line([width - border_margin - corner_length, height - border_margin,
width - border_margin, height - border_margin], fill=border_color, width=corner_width)
# Пытаемся загрузить шрифты
try:
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64)
font_score = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 160)
font_percent = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
font_normal = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24)
except:
try:
# Альтернативные шрифты для разных ОС
font_title = ImageFont.truetype("arial.ttf", 64)
font_score = ImageFont.truetype("arial.ttf", 160)
font_percent = ImageFont.truetype("arial.ttf", 56)
font_normal = ImageFont.truetype("arial.ttf", 32)
font_small = ImageFont.truetype("arial.ttf", 24)
except:
font_title = ImageFont.load_default()
font_score = ImageFont.load_default()
font_percent = ImageFont.load_default()
font_normal = ImageFont.load_default()
font_small = ImageFont.load_default()
# Центральный элемент - звезда/символ
center_x = width // 2
# Заголовок с тенью
title1 = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ"
title1_shadow = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ"
shadow_offset = 3
# Тень для заголовка
title1_width = draw.textlength(title1, font=font_title)
draw.text(((width - title1_width) // 2 + shadow_offset, 130 + shadow_offset),
title1_shadow, fill=(30, 30, 50), font=font_title)
draw.text(((width - title1_width) // 2, 130), title1, fill='white', font=font_title)
# Подзаголовок
title2 = "ДОНЕЦКОЙ НАРОДНОЙ РЕСПУБЛИКИ"
title2_width = draw.textlength(title2, font=font_normal)
draw.text(((width - title2_width) // 2 + shadow_offset, 210 + shadow_offset),
title2, fill=(30, 30, 50), font=font_normal)
draw.text(((width - title2_width) // 2, 210), title2, fill=(218, 165, 32), font=font_normal)
# Декоративная линия с точками
line_y = 270
line_length = 400
for i in range(0, line_length, 15):
x1 = center_x - line_length // 2 + i
if i % 30 == 0:
draw.ellipse([x1 - 3, line_y - 3, x1 + 3, line_y + 3], fill=(218, 165, 32))
else:
draw.line([x1, line_y, x1 + 10, line_y], fill=(218, 165, 32), width=2)
# Карточка с результатами
card_width = 600
card_height = 380 # Увеличена высота карточки
card_x = (width - card_width) // 2
card_y = 310 # Немного поднята карточка
# Полупрозрачная карточка
card_overlay = Image.new('RGBA', (card_width, card_height), (0, 0, 0, 0))
card_draw = ImageDraw.Draw(card_overlay)
card_draw.rectangle([0, 0, card_width, card_height], fill=(255, 255, 255, 30))
# Рамка карточки
card_draw.rectangle([0, 0, card_width, card_height], outline=(218, 165, 32), width=2)
# Вставляем карточку
img.paste(card_overlay, (card_x, card_y), card_overlay)
# Основной счёт (поднят выше)
score_y = card_y + 70
score_text = f"{result_data['score']} / {result_data['total']}"
score_width = draw.textlength(score_text, font=font_score)
draw.text(((width - score_width) // 2, score_y), score_text, fill=(255, 215, 0), font=font_score)
# Проценты (опущены ниже)
percent_y = card_y + 230 # Было 200, стало 230 - опустили на 30px
percent_text = f"{result_data['percentage']}%"
percent_width = draw.textlength(percent_text, font=font_percent)
# Радужный градиент для процентов
draw.text(((width - percent_width) // 2, percent_y), percent_text, fill=(255, 215, 0), font=font_percent)
# Звание (опущено ниже)
grade_y = card_y + 310 # Было 280, стало 310 - опустили на 30px
grade_text = result_data['grade']
grade_width = draw.textlength(grade_text, font=font_normal)
draw.text(((width - grade_width) // 2, grade_y), grade_text, fill=(255, 215, 0), font=font_normal)
# Нижняя часть
footer_y = height - 60
# Левая часть - QR-код
if QRCODE_AVAILABLE:
try:
result_url = f"http://localhost:5000/api/result/{result_data['game_id']}"
qr = qrcode.QRCode(version=1, box_size=4, border=2)
qr.add_data(result_url)
qr.make(fit=True)
qr_img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45))
qr_img = qr_img.resize((100, 100))
img.paste(qr_img, (80, footer_y - 70))
except:
pass
# Центральная часть - девиз
footer_text = "МОЛОДОСТЬ. ПАТРИОТИЗМ. РАЗВИТИЕ."
footer_width = draw.textlength(footer_text, font=font_small)
draw.text(((width - footer_width) // 2, footer_y), footer_text, fill=(218, 165, 32), font=font_small)
# Правая часть - дата
date_text = datetime.now().strftime("%d.%m.%Y")
date_width = draw.textlength(date_text, font=font_small)
draw.text((width - date_width - 80, footer_y), date_text, fill=(150, 150, 180), font=font_small)
# Подпись
sign_y = footer_y + 35
sign_text = "Молодёжный Парламент Донецкой Народной Республики"
sign_width = draw.textlength(sign_text, font=font_small)
draw.text(((width - sign_width) // 2, sign_y), sign_text, fill=(100, 100, 130), font=font_small)
return img
class QuizGame: class QuizGame:
def __init__(self): def __init__(self):
self.game_id = str(uuid.uuid4())[:8] self.game_id = str(uuid.uuid4())[:8]
self.questions_pool = QUESTION_POOL.copy() self.questions_pool = load_questions()
self.selected_questions = [] self.selected_questions = []
self.current_question_index = 0 self.current_question_index = 0
self.score = 0 self.score = 0
@@ -35,18 +259,18 @@ class QuizGame:
else: else:
self.selected_questions = self.questions_pool.copy() self.selected_questions = self.questions_pool.copy()
# Добавляем тайминг к каждому вопросу
for q in self.selected_questions: for q in self.selected_questions:
q['time_limit'] = 10 q['time_limit'] = 10
def get_current_question(self): def get_current_question(self):
"""Возвращает текущий вопрос (без правильного ответа)""" """Возвращает текущий вопрос (с правильным ответом для подсветки)"""
if self.current_question_index < len(self.selected_questions): if self.current_question_index < len(self.selected_questions):
q = self.selected_questions[self.current_question_index] q = self.selected_questions[self.current_question_index]
return { return {
'id': self.current_question_index, 'id': self.current_question_index,
'text': q['text'], 'text': q['text'],
'options': q['options'], 'options': q['options'],
'correct': q['correct'],
'time_limit': q.get('time_limit', 10) 'time_limit': q.get('time_limit', 10)
} }
return None return None
@@ -56,7 +280,6 @@ class QuizGame:
current_q = self.selected_questions[self.current_question_index] current_q = self.selected_questions[self.current_question_index]
is_correct = (answer_index == current_q['correct']) is_correct = (answer_index == current_q['correct'])
# Сохраняем ответ пользователя
self.user_answers.append({ self.user_answers.append({
'question_text': current_q['text'], 'question_text': current_q['text'],
'options': current_q['options'], 'options': current_q['options'],
@@ -87,21 +310,19 @@ class QuizGame:
total = len(self.selected_questions) total = len(self.selected_questions)
percentage = (self.score / total) * 100 percentage = (self.score / total) * 100
# Определение звания/результата
if percentage >= 90: if percentage >= 90:
grade = "🏆 Депутат высшей категории!" grade = "ДЕПУТАТ ВЫСШЕЙ КАТЕГОРИИ"
message = "Вы отлично знаете историю и структуру Молодёжного Парламента ДНР!" message = "Вы отлично знаете историю и структуру Молодёжного Парламента ДНР!"
elif percentage >= 70: elif percentage >= 70:
grade = "⭐ Активный парламентарий!" grade = "АКТИВНЫЙ ПАРЛАМЕНТАРИЙ"
message = "Хороший результат! Вы достойно показали свои знания." message = "Хороший результат! Вы достойно показали свои знания."
elif percentage >= 50: elif percentage >= 50:
grade = "📚 Кандидат в парламент!" grade = "КАНДИДАТ В ПАРЛАМЕНТ"
message = "Неплохо, но есть куда расти. Рекомендуем изучить материалы о работе Парламента." message = "Неплохо, но есть куда расти. Рекомендуем изучить материалы о работе Парламента."
else: else:
grade = "🌱 Будущий лидер!" grade = "БУДУЩИЙ ЛИДЕР"
message = "Не расстраивайтесь! Участие в викторине - первый шаг к большим достижениям." message = "Не расстраивайтесь! Участие в викторине - первый шаг к большим достижениям."
# Сохраняем результат для QR-кода
result_data = { result_data = {
'game_id': self.game_id, 'game_id': self.game_id,
'score': self.score, 'score': self.score,
@@ -121,7 +342,7 @@ class QuizGame:
'grade': grade, 'grade': grade,
'message': message, 'message': message,
'questions_summary': self.user_answers, 'questions_summary': self.user_answers,
'result_id': self.game_id # Возвращаем ID для QR-кода 'result_id': self.game_id
} }
@@ -130,6 +351,87 @@ def index():
return render_template('index.html') return render_template('index.html')
@app.route('/admin')
def admin():
return render_template('admin.html')
@app.route('/api/questions', methods=['GET'])
def get_questions():
questions = load_questions()
return jsonify(questions)
@app.route('/api/questions', methods=['POST'])
def add_question():
data = request.json
questions = load_questions()
new_question = {
'text': data['text'],
'options': data['options'],
'correct': data['correct'],
'explanation': data.get('explanation', '')
}
questions.append(new_question)
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос добавлен', 'question': new_question})
@app.route('/api/questions/<int:index>', methods=['PUT'])
def update_question(index):
data = request.json
questions = load_questions()
if 0 <= index < len(questions):
questions[index] = {
'text': data['text'],
'options': data['options'],
'correct': data['correct'],
'explanation': data.get('explanation', '')
}
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос обновлён'})
return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404
@app.route('/api/questions/<int:index>', methods=['DELETE'])
def delete_question(index):
questions = load_questions()
if 0 <= index < len(questions):
deleted = questions.pop(index)
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос удалён', 'question': deleted})
return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404
@app.route('/api/questions/reset', methods=['POST'])
def reset_questions():
from questions import QUESTION_POOL
save_questions(QUESTION_POOL.copy())
return jsonify({'success': True, 'message': 'Вопросы сброшены к стандартному пулу'})
@app.route('/api/questions/export', methods=['GET'])
def export_questions():
questions = load_questions()
return jsonify(questions)
@app.route('/api/questions/import', methods=['POST'])
def import_questions():
data = request.json
if isinstance(data, list) and len(data) > 0:
save_questions(data)
return jsonify({'success': True, 'message': f'Импортировано {len(data)} вопросов'})
return jsonify({'success': False, 'message': 'Неверный формат данных'}), 400
@app.route('/api/new_game', methods=['POST']) @app.route('/api/new_game', methods=['POST'])
def new_game(): def new_game():
game = QuizGame() game = QuizGame()
@@ -155,14 +457,12 @@ def check_answer():
result = game.check_answer(answer) result = game.check_answer(answer)
if result['is_finished']: if result['is_finished']:
# Игра завершена
final_result = game.get_result() final_result = game.get_result()
return jsonify({ return jsonify({
'is_finished': True, 'is_finished': True,
'result': final_result 'result': final_result
}) })
else: else:
# Следующий вопрос
return jsonify({ return jsonify({
'is_finished': False, 'is_finished': False,
'result': result, 'result': result,
@@ -173,31 +473,14 @@ def check_answer():
@app.route('/api/result/<result_id>') @app.route('/api/result/<result_id>')
def get_result_page(result_id): def get_result_image(result_id):
"""Страница с результатами по QR-коду""" """Возвращает изображение с результатами в формате 16:9"""
result = saved_results.get(result_id) result = saved_results.get(result_id)
if not result: if not result:
return "Результат не найден", 404 return "Результат не найден", 404
return render_template('result_page.html', result=result)
@app.route('/api/qrcode/<result_id>')
def generate_qrcode(result_id):
"""Генерация QR-кода со ссылкой на результат"""
result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}'
# Создаём QR-код
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(result_url)
qr.make(fit=True)
# Создаём изображение # Создаём изображение
img = qr.make_image(fill_color="black", back_color="white") img = create_result_image(result)
# Сохраняем в байты # Сохраняем в байты
img_io = BytesIO() img_io = BytesIO()
@@ -207,5 +490,50 @@ def generate_qrcode(result_id):
return send_file(img_io, mimetype='image/png') return send_file(img_io, mimetype='image/png')
@app.route('/api/qrcode/<result_id>')
def generate_qrcode(result_id):
if not QRCODE_AVAILABLE:
from PIL import Image, ImageDraw
img = Image.new('RGB', (200, 200), color='white')
draw = ImageDraw.Draw(img)
draw.text((20, 80), "QR Code", fill='black')
draw.text((20, 100), "not available", fill='black')
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}'
try:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(result_url)
qr.make(fit=True)
img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45))
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
except Exception as e:
print(f"QR Code generation error: {e}")
from PIL import Image, ImageDraw
img = Image.new('RGB', (200, 200), color='white')
draw = ImageDraw.Draw(img)
draw.text((10, 80), "QR Error", fill='black')
draw.text((10, 100), result_url[:30], fill='black')
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)
+749
View File
@@ -0,0 +1,749 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель - Управление вопросами викторины</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.admin-container {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header h1 {
font-size: 28px;
}
.header-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
transform: translateY(-2px);
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #da190b;
transform: translateY(-2px);
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #e68900;
transform: translateY(-2px);
}
.btn-info {
background: #2196F3;
color: white;
}
.btn-info:hover {
background: #0b7dda;
transform: translateY(-2px);
}
.btn-secondary {
background: #9e9e9e;
color: white;
}
.btn-secondary:hover {
background: #757575;
transform: translateY(-2px);
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
margin-top: 5px;
}
/* Search and filter */
.search-bar {
background: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.search-input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
/* Questions list */
.questions-list {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.question-item {
border-bottom: 1px solid #e0e0e0;
padding: 20px;
transition: background 0.3s;
}
.question-item:hover {
background: #f9f9f9;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.question-number {
font-weight: bold;
color: #667eea;
font-size: 18px;
}
.question-actions {
display: flex;
gap: 10px;
}
.question-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 10px;
color: #333;
}
.question-options {
margin: 10px 0;
padding-left: 20px;
}
.option {
font-size: 14px;
color: #666;
margin: 5px 0;
}
.option.correct {
color: #4CAF50;
font-weight: bold;
}
.question-explanation {
font-size: 13px;
color: #999;
font-style: italic;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 20px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: #333;
}
.close-modal {
font-size: 28px;
cursor: pointer;
color: #999;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.options-group {
margin-bottom: 15px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
}
.option-input {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.option-input input {
flex: 1;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
/* Alert */
.alert {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 10px;
color: white;
font-weight: bold;
z-index: 1100;
animation: slideIn 0.3s ease;
}
.alert-success {
background: #4CAF50;
}
.alert-error {
background: #f44336;
}
.alert-info {
background: #2196F3;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Empty state */
.empty-state {
text-align: center;
padding: 60px;
color: #999;
}
/* Responsive */
@media (max-width: 768px) {
.header {
flex-direction: column;
text-align: center;
}
.question-header {
flex-direction: column;
}
.question-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>
</head>
<body>
<div class="admin-container">
<div class="header">
<h1>📝 Админ-панель управления вопросами</h1>
<div class="header-buttons">
<button class="btn btn-primary" onclick="openAddModal()"> Добавить вопрос</button>
<button class="btn btn-info" onclick="exportQuestions()">📤 Экспорт</button>
<button class="btn btn-warning" onclick="importQuestions()">📥 Импорт</button>
<button class="btn btn-danger" onclick="resetQuestions()">🔄 Сбросить</button>
<a href="/" class="btn btn-secondary" target="_blank">🎮 Перейти в викторину</a>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="totalQuestions">0</div>
<div class="stat-label">Всего вопросов</div>
</div>
<div class="stat-card">
<div class="stat-number" id="activeGames">0</div>
<div class="stat-label">Активных игр</div>
</div>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="searchInput" placeholder="🔍 Поиск вопросов...">
</div>
<div class="questions-list" id="questionsList">
<div class="empty-state">Загрузка вопросов...</div>
</div>
</div>
<!-- Модальное окно добавления/редактирования -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Добавить вопрос</h2>
<span class="close-modal" onclick="closeModal()">&times;</span>
</div>
<form id="questionForm">
<div class="form-group">
<label>Вопрос *</label>
<textarea id="questionText" required placeholder="Введите текст вопроса..."></textarea>
</div>
<div class="form-group">
<label>Варианты ответов *</label>
<div id="optionsContainer">
<div class="options-group">
<div class="option-input">
<input type="text" class="option-input-0" placeholder="Вариант 1" required>
</div>
<div class="option-input">
<input type="text" class="option-input-1" placeholder="Вариант 2" required>
</div>
<div class="option-input">
<input type="text" class="option-input-2" placeholder="Вариант 3" required>
</div>
<div class="option-input">
<input type="text" class="option-input-3" placeholder="Вариант 4" required>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Правильный ответ *</label>
<select id="correctAnswer" required>
<option value="0">Вариант 1</option>
<option value="1">Вариант 2</option>
<option value="2">Вариант 3</option>
<option value="3">Вариант 4</option>
</select>
</div>
<div class="form-group">
<label>Пояснение</label>
<textarea id="explanation" placeholder="Пояснение к правильному ответу..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
let questions = [];
let editingIndex = null;
// Загрузка вопросов
async function loadQuestions() {
try {
const response = await fetch('/api/questions');
questions = await response.json();
document.getElementById('totalQuestions').textContent = questions.length;
renderQuestions();
} catch (error) {
console.error('Ошибка:', error);
showAlert('Ошибка загрузки вопросов', 'error');
}
}
// Отображение вопросов
function renderQuestions() {
const container = document.getElementById('questionsList');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const filtered = questions.filter(q =>
q.text.toLowerCase().includes(searchTerm) ||
q.options.some(opt => opt.toLowerCase().includes(searchTerm))
);
if (filtered.length === 0) {
container.innerHTML = '<div class="empty-state">📭 Вопросы не найдены</div>';
return;
}
container.innerHTML = filtered.map((q, idx) => {
const originalIndex = questions.findIndex(orig => orig === q);
return `
<div class="question-item">
<div class="question-header">
<span class="question-number">Вопрос #${originalIndex + 1}</span>
<div class="question-actions">
<button class="btn btn-info" onclick="editQuestion(${originalIndex})" style="padding: 5px 15px;">✏️ Редактировать</button>
<button class="btn btn-danger" onclick="deleteQuestion(${originalIndex})" style="padding: 5px 15px;">🗑️ Удалить</button>
</div>
</div>
<div class="question-text">${escapeHtml(q.text)}</div>
<div class="question-options">
${q.options.map((opt, optIdx) => `
<div class="option ${optIdx === q.correct ? 'correct' : ''}">
${String.fromCharCode(65+optIdx)}. ${escapeHtml(opt)} ${optIdx === q.correct ? '✓' : ''}
</div>
`).join('')}
</div>
${q.explanation ? `<div class="question-explanation">💡 ${escapeHtml(q.explanation)}</div>` : ''}
</div>
`;
}).join('');
}
// Экранирование HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Открыть модальное окно для добавления
function openAddModal() {
editingIndex = null;
document.getElementById('modalTitle').textContent = '➕ Добавить вопрос';
document.getElementById('questionForm').reset();
document.getElementById('correctAnswer').value = '0';
document.getElementById('modal').classList.add('active');
}
// Редактирование вопроса
function editQuestion(index) {
editingIndex = index;
const q = questions[index];
document.getElementById('modalTitle').textContent = '✏️ Редактировать вопрос';
document.getElementById('questionText').value = q.text;
document.getElementById('correctAnswer').value = q.correct;
document.getElementById('explanation').value = q.explanation || '';
// Заполняем варианты ответов
q.options.forEach((opt, i) => {
const input = document.querySelector(`.option-input-${i}`);
if (input) input.value = opt;
});
document.getElementById('modal').classList.add('active');
}
// Сохранение вопроса
document.getElementById('questionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const options = [];
for (let i = 0; i < 4; i++) {
const input = document.querySelector(`.option-input-${i}`);
if (!input.value.trim()) {
showAlert('Заполните все варианты ответов', 'error');
return;
}
options.push(input.value.trim());
}
const questionData = {
text: document.getElementById('questionText').value.trim(),
options: options,
correct: parseInt(document.getElementById('correctAnswer').value),
explanation: document.getElementById('explanation').value.trim()
};
if (!questionData.text) {
showAlert('Введите текст вопроса', 'error');
return;
}
try {
let response;
if (editingIndex !== null) {
response = await fetch(`/api/questions/${editingIndex}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questionData)
});
} else {
response = await fetch('/api/questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questionData)
});
}
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
closeModal();
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка сохранения', 'error');
}
});
// Удаление вопроса
async function deleteQuestion(index) {
if (confirm('Вы уверены, что хотите удалить этот вопрос?')) {
try {
const response = await fetch(`/api/questions/${index}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка удаления', 'error');
}
}
}
// Сброс вопросов
async function resetQuestions() {
if (confirm('Сбросить все вопросы к стандартному пулу? Все добавленные вопросы будут удалены!')) {
try {
const response = await fetch('/api/questions/reset', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
}
} catch (error) {
showAlert('Ошибка сброса', 'error');
}
}
}
// Экспорт вопросов
async function exportQuestions() {
try {
const response = await fetch('/api/questions/export');
const data = await response.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `questions_export_${new Date().toISOString().slice(0,19)}.json`;
a.click();
URL.revokeObjectURL(url);
showAlert('Вопросы экспортированы', 'success');
} catch (error) {
showAlert('Ошибка экспорта', 'error');
}
}
// Импорт вопросов
async function importQuestions() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = async (event) => {
try {
const questions = JSON.parse(event.target.result);
const response = await fetch('/api/questions/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questions)
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка парсинга файла', 'error');
}
};
reader.readAsText(file);
};
input.click();
}
// Показать уведомление
function showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 3000);
}
// Закрыть модальное окно
function closeModal() {
document.getElementById('modal').classList.remove('active');
editingIndex = null;
}
// Поиск
document.getElementById('searchInput').addEventListener('input', () => {
renderQuestions();
});
// Закрытие модального окна по клику вне его
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) {
closeModal();
}
});
// Загрузка активных игр
async function loadActiveGames() {
// Можно добавить эндпоинт для подсчёта активных игр
document.getElementById('activeGames').textContent = Object.keys(activeGames || {}).length;
}
// Инициализация
loadQuestions();
setInterval(loadActiveGames, 5000);
</script>
</body>
</html>
+103 -84
View File
@@ -101,12 +101,41 @@
color: #666; color: #666;
} }
/* Вопрос с возможностью подсветки */
.question-wrapper {
margin: 20px 30px;
padding: 20px;
border-radius: 15px;
transition: all 0.3s ease;
position: relative;
}
.question-wrapper.correct-highlight {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border: 2px solid #28a745;
box-shadow: 0 0 15px rgba(40, 167, 69, 0.3);
}
.question-wrapper.wrong-highlight {
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
border: 2px solid #dc3545;
box-shadow: 0 0 15px rgba(220, 53, 69, 0.3);
}
.question-text { .question-text {
padding: 30px 30px 20px;
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
line-height: 1.4; line-height: 1.4;
margin-bottom: 10px;
}
.question-wrapper.correct-highlight .question-text {
color: #155724;
}
.question-wrapper.wrong-highlight .question-text {
color: #721c24;
} }
.options { .options {
@@ -152,7 +181,7 @@
opacity: 0.7; opacity: 0.7;
} }
.next-btn, .restart-btn { .next-btn {
margin: 10px 30px 30px; margin: 10px 30px 30px;
padding: 15px; padding: 15px;
font-size: 18px; font-size: 18px;
@@ -162,9 +191,6 @@
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
width: calc(100% - 60px); width: calc(100% - 60px);
}
.next-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
} }
@@ -215,9 +241,6 @@
} }
.result-buttons { .result-buttons {
display: flex;
flex-direction: column;
gap: 15px;
margin: 20px 0 30px; margin: 20px 0 30px;
} }
@@ -230,56 +253,52 @@
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
width: 100%; width: 100%;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
} }
.btn-secondary { .result-buttons .btn:hover {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-qr {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
}
.btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
} }
.qr-container { .qr-container {
margin: 20px 0; margin: 30px 0;
display: none; padding: 20px;
background: #f8f9fa;
border-radius: 15px;
text-align: center; text-align: center;
} }
.qr-container.show {
display: block;
}
.qr-code { .qr-code {
display: inline-block; display: inline-block;
padding: 20px; padding: 15px;
background: white; background: white;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1); box-shadow: 0 5px 15px rgba(0,0,0,0.1);
margin-bottom: 15px;
} }
.qr-code img { .qr-code img {
max-width: 200px; max-width: 180px;
height: auto; height: auto;
display: block;
} }
.qr-link { .qr-link {
margin-top: 10px;
font-size: 12px; font-size: 12px;
color: #666; color: #666;
word-break: break-all; word-break: break-all;
margin-top: 10px;
}
.qr-link a {
color: #667eea;
text-decoration: none;
}
.qr-link a:hover {
text-decoration: underline;
} }
.questions-review { .questions-review {
@@ -371,7 +390,6 @@
@media (max-width: 600px) { @media (max-width: 600px) {
.question-text { .question-text {
font-size: 18px; font-size: 18px;
padding: 20px;
} }
.option-btn { .option-btn {
@@ -395,6 +413,15 @@
.result-grade { .result-grade {
font-size: 24px; font-size: 24px;
} }
.qr-code img {
max-width: 150px;
}
.question-wrapper {
margin: 15px 15px;
padding: 15px;
}
} }
</style> </style>
</head> </head>
@@ -419,9 +446,11 @@
<div class="timer-text" id="timerText">⏱️ 10 секунд</div> <div class="timer-text" id="timerText">⏱️ 10 секунд</div>
</div> </div>
<div class="question-wrapper" id="questionWrapper">
<div class="question-text" id="questionText"> <div class="question-text" id="questionText">
Загрузка вопроса... Загрузка вопроса...
</div> </div>
</div>
<div class="options" id="optionsContainer"> <div class="options" id="optionsContainer">
<button class="option-btn" data-index="0">Вариант 1</button> <button class="option-btn" data-index="0">Вариант 1</button>
@@ -444,19 +473,15 @@
<div class="result-grade" id="resultGrade"></div> <div class="result-grade" id="resultGrade"></div>
<div class="result-message" id="resultMessage"></div> <div class="result-message" id="resultMessage"></div>
<!-- КНОПКИ ПЕРЕД РАСШИФРОВКОЙ -->
<div class="result-buttons"> <div class="result-buttons">
<button class="btn btn-primary" id="restartBtn">🔄 Пройти викторину заново</button> <button class="btn" id="restartBtn">🔄 Пройти викторину заново</button>
<button class="btn btn-qr" id="showQrBtn">📱 Показать QR-код с результатом</button>
</div> </div>
<!-- КОНТЕЙНЕР ДЛЯ QR-КОДА -->
<div class="qr-container" id="qrContainer"> <div class="qr-container" id="qrContainer">
<div class="qr-code" id="qrCode"> <div class="qr-code">
<img id="qrImage" alt="QR-код с результатом"> <img id="qrImage" alt="QR-код с результатом">
</div> </div>
<div class="qr-link" id="qrLink"></div> <div class="qr-link" id="qrLink"></div>
<button class="btn btn-secondary" id="hideQrBtn" style="margin-top: 10px; padding: 8px 20px; width: auto;">Скрыть QR-код</button>
</div> </div>
<div id="questionsReview" class="questions-review hidden"></div> <div id="questionsReview" class="questions-review hidden"></div>
@@ -480,6 +505,7 @@
const timerProgress = document.getElementById('timerProgress'); const timerProgress = document.getElementById('timerProgress');
const timerText = document.getElementById('timerText'); const timerText = document.getElementById('timerText');
const questionText = document.getElementById('questionText'); const questionText = document.getElementById('questionText');
const questionWrapper = document.getElementById('questionWrapper');
const optionsContainer = document.getElementById('optionsContainer'); const optionsContainer = document.getElementById('optionsContainer');
const feedback = document.getElementById('feedback'); const feedback = document.getElementById('feedback');
const nextBtn = document.getElementById('nextBtn'); const nextBtn = document.getElementById('nextBtn');
@@ -490,10 +516,8 @@
const qrImage = document.getElementById('qrImage'); const qrImage = document.getElementById('qrImage');
const qrLink = document.getElementById('qrLink'); const qrLink = document.getElementById('qrLink');
// Опции кнопок
const optionBtns = document.querySelectorAll('.option-btn'); const optionBtns = document.querySelectorAll('.option-btn');
// Начать новую игру
async function startNewGame() { async function startNewGame() {
try { try {
const response = await fetch('/api/new_game', { const response = await fetch('/api/new_game', {
@@ -513,9 +537,10 @@
questionArea.classList.remove('hidden'); questionArea.classList.remove('hidden');
resultArea.classList.add('hidden'); resultArea.classList.add('hidden');
questionsReview.classList.add('hidden'); questionsReview.classList.add('hidden');
qrContainer.classList.remove('show');
isAnswered = false; isAnswered = false;
removeQuestionHighlight();
startTimer(10); startTimer(10);
} catch (error) { } catch (error) {
console.error('Ошибка:', error); console.error('Ошибка:', error);
@@ -523,7 +548,20 @@
} }
} }
// Отображение вопроса function highlightQuestion(isCorrect) {
removeQuestionHighlight();
if (isCorrect) {
questionWrapper.classList.add('correct-highlight');
} else {
questionWrapper.classList.add('wrong-highlight');
}
}
function removeQuestionHighlight() {
questionWrapper.classList.remove('correct-highlight');
questionWrapper.classList.remove('wrong-highlight');
}
function displayQuestion(question, qNumber) { function displayQuestion(question, qNumber) {
if (!question) return; if (!question) return;
@@ -531,7 +569,6 @@
questionCounter.textContent = `Вопрос ${qNumber}/${totalQuestions}`; questionCounter.textContent = `Вопрос ${qNumber}/${totalQuestions}`;
questionText.textContent = question.text; questionText.textContent = question.text;
// Обновляем варианты ответов
question.options.forEach((option, index) => { question.options.forEach((option, index) => {
if (optionBtns[index]) { if (optionBtns[index]) {
optionBtns[index].textContent = `${String.fromCharCode(65+index)}. ${option}`; optionBtns[index].textContent = `${String.fromCharCode(65+index)}. ${option}`;
@@ -540,13 +577,12 @@
} }
}); });
// Скрываем фидбек и кнопку далее
feedback.classList.add('hidden'); feedback.classList.add('hidden');
nextBtn.classList.add('hidden'); nextBtn.classList.add('hidden');
isAnswered = false; isAnswered = false;
removeQuestionHighlight();
} }
// Таймер
function startTimer(seconds) { function startTimer(seconds) {
if (timerInterval) clearInterval(timerInterval); if (timerInterval) clearInterval(timerInterval);
@@ -580,17 +616,16 @@
} }
} }
// Обработка timeout
async function handleTimeout() { async function handleTimeout() {
if (isAnswered) return; if (isAnswered) return;
isAnswered = true; isAnswered = true;
clearInterval(timerInterval); clearInterval(timerInterval);
// Отключаем кнопки highlightQuestion(false);
optionBtns.forEach(btn => btn.disabled = true); optionBtns.forEach(btn => btn.disabled = true);
// Отправляем пустой ответ (время вышло)
try { try {
const response = await fetch('/api/check_answer', { const response = await fetch('/api/check_answer', {
method: 'POST', method: 'POST',
@@ -618,30 +653,37 @@
} }
} }
// Обработка ответа пользователя
async function handleAnswer(answerIndex) { async function handleAnswer(answerIndex) {
if (isAnswered) return; if (isAnswered) return;
isAnswered = true; isAnswered = true;
clearInterval(timerInterval); clearInterval(timerInterval);
// Визуальная обратная связь
const selectedBtn = optionBtns[answerIndex]; const selectedBtn = optionBtns[answerIndex];
// ВАЖНО: проверяем правильность ответа
// currentQuestion.correct приходит от сервера с индексом правильного ответа
const isCorrect = (answerIndex === currentQuestion.correct); const isCorrect = (answerIndex === currentQuestion.correct);
console.log('Answer index:', answerIndex);
console.log('Correct index:', currentQuestion.correct);
console.log('Is correct:', isCorrect);
// Подсвечиваем вопрос
highlightQuestion(isCorrect);
if (isCorrect) { if (isCorrect) {
selectedBtn.classList.add('correct'); selectedBtn.classList.add('correct');
currentScore++;
updateScoreDisplay();
} else { } else {
selectedBtn.classList.add('wrong'); selectedBtn.classList.add('wrong');
// Показываем правильный ответ
const correctBtn = optionBtns[currentQuestion.correct]; const correctBtn = optionBtns[currentQuestion.correct];
if (correctBtn) correctBtn.classList.add('correct'); if (correctBtn) correctBtn.classList.add('correct');
} }
// Отключаем все кнопки
optionBtns.forEach(btn => btn.disabled = true); optionBtns.forEach(btn => btn.disabled = true);
// Отправляем ответ на сервер
try { try {
const response = await fetch('/api/check_answer', { const response = await fetch('/api/check_answer', {
method: 'POST', method: 'POST',
@@ -665,7 +707,6 @@
} }
} }
// Показать фидбек после ответа
function showFeedback(result) { function showFeedback(result) {
const feedbackTitle = document.getElementById('feedbackTitle'); const feedbackTitle = document.getElementById('feedbackTitle');
const feedbackText = document.getElementById('feedbackText'); const feedbackText = document.getElementById('feedbackText');
@@ -673,8 +714,6 @@
if (result.is_correct) { if (result.is_correct) {
feedbackTitle.textContent = '✅ Правильно!'; feedbackTitle.textContent = '✅ Правильно!';
feedbackTitle.style.color = '#28a745'; feedbackTitle.style.color = '#28a745';
currentScore = result.score;
updateScoreDisplay();
feedbackText.textContent = result.explanation || 'Отличный результат!'; feedbackText.textContent = result.explanation || 'Отличный результат!';
} else { } else {
feedbackTitle.textContent = '❌ Неправильно!'; feedbackTitle.textContent = '❌ Неправильно!';
@@ -685,7 +724,6 @@
feedback.classList.remove('hidden'); feedback.classList.remove('hidden');
} }
// Загрузить следующий вопрос
function loadNextQuestion(data) { function loadNextQuestion(data) {
if (data.next_question) { if (data.next_question) {
displayQuestion(data.next_question, data.question_number); displayQuestion(data.next_question, data.question_number);
@@ -693,7 +731,6 @@
} }
} }
// Показать все вопросы с ответами
function displayAllQuestions(questionsSummary) { function displayAllQuestions(questionsSummary) {
questionsReview.innerHTML = '<h3>📋 Детальный разбор всех вопросов</h3>'; questionsReview.innerHTML = '<h3>📋 Детальный разбор всех вопросов</h3>';
questionsReview.classList.remove('hidden'); questionsReview.classList.remove('hidden');
@@ -728,8 +765,7 @@
}); });
} }
// Показать QR-код async function displayQRCode(resultId) {
async function showQRCode(resultId) {
try { try {
const response = await fetch(`/api/qrcode/${resultId}`); const response = await fetch(`/api/qrcode/${resultId}`);
if (response.ok) { if (response.ok) {
@@ -737,21 +773,18 @@
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
qrImage.src = url; qrImage.src = url;
// Показываем ссылку // Ссылка на изображение с результатами
const fullUrl = window.location.origin + `/api/result/${resultId}`; const fullUrl = window.location.origin + `/api/result/${resultId}`;
qrLink.innerHTML = `<a href="${fullUrl}" target="_blank">${fullUrl}</a>`; qrLink.innerHTML = `📎 Ссылка на результат: <a href="${fullUrl}" target="_blank">${fullUrl}</a>`;
qrContainer.classList.add('show');
} else { } else {
alert('Не удалось сгенерировать QR-код'); qrContainer.innerHTML = '<p style="color: red;">❌ Не удалось сгенерировать QR-код</p>';
} }
} catch (error) { } catch (error) {
console.error('Ошибка:', error); console.error('Ошибка:', error);
alert('Ошибка при генерации QR-кода'); qrContainer.innerHTML = '<p style="color: red;">❌ Ошибка при генерации QR-кода</p>';
} }
} }
// Показать результат
function showResult(result) { function showResult(result) {
clearInterval(timerInterval); clearInterval(timerInterval);
questionArea.classList.add('hidden'); questionArea.classList.add('hidden');
@@ -766,18 +799,17 @@
resultMessage.textContent = result.message; resultMessage.textContent = result.message;
currentResultId = result.result_id; currentResultId = result.result_id;
// Показываем разбор вопросов (после кнопок) displayQRCode(currentResultId);
if (result.questions_summary) { if (result.questions_summary) {
displayAllQuestions(result.questions_summary); displayAllQuestions(result.questions_summary);
} }
} }
// Обновить отображение счёта
function updateScoreDisplay() { function updateScoreDisplay() {
scoreDisplay.textContent = `Счёт: ${currentScore}`; scoreDisplay.textContent = `Счёт: ${currentScore}`;
} }
// Обработчики событий
optionBtns.forEach(btn => { optionBtns.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const index = parseInt(btn.dataset.index); const index = parseInt(btn.dataset.index);
@@ -789,19 +821,6 @@
startNewGame(); startNewGame();
}); });
document.getElementById('showQrBtn').addEventListener('click', () => {
if (currentResultId) {
showQRCode(currentResultId);
} else {
alert('Результаты ещё не готовы');
}
});
document.getElementById('hideQrBtn').addEventListener('click', () => {
qrContainer.classList.remove('show');
});
// Запуск игры
startNewGame(); startNewGame();
</script> </script>
</body> </body>