You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1248 lines
52 KiB
1248 lines
52 KiB
<template>
|
|
<div id="lilcity__course-redactor" v-on:course_publish="onCoursePublish" v-on:course_preview="onCoursePreview">
|
|
<div v-if="!courseLoading && !mounting">
|
|
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit">
|
|
<div class="info">
|
|
<div class="info__section" :style="coverBackgroundStyle">
|
|
<div class="info__main">
|
|
<div class="info__head">
|
|
<div class="info__user">
|
|
<div class="info__ava ava">
|
|
<img :src="authorPicture" alt="Аватар" class="ava__pic">
|
|
</div>
|
|
<div v-if="me" class="info__group info__field--light">
|
|
<div class="info__label">АВТОР</div>
|
|
<div class="info__value">{{ authorName }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="info__upload upload">
|
|
Загрузить фон
|
|
<input type="file" class="upload__file" @change="onCoverImageSelected">
|
|
</div>
|
|
</div>
|
|
<div class="info__title">
|
|
<div class="info__field field field_info"
|
|
v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
|
|
<div class="field__label">{{titles.courseTitle}}</div>
|
|
<div class="field__wrap">
|
|
<textarea class="field__textarea"
|
|
rows="1"
|
|
:title="titles.courseTitle"
|
|
v-autosize="course.title"
|
|
@change="onCourseNameInput"
|
|
v-model="course.title"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="info__field field field_info field_short_description"
|
|
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
|
|
<div class="field__label">{{titles.shortDescription}}</div>
|
|
<div class="field__wrap">
|
|
<textarea class="field__textarea"
|
|
v-autosize="course.short_description"
|
|
@input="$v.course.short_description.$touch()"
|
|
v-model="course.short_description"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="info__foot" v-if="!live">
|
|
<div class="info__field field field_info info__field--light"
|
|
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
|
|
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
|
|
<div class="field__wrap">
|
|
<lil-select :value.sync="categorySelect" :options="categoryOptions"
|
|
placeholder="Выберите категорию"/>
|
|
</div>
|
|
</div>
|
|
<div class="info__field field field_info"
|
|
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
|
|
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
|
|
<div class="field__wrap field__wrap__appended">
|
|
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
|
|
@input="$v.course.duration.$touch()">
|
|
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="info__sidebar">
|
|
<div class="info__wrap">
|
|
<div class="info__fieldset">
|
|
<div v-if="!live" class="info__field field">
|
|
<div class="field__label field__label_gray">ССЫЛКА</div>
|
|
<div class="field__wrap">
|
|
<input type="text" class="field__input" v-model="course.url" @input="slugChanged = true">
|
|
</div>
|
|
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div>
|
|
</div>
|
|
|
|
<div v-if="live" class="info__field field"
|
|
v-bind:class="{ error: ($v.course.stream.$dirty || showErrors) && $v.course.stream.$invalid }">
|
|
<div class="field__label field__label_gray">ССЫЛКА НА VIMEO</div>
|
|
<div class="field__wrap">
|
|
<input type="text" class="field__input" v-model="course.stream">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- <div v-if="live" class="info__field field"
|
|
v-bind:class="{ error: ($v.course.date.$dirty || showErrors) && $v.course.date.$invalid }">
|
|
<div class="field__label">ДАТА</div>
|
|
<div class="field__wrap">
|
|
<lil-select :value.sync="course.date" :options="scheduleOptions" placeholder="Выберите дату"/>
|
|
</div>
|
|
</div> -->
|
|
|
|
<div v-if="!live" class="info__field field">
|
|
<div class="field__label field__label_gray">ДОСТУП</div>
|
|
<div class="field__wrap">
|
|
<label class="field__switch switch switch_lg switch_circle">
|
|
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid">
|
|
<span class="switch__content">Бесплатный</span>
|
|
</label>
|
|
<label class="field__switch switch switch_lg switch_circle">
|
|
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
|
|
<span class="switch__content">Платный</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="course.is_paid" class="info__field field">
|
|
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
|
|
<div class="field__wrap field__wrap__appended field__wrap__100px">
|
|
<input type="text" class="field__input field__input__appended" v-model.number="displayPrice">
|
|
<button disabled class="field__append">руб.</button>
|
|
</div>
|
|
</div>
|
|
<label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg">
|
|
<input type="checkbox" class="switch__input" v-model="course.is_featured">
|
|
<span class="switch__content">Выделить</span>
|
|
</label>
|
|
</div>
|
|
<div v-if="!live" class="info__fieldset">
|
|
<div class="info__field field">
|
|
<div class="field__label field__label_gray">ЗАПУСК</div>
|
|
<div class="field__wrap">
|
|
<label class="field__switch switch switch_lg switch_circle">
|
|
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred">
|
|
<span class="switch__content">Мгновенный</span>
|
|
</label>
|
|
<label class="field__switch switch switch_lg switch_circle">
|
|
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred">
|
|
<span class="switch__content">Отложенный</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="info__field field" v-show="course.is_deferred">
|
|
<div class="field__label">ДАТА</div>
|
|
<div class="field__wrap">
|
|
<vue-datepicker :disabled="disabledDates" input-class="field__input" v-model="course.date" language="ru" format="dd/MM/yyyy"/>
|
|
</div>
|
|
</div>
|
|
<div class="info__field field" v-show="course.is_deferred">
|
|
<div class="field__label">ВРЕМЯ</div>
|
|
<div class="field__wrap">
|
|
<lil-select :value.sync="course.time" :options="timeOptions" placeholder="Выберите время"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="section">
|
|
<div class="section__center center">
|
|
<div class="kit">
|
|
<div v-if="!live" id="course-redactor__nav" class="kit__nav">
|
|
<button class="kit__btn btn btn_lg"
|
|
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }"
|
|
type="button" @click="showCourse">Описание
|
|
курса
|
|
</button>
|
|
<button class="kit__btn btn btn_lg"
|
|
v-bind:class="{ 'btn_stroke': viewSection === 'lessons', 'btn_gray': viewSection !== 'lessons' }"
|
|
type="button"
|
|
@click="showLessons"
|
|
:disabled="!course.id">
|
|
Уроки
|
|
</button>
|
|
</div>
|
|
<div v-if="viewSection === 'course'" class="kit__body">
|
|
<vue-draggable v-model="course.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
|
|
<div v-for="(block, index) in course.content" :key="block.data.id ? block.data.id : block.data.guid">
|
|
<block-text v-if="block.type === 'text'"
|
|
:index="index"
|
|
:title.sync="block.data.title"
|
|
:text.sync="block.data.text"
|
|
v-on:remove="onBlockRemoved"
|
|
:access-token="accessToken"/>
|
|
<block-image-text v-if="block.type === 'image-text'"
|
|
:index="index"
|
|
:title.sync="block.data.title"
|
|
:text.sync="block.data.text"
|
|
:image-id.sync="block.data.image_id"
|
|
:image-url.sync="block.data.image_url"
|
|
v-on:remove="onBlockRemoved"
|
|
:access-token="accessToken"/>
|
|
<block-image v-if="block.type === 'image'"
|
|
:index="index"
|
|
:title.sync="block.data.title"
|
|
:image-id.sync="block.data.image_id"
|
|
:image-url.sync="block.data.image_url"
|
|
v-on:remove="onBlockRemoved"
|
|
:access-token="accessToken"/>
|
|
<block-images v-if="block.type === 'images'"
|
|
:index="index"
|
|
:title.sync="block.data.title"
|
|
:text.sync="block.data.text"
|
|
:images.sync="block.data.images"
|
|
v-on:remove="onBlockRemoved"
|
|
:access-token="accessToken"/>
|
|
<block-video v-if="block.type === 'video'"
|
|
:index="index"
|
|
:title.sync="block.data.title"
|
|
v-on:remove="onBlockRemoved"
|
|
:video-url.sync="block.data.video_url"/>
|
|
</div>
|
|
</vue-draggable>
|
|
|
|
<block-add v-on:added="onBlockAdded"/>
|
|
|
|
<!--<div class="kit__foot">
|
|
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }">
|
|
Сохранить
|
|
</button>
|
|
</div>-->
|
|
</div>
|
|
<div v-if="viewSection === 'lessons'" class="kit__body">
|
|
<div class="lessons__title title">Содержание курса</div>
|
|
<div v-if="!lessonsLoading" class="lessons__list">
|
|
<vue-draggable v-model="lessons" @start="drag=true" @end="onLessonsChanged" :options="{ handle: '.sortable__handle' }">
|
|
<div class="lessons__item" v-for="(lesson, index) in lessons" :key="lesson.id">
|
|
<div class="lessons__actions">
|
|
<button class="sortable__handle" type="button">
|
|
<svg class="icon icon-hamburger">
|
|
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="lessons__action" @click="removeLesson(index)">
|
|
<svg class="icon icon-delete">
|
|
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="lessons__action" @click="editLesson(index)">
|
|
<svg class="icon icon-edit">
|
|
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
|
|
<div class="lessons__row">
|
|
<div class="lessons__content">{{ lesson.short_description | linebreaks }}</div>
|
|
</div>
|
|
</div>
|
|
</vue-draggable>
|
|
</div>
|
|
<div v-if="lessonsLoading">Загрузка...</div>
|
|
<div class="lessons__foot">
|
|
<button type="button" class="lessons__btn btn btn_md" @click="addLesson">СОЗДАТЬ УРОК</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<form v-if="viewSection === 'lessons-edit'" @submit.prevent="onLessonSubmit">
|
|
<lesson-redactor :$v="$v" :lesson.sync="currentLesson" :saving.sync="lessonSaving" :access-token="accessToken"
|
|
v-on:back="goToLessons" />
|
|
</form>
|
|
</div>
|
|
<div v-else>
|
|
<div class="section">
|
|
<div class="section__center center">
|
|
<h1>Загрузка...</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ROLE_ADMIN, ROLE_AUTHOR } from './consts'
|
|
import LinkInput from './inputs/LinkInput'
|
|
import DatePicker from 'vuejs-datepicker'
|
|
import BlockText from './blocks/BlockText'
|
|
import BlockImage from './blocks/BlockImage'
|
|
import BlockImages from './blocks/BlockImages'
|
|
import BlockImageText from './blocks/BlockImageText'
|
|
import BlockVideo from './blocks/BlockVideo'
|
|
import LilSelect from "./inputs/LilSelect";
|
|
import LessonRedactor from "./LessonRedactor";
|
|
import {api} from "../js/modules/api";
|
|
import BlockAdd from "./blocks/BlockAdd";
|
|
import $ from 'jquery';
|
|
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
|
|
import slugify from 'slugify';
|
|
import Draggable from 'vuedraggable';
|
|
import {showNotification} from "../js/modules/notification";
|
|
import createHistory from "history/createBrowserHistory";
|
|
import moment from 'moment'
|
|
import _ from 'lodash'
|
|
|
|
const history = createHistory();
|
|
|
|
export default {
|
|
name: "course-redactor",
|
|
props: ["authorName", "authorPicture", "accessToken", "courseId", "live"],
|
|
data() {
|
|
return {
|
|
disabledDates: {
|
|
to: new Date(new Date().setDate(new Date().getDate() - 1)),
|
|
},
|
|
titles: {},
|
|
mounting: false,
|
|
viewSection: 'course',
|
|
me: null,
|
|
users: null,
|
|
ROLE_ADMIN: ROLE_ADMIN,
|
|
slugChanged: false,
|
|
course: {
|
|
title: '',
|
|
status: null,
|
|
category: null,
|
|
categorySelect: null,
|
|
duration: null,
|
|
author: null,
|
|
price: null,
|
|
url: '',
|
|
coverImage: '',
|
|
kit__body: null,
|
|
is_paid: false,
|
|
is_featured: true,
|
|
is_deferred: false,
|
|
date: '',
|
|
stream: '',
|
|
time: null,
|
|
short_description: '',
|
|
content: [],
|
|
gallery: {
|
|
images: [],
|
|
}
|
|
},
|
|
courseLoading: false,
|
|
courseSaving: false,
|
|
lessons: [],
|
|
lessonsLoading: false,
|
|
lessonSaving: false,
|
|
currentLesson: {
|
|
title: '',
|
|
short_description: '',
|
|
content: [],
|
|
},
|
|
is_adding_block: false,
|
|
timeOptions: [
|
|
{
|
|
'title': '10:00',
|
|
'value': '10:00',
|
|
},
|
|
{
|
|
'title': '11:00',
|
|
'value': '11:00',
|
|
},
|
|
{
|
|
'title': '12:00',
|
|
'value': '12:00',
|
|
},
|
|
{
|
|
'title': '13:00',
|
|
'value': '13:00',
|
|
},
|
|
{
|
|
'title': '14:00',
|
|
'value': '14:00',
|
|
},
|
|
{
|
|
'title': '15:00',
|
|
'value': '15:00',
|
|
},
|
|
{
|
|
'title': '16:00',
|
|
'value': '16:00',
|
|
},
|
|
{
|
|
'title': '17:00',
|
|
'value': '17:00',
|
|
},
|
|
{
|
|
'title': '18:00',
|
|
'value': '18:00',
|
|
}
|
|
],
|
|
|
|
weekdays: [
|
|
'',
|
|
'Понедельник',
|
|
'Вторник',
|
|
'Среда',
|
|
'Четверг',
|
|
'Пятница',
|
|
'Суббота',
|
|
'Воскресенье',
|
|
],
|
|
fields: {
|
|
title: "Заголовок",
|
|
short_description: "Краткое описание",
|
|
stream: "Ссылка на Vimeo",
|
|
date: "Дата",
|
|
duration: "Продолжительность",
|
|
category: "Категория",
|
|
},
|
|
lessonFields: {
|
|
title: "Название урока",
|
|
short_description: "Описание урока",
|
|
},
|
|
showErrors: false,
|
|
savingTimeout: null,
|
|
savingDebounceTimeout: null,
|
|
categoryOptions: [],
|
|
scheduleOptions: [],
|
|
courseSyncHook: false, // Если true, то watch не будет отправлять курс на обновление
|
|
}
|
|
},
|
|
validations() {
|
|
if(this.live) {
|
|
return {
|
|
course: {
|
|
title: {
|
|
required
|
|
},
|
|
short_description: {
|
|
required
|
|
},
|
|
stream: {
|
|
required,
|
|
// url
|
|
},
|
|
date: {
|
|
required
|
|
},
|
|
},
|
|
};
|
|
} else {
|
|
return {
|
|
course: {
|
|
title: {
|
|
required
|
|
},
|
|
short_description: {
|
|
required
|
|
},
|
|
duration: {
|
|
required,
|
|
numeric,
|
|
minValue: minValue(1)
|
|
},
|
|
category: {
|
|
required,
|
|
numeric,
|
|
minValue: minValue(1)
|
|
},
|
|
},
|
|
currentLesson: {
|
|
title: {
|
|
required
|
|
},
|
|
short_description: {
|
|
required
|
|
},
|
|
}
|
|
};
|
|
}
|
|
},
|
|
methods: {
|
|
onCoverImageSelected(event) {
|
|
let file = event.target.files[0];
|
|
let reader = new FileReader();
|
|
reader.onload = () => {
|
|
this.$set(this.course, 'coverImage', reader.result);
|
|
api.uploadImage(reader.result, this.accessToken)
|
|
.then((response) => {
|
|
this.course.coverImageId = response.data.id;
|
|
})
|
|
.catch((error) => {
|
|
//console.log('error', error);
|
|
});
|
|
};
|
|
if (file) {
|
|
reader.readAsDataURL(file);
|
|
}
|
|
},
|
|
onCoursePriceChange(event) {
|
|
this.course.price = event.target.value;
|
|
},
|
|
onCourseNameInput() {
|
|
this.$v.course.title.$touch();
|
|
if (!this.slugChanged) {
|
|
this.course.url = slugify(this.course.title);
|
|
}
|
|
},
|
|
updateCategory() {
|
|
if (this.categoryOptions && Array.isArray(this.categoryOptions) && this.course.category) {
|
|
this.categoryOptions.forEach((category) => {
|
|
if (category.id === this.course.category) {
|
|
this.course.categorySelect = category;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
onBlockRemoved(blockIndex) {
|
|
const blockToRemove = this.course.content[blockIndex];
|
|
// Удаляем блок из Vue
|
|
this.course.content.splice(blockIndex, 1);
|
|
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
|
|
if (blockToRemove.data.id) {
|
|
api.removeContentBlock(blockToRemove, this.accessToken);
|
|
}
|
|
},
|
|
onBlockAdded(blockData) {
|
|
this.course.content.push(blockData);
|
|
},
|
|
removeLesson(lessonIndex) {
|
|
if (!confirm('Вы действительно хотите удалить этот урок?')) {
|
|
return;
|
|
}
|
|
const lesson = this.lessons[lessonIndex];
|
|
if (lesson.hasOwnProperty('id') && lesson.id) {
|
|
api.removeCourseLesson(lesson.id, this.accessToken);
|
|
}
|
|
this.lessons.splice(lessonIndex, 1);
|
|
this.onLessonsChanged();
|
|
},
|
|
editLesson(lessonIndex) {
|
|
this.currentLesson = this.lessons[lessonIndex];
|
|
history.push("/course/create/lessons/edit/"+this.currentLesson.id);
|
|
this.viewSection = 'lessons-edit';
|
|
},
|
|
showCourse() {
|
|
if (this.viewSection !== 'course') {
|
|
history.push("/course/create");
|
|
}
|
|
this.viewSection = 'course'
|
|
},
|
|
showLessons() {
|
|
if (this.viewSection !== 'lessons') {
|
|
history.push("/course/create/lessons");
|
|
}
|
|
this.viewSection = 'lessons';
|
|
},
|
|
addLesson() {
|
|
this.currentLesson = {
|
|
title: '',
|
|
short_description: '',
|
|
content: [],
|
|
};
|
|
if (this.viewSection !== 'lessons-edit') {
|
|
history.push("/course/create/lessons/new");
|
|
}
|
|
this.viewSection = 'lessons-edit';
|
|
window.scrollTo(0, 0);
|
|
},
|
|
onSubmit() {
|
|
//console.log('onSubmit');
|
|
this.courseSaving = true;
|
|
api.saveCourse(this.course, this.accessToken)
|
|
.then((response) => {
|
|
this.courseSaving = false;
|
|
this.course = api.convertCourseJson(response.data);
|
|
this.course.live = this.live;
|
|
})
|
|
.catch((err) => {
|
|
this.courseSaving = false;
|
|
});
|
|
},
|
|
onLessonSubmit() {
|
|
if(!this.validateLesson()) {
|
|
return new Promise(function(resolve, reject) {
|
|
reject('validation reject');
|
|
});
|
|
}
|
|
showNotification("success", "Момент, вносим последние правки!");
|
|
|
|
return this.saveLesson();
|
|
},
|
|
saveLesson() {
|
|
this.lessonSaving = true;
|
|
const currentLessonId = this.currentLesson.id;
|
|
this.currentLesson.course_id = this.course.id;
|
|
let req = api.saveLesson(this.currentLesson, this.accessToken);
|
|
|
|
req.then((response) => {
|
|
const newLesson = api.convertLessonJson(response.data);
|
|
newLesson.course_id = this.course.id;
|
|
this.currentLesson = newLesson;
|
|
if (!currentLessonId) {
|
|
this.lessons.push(newLesson);
|
|
}
|
|
if (this.lessons && Array.isArray(this.lessons)) {
|
|
this.lessons.forEach((lesson, index) => {
|
|
if (newLesson.id === lesson.id) {
|
|
this.$set(this.lessons, index, newLesson);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.changeSavingStatus(true);
|
|
showNotification("success", 'Урок сохранён');
|
|
// this.goToLessons();
|
|
|
|
this.lessonSaving = false;
|
|
})
|
|
.catch((err) => {
|
|
this.lessonSaving = false;
|
|
//console.error(err);
|
|
this.changeSavingStatus(true, true);
|
|
// alert('Произошло что-то страшное: '+err.toString());
|
|
console.log(err);
|
|
if(err.response) {
|
|
for(let i in err.response.data) {
|
|
if(typeof err.response.data[i] === "array") {
|
|
showNotification("error", this.lessonFields[i]+": "+err.response.data[i].join(', '));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return req;
|
|
},
|
|
goToLessons() {
|
|
history.push("/course/create/lessons");
|
|
this.viewSection = 'lessons';
|
|
this.$nextTick(() => {
|
|
const elementTop = $('#course-redactor__nav').position().top - 130;
|
|
$(window).scrollTop(elementTop);
|
|
});
|
|
},
|
|
processCourseJson(data) {
|
|
this.course = api.convertCourseJson(data);
|
|
this.course.live = this.live;
|
|
this.lessons = data.lessons.map((lessonJson) => {
|
|
return api.convertLessonJson(lessonJson);
|
|
});
|
|
this.course.duration = this.course.duration || '';
|
|
},
|
|
loadCourseDraft() {
|
|
//console.log('loadCourseDraft');
|
|
if(this.live) { return; }
|
|
|
|
this.courseLoading = true;
|
|
let response = api.getCourseDraft(this.accessToken);
|
|
|
|
response
|
|
.then((response) => {
|
|
this.processCourseJson(response.data);
|
|
this.courseLoading = false;
|
|
})
|
|
.catch((err) => {
|
|
this.courseLoading = false;
|
|
//console.log('error course loading', err);
|
|
});
|
|
|
|
return response;
|
|
},
|
|
loadCourse() {
|
|
//console.log('loadCourse');
|
|
this.courseLoading = true;
|
|
let request = null;
|
|
if(this.live) {
|
|
request = api.loadLive(this.courseId, this.accessToken)
|
|
} else {
|
|
request = api.loadCourse(this.courseId, this.accessToken)
|
|
}
|
|
request
|
|
.then((response) => {
|
|
this.processCourseJson(response.data);
|
|
this.$nextTick(() => {
|
|
this.courseLoading = false;
|
|
});
|
|
this.lessons.sort((a, b) => {
|
|
if (a.position > b.position) {
|
|
return 1;
|
|
}
|
|
if (a.position < b.position) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
this.courseLoading = false;
|
|
//console.log('error course loading', err);
|
|
});
|
|
|
|
return request;
|
|
},
|
|
loadLessons(courseId) {
|
|
|
|
},
|
|
onCoursePublish() {
|
|
if(this.validate()) {
|
|
const publishButton = $('#course-redactor__publish-button');
|
|
publishButton.attr('disabled', 'disabled');
|
|
|
|
if(this.live) {
|
|
window.location = '/school/lessons';
|
|
} else {
|
|
api.publishCourse(this.course.id, this.accessToken)
|
|
.then((response) => {
|
|
window.location = '/course/on-moderation';
|
|
})
|
|
.catch(() => {
|
|
publishButton.removeAttr('disabled');
|
|
});
|
|
}
|
|
}
|
|
},
|
|
validate(silent) {
|
|
console.log('validate', this.$v.$invalid);
|
|
|
|
if(!silent) {
|
|
this.showErrors = true;
|
|
if (this.$v.course.$invalid) {
|
|
for(let i in this.$v.course) {
|
|
if(this.$v.course[i].$invalid) {
|
|
showNotification("error", "Ошибка валидации поля "+this.fields[i]);
|
|
}
|
|
}
|
|
// showNotification("error", "Заполните все необходимые поля");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
},
|
|
validateLesson(silent) {
|
|
console.log('validate', this.$v.$invalid);
|
|
this.showErrors = true;
|
|
if (this.$v.currentLesson.$invalid) {
|
|
if(!silent) {
|
|
for(let i in this.$v.currentLesson) {
|
|
if(this.$v.currentLesson[i].$invalid) {
|
|
showNotification("error", "Ошибка валидации поля "+this.lessonFields[i]);
|
|
}
|
|
}
|
|
}
|
|
// showNotification("error", "Заполните все необходимые поля");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
onCoursePreview() {
|
|
if(this.course.id) {
|
|
let url;
|
|
|
|
if(this.viewSection === 'lessons-edit') {
|
|
this.onLessonSubmit().then(()=>{
|
|
url = `/lesson/${this.currentLesson.id}`;
|
|
|
|
let newTab = window.open(url, '_blank');
|
|
newTab.focus();
|
|
}).catch((e)=>{
|
|
// showNotification("error", "Пока еще нечего посмотреть, давайте что-нибудь напишем");
|
|
|
|
console.log(e);
|
|
});
|
|
} else {
|
|
if(this.live) {
|
|
url = `/school/lessons/${this.course.id}`;
|
|
} else {
|
|
if (this.course.url) {
|
|
url = `/course/${this.course.url}`;
|
|
}
|
|
url = `/course/${this.course.id}`;
|
|
}
|
|
|
|
let newTab = window.open(url, '_blank');
|
|
newTab.focus();
|
|
}
|
|
} else {
|
|
showNotification("error", "Пока еще нечего посмотреть, давайте что-нибудь напишем");
|
|
}
|
|
},
|
|
saveCourseDraft: function (newValue, oldValue) {
|
|
//console.log('saveCourseDraft');
|
|
// if (!oldValue.id) {
|
|
// return;
|
|
// }
|
|
|
|
|
|
// if(this.live) {
|
|
// if(!this.course.date || this.course.short_description == '' || this.course.title == '') {
|
|
// //console.log('live valiedation error');
|
|
// return;
|
|
// }
|
|
// } else {
|
|
// if(this.course.short_description == '' || this.course.title == '') {
|
|
// //console.log('course validation error');
|
|
// return;
|
|
// }
|
|
// }
|
|
|
|
if (this.savingDebounceTimeout) {
|
|
clearTimeout(this.savingDebounceTimeout);
|
|
}
|
|
this.courseSyncHook = false;
|
|
this.savingDebounceTimeout = setTimeout(() => {
|
|
if(!this.validate(true)) {
|
|
return;
|
|
}
|
|
|
|
this.courseSaving = true;
|
|
this.changeSavingStatus();
|
|
const courseObject = this.course;
|
|
courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url;
|
|
api.saveCourse(courseObject, this.accessToken)
|
|
.then((response) => {
|
|
this.courseSaving = false;
|
|
this.changeSavingStatus(true);
|
|
this.courseSyncHook = true;
|
|
const courseData = api.convertCourseJson(response.data);
|
|
if (this.course.coverImage) {
|
|
courseData.coverImage = this.course.coverImage;
|
|
}
|
|
if (this.course.is_deferred) {
|
|
courseData.is_deferred = true;
|
|
}
|
|
let remoteUUIDMapper = {}
|
|
let remoteDataMapper = {}
|
|
if (courseData.content) {
|
|
courseData.content.forEach((contentElement) => {
|
|
remoteUUIDMapper[contentElement.uuid] = contentElement.data.id;
|
|
remoteDataMapper[contentElement.uuid] = contentElement.data;
|
|
})
|
|
}
|
|
|
|
this.course.content.forEach((contentElement, index) => {
|
|
if (!contentElement.data.id) {
|
|
this.$set(this.course.content[index].data, 'id', remoteUUIDMapper[contentElement.uuid])
|
|
}
|
|
|
|
if(contentElement.type === 'images') {
|
|
remoteDataMapper[contentElement.uuid].images.forEach((image, imageIndex) => {
|
|
this.$set(this.course.content[index].data.images[imageIndex], 'id', image.id)
|
|
})
|
|
}
|
|
});
|
|
if (courseData.url) {
|
|
this.slugChanged = true;
|
|
}
|
|
|
|
if(courseData.id) {
|
|
this.course.id = courseData.id;
|
|
}
|
|
|
|
/*if(this.live && courseData.date) {
|
|
this.course.date = _.find(this.scheduleOptions, function(item){
|
|
return item.value == courseData.date;
|
|
});
|
|
}*/
|
|
this.$nextTick(() => {
|
|
this.courseSyncHook = false;
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
this.courseSyncHook = false;
|
|
this.courseSaving = false;
|
|
//console.error(err);
|
|
this.changeSavingStatus(true, true);
|
|
// alert('Произошло что-то страшное: '+err.toString());
|
|
//console.log(err.response.data);
|
|
if(err.response) {
|
|
for(let i in err.response.data) {
|
|
if(typeof err.response.data[i] === "array") {
|
|
showNotification("error", this.fields[i] + ": " + err.response.data[i].join(', '));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, 500);
|
|
},
|
|
updateViewSection(location, action) {
|
|
//console.log('updateViewSection[action]', action);
|
|
if (location.pathname === '/course/create/lessons') {
|
|
this.viewSection = 'lessons';
|
|
} else if (location.pathname === '/course/create') {
|
|
this.viewSection = 'course';
|
|
} else if (location.pathname === '/course/create/lessons/new') {
|
|
this.viewSection = 'lessons-edit';
|
|
} else if (location.pathname.indexOf('/course/create/lessons/edit') !== -1) {
|
|
let lessonId = parseInt(location.pathname.split('/').pop());
|
|
//console.log('lessonId', lessonId, this.lessons.toString());
|
|
//console.log('lessod edit', this.lessons.find((i)=>{return i.id === lessonId}));
|
|
this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId});
|
|
this.viewSection = 'lessons-edit';
|
|
}
|
|
},
|
|
onLessonsChanged() {
|
|
let promises = [];
|
|
this.courseSaving = true;
|
|
this.lessons.map((lesson, index) => {
|
|
lesson.position = index + 1;
|
|
lesson.course_id = this.course.id;
|
|
let res = api.saveLesson(lesson, this.accessToken);
|
|
promises.push(res);
|
|
});
|
|
Promise.all(promises).then(() => {
|
|
this.courseSaving = false;
|
|
this.changeSavingStatus(true);
|
|
}, () => {
|
|
this.courseSaving = false;
|
|
this.changeSavingStatus(true, true);
|
|
});
|
|
},
|
|
pluralize(count, words) {
|
|
var cases = [2, 0, 1, 1, 1, 2];
|
|
return words[ (count % 100 > 4 && count % 100 < 20) ? 2 : cases[ Math.min(count % 10, 5)] ];
|
|
},
|
|
changeSavingStatus(saved, error) {
|
|
let text = '';
|
|
if(error) {
|
|
text = 'ОШИБКА';
|
|
} else {
|
|
text = saved ? 'СОХРАНЕНО' : 'СОХРАНЕНИЕ...';
|
|
}
|
|
clearTimeout(this.savingTimeout);
|
|
document.getElementById('course-redactor__saving-status').innerText = text;
|
|
if(saved || error){
|
|
this.savingTimeout = setTimeout(() => {
|
|
document.getElementById('course-redactor__saving-status').innerText = '';
|
|
}, 2000);
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.mounting = true;
|
|
moment.locale('ru');
|
|
|
|
this.titles['courseTitle'] = 'НАЗВАНИЕ КУРСА';
|
|
this.titles['shortDescription'] = 'КРАТКО О КУРСЕ';
|
|
|
|
if(this.live) {
|
|
this.titles['courseTitle'] = 'НАЗВАНИЕ УРОКА';
|
|
this.titles['shortDescription'] = 'КРАТКО ОБ УРОКЕ';
|
|
}
|
|
|
|
this.course.live = this.live;
|
|
// Listen for changes to the current location.
|
|
this.unlisten = history.listen(this.updateViewSection);
|
|
|
|
let promises = [];
|
|
|
|
let cats = api.getCategories(this.accessToken);
|
|
promises.push(cats);
|
|
cats.then((response) => {
|
|
if (response.data) {
|
|
this.categoryOptions = response.data.results.map((category) => {
|
|
return {
|
|
title: category.title,
|
|
value: category.id
|
|
}
|
|
});
|
|
}
|
|
this.updateCategory();
|
|
});
|
|
|
|
if(this.live) {
|
|
let schedule = api.getSchedule(this.accessToken, {live_lesson_exist: false});
|
|
promises.push(schedule);
|
|
|
|
schedule.then((response) => {
|
|
if (response.data) {
|
|
|
|
let schedule = [];
|
|
response.data.results.forEach((item) => {
|
|
schedule[item.weekday] = item.title;
|
|
});
|
|
|
|
console.log('schedule', schedule);
|
|
|
|
let options = [];
|
|
|
|
for(let i=-7;i<=10;i++) {
|
|
let now = new Date();
|
|
now.setDate(now.getDate() + i);
|
|
|
|
let weekday = now.getDay() || 7;
|
|
|
|
|
|
console.log('data', i, now.getDay(), weekday, now, moment(now).format("D MMM"));
|
|
|
|
if(schedule[weekday]) {
|
|
options.push({
|
|
title: `${schedule[weekday]} (${this.weekdays[weekday]}, ${moment(now).format("D MMM")})`,
|
|
value: moment(now).format('YYYY-MM-DD')
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log('options',options);
|
|
|
|
this.scheduleOptions = _.orderBy(options, (item)=>{return moment(item.value)});
|
|
}
|
|
this.updateCategory();
|
|
});
|
|
}
|
|
|
|
// let user = api.getCurrentUser(this.accessToken);
|
|
// promises.push(user);
|
|
//
|
|
// user.then((response) => {
|
|
// if (response.data) {
|
|
// this.me = response.data;
|
|
//
|
|
// if(this.me.role == ROLE_ADMIN) {
|
|
// api.getUsers({role: [ROLE_AUTHOR,ROLE_ADMIN], page_size: 1000}, this.accessToken)
|
|
// .then((usersResponse) => {
|
|
// if (usersResponse.data) {
|
|
// this.users = usersResponse.data.results.map((user) => {
|
|
// return {
|
|
// title: `${user.first_name} ${user.last_name}`,
|
|
// value: user.id
|
|
// }
|
|
// });
|
|
// }
|
|
// });
|
|
// }
|
|
// }
|
|
// });
|
|
|
|
// if (this.courseId) {
|
|
// this.loadCourse().then(()=>{this.updateViewSection(window.location, 'load')}).catch(()=>{
|
|
// this.updateViewSection(window.location, 'load err')
|
|
// })
|
|
// } else {
|
|
// this.loadCourseDraft().then(()=>{this.updateViewSection(window.location, 'load draft')}).catch(()=>{
|
|
// this.updateViewSection(window.location, 'load draft err')
|
|
// });
|
|
// }
|
|
|
|
//console.log('wait promises');
|
|
Promise.all(promises.map(p => p.catch(e => e))).then(()=>{
|
|
//console.log('promises end');
|
|
this.mounting = false;
|
|
let load;
|
|
if (this.courseId) {
|
|
load = this.loadCourse()
|
|
} else {
|
|
load = this.loadCourseDraft();
|
|
}
|
|
|
|
load.then(()=>{
|
|
this.updateViewSection(window.location, 'load '+this.courseId)
|
|
})
|
|
.catch(()=>{
|
|
this.updateViewSection(window.location, 'load err '+this.courseId)
|
|
})
|
|
});
|
|
|
|
//console.log('mounted end');
|
|
// this.updateViewSection(window.location);
|
|
},
|
|
computed: {
|
|
coverBackgroundStyle() {
|
|
return this.course.coverImage ? `background-image: url(${this.course.coverImage});` : '';
|
|
},
|
|
displayPrice: {
|
|
get: function () {
|
|
return this.course.is_paid ? (this.course.price || '') : '';
|
|
},
|
|
set: function (value) {
|
|
this.course.price = value || 0;
|
|
}
|
|
},
|
|
categorySelect: {
|
|
get() {
|
|
if (!this.categoryOptions || this.categoryOptions.length === 0 || !this.course || !this.course.category) {
|
|
return null;
|
|
}
|
|
let value;
|
|
this.categoryOptions.forEach((category) => {
|
|
if (category.value === this.course.category) {
|
|
value = category;
|
|
}
|
|
});
|
|
return value;
|
|
},
|
|
set(value) {
|
|
this.course.category = value.value;
|
|
}
|
|
},
|
|
// userSelect: {
|
|
// get() {
|
|
// if (!this.users || this.users.length === 0 || !this.course || !this.course.author) {
|
|
// return null;
|
|
// }
|
|
// let value;
|
|
// this.users.forEach((user) => {
|
|
// if (user.value === this.course.author) {
|
|
// value = user;
|
|
// }
|
|
// });
|
|
// return value;
|
|
// },
|
|
// set(value) {
|
|
// this.course.author = value.value;
|
|
// }
|
|
// },
|
|
courseFullUrl() {
|
|
if (!this.course.url) {
|
|
return `https://lil.city/course/${this.course.id}`;
|
|
}
|
|
let suffix = this.course.url ? this.course.url : 'ваша_ссылка';
|
|
return `https://lil.city/course/${suffix}`;
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
this.unlisten();
|
|
},
|
|
watch: {
|
|
'course': {
|
|
handler: function (newValue, oldValue) {
|
|
// //console.log('watch', JSON.stringify(newValue), JSON.stringify(oldValue));
|
|
// Если курс загрузился и есть ID - делаем кнопки превью и публикации активными
|
|
//console.log('newValue.id', newValue.id);
|
|
if (newValue.id) {
|
|
// //console.log('newValue.id disabled remove', newValue.id);
|
|
$('#course-redactor__preview-button').removeAttr('disabled');
|
|
$('#course-redactor__publish-button').removeAttr('disabled');
|
|
}
|
|
// //console.log('courseSyncHook', this.courseSyncHook);
|
|
if (this.courseSyncHook || this.courseLoading) {
|
|
//console.log('abort save draft', this.courseSyncHook, this.courseLoading);
|
|
return;
|
|
}
|
|
this.saveCourseDraft(newValue, oldValue);
|
|
},
|
|
deep: true,
|
|
},
|
|
|
|
},
|
|
components: {
|
|
BlockAdd,
|
|
LessonRedactor,
|
|
LilSelect,
|
|
BlockText,
|
|
'link-input': LinkInput,
|
|
'vue-datepicker': DatePicker,
|
|
'block-text': BlockText,
|
|
'block-image': BlockImage,
|
|
'block-image-text': BlockImageText,
|
|
'block-images': BlockImages,
|
|
'block-video': BlockVideo,
|
|
'lesson-redactor': LessonRedactor,
|
|
'vue-draggable': Draggable,
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.vdp-datepicker__calendar {
|
|
width: 240px;
|
|
margin-top: 10px;
|
|
padding: 5px;
|
|
background: white;
|
|
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.1);
|
|
z-index: 99 !important;
|
|
|
|
header {
|
|
display: flex;
|
|
margin-bottom: 5px;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.prev, .next {
|
|
font-size: 0;
|
|
cursor: pointer;
|
|
order: 1;
|
|
width: auto !important;
|
|
padding: 10px;
|
|
}
|
|
|
|
.prev {
|
|
order: 1;
|
|
}
|
|
|
|
.next {
|
|
order: 3;
|
|
}
|
|
|
|
.prev:before, .next:before {
|
|
content: '';
|
|
display: block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border: solid #E6E6E6;
|
|
border-width: 2px 2px 0 0;
|
|
}
|
|
|
|
.prev:after, .next:after {
|
|
content: none !important;
|
|
}
|
|
|
|
.prev:before {
|
|
transform: rotate(-135deg);
|
|
}
|
|
|
|
.next:before {
|
|
transform: rotate(45deg);
|
|
}
|
|
}
|
|
|
|
.kit__preview {
|
|
img {
|
|
width: 140px;
|
|
height: 140px;
|
|
}
|
|
}
|
|
|
|
.kit__photo {
|
|
width: 140px;
|
|
height: 140px;
|
|
}
|
|
|
|
.kit__section-remove {
|
|
button.sortable__handle {
|
|
margin-right: 10px;
|
|
cursor: -webkit-grab;
|
|
cursor: grab;
|
|
|
|
svg.icon-hamburger {
|
|
width: 1em;
|
|
height: 1em;
|
|
}
|
|
}
|
|
}
|
|
|
|
.sortable-ghost, .sortable-chosen {
|
|
background: white;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.course-redactor__preview-button-bg-save {
|
|
background-color: #58fffb;
|
|
}
|
|
.course-redactor__preview-button {
|
|
transition: backgroundColor 0.5s ease-in-out;
|
|
}
|
|
|
|
.field_text {
|
|
height: 270px;
|
|
overflow: scroll;
|
|
}
|
|
|
|
.field_short_description {
|
|
max-height: 200px;
|
|
overflow: scroll;
|
|
}
|
|
</style>
|
|
|
|
|