forked from bro-students/multy-stub
fix путей
This commit is contained in:
parent
1fcc5ed70d
commit
dd589790c2
583
server/routers/questioneer/openapi.yaml
Normal file
583
server/routers/questioneer/openapi.yaml
Normal file
@ -0,0 +1,583 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Анонимные опросы API
|
||||
description: API для работы с системой анонимных опросов
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /questioneer/api
|
||||
description: Базовый URL API
|
||||
paths:
|
||||
/questionnaires:
|
||||
get:
|
||||
summary: Получить список опросов пользователя
|
||||
description: Возвращает список всех опросов, сохраненных в локальном хранилище браузера
|
||||
operationId: getQuestionnaires
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnairesResponse'
|
||||
post:
|
||||
summary: Создать новый опрос
|
||||
description: Создает новый опрос с указанными параметрами
|
||||
operationId: createQuestionnaire
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireCreate'
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно создан
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
/questionnaires/public/{publicLink}:
|
||||
get:
|
||||
summary: Получить опрос для участия
|
||||
description: Возвращает данные опроса по публичной ссылке
|
||||
operationId: getPublicQuestionnaire
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/questionnaires/admin/{adminLink}:
|
||||
get:
|
||||
summary: Получить опрос для редактирования и просмотра результатов
|
||||
description: Возвращает данные опроса по административной ссылке
|
||||
operationId: getAdminQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
put:
|
||||
summary: Обновить опрос
|
||||
description: Обновляет существующий опрос
|
||||
operationId: updateQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно обновлен
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
delete:
|
||||
summary: Удалить опрос
|
||||
description: Удаляет опрос вместе со всеми ответами
|
||||
operationId: deleteQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно удален
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/vote/{publicLink}:
|
||||
post:
|
||||
summary: Отправить ответы на опрос
|
||||
description: Отправляет ответы пользователя на опрос
|
||||
operationId: submitVote
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VoteRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Ответы успешно отправлены
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/results/{publicLink}:
|
||||
get:
|
||||
summary: Получить результаты опроса
|
||||
description: Возвращает текущие результаты опроса
|
||||
operationId: getResults
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResultsResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
components:
|
||||
schemas:
|
||||
QuestionnaireCreate:
|
||||
type: object
|
||||
required:
|
||||
- title
|
||||
- questions
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/Question'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
default: standard
|
||||
QuestionnaireUpdate:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/Question'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
Question:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
- type
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
enum: [single, multiple, text, scale, rating, tagcloud]
|
||||
required:
|
||||
type: boolean
|
||||
description: Является ли вопрос обязательным
|
||||
default: false
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа (для single, multiple)
|
||||
items:
|
||||
$ref: '#/components/schemas/Option'
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов (для tagcloud)
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
scaleMin:
|
||||
type: integer
|
||||
description: Минимальное значение шкалы (для scale)
|
||||
default: 0
|
||||
scaleMax:
|
||||
type: integer
|
||||
description: Максимальное значение шкалы (для scale)
|
||||
default: 10
|
||||
scaleMinLabel:
|
||||
type: string
|
||||
description: Метка для минимального значения шкалы
|
||||
default: "Минимум"
|
||||
scaleMaxLabel:
|
||||
type: string
|
||||
description: Метка для максимального значения шкалы
|
||||
default: "Максимум"
|
||||
Option:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
votes:
|
||||
type: integer
|
||||
description: Количество голосов за этот вариант
|
||||
default: 0
|
||||
Tag:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов данного тега
|
||||
default: 0
|
||||
VoteRequest:
|
||||
type: object
|
||||
required:
|
||||
- answers
|
||||
properties:
|
||||
answers:
|
||||
type: array
|
||||
description: Список ответов пользователя
|
||||
items:
|
||||
$ref: '#/components/schemas/Answer'
|
||||
Answer:
|
||||
type: object
|
||||
required:
|
||||
- questionIndex
|
||||
properties:
|
||||
questionIndex:
|
||||
type: integer
|
||||
description: Индекс вопроса
|
||||
optionIndices:
|
||||
type: array
|
||||
description: Индексы выбранных вариантов (для single, multiple)
|
||||
items:
|
||||
type: integer
|
||||
textAnswer:
|
||||
type: string
|
||||
description: Текстовый ответ пользователя (для text)
|
||||
scaleValue:
|
||||
type: integer
|
||||
description: Значение шкалы (для scale, rating)
|
||||
tagTexts:
|
||||
type: array
|
||||
description: Тексты выбранных или введенных тегов (для tagcloud)
|
||||
items:
|
||||
type: string
|
||||
QuestionnairesResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
type: array
|
||||
description: Список опросов
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionnaireInfo'
|
||||
QuestionnaireResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
$ref: '#/components/schemas/QuestionnaireData'
|
||||
QuestionnaireInfo:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
adminLink:
|
||||
type: string
|
||||
description: Административная ссылка
|
||||
publicLink:
|
||||
type: string
|
||||
description: Публичная ссылка
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата создания опроса
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата последнего обновления опроса
|
||||
QuestionnaireData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор опроса
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionData'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
adminLink:
|
||||
type: string
|
||||
description: Административная ссылка
|
||||
publicLink:
|
||||
type: string
|
||||
description: Публичная ссылка
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата создания опроса
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата последнего обновления опроса
|
||||
QuestionData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор вопроса
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
required:
|
||||
type: boolean
|
||||
description: Является ли вопрос обязательным
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа (для single, multiple)
|
||||
items:
|
||||
$ref: '#/components/schemas/OptionData'
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов (для tagcloud)
|
||||
items:
|
||||
$ref: '#/components/schemas/TagData'
|
||||
scaleMin:
|
||||
type: integer
|
||||
description: Минимальное значение шкалы (для scale)
|
||||
scaleMax:
|
||||
type: integer
|
||||
description: Максимальное значение шкалы (для scale)
|
||||
scaleMinLabel:
|
||||
type: string
|
||||
description: Метка для минимального значения шкалы
|
||||
scaleMaxLabel:
|
||||
type: string
|
||||
description: Метка для максимального значения шкалы
|
||||
answers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
scaleValues:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
textAnswers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
OptionData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор варианта ответа
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
votes:
|
||||
type: integer
|
||||
description: Количество голосов за этот вариант
|
||||
count:
|
||||
type: integer
|
||||
description: Альтернативное поле для количества голосов
|
||||
TagData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор тега
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов данного тега
|
||||
ResultsResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
$ref: '#/components/schemas/ResultsData'
|
||||
ResultsData:
|
||||
type: object
|
||||
properties:
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов с результатами
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionResults'
|
||||
QuestionResults:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа с количеством голосов (для single, multiple)
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
count:
|
||||
type: integer
|
||||
description: Количество голосов
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов с количеством выборов (для tagcloud)
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов
|
||||
scaleValues:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
scaleAverage:
|
||||
type: number
|
||||
description: Среднее значение шкалы (для scale, rating)
|
||||
answers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
SuccessResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
description: Сообщение об успешном выполнении
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
example: false
|
||||
error:
|
||||
type: string
|
||||
description: Сообщение об ошибке
|
@ -4,7 +4,45 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Управление опросом</title>
|
||||
<link rel="stylesheet" href="/questioneer/static/css/style.css">
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
return window.location.pathname.split('/admin')[0] + '/static';
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/admin.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
@ -59,12 +97,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script src="/questioneer/static/js/common.js"></script>
|
||||
<script src="/questioneer/static/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -3,8 +3,45 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Создание нового опроса</title>
|
||||
<link rel="stylesheet" href="/questioneer/static/css/style.css">
|
||||
<title>Создать опрос</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
return window.location.pathname.split('/create')[0] + '/static';
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery и остальные скрипты с учетом переменного пути -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/create.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@ -119,11 +156,5 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/questioneer/static/js/common.js"></script>
|
||||
<script src="/questioneer/static/js/create.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -4,7 +4,45 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактирование опроса</title>
|
||||
<link rel="stylesheet" href="/questioneer/static/css/style.css">
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
return window.location.pathname.split('/edit')[0] + '/static';
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/edit.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@ -135,12 +173,5 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script src="/questioneer/static/js/common.js"></script>
|
||||
<script src="/questioneer/static/js/edit.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -4,7 +4,50 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Анонимные опросы</title>
|
||||
<link rel="stylesheet" href="/questioneer/static/css/style.css">
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
// Если путь заканчивается на слеш или на /questioneer, добавляем /static
|
||||
if (pathname.endsWith('/') || pathname.endsWith('/questioneer')) {
|
||||
return pathname + 'static';
|
||||
} else {
|
||||
return pathname + '/static';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/index.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
@ -32,11 +75,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/questioneer/static/js/common.js"></script>
|
||||
<script src="/questioneer/static/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -4,7 +4,44 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Участие в опросе</title>
|
||||
<link rel="stylesheet" href="/questioneer/static/css/style.css">
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
return window.location.pathname.split('/poll')[0] + '/static';
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/poll.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@ -35,11 +72,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/questioneer/static/js/common.js"></script>
|
||||
<script src="/questioneer/static/js/poll.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -66,11 +66,12 @@ p {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4caf50;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #388e3c;
|
||||
background-color: #1565c0;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@ -593,13 +594,14 @@ select:focus {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #444;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#question-counter {
|
||||
.question-counter {
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
color: #bbdefb;
|
||||
color: #64b5f6;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Улучшения для облака тегов */
|
||||
@ -755,7 +757,7 @@ select:focus {
|
||||
.scale-average .highlight {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5em;
|
||||
animation: pulse 2s infinite;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-bar-container {
|
||||
@ -837,15 +839,9 @@ select:focus {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
0% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.question-result {
|
||||
@ -863,7 +859,7 @@ select:focus {
|
||||
}
|
||||
|
||||
.question-counter.update {
|
||||
animation: pulse 0.5s;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@ -908,7 +904,7 @@ select:focus {
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
animation: pulse 1.5s infinite;
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@ -980,7 +976,8 @@ select:focus {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
animation: pulse 2s infinite;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.question-item.error {
|
||||
@ -1002,7 +999,9 @@ select:focus {
|
||||
}
|
||||
|
||||
.tag-item.selected {
|
||||
animation: pulse 1s;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.scale-item label:hover {
|
||||
@ -1011,7 +1010,9 @@ select:focus {
|
||||
}
|
||||
|
||||
.scale-item input:checked + label {
|
||||
animation: pulse 0.5s;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Анимированный лоадер */
|
||||
@ -1155,10 +1156,15 @@ input[type="checkbox"], input[type="radio"] {
|
||||
.welcome-icon svg,
|
||||
.thank-you-icon svg,
|
||||
.completed-icon svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: var(--color-primary, #2196f3);
|
||||
opacity: 0.8;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.welcome-icon svg:hover,
|
||||
.thank-you-icon svg:hover,
|
||||
.completed-icon svg:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.welcome-title,
|
||||
@ -1180,11 +1186,19 @@ input[type="checkbox"], input[type="radio"] {
|
||||
|
||||
.welcome-start-btn,
|
||||
.view-results-btn {
|
||||
margin-top: 20px;
|
||||
font-size: 1.1rem;
|
||||
padding: 12px 30px;
|
||||
font-size: 1.2rem;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
animation: pulse 2s infinite;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.welcome-start-btn:hover,
|
||||
.view-results-btn:hover {
|
||||
background-color: #0d47a1;
|
||||
}
|
||||
|
||||
.start-again-btn {
|
||||
@ -1207,12 +1221,6 @@ input[type="checkbox"], input[type="radio"] {
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-icon svg,
|
||||
.thank-you-icon svg,
|
||||
.completed-icon svg {
|
||||
animation: pulse 3s infinite;
|
||||
}
|
||||
|
||||
/* CSS переменные для цветов */
|
||||
:root {
|
||||
--color-primary: #2196f3;
|
||||
@ -1236,9 +1244,9 @@ input[type="checkbox"], input[type="radio"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 110px;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-btn svg {
|
||||
@ -1246,8 +1254,13 @@ input[type="checkbox"], input[type="radio"] {
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
background-color: #0d47a1;
|
||||
}
|
||||
|
||||
.nav-btn:disabled {
|
||||
background-color: #455a64;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Стили для статистики в админке */
|
||||
@ -1827,4 +1840,342 @@ input[type="checkbox"], input[type="radio"] {
|
||||
flex-grow: 1;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Добавляем стили для анимации конфети */
|
||||
.confetti-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #90caf9;
|
||||
border-radius: 3px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.confetti.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.confetti.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
border-style: solid;
|
||||
border-width: 0 5px 8.7px 5px;
|
||||
border-color: transparent transparent #90caf9 transparent;
|
||||
}
|
||||
|
||||
.confetti.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.confetti.red {
|
||||
background-color: #f44336;
|
||||
border-color: transparent transparent #f44336 transparent;
|
||||
}
|
||||
|
||||
.confetti.blue {
|
||||
background-color: #2196f3;
|
||||
border-color: transparent transparent #2196f3 transparent;
|
||||
}
|
||||
|
||||
.confetti.green {
|
||||
background-color: #4caf50;
|
||||
border-color: transparent transparent #4caf50 transparent;
|
||||
}
|
||||
|
||||
.confetti.yellow {
|
||||
background-color: #ffeb3b;
|
||||
border-color: transparent transparent #ffeb3b transparent;
|
||||
}
|
||||
|
||||
.confetti.purple {
|
||||
background-color: #9c27b0;
|
||||
border-color: transparent transparent #9c27b0 transparent;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {
|
||||
transform: translateY(-100vh) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes confetti-sway {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Медиа-запросы для адаптивности */
|
||||
@media (max-width: 768px) {
|
||||
/* Основные стили */
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Навигация */
|
||||
.nav-container {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* Формы */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
textarea,
|
||||
select {
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.questionnaire-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.questionnaire-actions .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Опросы */
|
||||
.question-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Навигация по опросу */
|
||||
.step-navigation {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-navigation .btn {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.question-counter {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Шкала оценки */
|
||||
.scale-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.scale-values {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scale-item {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
/* Облако тегов */
|
||||
.tag-cloud-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin: 5px;
|
||||
font-size: 14px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* Результаты опроса */
|
||||
.questionnaire-links {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.link-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.link-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.link-input-group input {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-table th,
|
||||
.stats-table td {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Модальные окна */
|
||||
.modal-content {
|
||||
width: 90%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
/* Улучшенные стили для навигации в опросе на мобильных устройствах */
|
||||
.step-navigation {
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.step-navigation .btn {
|
||||
margin: 5px 0;
|
||||
width: 100%;
|
||||
height: 44px; /* Увеличиваем высоту для лучшего тача */
|
||||
}
|
||||
|
||||
.question-counter {
|
||||
margin: 10px 0;
|
||||
order: -1; /* Показываем счетчик вопросов сверху на мобильных */
|
||||
}
|
||||
|
||||
/* Улучшения для формы */
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: 100%;
|
||||
height: 44px; /* Увеличиваем высоту для лучшего тача */
|
||||
}
|
||||
|
||||
/* Улучшения для textarea */
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Улучшения для облака тегов */
|
||||
.tag-input {
|
||||
width: 100%;
|
||||
height: 44px; /* Увеличиваем высоту для лучшего тача */
|
||||
font-size: 16px; /* Оптимальный размер шрифта для мобильных устройств */
|
||||
}
|
||||
|
||||
/* Убираем зум при фокусе на поля ввода */
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px; /* Оптимальный размер шрифта для мобильных устройств */
|
||||
}
|
||||
}
|
||||
|
||||
/* Медиа-запросы для маленьких экранов */
|
||||
@media (max-width: 480px) {
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin: 3px;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.scale-item label {
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Уменьшаем размер чекбоксов и радиокнопок для лучшего тача */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
transform: scale(1.2);
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
@ -3,13 +3,27 @@ $(document).ready(function() {
|
||||
const adminLink = window.location.pathname.split('/').pop();
|
||||
let questionnaireData = null;
|
||||
|
||||
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последние две части пути (admin/:adminLink)
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: используем обычный путь
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
@ -40,7 +54,17 @@ $(document).ready(function() {
|
||||
|
||||
// Формируем ссылки
|
||||
const baseUrl = window.location.origin;
|
||||
const baseQuestionnairePath = window.location.pathname.split('/admin')[0];
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
|
||||
let baseQuestionnairePath;
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
baseQuestionnairePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
baseQuestionnairePath = window.location.pathname.split('/admin')[0];
|
||||
}
|
||||
|
||||
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
|
||||
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
|
||||
|
||||
@ -65,23 +89,32 @@ $(document).ready(function() {
|
||||
|
||||
// Проверяем наличие ответов для каждого типа вопросов
|
||||
for (const question of questions) {
|
||||
if (question.type === 'single' || question.type === 'multiple') {
|
||||
if (question.options && question.options.some(option => option.votes && option.votes > 0)) {
|
||||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
|
||||
if (questionType === 'single' || questionType === 'multiple') {
|
||||
if (question.options && question.options.some(option => (option.votes > 0 || option.count > 0))) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'tagcloud') {
|
||||
if (question.tags && question.tags.some(tag => tag.count && tag.count > 0)) {
|
||||
} else if (questionType === 'tagcloud') {
|
||||
if (question.tags && question.tags.some(tag => tag.count > 0)) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'scale' || question.type === 'rating') {
|
||||
if (question.responses && question.responses.length > 0) {
|
||||
} else if (questionType === 'scale' || questionType === 'rating') {
|
||||
// Проверяем оба возможных поля для данных шкалы
|
||||
const hasScaleValues = question.scaleValues && question.scaleValues.length > 0;
|
||||
const hasResponses = question.responses && question.responses.length > 0;
|
||||
if (hasScaleValues || hasResponses) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (question.type === 'text') {
|
||||
if (question.textAnswers && question.textAnswers.length > 0) {
|
||||
} else if (questionType === 'text') {
|
||||
// Проверяем оба возможных поля для текстовых ответов
|
||||
const hasTextAnswers = question.textAnswers && question.textAnswers.length > 0;
|
||||
const hasAnswers = question.answers && question.answers.length > 0;
|
||||
if (hasTextAnswers || hasAnswers) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
@ -99,138 +132,256 @@ $(document).ready(function() {
|
||||
const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
|
||||
$questionStats.append($questionTitle);
|
||||
|
||||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
|
||||
// В зависимости от типа вопроса отображаем разную статистику
|
||||
if (question.type === 'single' || question.type === 'multiple') {
|
||||
if (questionType === 'single' || questionType === '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') {
|
||||
renderChoiceStats(question, $questionStats);
|
||||
} else if (questionType === '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') {
|
||||
renderTagCloudStats(question, $questionStats);
|
||||
} else if (questionType === 'scale' || questionType === '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') {
|
||||
renderScaleStats(question, $questionStats);
|
||||
} else if (questionType === '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);
|
||||
}
|
||||
renderTextStats(question, $questionStats);
|
||||
}
|
||||
|
||||
$statsContainer.append($questionStats);
|
||||
});
|
||||
};
|
||||
|
||||
// Приводит тип вопроса к стандартному формату
|
||||
const normalizeQuestionType = (type) => {
|
||||
const typeMap = {
|
||||
'single_choice': 'single',
|
||||
'multiple_choice': 'multiple',
|
||||
'tag_cloud': 'tagcloud',
|
||||
'single': 'single',
|
||||
'multiple': 'multiple',
|
||||
'tagcloud': 'tagcloud',
|
||||
'scale': 'scale',
|
||||
'rating': 'rating',
|
||||
'text': 'text'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// Отображение статистики для вопросов с выбором
|
||||
const renderChoiceStats = (question, $container) => {
|
||||
// Преобразуем опции к единому формату
|
||||
const options = question.options.map(option => ({
|
||||
text: option.text,
|
||||
votes: option.votes || option.count || 0
|
||||
}));
|
||||
|
||||
const totalVotes = options.reduce((sum, option) => sum + option.votes, 0);
|
||||
|
||||
if (totalVotes === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const $table = $('<table>', { class: 'stats-table' });
|
||||
const $thead = $('<thead>').append(
|
||||
$('<tr>').append(
|
||||
$('<th>', { text: 'Вариант' }),
|
||||
$('<th>', { text: 'Голоса' }),
|
||||
$('<th>', { text: '%' }),
|
||||
$('<th>', { text: 'Визуализация' })
|
||||
)
|
||||
);
|
||||
|
||||
const $tbody = $('<tbody>');
|
||||
|
||||
options.forEach(option => {
|
||||
const votes = option.votes;
|
||||
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);
|
||||
$container.append($table);
|
||||
$container.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
|
||||
};
|
||||
|
||||
// Отображение статистики для облака тегов
|
||||
const renderTagCloudStats = (question, $container) => {
|
||||
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
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` }
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$container.append($tagCloud);
|
||||
};
|
||||
|
||||
// Отображение статистики для шкалы и рейтинга
|
||||
const renderScaleStats = (question, $container) => {
|
||||
// Используем scaleValues или responses, в зависимости от того, что доступно
|
||||
const values = question.responses && question.responses.length > 0
|
||||
? question.responses
|
||||
: (question.scaleValues || []);
|
||||
|
||||
if (values.length === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
|
||||
return;
|
||||
}
|
||||
|
||||
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-summary' }).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 })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Создаем таблицу для визуализации распределения голосов
|
||||
const $table = $('<table>', { class: 'stats-table' });
|
||||
const $thead = $('<thead>').append(
|
||||
$('<tr>').append(
|
||||
$('<th>', { text: 'Значение' }),
|
||||
$('<th>', { text: 'Голоса' }),
|
||||
$('<th>', { text: '%' }),
|
||||
$('<th>', { text: 'Визуализация' })
|
||||
)
|
||||
);
|
||||
|
||||
const $tbody = $('<tbody>');
|
||||
|
||||
// Определяем минимальное и максимальное значение шкалы из самих данных
|
||||
// либо используем значения из настроек вопроса, если они есть
|
||||
const scaleMin = question.scaleMin !== undefined ? question.scaleMin : min;
|
||||
const scaleMax = question.scaleMax !== undefined ? question.scaleMax : max;
|
||||
|
||||
// Создаем счетчик для каждого возможного значения шкалы
|
||||
const countByValue = {};
|
||||
for (let i = scaleMin; i <= scaleMax; i++) {
|
||||
countByValue[i] = 0;
|
||||
}
|
||||
|
||||
// Подсчитываем количество голосов для каждого значения
|
||||
values.forEach(value => {
|
||||
if (countByValue[value] !== undefined) {
|
||||
countByValue[value]++;
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем строки таблицы для каждого значения шкалы
|
||||
for (let value = scaleMin; value <= scaleMax; value++) {
|
||||
const count = countByValue[value] || 0;
|
||||
const percent = values.length > 0 ? Math.round((count / values.length) * 100) : 0;
|
||||
|
||||
const $tr = $('<tr>').append(
|
||||
$('<td>', { text: value }),
|
||||
$('<td>', { text: count }),
|
||||
$('<td>', { text: `${percent}%` }),
|
||||
$('<td>').append(
|
||||
$('<div>', { class: 'bar-container' }).append(
|
||||
$('<div>', {
|
||||
class: 'bar',
|
||||
css: { width: `${percent}%` }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$tbody.append($tr);
|
||||
}
|
||||
|
||||
$table.append($thead, $tbody);
|
||||
$scaleStats.append($table);
|
||||
|
||||
$container.append($scaleStats);
|
||||
};
|
||||
|
||||
// Отображение статистики для текстовых ответов
|
||||
const renderTextStats = (question, $container) => {
|
||||
// Используем textAnswers или answers, в зависимости от того, что доступно
|
||||
const answers = question.textAnswers && question.textAnswers.length > 0
|
||||
? question.textAnswers
|
||||
: (question.answers || []);
|
||||
|
||||
if (answers.length === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const $textAnswers = $('<div>', { class: 'text-answers-list' });
|
||||
|
||||
answers.forEach((answer, i) => {
|
||||
$textAnswers.append(
|
||||
$('<div>', { class: 'text-answer-item' }).append(
|
||||
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
|
||||
$('<div>', { class: 'answer-text', text: answer })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
$container.append($textAnswers);
|
||||
};
|
||||
|
||||
// Копирование ссылок
|
||||
$('#copy-public-link').on('click', function() {
|
||||
$('#public-link').select();
|
||||
@ -252,7 +403,18 @@ $(document).ready(function() {
|
||||
|
||||
// Редактирование опроса
|
||||
$('#edit-questionnaire').on('click', function() {
|
||||
const basePath = window.location.pathname.split('/admin')[0];
|
||||
// Перенаправляем на страницу редактирования
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/admin')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/edit/${adminLink}`;
|
||||
});
|
||||
|
||||
@ -268,7 +430,7 @@ $(document).ready(function() {
|
||||
// Функция удаления опроса
|
||||
const deleteQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
url: `${getApiPath()}/questionnaires/${adminLink}`,
|
||||
method: 'DELETE',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
|
@ -6,12 +6,27 @@ $(document).ready(function() {
|
||||
|
||||
let questionCount = 0;
|
||||
|
||||
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последнюю часть пути (create)
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: используем обычный путь
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Добавление нового вопроса
|
||||
@ -217,8 +232,18 @@ $(document).ready(function() {
|
||||
data: JSON.stringify(questionnaire),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
// Перенаправление на страницу администрирования опроса
|
||||
const basePath = window.location.pathname.split('/create')[0];
|
||||
// Перенаправляем на страницу администратора
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/create')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/admin/${result.data.adminLink}`;
|
||||
} else {
|
||||
showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');
|
||||
|
@ -8,13 +8,27 @@ $(document).ready(function() {
|
||||
let questionCount = 0;
|
||||
let questionnaireData = null;
|
||||
|
||||
// Получаем базовый путь API
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последние две части пути (edit/:adminLink)
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: используем обычный путь
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
@ -312,10 +326,20 @@ $(document).ready(function() {
|
||||
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}`;
|
||||
});
|
||||
showAlert('Опрос успешно сохранен!', 'Успех', { autoClose: true });
|
||||
// Перенаправляем на страницу администратора
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/edit')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/admin/${adminLink}`;
|
||||
} else {
|
||||
showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
|
@ -2,15 +2,25 @@
|
||||
$(document).ready(function() {
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: используем обычный путь
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = pathname.split('/');
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для загрузки списка опросов
|
||||
@ -40,9 +50,18 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
// Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
|
||||
const basePath = window.location.pathname.endsWith('/')
|
||||
? window.location.pathname
|
||||
: window.location.pathname + '/';
|
||||
const basePath = (() => {
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: нужно использовать /ms/questioneer/ для ссылок
|
||||
return '/ms/questioneer/';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
return pathname.endsWith('/') ? pathname : pathname + '/';
|
||||
}
|
||||
})();
|
||||
|
||||
const questionnairesHTML = questionnaires.map(q => `
|
||||
<div class="questionnaire-item">
|
||||
|
@ -23,6 +23,9 @@ $(document).ready(function() {
|
||||
// Для пошаговых опросов
|
||||
let currentQuestionIndex = 0;
|
||||
|
||||
// Объявляем переменную для хранения накопленных ответов
|
||||
let accumulatedAnswers = [];
|
||||
|
||||
// Проверка доступности localStorage
|
||||
const isLocalStorageAvailable = () => {
|
||||
try {
|
||||
@ -50,13 +53,33 @@ $(document).ready(function() {
|
||||
window.localStorage.setItem(getLocalStorageKey(), 'true');
|
||||
};
|
||||
|
||||
// Получаем базовый путь API
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
// Убираем последние две части пути (poll/:publicLink)
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
return pathParts.join('/') + '/api';
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: используем обычный путь
|
||||
// Извлекаем базовый путь из URL страницы
|
||||
const pathParts = pathname.split('/');
|
||||
// Убираем slug и последний сегмент (poll/[id])
|
||||
if (pathParts.length > 2) {
|
||||
// Удаляем 2 последних сегмента пути (poll/[id])
|
||||
pathParts.pop();
|
||||
pathParts.pop();
|
||||
}
|
||||
// Если последний сегмент пустой (из-за /) - удаляем его
|
||||
if (pathParts[pathParts.length - 1] === '') {
|
||||
pathParts.pop();
|
||||
}
|
||||
|
||||
// Путь до корня приложения
|
||||
return pathParts.join('/') + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
@ -231,7 +254,7 @@ $(document).ready(function() {
|
||||
</svg>
|
||||
Назад
|
||||
</button>
|
||||
<span id="question-counter">Вопрос 1 из ${questionnaireData.questions.length}</span>
|
||||
<span id="question-counter" class="question-counter">Вопрос 1 из ${questionnaireData.questions.length}</span>
|
||||
<button type="button" id="next-question" class="btn nav-btn">
|
||||
Далее
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
@ -317,6 +340,9 @@ $(document).ready(function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем ответ на текущий вопрос
|
||||
saveCurrentAnswer(currentQuestion, currentQuestionIndex);
|
||||
|
||||
// Если есть еще вопросы, переходим к следующему
|
||||
if (currentQuestionIndex < questionnaireData.questions.length - 1) {
|
||||
// Анимируем текущий вопрос перед переходом к следующему
|
||||
@ -343,6 +369,114 @@ $(document).ready(function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для сохранения ответа на текущий вопрос
|
||||
const saveCurrentAnswer = (question, questionIndex) => {
|
||||
// Нормализуем тип вопроса для согласованной обработки
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
const $questionEl = $(`.question-item[data-index="${questionIndex}"]`);
|
||||
|
||||
// Удаляем предыдущий ответ на этот вопрос, если он был
|
||||
accumulatedAnswers = accumulatedAnswers.filter(answer => answer.questionIndex !== questionIndex);
|
||||
|
||||
let optionIndices;
|
||||
let selectedOption;
|
||||
let selectedOptions;
|
||||
let selectedTags;
|
||||
let textAnswer;
|
||||
let scaleValue;
|
||||
|
||||
switch (questionType) {
|
||||
case 'single':
|
||||
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
|
||||
if (selectedOption.length) {
|
||||
accumulatedAnswers.push({
|
||||
questionIndex,
|
||||
optionIndices: [parseInt(selectedOption.val())]
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'multiple':
|
||||
selectedOptions = $questionEl.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)');
|
||||
if (selectedOptions.length) {
|
||||
optionIndices = $.map(selectedOptions, function(option) {
|
||||
const nameParts = $(option).attr('name').split('_');
|
||||
return parseInt(nameParts[nameParts.length - 1]);
|
||||
});
|
||||
accumulatedAnswers.push({
|
||||
questionIndex,
|
||||
optionIndices
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
textAnswer = $questionEl.find('textarea').val().trim();
|
||||
if (textAnswer) {
|
||||
accumulatedAnswers.push({
|
||||
questionIndex,
|
||||
textAnswer
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'scale':
|
||||
case 'rating': // Обратная совместимость
|
||||
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
|
||||
if (selectedOption.length) {
|
||||
const scaleValue = parseInt(selectedOption.val());
|
||||
accumulatedAnswers.push({
|
||||
questionIndex,
|
||||
scaleValue,
|
||||
optionIndices: [scaleValue]
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tagcloud':
|
||||
selectedTags = $questionEl.find('.tag-item.selected');
|
||||
if (selectedTags.length) {
|
||||
const tagTexts = $.map(selectedTags, function(tag) {
|
||||
return $(tag).text().replace('×', '').trim();
|
||||
});
|
||||
accumulatedAnswers.push({
|
||||
questionIndex,
|
||||
tagTexts
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Отправка формы
|
||||
const submitForm = function() {
|
||||
// Отключаем атрибут required у невидимых полей перед отправкой
|
||||
$('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
|
||||
|
||||
// Отправляем накопленные ответы на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/vote/${publicLink}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ answers: accumulatedAnswers }),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
// Сохраняем информацию о прохождении опроса
|
||||
markAsCompleted();
|
||||
|
||||
// Показываем анимацию благодарности вместо сразу скрытия формы
|
||||
showThankYouAnimation();
|
||||
} else {
|
||||
showAlert(`Ошибка: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error submitting poll:', error);
|
||||
showAlert('Не удалось отправить ответы. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Проверка наличия ответа на вопрос
|
||||
const checkQuestionAnswer = (question, questionIndex) => {
|
||||
switch (question.type) {
|
||||
@ -605,7 +739,7 @@ $(document).ready(function() {
|
||||
|
||||
const $tagInput = $('<input>', {
|
||||
type: 'text',
|
||||
placeholder: 'Введите теги, разделенные пробелом, и нажмите Enter',
|
||||
placeholder: 'Введите тег и нажмите Enter',
|
||||
class: 'tag-input'
|
||||
});
|
||||
|
||||
@ -620,32 +754,25 @@ $(document).ready(function() {
|
||||
|
||||
const inputText = $(this).val().trim();
|
||||
if (inputText) {
|
||||
// Разбиваем ввод на отдельные теги
|
||||
const tags = inputText.split(/\s+/);
|
||||
// Добавляем тег как отдельный элемент
|
||||
const $tagItem = $('<div>', {
|
||||
class: 'tag-item selected',
|
||||
text: inputText
|
||||
});
|
||||
|
||||
// Добавляем каждый тег как отдельный элемент
|
||||
tags.forEach(tagText => {
|
||||
if (tagText) {
|
||||
const $tagItem = $('<div>', {
|
||||
class: 'tag-item selected',
|
||||
text: tagText
|
||||
});
|
||||
|
||||
const $tagRemove = $('<span>', {
|
||||
class: 'tag-remove',
|
||||
html: '×',
|
||||
click: function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
updateTagValidation();
|
||||
}
|
||||
});
|
||||
|
||||
$tagItem.append($tagRemove);
|
||||
$tagItems.append($tagItem);
|
||||
const $tagRemove = $('<span>', {
|
||||
class: 'tag-remove',
|
||||
html: '×',
|
||||
click: function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
updateTagValidation();
|
||||
}
|
||||
});
|
||||
|
||||
$tagItem.append($tagRemove);
|
||||
$tagItems.append($tagItem);
|
||||
|
||||
$(this).val('');
|
||||
updateTagValidation();
|
||||
}
|
||||
@ -679,11 +806,74 @@ $(document).ready(function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Создание анимации конфети
|
||||
const createConfetti = () => {
|
||||
// Создаем контейнер для конфети, если его еще нет
|
||||
let $confettiContainer = $('.confetti-container');
|
||||
if ($confettiContainer.length === 0) {
|
||||
$confettiContainer = $('<div>', { class: 'confetti-container' });
|
||||
$('body').append($confettiContainer);
|
||||
} else {
|
||||
$confettiContainer.empty(); // Очищаем предыдущие конфети
|
||||
}
|
||||
|
||||
// Параметры конфети
|
||||
const confettiCount = 100; // Количество конфети
|
||||
const shapes = ['square', 'circle', 'triangle']; // Формы конфети
|
||||
const colors = ['red', 'blue', 'green', 'yellow', 'purple']; // Цвета конфети
|
||||
|
||||
// Создаем конфети
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
// Случайные параметры для конфети
|
||||
const shape = shapes[Math.floor(Math.random() * shapes.length)];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const left = Math.random() * 100; // Позиция по горизонтали (%)
|
||||
const size = Math.random() * 10 + 5; // Размер (px)
|
||||
|
||||
// Случайные параметры для анимации
|
||||
const fallDuration = Math.random() * 3 + 2; // Длительность падения (s)
|
||||
const swayDuration = Math.random() * 2 + 1; // Длительность качания (s)
|
||||
const fallDelay = Math.random() * 2; // Задержка начала падения (s)
|
||||
const swayDelay = Math.random() * 1; // Задержка начала качания (s)
|
||||
|
||||
// Создаем элемент конфети
|
||||
const $confetti = $('<div>', {
|
||||
class: `confetti ${shape} ${color}`,
|
||||
css: {
|
||||
left: `${left}%`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
animation: `confetti-fall ${fallDuration}s linear ${fallDelay}s forwards, confetti-sway ${swayDuration}s ease-in-out ${swayDelay}s infinite`
|
||||
}
|
||||
});
|
||||
|
||||
// Если это треугольник, корректируем размер
|
||||
if (shape === 'triangle') {
|
||||
$confetti.css({
|
||||
'border-width': `0 ${size/2}px ${size*0.87}px ${size/2}px`
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем конфети в контейнер
|
||||
$confettiContainer.append($confetti);
|
||||
}
|
||||
|
||||
// Удаляем контейнер после завершения анимации
|
||||
setTimeout(() => {
|
||||
$confettiContainer.fadeOut(1000, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Показываем анимацию благодарности
|
||||
const showThankYouAnimation = () => {
|
||||
// Скрываем форму
|
||||
formEl.hide();
|
||||
|
||||
// Создаем анимацию конфети
|
||||
createConfetti();
|
||||
|
||||
// Создаем анимацию благодарности
|
||||
const $thankYouEl = $('<div>', {
|
||||
class: 'thank-you-animation',
|
||||
@ -730,109 +920,6 @@ $(document).ready(function() {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Отправка формы
|
||||
const submitForm = function() {
|
||||
// Отключаем атрибут required у невидимых полей перед отправкой
|
||||
$('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
|
||||
|
||||
// Собираем ответы
|
||||
const answers = [];
|
||||
|
||||
$.each(questionnaireData.questions, function(questionIndex, question) {
|
||||
const $questionEl = $(`.question-item[data-index="${questionIndex}"]`);
|
||||
|
||||
let optionIndices;
|
||||
let selectedOption;
|
||||
let selectedOptions;
|
||||
let selectedTags;
|
||||
let textAnswer;
|
||||
let scaleValue;
|
||||
|
||||
switch (question.type) {
|
||||
case 'single_choice':
|
||||
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
|
||||
if (selectedOption.length) {
|
||||
answers.push({
|
||||
questionIndex,
|
||||
optionIndices: [parseInt(selectedOption.val())]
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'multiple_choice':
|
||||
selectedOptions = $questionEl.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)');
|
||||
if (selectedOptions.length) {
|
||||
optionIndices = $.map(selectedOptions, function(option) {
|
||||
const nameParts = $(option).attr('name').split('_');
|
||||
return parseInt(nameParts[nameParts.length - 1]);
|
||||
});
|
||||
answers.push({
|
||||
questionIndex,
|
||||
optionIndices
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
textAnswer = $questionEl.find('textarea').val().trim();
|
||||
if (textAnswer) {
|
||||
answers.push({
|
||||
questionIndex,
|
||||
textAnswer
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'scale':
|
||||
case 'rating': // Обратная совместимость
|
||||
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
|
||||
if (selectedOption.length) {
|
||||
answers.push({
|
||||
questionIndex,
|
||||
scaleValue: parseInt(selectedOption.val())
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tag_cloud':
|
||||
selectedTags = $questionEl.find('.tag-item.selected');
|
||||
if (selectedTags.length) {
|
||||
const tagTexts = $.map(selectedTags, function(tag) {
|
||||
return $(tag).text().replace('×', '').trim();
|
||||
});
|
||||
answers.push({
|
||||
questionIndex,
|
||||
tagTexts
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Отправляем ответы на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/vote/${publicLink}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ answers }),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
// Сохраняем информацию о прохождении опроса
|
||||
markAsCompleted();
|
||||
|
||||
// Показываем анимацию благодарности вместо сразу скрытия формы
|
||||
showThankYouAnimation();
|
||||
} else {
|
||||
showAlert(`Ошибка: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error submitting poll:', error);
|
||||
showAlert('Не удалось отправить ответы. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Загрузка результатов опроса
|
||||
const loadPollResults = () => {
|
||||
$.ajax({
|
||||
@ -864,6 +951,15 @@ $(document).ready(function() {
|
||||
|
||||
pollResultsContainerEl.append($resultsTitle);
|
||||
|
||||
// Проверяем наличие вопросов в данных
|
||||
if (!data.questions || data.questions.length === 0) {
|
||||
pollResultsContainerEl.append($('<p>', {
|
||||
text: 'Нет данных для отображения',
|
||||
class: 'no-results'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Отображаем результаты с отложенной анимацией
|
||||
data.questions.forEach((question, index) => {
|
||||
setTimeout(() => {
|
||||
@ -879,159 +975,24 @@ $(document).ready(function() {
|
||||
|
||||
$questionResult.append($questionTitle);
|
||||
|
||||
// Нормализация типа вопроса
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
|
||||
// Отображаем результаты в зависимости от типа вопроса
|
||||
switch (question.type) {
|
||||
case 'single_choice':
|
||||
case 'multiple_choice':
|
||||
if (question.options && question.options.length > 0) {
|
||||
// Находим общее количество голосов
|
||||
const totalVotes = question.options.reduce((sum, option) => sum + (option.count || 0), 0);
|
||||
|
||||
if (totalVotes > 0) {
|
||||
question.options.forEach((option) => {
|
||||
const percent = totalVotes > 0 ? Math.round((option.count || 0) * 100 / totalVotes) : 0;
|
||||
|
||||
const $optionResult = $('<div>', {
|
||||
class: 'result-bar-container'
|
||||
});
|
||||
|
||||
const $optionLabel = $('<div>', {
|
||||
class: 'result-label',
|
||||
text: option.text
|
||||
});
|
||||
|
||||
const $resultBar = $('<div>', {
|
||||
class: 'result-bar'
|
||||
});
|
||||
|
||||
const $resultBarFill = $('<div>', {
|
||||
class: 'result-bar-fill',
|
||||
css: { width: '0%' }
|
||||
});
|
||||
|
||||
const $resultPercent = $('<div>', {
|
||||
class: 'result-percent',
|
||||
text: `${percent}% (${option.count || 0} голосов)`
|
||||
});
|
||||
|
||||
$resultBar.append($resultBarFill);
|
||||
$optionResult.append($optionLabel, $resultBar, $resultPercent);
|
||||
$questionResult.append($optionResult);
|
||||
|
||||
// Анимируем заполнение полосы после добавления в DOM
|
||||
setTimeout(() => {
|
||||
$resultBarFill.css('width', `${percent}%`);
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
$questionResult.append($('<p>', {
|
||||
text: 'Пока нет голосов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
}
|
||||
switch (questionType) {
|
||||
case 'single':
|
||||
case 'multiple':
|
||||
renderChoiceResults(question, $questionResult);
|
||||
break;
|
||||
|
||||
case 'tag_cloud':
|
||||
if (question.tags && question.tags.length > 0) {
|
||||
const $tagCloud = $('<div>', {
|
||||
class: 'results-tag-cloud'
|
||||
});
|
||||
|
||||
// Находим максимальное количество для масштабирования
|
||||
const maxCount = Math.max(...question.tags.map(tag => tag.count || 1));
|
||||
|
||||
question.tags.forEach((tag, tagIndex) => {
|
||||
// Масштабируем размер тега от 1 до 3 в зависимости от частоты
|
||||
const scale = maxCount > 1 ? 1 + (tag.count / maxCount) * 2 : 1;
|
||||
|
||||
const $tag = $('<div>', {
|
||||
class: 'result-tag',
|
||||
text: tag.text,
|
||||
css: {
|
||||
opacity: 0,
|
||||
transform: 'scale(0.5)',
|
||||
fontSize: `${scale}em`
|
||||
}
|
||||
});
|
||||
|
||||
$tagCloud.append($tag);
|
||||
|
||||
// Анимируем появление тега с задержкой
|
||||
setTimeout(() => {
|
||||
$tag.css({
|
||||
opacity: 1,
|
||||
transform: 'scale(1)'
|
||||
});
|
||||
}, tagIndex * 100);
|
||||
});
|
||||
|
||||
$questionResult.append($tagCloud);
|
||||
} else {
|
||||
$questionResult.append($('<p>', {
|
||||
text: 'Пока нет тегов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
case 'tagcloud':
|
||||
renderTagCloudResults(question, $questionResult);
|
||||
break;
|
||||
|
||||
case 'scale':
|
||||
case 'rating':
|
||||
if (question.scaleValues && question.scaleValues.length > 0) {
|
||||
const average = question.scaleAverage ||
|
||||
(question.scaleValues.reduce((sum, val) => sum + val, 0) / question.scaleValues.length);
|
||||
|
||||
const min = Math.min(...question.scaleValues);
|
||||
const max = Math.max(...question.scaleValues);
|
||||
|
||||
const $scaleAverage = $('<div>', {
|
||||
class: 'scale-average',
|
||||
html: `Средняя оценка: <span class="highlight">${average.toFixed(1)}</span> (мин: ${min}, макс: ${max})`
|
||||
});
|
||||
|
||||
$questionResult.append($scaleAverage);
|
||||
} else {
|
||||
$questionResult.append($('<p>', {
|
||||
text: 'Пока нет оценок для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
renderScaleResults(question, $questionResult);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
if (question.answers && question.answers.length > 0) {
|
||||
const $answersContainer = $('<div>', {
|
||||
class: 'text-answers-container'
|
||||
});
|
||||
|
||||
question.answers.forEach((answer, answerIndex) => {
|
||||
const $textAnswer = $('<div>', {
|
||||
class: 'text-answer',
|
||||
text: answer,
|
||||
css: {
|
||||
opacity: 0,
|
||||
transform: 'translateX(20px)'
|
||||
}
|
||||
});
|
||||
|
||||
$answersContainer.append($textAnswer);
|
||||
|
||||
// Анимируем появление ответа с задержкой
|
||||
setTimeout(() => {
|
||||
$textAnswer.css({
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
});
|
||||
}, answerIndex * 200);
|
||||
});
|
||||
|
||||
$questionResult.append($answersContainer);
|
||||
} else {
|
||||
$questionResult.append($('<p>', {
|
||||
text: 'Пока нет текстовых ответов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
renderTextResults(question, $questionResult);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1043,14 +1004,196 @@ $(document).ready(function() {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
});
|
||||
}, 100);
|
||||
}, index * 300); // Задержка между вопросами
|
||||
}, index * 300); // Задержка между вопросами
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Нормализация типа вопроса
|
||||
const normalizeQuestionType = (type) => {
|
||||
const typeMap = {
|
||||
'single_choice': 'single',
|
||||
'multiple_choice': 'multiple',
|
||||
'tag_cloud': 'tagcloud',
|
||||
'single': 'single',
|
||||
'multiple': 'multiple',
|
||||
'tagcloud': 'tagcloud',
|
||||
'scale': 'scale',
|
||||
'rating': 'rating',
|
||||
'text': 'text'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// Отображение результатов для вопросов с выбором
|
||||
const renderChoiceResults = (question, $container) => {
|
||||
// Адаптируем формат опций
|
||||
const options = question.options || [];
|
||||
|
||||
// Если нет данных для отображения
|
||||
if (!data.questions || data.questions.length === 0) {
|
||||
pollResultsContainerEl.append($('<p>', {
|
||||
text: 'Нет данных для отображения',
|
||||
// Находим общее количество голосов
|
||||
const totalVotes = options.reduce((sum, option) => sum + (option.count || option.votes || 0), 0);
|
||||
|
||||
if (totalVotes > 0) {
|
||||
options.forEach((option) => {
|
||||
const votes = option.count || option.votes || 0;
|
||||
const percent = totalVotes > 0 ? Math.round(votes * 100 / totalVotes) : 0;
|
||||
|
||||
const $optionResult = $('<div>', {
|
||||
class: 'result-bar-container'
|
||||
});
|
||||
|
||||
const $optionLabel = $('<div>', {
|
||||
class: 'result-label',
|
||||
text: option.text
|
||||
});
|
||||
|
||||
const $resultBar = $('<div>', {
|
||||
class: 'result-bar'
|
||||
});
|
||||
|
||||
const $resultBarFill = $('<div>', {
|
||||
class: 'result-bar-fill',
|
||||
css: { width: '0%' }
|
||||
});
|
||||
|
||||
const $resultPercent = $('<div>', {
|
||||
class: 'result-percent',
|
||||
text: `${percent}% (${votes} голосов)`
|
||||
});
|
||||
|
||||
$resultBar.append($resultBarFill);
|
||||
$optionResult.append($optionLabel, $resultBar, $resultPercent);
|
||||
$container.append($optionResult);
|
||||
|
||||
// Анимируем заполнение полосы после добавления в DOM
|
||||
setTimeout(() => {
|
||||
$resultBarFill.css('width', `${percent}%`);
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
$container.append($('<p>', {
|
||||
text: 'Пока нет голосов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Отображение результатов для облака тегов
|
||||
const renderTagCloudResults = (question, $container) => {
|
||||
// Используем правильное поле для тегов
|
||||
const tags = question.tags || [];
|
||||
|
||||
if (tags.length > 0 && tags.some(tag => (tag.count || 0) > 0)) {
|
||||
const $tagCloud = $('<div>', {
|
||||
class: 'results-tag-cloud'
|
||||
});
|
||||
|
||||
// Находим максимальное количество для масштабирования
|
||||
const maxCount = Math.max(...tags.map(tag => tag.count || 1));
|
||||
|
||||
tags.forEach((tag, tagIndex) => {
|
||||
if (!tag.text) return; // Пропускаем некорректные теги
|
||||
|
||||
// Масштабируем размер тега от 1 до 3 в зависимости от частоты
|
||||
const scale = maxCount > 1 ? 1 + ((tag.count || 1) / maxCount) * 2 : 1;
|
||||
|
||||
const $tag = $('<div>', {
|
||||
class: 'result-tag',
|
||||
text: tag.text,
|
||||
css: {
|
||||
opacity: 0,
|
||||
transform: 'scale(0.5)',
|
||||
fontSize: `${scale}em`
|
||||
}
|
||||
});
|
||||
|
||||
$tagCloud.append($tag);
|
||||
|
||||
// Анимируем появление тега с задержкой
|
||||
setTimeout(() => {
|
||||
$tag.css({
|
||||
opacity: 1,
|
||||
transform: 'scale(1)'
|
||||
});
|
||||
}, tagIndex * 100);
|
||||
});
|
||||
|
||||
$container.append($tagCloud);
|
||||
} else {
|
||||
$container.append($('<p>', {
|
||||
text: 'Пока нет тегов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Отображение результатов для шкалы и рейтинга
|
||||
const renderScaleResults = (question, $container) => {
|
||||
// Используем доступное поле для значений шкалы
|
||||
const values = question.responses && question.responses.length > 0
|
||||
? question.responses
|
||||
: (question.scaleValues || []);
|
||||
|
||||
if (values.length > 0) {
|
||||
// Используем уже рассчитанное среднее или рассчитываем сами
|
||||
const average = question.scaleAverage !== undefined
|
||||
? question.scaleAverage
|
||||
: (values.reduce((sum, val) => sum + val, 0) / values.length);
|
||||
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
|
||||
const $scaleAverage = $('<div>', {
|
||||
class: 'scale-average',
|
||||
html: `Средняя оценка: <span class="highlight">${average.toFixed(1)}</span> (мин: ${min}, макс: ${max})`
|
||||
});
|
||||
|
||||
$container.append($scaleAverage);
|
||||
} else {
|
||||
$container.append($('<p>', {
|
||||
text: 'Пока нет оценок для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Отображение результатов для текстовых вопросов
|
||||
const renderTextResults = (question, $container) => {
|
||||
// Используем доступное поле для текстовых ответов
|
||||
const answers = question.textAnswers && question.textAnswers.length > 0
|
||||
? question.textAnswers
|
||||
: (question.answers || []);
|
||||
|
||||
if (answers.length > 0) {
|
||||
const $answersContainer = $('<div>', {
|
||||
class: 'text-answers-container'
|
||||
});
|
||||
|
||||
answers.forEach((answer, answerIndex) => {
|
||||
const $textAnswer = $('<div>', {
|
||||
class: 'text-answer',
|
||||
text: answer,
|
||||
css: {
|
||||
opacity: 0,
|
||||
transform: 'translateX(20px)'
|
||||
}
|
||||
});
|
||||
|
||||
$answersContainer.append($textAnswer);
|
||||
|
||||
// Анимируем появление ответа с задержкой
|
||||
setTimeout(() => {
|
||||
$textAnswer.css({
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
});
|
||||
}, answerIndex * 200);
|
||||
});
|
||||
|
||||
$container.append($answersContainer);
|
||||
} else {
|
||||
$container.append($('<p>', {
|
||||
text: 'Пока нет текстовых ответов для этого вопроса',
|
||||
class: 'no-results'
|
||||
}));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user