init Questionnaire
This commit is contained in:
1830
server/routers/questioneer/public/static/css/style.css
Normal file
1830
server/routers/questioneer/public/static/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
294
server/routers/questioneer/public/static/js/admin.js
Normal file
294
server/routers/questioneer/public/static/js/admin.js
Normal file
@@ -0,0 +1,294 @@
|
||||
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
|
||||
$(document).ready(function() {
|
||||
const adminLink = window.location.pathname.split('/').pop();
|
||||
let questionnaireData = null;
|
||||
|
||||
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последние две части пути (admin/:adminLink)
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
const loadQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
questionnaireData = result.data;
|
||||
renderQuestionnaire();
|
||||
} else {
|
||||
$('#loading').text(`Ошибка: ${result.error}`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaire:', error);
|
||||
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Отображение данных опроса
|
||||
const renderQuestionnaire = () => {
|
||||
// Заполняем основные данные
|
||||
$('#questionnaire-title').text(questionnaireData.title);
|
||||
$('#questionnaire-description').text(questionnaireData.description || 'Нет описания');
|
||||
|
||||
// Формируем ссылки
|
||||
const baseUrl = window.location.origin;
|
||||
const baseQuestionnairePath = window.location.pathname.split('/admin')[0];
|
||||
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
|
||||
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
|
||||
|
||||
$('#public-link').val(publicUrl);
|
||||
$('#admin-link').val(adminUrl);
|
||||
|
||||
// Отображаем статистику
|
||||
renderStats(questionnaireData.questions);
|
||||
|
||||
// Показываем контейнер с данными
|
||||
$('#loading').hide();
|
||||
$('#questionnaire-container').show();
|
||||
};
|
||||
|
||||
// Отображение статистики опроса
|
||||
const renderStats = (questions) => {
|
||||
const $statsContainer = $('#stats-container');
|
||||
$statsContainer.empty();
|
||||
|
||||
// Проверяем, есть ли ответы
|
||||
let hasAnyResponses = false;
|
||||
|
||||
// Проверяем наличие ответов для каждого типа вопросов
|
||||
for (const question of questions) {
|
||||
if (question.type === 'single' || question.type === 'multiple') {
|
||||
if (question.options && question.options.some(option => option.votes && option.votes > 0)) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'tagcloud') {
|
||||
if (question.tags && question.tags.some(tag => tag.count && tag.count > 0)) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'scale' || question.type === 'rating') {
|
||||
if (question.responses && question.responses.length > 0) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'text') {
|
||||
if (question.textAnswers && question.textAnswers.length > 0) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnyResponses) {
|
||||
$statsContainer.html('<div class="no-stats">Пока нет ответов на опрос</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Для каждого вопроса создаем блок статистики
|
||||
questions.forEach((question, index) => {
|
||||
const $questionStats = $('<div>', { class: 'question-stats' });
|
||||
const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
|
||||
$questionStats.append($questionTitle);
|
||||
|
||||
// В зависимости от типа вопроса отображаем разную статистику
|
||||
if (question.type === 'single' || question.type === 'multiple') {
|
||||
// Для вопросов с выбором вариантов
|
||||
const totalVotes = question.options.reduce((sum, option) => sum + (option.votes || 0), 0);
|
||||
|
||||
if (totalVotes === 0) {
|
||||
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
|
||||
} else {
|
||||
const $table = $('<table>', { class: 'stats-table' });
|
||||
const $thead = $('<thead>').append(
|
||||
$('<tr>').append(
|
||||
$('<th>', { text: 'Вариант' }),
|
||||
$('<th>', { text: 'Голоса' }),
|
||||
$('<th>', { text: '%' }),
|
||||
$('<th>', { text: 'Визуализация' })
|
||||
)
|
||||
);
|
||||
|
||||
const $tbody = $('<tbody>');
|
||||
|
||||
question.options.forEach(option => {
|
||||
const votes = option.votes || 0;
|
||||
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
|
||||
|
||||
const $tr = $('<tr>').append(
|
||||
$('<td>', { text: option.text }),
|
||||
$('<td>', { text: votes }),
|
||||
$('<td>', { text: `${percent}%` }),
|
||||
$('<td>').append(
|
||||
$('<div>', { class: 'bar-container' }).append(
|
||||
$('<div>', {
|
||||
class: 'bar',
|
||||
css: { width: `${percent}%` }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$tbody.append($tr);
|
||||
});
|
||||
|
||||
$table.append($thead, $tbody);
|
||||
$questionStats.append($table);
|
||||
$questionStats.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
|
||||
}
|
||||
} else if (question.type === 'tagcloud') {
|
||||
// Для облака тегов
|
||||
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
|
||||
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
|
||||
} else {
|
||||
const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
|
||||
|
||||
// Находим максимальное количество для масштабирования
|
||||
const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
|
||||
|
||||
// Сортируем теги по популярности
|
||||
const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
|
||||
|
||||
sortedTags.forEach(tag => {
|
||||
if (tag.count && tag.count > 0) {
|
||||
const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
|
||||
|
||||
$tagCloud.append(
|
||||
$('<span>', {
|
||||
class: 'tag-item',
|
||||
text: `${tag.text} (${tag.count})`,
|
||||
css: { fontSize: `${fontSize}em` }
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$questionStats.append($tagCloud);
|
||||
}
|
||||
} else if (question.type === 'scale' || question.type === 'rating') {
|
||||
// Для шкалы и рейтинга
|
||||
if (!question.responses || question.responses.length === 0) {
|
||||
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
|
||||
} else {
|
||||
const values = question.responses;
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / values.length;
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
|
||||
const $scaleStats = $('<div>', { class: 'scale-stats' });
|
||||
|
||||
$scaleStats.append(
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
|
||||
$('<span>', { class: 'stat-value', text: avg.toFixed(1) })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Минимум:' }),
|
||||
$('<span>', { class: 'stat-value', text: min })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Максимум:' }),
|
||||
$('<span>', { class: 'stat-value', text: max })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
|
||||
$('<span>', { class: 'stat-value', text: values.length })
|
||||
)
|
||||
);
|
||||
|
||||
$questionStats.append($scaleStats);
|
||||
}
|
||||
} else if (question.type === 'text') {
|
||||
// Для текстовых ответов
|
||||
if (!question.textAnswers || question.textAnswers.length === 0) {
|
||||
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
|
||||
} else {
|
||||
const $textAnswers = $('<div>', { class: 'text-answers-list' });
|
||||
|
||||
question.textAnswers.forEach((answer, i) => {
|
||||
$textAnswers.append(
|
||||
$('<div>', { class: 'text-answer-item' }).append(
|
||||
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
|
||||
$('<div>', { class: 'answer-text', text: answer })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
$questionStats.append($textAnswers);
|
||||
}
|
||||
}
|
||||
|
||||
$statsContainer.append($questionStats);
|
||||
});
|
||||
};
|
||||
|
||||
// Копирование ссылок
|
||||
$('#copy-public-link').on('click', function() {
|
||||
$('#public-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование', null, true);
|
||||
});
|
||||
|
||||
$('#copy-admin-link').on('click', function() {
|
||||
$('#admin-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование', null, true);
|
||||
});
|
||||
|
||||
// Отображение QR-кода
|
||||
$('#show-qr-code').on('click', function() {
|
||||
const publicUrl = $('#public-link').val();
|
||||
showQRCodeModal(publicUrl, 'QR-код для голосования');
|
||||
});
|
||||
|
||||
// Редактирование опроса
|
||||
$('#edit-questionnaire').on('click', function() {
|
||||
const basePath = window.location.pathname.split('/admin')[0];
|
||||
window.location.href = `${basePath}/edit/${adminLink}`;
|
||||
});
|
||||
|
||||
// Удаление опроса
|
||||
$('#delete-questionnaire').on('click', function() {
|
||||
showConfirm('Вы уверены, что хотите удалить опрос? Все ответы будут удалены безвозвратно.', function(confirmed) {
|
||||
if (confirmed) {
|
||||
deleteQuestionnaire();
|
||||
}
|
||||
}, 'Удаление опроса');
|
||||
});
|
||||
|
||||
// Функция удаления опроса
|
||||
const deleteQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
method: 'DELETE',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
showAlert('Опрос успешно удален', 'Удаление опроса', function() {
|
||||
window.location.href = window.location.pathname.split('/admin')[0];
|
||||
}, true);
|
||||
} else {
|
||||
showAlert(`Ошибка при удалении опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error deleting questionnaire:', error);
|
||||
showAlert('Не удалось удалить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Инициализация
|
||||
loadQuestionnaire();
|
||||
|
||||
// Обновление данных каждые 10 секунд
|
||||
setInterval(loadQuestionnaire, 10000);
|
||||
});
|
||||
236
server/routers/questioneer/public/static/js/common.js
Normal file
236
server/routers/questioneer/public/static/js/common.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/* global $, document */
|
||||
|
||||
// Функция для создания модального окна
|
||||
function createModal(options) {
|
||||
// Если модальное окно уже существует, удаляем его
|
||||
$('.modal-overlay').remove();
|
||||
|
||||
// Опции по умолчанию
|
||||
const defaultOptions = {
|
||||
title: 'Сообщение',
|
||||
content: '',
|
||||
closeText: 'Закрыть',
|
||||
onClose: null,
|
||||
showCancel: false,
|
||||
cancelText: 'Отмена',
|
||||
confirmText: 'Подтвердить',
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
size: 'normal', // 'normal', 'large', 'small'
|
||||
customClass: '',
|
||||
autoClose: false, // Автоматическое закрытие по таймеру
|
||||
autoCloseTime: 2000 // Время до автоматического закрытия (2 секунды)
|
||||
};
|
||||
|
||||
// Объединяем пользовательские опции с опциями по умолчанию
|
||||
const settings = $.extend({}, defaultOptions, options);
|
||||
|
||||
// Создаем структуру модального окна
|
||||
const $modalOverlay = $('<div>', { class: 'modal-overlay' });
|
||||
const $modal = $('<div>', { class: `modal ${settings.customClass}` });
|
||||
|
||||
// Устанавливаем ширину в зависимости от размера
|
||||
if (settings.size === 'large') {
|
||||
$modal.css('max-width', '700px');
|
||||
} else if (settings.size === 'small') {
|
||||
$modal.css('max-width', '400px');
|
||||
}
|
||||
|
||||
// Создаем заголовок
|
||||
const $modalHeader = $('<div>', { class: 'modal-header' });
|
||||
const $modalTitle = $('<h3>', { text: settings.title });
|
||||
const $modalClose = $('<button>', {
|
||||
class: 'modal-close',
|
||||
html: '×',
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$modalHeader.append($modalTitle, $modalClose);
|
||||
|
||||
// Создаем тело
|
||||
const $modalBody = $('<div>', { class: 'modal-body' });
|
||||
if (typeof settings.content === 'string') {
|
||||
$modalBody.html(settings.content);
|
||||
} else {
|
||||
$modalBody.append(settings.content);
|
||||
}
|
||||
|
||||
// Создаем футер
|
||||
const $modalFooter = $('<div>', { class: 'modal-footer' });
|
||||
|
||||
// Если нужно показать кнопку отмены
|
||||
if (settings.showCancel) {
|
||||
const $cancelButton = $('<button>', {
|
||||
class: 'btn btn-secondary',
|
||||
text: settings.cancelText,
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (typeof settings.onCancel === 'function') {
|
||||
settings.onCancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
$modalFooter.append($cancelButton);
|
||||
}
|
||||
|
||||
// Кнопка подтверждения/закрытия
|
||||
const $confirmButton = $('<button>', {
|
||||
class: settings.showCancel ? 'btn btn-primary' : 'btn',
|
||||
text: settings.showCancel ? settings.confirmText : settings.closeText,
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (settings.showCancel && typeof settings.onConfirm === 'function') {
|
||||
settings.onConfirm();
|
||||
} else if (!settings.showCancel && typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
$modalFooter.append($confirmButton);
|
||||
|
||||
// Добавляем прогресс-бар, если включено автоматическое закрытие
|
||||
if (settings.autoClose) {
|
||||
const $progressBar = $('<div>', { class: 'modal-progress' });
|
||||
$modal.append($progressBar);
|
||||
}
|
||||
|
||||
// Собираем модальное окно
|
||||
$modal.append($modalHeader, $modalBody, $modalFooter);
|
||||
$modalOverlay.append($modal);
|
||||
|
||||
// Добавляем модальное окно в DOM
|
||||
$('body').append($modalOverlay);
|
||||
|
||||
// Закрытие по клику на фоне
|
||||
$modalOverlay.on('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция закрытия модального окна
|
||||
function closeModal() {
|
||||
$modalOverlay.removeClass('active');
|
||||
setTimeout(function() {
|
||||
$modalOverlay.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Активируем модальное окно
|
||||
setTimeout(function() {
|
||||
$modalOverlay.addClass('active');
|
||||
|
||||
// Активируем прогресс-бар и запускаем таймер закрытия, если включено автоматическое закрытие
|
||||
if (settings.autoClose) {
|
||||
const $progressBar = $modal.find('.modal-progress');
|
||||
|
||||
setTimeout(() => {
|
||||
$progressBar.addClass('active');
|
||||
}, 50);
|
||||
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}, settings.autoCloseTime);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
// Возвращаем объект модального окна
|
||||
return {
|
||||
$modal: $modal,
|
||||
$overlay: $modalOverlay,
|
||||
close: closeModal
|
||||
};
|
||||
}
|
||||
|
||||
// Функция для отображения модального окна с сообщением (замена alert)
|
||||
function showAlert(message, title, callback, autoClose = false) {
|
||||
return createModal({
|
||||
title: title || 'Сообщение',
|
||||
content: message,
|
||||
onClose: callback,
|
||||
autoClose: autoClose,
|
||||
autoCloseTime: 2000
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для отображения модального окна с подтверждением (замена confirm)
|
||||
function showConfirm(message, callback, title) {
|
||||
return createModal({
|
||||
title: title || 'Подтверждение',
|
||||
content: message,
|
||||
showCancel: true,
|
||||
onConfirm: function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
onCancel: function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для генерации QR-кода
|
||||
function generateQRCode(data, size) {
|
||||
const typeNumber = 0; // Автоматическое определение
|
||||
const errorCorrectionLevel = 'L'; // Низкий уровень коррекции ошибок
|
||||
const qr = qrcode(typeNumber, errorCorrectionLevel);
|
||||
qr.addData(data);
|
||||
qr.make();
|
||||
return qr.createImgTag(size || 8, 0);
|
||||
}
|
||||
|
||||
// Функция для отображения QR-кода в модальном окне
|
||||
function showQRCodeModal(url, title) {
|
||||
const qrCode = generateQRCode(url);
|
||||
const content = `
|
||||
<div class="qr-container">
|
||||
<div class="qr-code">
|
||||
${qrCode}
|
||||
</div>
|
||||
<div class="qr-link-container">
|
||||
<input type="text" class="qr-link-input" value="${url}" readonly>
|
||||
<button class="btn btn-copy-link">Копировать</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const modal = createModal({
|
||||
title: title || 'QR-код для доступа',
|
||||
content: content,
|
||||
size: 'large'
|
||||
});
|
||||
|
||||
// Добавляем обработчик для кнопки копирования
|
||||
modal.$modal.find('.btn-copy-link').on('click', function() {
|
||||
const input = modal.$modal.find('.qr-link-input');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Показываем уведомление о копировании
|
||||
const $button = $(this);
|
||||
const originalText = $button.text();
|
||||
$button.text('Скопировано!');
|
||||
$button.addClass('copied');
|
||||
|
||||
setTimeout(function() {
|
||||
$button.text(originalText);
|
||||
$button.removeClass('copied');
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
343
server/routers/questioneer/public/static/js/create.js
Normal file
343
server/routers/questioneer/public/static/js/create.js
Normal file
@@ -0,0 +1,343 @@
|
||||
/* global $, window, document, alert, showAlert, showConfirm */
|
||||
$(document).ready(function() {
|
||||
const form = $('#create-questionnaire-form');
|
||||
const questionsList = $('#questions-list');
|
||||
const addQuestionBtn = $('#add-question');
|
||||
|
||||
let questionCount = 0;
|
||||
|
||||
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последнюю часть пути (create)
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
};
|
||||
|
||||
// Добавление нового вопроса
|
||||
addQuestionBtn.on('click', function() {
|
||||
addQuestion();
|
||||
});
|
||||
|
||||
// Обработка отправки формы
|
||||
form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveQuestionnaire();
|
||||
});
|
||||
|
||||
// Делегирование событий для динамических элементов
|
||||
questionsList.on('click', '.delete-question', function() {
|
||||
// Удаление вопроса
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
showConfirm('Вы уверены, что хотите удалить этот вопрос?', function(confirmed) {
|
||||
if (confirmed) {
|
||||
questionItem.remove();
|
||||
renumberQuestions();
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
questionsList.on('click', '.add-option', function() {
|
||||
// Добавление варианта ответа
|
||||
const questionIndex = $(this).data('question-index');
|
||||
addOption(questionIndex);
|
||||
});
|
||||
|
||||
questionsList.on('click', '.delete-option', function() {
|
||||
// Удаление варианта ответа
|
||||
$(this).closest('.option-item').remove();
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Делегирование для изменения типа вопроса
|
||||
questionsList.on('change', '.question-type-select', function() {
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
const questionIndex = questionItem.data('index');
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
const scaleContainer = $(`#scale-container-${questionIndex}`);
|
||||
|
||||
// Скрыть/показать варианты ответа в зависимости от типа вопроса
|
||||
const questionType = $(this).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
optionsContainer.show();
|
||||
scaleContainer.hide();
|
||||
|
||||
// Если нет вариантов, добавляем два
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
if (optionsList.children().length === 0) {
|
||||
addOption(questionIndex);
|
||||
addOption(questionIndex);
|
||||
}
|
||||
|
||||
// Включаем required для полей ввода вариантов
|
||||
optionsList.find('input[type="text"]').prop('required', true);
|
||||
} else if (questionType === 'scale') {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.show();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Функция для добавления нового вопроса
|
||||
function addQuestion() {
|
||||
const template = $('#question-template').html();
|
||||
const index = questionCount++;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let questionHtml = template
|
||||
.replace(/\{\{index\}\}/g, index)
|
||||
.replace(/\{\{number\}\}/g, index + 1);
|
||||
|
||||
questionsList.append(questionHtml);
|
||||
|
||||
// Показываем/скрываем контейнер вариантов в зависимости от типа вопроса
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
if (!['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-container-${index}`).hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${index}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
// Добавляем пару начальных вариантов ответа
|
||||
addOption(index);
|
||||
addOption(index);
|
||||
}
|
||||
|
||||
if (questionType === 'scale') {
|
||||
$(`#scale-container-${index}`).show();
|
||||
} else {
|
||||
$(`#scale-container-${index}`).hide();
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
|
||||
// Функция для добавления варианта ответа
|
||||
function addOption(questionIndex) {
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
const template = $('#option-template').html();
|
||||
|
||||
const optionIndex = optionsList.children().length;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let optionHtml = template
|
||||
.replace(/\{\{questionIndex\}\}/g, questionIndex)
|
||||
.replace(/\{\{optionIndex\}\}/g, optionIndex);
|
||||
|
||||
optionsList.append(optionHtml);
|
||||
|
||||
// Проверяем, видим ли контейнер опций
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
if (optionsContainer.is(':hidden')) {
|
||||
// Если контейнер скрыт, отключаем required у полей ввода
|
||||
optionsList.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
|
||||
// Перенумерация вопросов
|
||||
function renumberQuestions() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления нумерации вопросов
|
||||
function updateQuestionNumbers() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Сохранение опроса
|
||||
function saveQuestionnaire() {
|
||||
const questionnaire = {
|
||||
title: $('#title').val(),
|
||||
description: $('#description').val(),
|
||||
displayType: 'step_by_step', // Всегда устанавливаем пошаговый режим
|
||||
questions: []
|
||||
};
|
||||
|
||||
// Собираем данные о вопросах
|
||||
$('.question-item').each(function() {
|
||||
const index = $(this).data('index');
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
|
||||
const question = {
|
||||
text: $(`#question-text-${index}`).val(),
|
||||
type: questionType,
|
||||
required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
|
||||
options: []
|
||||
};
|
||||
|
||||
// Добавляем настройки шкалы если нужно
|
||||
if (questionType === 'scale') {
|
||||
question.scaleMin = parseInt($(`#scale-min-${index}`).val()) || 0;
|
||||
question.scaleMax = parseInt($(`#scale-max-${index}`).val()) || 10;
|
||||
question.scaleMinLabel = $(`#scale-min-label-${index}`).val() || 'Минимум';
|
||||
question.scaleMaxLabel = $(`#scale-max-label-${index}`).val() || 'Максимум';
|
||||
}
|
||||
|
||||
// Собираем варианты ответа если это не текстовый вопрос или шкала
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-list-${index} .option-item`).each(function() {
|
||||
const optionText = $(this).find('input[type="text"]').val();
|
||||
|
||||
if (optionText) {
|
||||
question.options.push({
|
||||
text: optionText,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
questionnaire.questions.push(question);
|
||||
});
|
||||
|
||||
// Отправка на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(questionnaire),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
// Перенаправление на страницу администрирования опроса
|
||||
const basePath = window.location.pathname.split('/create')[0];
|
||||
window.location.href = `${basePath}/admin/${result.data.adminLink}`;
|
||||
} else {
|
||||
showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error creating questionnaire:', error);
|
||||
showAlert('Не удалось создать опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления атрибута required в зависимости от видимости полей
|
||||
function updateRequiredAttributes() {
|
||||
// Для полей вопросов
|
||||
$('.question-item').each(function() {
|
||||
const questionType = $(this).find('.question-type-select').val();
|
||||
const textInput = $(this).find('.question-text');
|
||||
const optionsContainer = $(this).find('.options-container');
|
||||
|
||||
// Обновляем required для текстового поля вопроса
|
||||
if (textInput.is(':visible')) {
|
||||
textInput.prop('required', true);
|
||||
} else {
|
||||
textInput.prop('required', false);
|
||||
}
|
||||
|
||||
// Обновляем required для полей опций
|
||||
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
|
||||
optionsContainer.find('input[type="text"]').each(function() {
|
||||
if ($(this).is(':visible')) {
|
||||
$(this).prop('required', true);
|
||||
} else {
|
||||
$(this).prop('required', false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
optionsContainer.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Для шкалы оценки
|
||||
if (questionType === 'scale') {
|
||||
const minInput = $(this).find('.scale-min');
|
||||
const maxInput = $(this).find('.scale-max');
|
||||
const minLabelInput = $(this).find('.scale-min-label');
|
||||
const maxLabelInput = $(this).find('.scale-max-label');
|
||||
|
||||
if (minInput.is(':visible')) minInput.prop('required', true);
|
||||
else minInput.prop('required', false);
|
||||
|
||||
if (maxInput.is(':visible')) maxInput.prop('required', true);
|
||||
else maxInput.prop('required', false);
|
||||
|
||||
if (minLabelInput.is(':visible')) minLabelInput.prop('required', true);
|
||||
else minLabelInput.prop('required', false);
|
||||
|
||||
if (maxLabelInput.is(':visible')) maxLabelInput.prop('required', true);
|
||||
else maxLabelInput.prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Для основных полей формы
|
||||
const titleInput = $('#title');
|
||||
const descriptionInput = $('#description');
|
||||
|
||||
if (titleInput.is(':visible')) titleInput.prop('required', true);
|
||||
else titleInput.prop('required', false);
|
||||
|
||||
if (descriptionInput.is(':visible')) descriptionInput.prop('required', false); // Описание не обязательно
|
||||
}
|
||||
|
||||
// Инициализация с одним вопросом
|
||||
addQuestion();
|
||||
|
||||
// Обработчик отправки формы
|
||||
$('#create-questionnaire-form').on('submit', function(e) {
|
||||
// Обновляем атрибуты required перед отправкой
|
||||
updateRequiredAttributes();
|
||||
|
||||
// Проверяем валидность формы
|
||||
if (!this.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Находим первый невалидный элемент и прокручиваем к нему
|
||||
const firstInvalid = $(this).find(':invalid').first();
|
||||
if (firstInvalid.length) {
|
||||
$('html, body').animate({
|
||||
scrollTop: firstInvalid.offset().top - 100
|
||||
}, 500);
|
||||
|
||||
// Добавляем класс ошибки к родительскому элементу вопроса
|
||||
firstInvalid.closest('.question-item').addClass('error');
|
||||
setTimeout(() => {
|
||||
firstInvalid.closest('.question-item').removeClass('error');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
$(this).addClass('was-validated');
|
||||
});
|
||||
|
||||
// Инициализируем атрибуты required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Обработчик удаления вопроса
|
||||
$(document).on('click', '.remove-question', function() {
|
||||
$(this).closest('.question-item').remove();
|
||||
updateQuestionNumbers();
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Обработчик удаления опции
|
||||
$(document).on('click', '.remove-option', function() {
|
||||
$(this).closest('.option-item').remove();
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
332
server/routers/questioneer/public/static/js/edit.js
Normal file
332
server/routers/questioneer/public/static/js/edit.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
|
||||
$(document).ready(function() {
|
||||
const form = $('#edit-questionnaire-form');
|
||||
const questionsList = $('#questions-list');
|
||||
const addQuestionBtn = $('#add-question');
|
||||
const adminLink = window.location.pathname.split('/').pop();
|
||||
|
||||
let questionCount = 0;
|
||||
let questionnaireData = null;
|
||||
|
||||
// Получаем базовый путь API
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последние две части пути (edit/:adminLink)
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
const loadQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
questionnaireData = result.data;
|
||||
fillFormData();
|
||||
$('#loading').hide();
|
||||
$('#edit-form-container').show();
|
||||
} else {
|
||||
$('#loading').text(`Ошибка: ${result.error}`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaire:', error);
|
||||
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Заполнение формы данными опроса
|
||||
const fillFormData = () => {
|
||||
// Заполняем основные данные
|
||||
$('#title').val(questionnaireData.title);
|
||||
$('#description').val(questionnaireData.description || '');
|
||||
$('#display-type').val(questionnaireData.displayType);
|
||||
|
||||
// Формируем ссылки
|
||||
const baseUrl = window.location.origin;
|
||||
const baseQuestionnairePath = window.location.pathname.split('/edit')[0];
|
||||
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
|
||||
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
|
||||
|
||||
$('#public-link').val(publicUrl);
|
||||
$('#admin-link').val(adminUrl);
|
||||
|
||||
// Добавляем вопросы
|
||||
questionsList.empty();
|
||||
|
||||
if (questionnaireData.questions && questionnaireData.questions.length > 0) {
|
||||
questionnaireData.questions.forEach((question, index) => {
|
||||
addQuestion(question);
|
||||
});
|
||||
} else {
|
||||
// Если нет вопросов, добавляем пустой
|
||||
addQuestion();
|
||||
}
|
||||
|
||||
renumberQuestions();
|
||||
};
|
||||
|
||||
// Добавление нового вопроса
|
||||
addQuestionBtn.on('click', function() {
|
||||
addQuestion();
|
||||
renumberQuestions();
|
||||
});
|
||||
|
||||
// Обработка отправки формы
|
||||
form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveQuestionnaire();
|
||||
});
|
||||
|
||||
// Делегирование событий для динамических элементов
|
||||
questionsList.on('click', '.delete-question', function() {
|
||||
// Удаление вопроса
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
questionItem.remove();
|
||||
renumberQuestions();
|
||||
});
|
||||
|
||||
questionsList.on('click', '.add-option', function() {
|
||||
// Добавление варианта ответа
|
||||
const questionIndex = $(this).data('question-index');
|
||||
addOption(questionIndex);
|
||||
});
|
||||
|
||||
questionsList.on('click', '.delete-option', function() {
|
||||
// Удаление варианта ответа
|
||||
$(this).closest('.option-item').remove();
|
||||
});
|
||||
|
||||
// Делегирование для изменения типа вопроса
|
||||
questionsList.on('change', '.question-type-select', function() {
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
const questionIndex = questionItem.data('index');
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
const scaleContainer = $(`#scale-container-${questionIndex}`);
|
||||
|
||||
// Показываем/скрываем контейнеры в зависимости от типа вопроса
|
||||
const questionType = $(this).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
optionsContainer.show();
|
||||
scaleContainer.hide();
|
||||
|
||||
// Если нет вариантов, добавляем два
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
if (optionsList.children().length === 0) {
|
||||
addOption(questionIndex);
|
||||
addOption(questionIndex);
|
||||
}
|
||||
|
||||
// Включаем required для полей ввода вариантов
|
||||
optionsList.find('input[type="text"]').prop('required', true);
|
||||
} else if (questionType === 'scale') {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.show();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Копирование ссылок
|
||||
$('#copy-public-link').on('click', function() {
|
||||
$('#public-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование');
|
||||
});
|
||||
|
||||
$('#copy-admin-link').on('click', function() {
|
||||
$('#admin-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование');
|
||||
});
|
||||
|
||||
// Отображение QR-кода
|
||||
$('#show-qr-code').on('click', function() {
|
||||
const publicUrl = $('#public-link').val();
|
||||
showQRCodeModal(publicUrl, 'QR-код для голосования');
|
||||
});
|
||||
|
||||
// Возврат к админке
|
||||
$('#back-to-admin').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const basePath = window.location.pathname.split('/edit')[0];
|
||||
window.location.href = `${basePath}/admin/${adminLink}`;
|
||||
});
|
||||
|
||||
// Функция для добавления нового вопроса
|
||||
function addQuestion(questionData) {
|
||||
const template = $('#question-template').html();
|
||||
const index = questionCount++;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let questionHtml = template
|
||||
.replace(/\{\{index\}\}/g, index)
|
||||
.replace(/\{\{number\}\}/g, index + 1);
|
||||
|
||||
questionsList.append(questionHtml);
|
||||
|
||||
// Если есть данные вопроса - заполняем поля
|
||||
if (questionData) {
|
||||
$(`#question-text-${index}`).val(questionData.text);
|
||||
$(`#question-type-${index}`).val(questionData.type);
|
||||
|
||||
if (questionData.required) {
|
||||
$(`input[name="questions[${index}][required]"]`).prop('checked', true);
|
||||
}
|
||||
|
||||
// Добавляем варианты ответа если они есть
|
||||
if (questionData.options && questionData.options.length > 0) {
|
||||
questionData.options.forEach(option => {
|
||||
addOption(index, option.text);
|
||||
});
|
||||
}
|
||||
|
||||
// Заполняем настройки шкалы если нужно
|
||||
if (questionData.scaleMax) {
|
||||
$(`#scale-max-${index}`).val(questionData.scaleMax);
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем/скрываем контейнеры в зависимости от типа вопроса
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-container-${index}`).show();
|
||||
$(`#scale-container-${index}`).hide();
|
||||
|
||||
// Если нет вариантов и не загружены данные, добавляем два
|
||||
if (!questionData && $(`#options-list-${index}`).children().length === 0) {
|
||||
addOption(index);
|
||||
addOption(index);
|
||||
}
|
||||
} else if (questionType === 'scale') {
|
||||
$(`#options-container-${index}`).hide();
|
||||
$(`#scale-container-${index}`).show();
|
||||
} else {
|
||||
$(`#options-container-${index}`).hide();
|
||||
$(`#scale-container-${index}`).hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для добавления варианта ответа
|
||||
function addOption(questionIndex, optionText) {
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
const template = $('#option-template').html();
|
||||
|
||||
const optionIndex = optionsList.children().length;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let optionHtml = template
|
||||
.replace(/\{\{questionIndex\}\}/g, questionIndex)
|
||||
.replace(/\{\{optionIndex\}\}/g, optionIndex);
|
||||
|
||||
optionsList.append(optionHtml);
|
||||
|
||||
// Если есть текст варианта - устанавливаем его
|
||||
if (optionText) {
|
||||
optionsList.children().last().find('input[type="text"]').val(optionText);
|
||||
}
|
||||
|
||||
// Проверяем, видим ли контейнер опций
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
if (optionsContainer.is(':hidden')) {
|
||||
// Если контейнер скрыт, отключаем required у полей ввода
|
||||
optionsList.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
}
|
||||
|
||||
// Перенумерация вопросов
|
||||
function renumberQuestions() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Сохранение опроса
|
||||
function saveQuestionnaire() {
|
||||
const questionnaire = {
|
||||
title: $('#title').val(),
|
||||
description: $('#description').val(),
|
||||
displayType: $('#display-type').val(),
|
||||
questions: []
|
||||
};
|
||||
|
||||
// Собираем данные о вопросах
|
||||
$('.question-item').each(function() {
|
||||
const index = $(this).data('index');
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
|
||||
const question = {
|
||||
text: $(`#question-text-${index}`).val(),
|
||||
type: questionType,
|
||||
required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
|
||||
options: []
|
||||
};
|
||||
|
||||
// Добавляем настройки шкалы если нужно
|
||||
if (questionType === 'scale') {
|
||||
question.scaleMax = parseInt($(`#scale-max-${index}`).val());
|
||||
}
|
||||
|
||||
// Собираем варианты ответа если это не текстовый вопрос или оценка
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-list-${index} .option-item`).each(function() {
|
||||
const optionText = $(this).find('input[type="text"]').val();
|
||||
|
||||
if (optionText) {
|
||||
// Сохраняем количество голосов из старых данных
|
||||
let count = 0;
|
||||
const optionIndex = $(this).data('index');
|
||||
|
||||
if (questionnaireData &&
|
||||
questionnaireData.questions[index] &&
|
||||
questionnaireData.questions[index].options &&
|
||||
questionnaireData.questions[index].options[optionIndex]) {
|
||||
count = questionnaireData.questions[index].options[optionIndex].count || 0;
|
||||
}
|
||||
|
||||
question.options.push({
|
||||
text: optionText,
|
||||
count: count
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
questionnaire.questions.push(question);
|
||||
});
|
||||
|
||||
// Отправка на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/${adminLink}`,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(questionnaire),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
showAlert('Опрос успешно обновлен', 'Успешно', function() {
|
||||
const basePath = window.location.pathname.split('/edit')[0];
|
||||
window.location.href = `${basePath}/admin/${adminLink}`;
|
||||
});
|
||||
} else {
|
||||
showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating questionnaire:', error);
|
||||
showAlert('Не удалось обновить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
loadQuestionnaire();
|
||||
});
|
||||
67
server/routers/questioneer/public/static/js/index.js
Normal file
67
server/routers/questioneer/public/static/js/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* global $, window, document */
|
||||
$(document).ready(function() {
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
};
|
||||
|
||||
// Функция для загрузки списка опросов
|
||||
const loadQuestionnaires = () => {
|
||||
$.ajax({
|
||||
url: getApiPath() + '/questionnaires',
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
renderQuestionnaires(result.data);
|
||||
} else {
|
||||
$('#questionnaires-container').html(`<p class="error">Ошибка: ${result.error}</p>`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaires:', error);
|
||||
$('#questionnaires-container').html('<p class="error">Не удалось загрузить опросы. Пожалуйста, попробуйте позже.</p>');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Функция для отображения списка опросов
|
||||
const renderQuestionnaires = (questionnaires) => {
|
||||
if (!questionnaires || questionnaires.length === 0) {
|
||||
$('#questionnaires-container').html('<p>У вас еще нет созданных опросов.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
|
||||
const basePath = window.location.pathname.endsWith('/')
|
||||
? window.location.pathname
|
||||
: window.location.pathname + '/';
|
||||
|
||||
const questionnairesHTML = questionnaires.map(q => `
|
||||
<div class="questionnaire-item">
|
||||
<h3>${q.title}</h3>
|
||||
<p>${q.description || 'Нет описания'}</p>
|
||||
<p>Создан: ${new Date(q.createdAt).toLocaleString()}</p>
|
||||
<div class="questionnaire-links">
|
||||
<a href="${basePath}admin/${q.adminLink}" class="btn btn-small">Редактировать</a>
|
||||
<a href="${basePath}poll/${q.publicLink}" class="btn btn-small btn-primary" target="_blank">Смотреть как участник</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
$('#questionnaires-container').html(questionnairesHTML);
|
||||
};
|
||||
|
||||
// Инициализация страницы
|
||||
loadQuestionnaires();
|
||||
|
||||
// Обновление данных каждые 30 секунд
|
||||
setInterval(loadQuestionnaires, 30000);
|
||||
});
|
||||
1069
server/routers/questioneer/public/static/js/poll.js
Normal file
1069
server/routers/questioneer/public/static/js/poll.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user