index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. <template>
  2. <v-container class="spacing-playground" fluid>
  3. <v-row no-gutters>
  4. <v-col cols="8" class="col detail__main pa-6">
  5. <treeselect
  6. placeholder="Категория"
  7. value="id"
  8. noChildrenText="Нет подкатегорий"
  9. valueFormat="object"
  10. v-model="category"
  11. :options="treeCategories"
  12. class="tree-select"
  13. />
  14. <div class="error" v-if="$v.$dirty && !$v.category.required">
  15. Обязательное поле
  16. </div>
  17. <v-text-field
  18. v-model="title"
  19. label="Заголовок"
  20. @blur="getAlias"
  21. v-if="category"
  22. >
  23. </v-text-field>
  24. <div class="error" v-if="$v.$dirty && !$v.title.required">
  25. Обязательное поле
  26. </div>
  27. <v-text-field v-model="generatedAlias" disabled label="Алиас">
  28. </v-text-field>
  29. <v-combobox
  30. v-model="moduleId"
  31. label="Модуль"
  32. required
  33. :items="modules"
  34. item-text="name"
  35. item-value="id"
  36. @click="getContainer('module')"
  37. ></v-combobox>
  38. <v-text-field v-model="sliderId" label="Слайдер"> </v-text-field>
  39. <v-combobox
  40. v-model="sectionId"
  41. label="Секция"
  42. required
  43. :items="sections"
  44. item-text="name"
  45. item-value="id"
  46. @click="getContainer('section')"
  47. :disabled="$v.generatedAlias.invalid"
  48. :class="!$v.generatedAlias.invalid ? '' : 'green'"
  49. >
  50. </v-combobox>
  51. <v-text-field v-model="sorting" label="Позиция"> </v-text-field>
  52. <v-textarea
  53. name="input-7-1"
  54. label="Описание"
  55. hint=""
  56. v-model="description"
  57. ></v-textarea>
  58. <div class="error" v-if="$v.$dirty && !$v.description.required">
  59. Обязательное поле
  60. </div>
  61. <v-textarea
  62. label="Видео"
  63. v-model="video"
  64. rows="3"
  65. auto-grow
  66. ></v-textarea>
  67. <v-text-field
  68. v-model="externalLink"
  69. label="Внешняя ссылка"
  70. ></v-text-field>
  71. <v-file-input
  72. prepend-icon=""
  73. append-icon="$file"
  74. label="Прикрепить файлы (PDF, ZIP, DOC, DOCX)"
  75. chips
  76. multiple
  77. small-chips
  78. v-model="filesModel"
  79. @change="loadFiles"
  80. accept=".pdf, .zip, .doc, .docx"
  81. ></v-file-input>
  82. <v-chip-group v-if="files.length">
  83. <v-chip
  84. @click:close="deleteFile(file, index)"
  85. v-for="(file, index) in files"
  86. :key="file.id"
  87. close
  88. >
  89. {{ file.original_file_name }}
  90. </v-chip>
  91. </v-chip-group>
  92. <AdminEditor :value="content" @input="content = $event"></AdminEditor>
  93. <div class="error" v-if="$v.$dirty && !$v.content.minLength">
  94. Обязательное поле
  95. </div>
  96. <v-btn
  97. class="ml-auto mr-0 pa-6 my-6"
  98. @click="publishNews"
  99. v-if="previewID"
  100. >Опубликовать</v-btn
  101. >
  102. <v-btn class="ml-auto mr-0 pa-6 my-6" @click="createNews" v-else
  103. >Создать публикацию</v-btn
  104. >
  105. <v-btn class="ml-auto mr-0 pa-6 my-6" @click="previewNews"
  106. >Предпросмотр</v-btn
  107. >
  108. <v-btn class="ml-auto mr-0 pa-6 my-6" @click="shareNews"
  109. >Поделиться</v-btn
  110. >
  111. </v-col>
  112. <v-col cols="3" class="col detail__sidebar pa-6">
  113. <v-img
  114. :lazy-src="url(img ? img.relative_path : '')"
  115. :src="url(img ? img.relative_path : '')"
  116. :max-width="400"
  117. ></v-img>
  118. <!-- <v-text-field v-model="photoTitle" label="Источник фото" placeholder="Чтобы вставить картинку заполните это поле"> </v-text-field>-->
  119. <v-file-input
  120. v-model="file"
  121. @change="fileHandler('main-img', 814, 458)"
  122. label="Картинка:"
  123. type="file"
  124. accept="image/*"
  125. ref="input1"
  126. :disabled="!$v.generatedAlias.required"
  127. :class="!$v.generatedAlias.required ? '' : ''"
  128. ></v-file-input>
  129. <v-img
  130. :lazy-src="url(previewImg ? previewImg.relative_path : '')"
  131. :max-width="400"
  132. :src="url(previewImg ? previewImg.relative_path : '')"
  133. ></v-img>
  134. <v-file-input
  135. v-model="previewFile"
  136. @change="fileHandler('preview-img', 850, 420)"
  137. label="Превью картинка:"
  138. type="file"
  139. accept="image/*"
  140. ref="inputPreview"
  141. :disabled="!$v.generatedAlias.required"
  142. ></v-file-input>
  143. <v-select
  144. class="tree-select"
  145. :items="authorsList"
  146. item-text="name"
  147. item-value="value"
  148. v-model="author"
  149. label="Автор"
  150. ></v-select>
  151. <v-checkbox label="Показывать автора" v-model="showAuthor">
  152. </v-checkbox>
  153. <v-checkbox label="Не отдавать в Яндекс" v-model="dont_send_to_rss">
  154. </v-checkbox>
  155. <v-checkbox label="МОЛНИЯ" v-model="lightning"> </v-checkbox>
  156. <v-checkbox label="Включить комментарии" v-model="allowComments">
  157. </v-checkbox>
  158. <v-checkbox
  159. label="На правах рекламы"
  160. v-model="advertisement"
  161. ></v-checkbox>
  162. <v-checkbox label="Партнерский материал" v-model="partner">
  163. </v-checkbox>
  164. <v-checkbox
  165. label="Имеются противопоказания"
  166. v-model="contraindications"
  167. >
  168. </v-checkbox>
  169. {{ properties }}
  170. <v-checkbox v-model="activity" label="Активность"> </v-checkbox>
  171. <v-text-field v-model="hash_tags" label="Хэш тэги"> </v-text-field>
  172. <v-img :src="url(storieImg ? storieImg.relative_path : '')"></v-img>
  173. <v-checkbox v-model="showInStories" label="Показывать в сторис">
  174. </v-checkbox>
  175. <v-checkbox v-model="verified" label="Проверено">
  176. </v-checkbox>
  177. <!--<v-file-input
  178. label="Картинка для истории:"
  179. type="file"
  180. accept="image/*"
  181. ref="inputStorie"
  182. v-model="storieFile"
  183. :disabled="!$v.generatedAlias.required"
  184. :class="!$v.generatedAlias.required ? '' : ''"
  185. @change="fileHandler('storie-img', 800, 1200)"
  186. ></v-file-input>-->
  187. <v-menu
  188. ref="menu"
  189. :close-on-content-click="false"
  190. :return-value.sync="startActivity.date"
  191. transition="scale-transition"
  192. offset-y
  193. min-width="auto"
  194. >
  195. <template v-slot:activator="{ on, attrs }">
  196. <v-text-field
  197. v-model="startActivity.date"
  198. label="Выберите дату"
  199. prepend-icon="mdi-calendar"
  200. readonly
  201. v-bind="attrs"
  202. v-on="on"
  203. ></v-text-field>
  204. </template>
  205. <v-date-picker v-model="startActivity.date" no-title scrollable>
  206. <v-spacer></v-spacer>
  207. <v-btn text color="primary" @click="menu = false"> Cancel </v-btn>
  208. <v-btn
  209. text
  210. color="primary"
  211. @click="$refs.menu.save(startActivity.date)"
  212. >
  213. OK
  214. </v-btn>
  215. </v-date-picker>
  216. </v-menu>
  217. <vue-timepicker
  218. v-model="startActivity.time"
  219. format="HH:mm:ss"
  220. ></vue-timepicker>
  221. <div>Выбранное время: {{ start_activity }}</div>
  222. <div class="error" v-if="$v.$dirty && !$v.start_activity.required">
  223. Обязательное поле
  224. </div>
  225. <v-text-field v-model="sliderId" label="Слайдер"> </v-text-field>
  226. <v-text-field v-model="source" label="Источник" @blur="getAlias">
  227. </v-text-field>
  228. <v-text-field v-model="metaDescription" label="Мета описание для SEO">
  229. </v-text-field>
  230. <v-text-field v-model="metaKeys" label="Мета ключи для SEO">
  231. </v-text-field>
  232. </v-col>
  233. <div v-if="message.length" class="message-box">
  234. <v-alert
  235. class="message"
  236. dismissible
  237. v-model="message"
  238. :type="messageClass ? messageClass : 'primary'"
  239. >
  240. {{ message }}
  241. </v-alert>
  242. </div>
  243. </v-row>
  244. </v-container>
  245. </template>
  246. <script>
  247. import { required, minLength } from "vuelidate/lib/validators";
  248. import VueTimepicker from "vue2-timepicker";
  249. import "vue2-timepicker/dist/VueTimepicker.css";
  250. import Treeselect from "@riophae/vue-treeselect";
  251. import "@riophae/vue-treeselect/dist/vue-treeselect.css";
  252. const nest = (items, id = null, link = "parent_id") => {
  253. return items
  254. .filter((item) => item[link] === id)
  255. .map((item) => ({
  256. ...item,
  257. label: item.title,
  258. children: nest(items, item.id),
  259. }));
  260. };
  261. export default {
  262. components: {
  263. VueTimepicker,
  264. Treeselect,
  265. },
  266. methods: {
  267. createNews() {
  268. // Проверяем валидацию инпутов
  269. this.$v.$touch();
  270. if (!this.$v.$invalid) {
  271. this.$axios
  272. .post("admin/news/create", this.newsItem)
  273. .then((res) => {
  274. res.data.success == true;
  275. this.message = "Новость создана";
  276. setTimeout(() => {
  277. console.log(this.$route);
  278. this.$router.push("/admin/publications");
  279. }, 500);
  280. })
  281. .catch((err) => {
  282. console.dir(err);
  283. this.notificationHandler(err.message, true);
  284. return " ";
  285. });
  286. }
  287. },
  288. previewNews() {
  289. this.$v.$touch();
  290. if (this.$v.$invalid) return;
  291. const path = this.isVideo ? '/preview/video' : '/preview';
  292. let { href } = this.$router.resolve({ path });
  293. this.$axios
  294. .post("admin/news/preview", this.newsItem)
  295. .then((res) => {
  296. let newsPreview = JSON.stringify(res.data.data);
  297. window.localStorage.setItem("preview", newsPreview);
  298. window.open(href, "_blank");
  299. })
  300. .catch((err) => this.notificationHandler(err.message, true));
  301. },
  302. shareNews() {
  303. this.$v.$touch();
  304. if (this.$v.$invalid) return;
  305. this.activity = false;
  306. this.$axios.post("admin/news/create", this.newsItem).then((res) => {
  307. this.previewID = res.data.data.news_id;
  308. const path = this.isVideo ?
  309. `/preview/video?uuid=${this.previewID}` :
  310. `/preview?uuid=${this.previewID}`;
  311. let { href } = this.$router.resolve({ path });
  312. window.open(href, "_blank");
  313. });
  314. },
  315. publishNews() {
  316. this.$v.$touch();
  317. if (this.$v.$invalid) return;
  318. this.$axios.post("admin/news/update", {
  319. ...this.newsItem,
  320. id: this.previewID,
  321. });
  322. -setTimeout(() => {
  323. this.message = "Новость опубликована";
  324. this.$router.push("/admin/publications");
  325. }, 500);
  326. },
  327. getAlias() {
  328. this.$axios
  329. .post("admin/alias/generation", { text: this.title }) // сначала генерируем алиас
  330. .then((res) => {
  331. if (res.status == 200) {
  332. this.generatedAlias = res.data.data.text;
  333. this.$axios // проверяем его доступность, если все ок - отдаем алиас без возможности редактировать
  334. .get(
  335. "admin/alias/check/news/" +
  336. this.generatedAlias +
  337. "/" +
  338. this.category.id
  339. )
  340. .then((res) => {
  341. if (res.success) {
  342. this.alias = this.generatedAlias;
  343. return generatedAlias;
  344. }
  345. })
  346. .catch((err) => {
  347. this.notificationHandler(err.errors.join("* "), true);
  348. return err;
  349. });
  350. }
  351. return res;
  352. });
  353. },
  354. // Файл хэндлер. Для каждого отдельные методы
  355. getFile(type) {
  356. if (type == "main-img") {
  357. return this.file;
  358. } else if (type == "preview-img") {
  359. return this.previewFile;
  360. } else if (type == "storie-img") {
  361. return this.storieFile;
  362. }
  363. },
  364. notificationHandler(message, error) {
  365. this.messageClass = "";
  366. this.message = message;
  367. if (error) {
  368. this.messageClass = "error";
  369. }
  370. const interva = setTimeout(() => {
  371. this.message = "";
  372. }, 2000);
  373. },
  374. fileHandler(type, width, height) {
  375. const fileData = new FormData();
  376. fileData.append("file", this.getFile(type));
  377. console.log(this.getFile(type));
  378. if (type == "main-img" && this.category.alias == "articles") {
  379. fileData.append("destination_width", 1900);
  380. fileData.append("destination_height", 800);
  381. } else {
  382. fileData.append("destination_width", width);
  383. fileData.append("destination_height", height);
  384. }
  385. fileData.append("title", "example");
  386. // TODO - добавить поле для названия фотографии. Создать метод - либо добавлять после создания, либо во время, либо до
  387. this.$axios
  388. .$post("authorized/admin/files/upload/image/news/raw", fileData, {
  389. headers: {
  390. "Content-Type": `multipart/form-data;`,
  391. },
  392. })
  393. .then((res) => {
  394. if (type == "main-img") {
  395. this.img = res.data;
  396. } else if (type == "preview-img") {
  397. this.previewImg = res.data;
  398. } else if (type == "storie-img") {
  399. this.storieImg = res.data;
  400. }
  401. console.log(res, ".THEN");
  402. if (res.success) {
  403. this.notificationHandler("Картинка удачно загрузилась!", false);
  404. } else {
  405. this.notificationHandler(res.errors.join("* "), true);
  406. }
  407. return res;
  408. })
  409. .catch((err) => {
  410. console.log(err, ".CATCH");
  411. this.notificationHandler(err.errors.join("* "), true);
  412. return err;
  413. });
  414. },
  415. getContainer(str) {
  416. this.$axios.get("admin/containers/type_" + str).then((res) => {
  417. if (str == "module") {
  418. this.modules = res.data.data;
  419. return res.data;
  420. } else if (str == "section") {
  421. this.sections = res.data.data;
  422. return res.data;
  423. } else if (str == "slider") {
  424. this.sliders = res.data.data;
  425. return res.data;
  426. }
  427. });
  428. },
  429. async getCategory() {
  430. await this.$axios.$get("/admin/categories").then((res) => {
  431. console.log(res.data);
  432. this.categories = res.data;
  433. return res.data;
  434. });
  435. },
  436. async getAuthors() {
  437. await this.$axios
  438. .$post("authorized/admin/user/profiles/list/by_group", {
  439. groups: [3],
  440. })
  441. .then((res) => {
  442. console.log(res.data);
  443. if (!res.data.users) return [];
  444. this.authors = res.data.users.map((item) => ({
  445. value: item.user_id,
  446. name: item.first_name,
  447. }));
  448. return this.authors;
  449. });
  450. },
  451. url(relative_path) {
  452. let url = "https://api.amic.ru/" + relative_path;
  453. return url;
  454. },
  455. async loadFile(File) {
  456. const fileData = new FormData();
  457. fileData.append("file", File);
  458. fileData.append("title", File.name);
  459. return await this.$axios
  460. .post("authorized/admin/files/upload/document/news", fileData)
  461. .then((res) => res.data.data)
  462. .catch((e) => console.log(e));
  463. },
  464. async deleteFile(file, index) {
  465. this.files.splice(index, 1);
  466. await this.$axios
  467. .get(`authorized/admin/files/delete/image/${file.id}`)
  468. .catch((e) => console.log(e));
  469. },
  470. async loadFiles(files) {
  471. if (!files) return;
  472. const promises = files.map((file) => this.loadFile(file));
  473. const newFiles = await Promise.all(promises);
  474. this.files = [...this.files, ...newFiles];
  475. this.filesModel = null;
  476. },
  477. },
  478. data() {
  479. return {
  480. sliderId: "",
  481. messageClass: "",
  482. menu: "",
  483. message: "",
  484. author: "",
  485. authors: [],
  486. activity: true,
  487. categories: [],
  488. dont_send_to_rss: false,
  489. photoTitle: "",
  490. hash_tags: "",
  491. sorting: null,
  492. showAuthor: true,
  493. allowComments: true,
  494. advertisement: false,
  495. partner: false,
  496. contraindications: false,
  497. propertiesList: [],
  498. modules: [],
  499. sliders: [],
  500. sections: [],
  501. metaDescription: "",
  502. sectionId: "",
  503. moduleId: "",
  504. title: "",
  505. description: "",
  506. category: null,
  507. metaKeys: " ",
  508. showInStories: false,
  509. storieImg: {},
  510. img: {},
  511. source: "",
  512. previewImg: {},
  513. previewFile: null,
  514. storieFile: null,
  515. lightning: false,
  516. file: null,
  517. active: true,
  518. alias: "",
  519. verified: false,
  520. generatedAlias: "",
  521. startActivity: {
  522. date: "",
  523. time: "00:00:00",
  524. },
  525. previewID: null,
  526. content: "",
  527. video: "",
  528. externalLink: "",
  529. files: [],
  530. filesModel: null,
  531. };
  532. },
  533. validations: {
  534. category: {
  535. required,
  536. },
  537. generatedAlias: {
  538. required,
  539. },
  540. start_activity: {
  541. required,
  542. },
  543. content: {
  544. minLength: minLength(10),
  545. },
  546. description: {
  547. required,
  548. },
  549. title: { required },
  550. },
  551. layout: "admin",
  552. mounted() {
  553. this.$axios
  554. .get("admin/properties", {
  555. headers: { AccessToken: this.$auth.strategy.token.get() },
  556. })
  557. .then((res) => (this.propertiesList = res.data.data));
  558. window.addEventListener("beforeunload", (event) => {
  559. event.preventDefault();
  560. event.returnValue = "";
  561. });
  562. this.getCategory();
  563. this.getAuthors();
  564. },
  565. computed: {
  566. isVideo() {
  567. if(!this.category) return false;
  568. const parentCategory = this.category.parent_for_admin;
  569. return parentCategory && parentCategory.alias == 'video';
  570. },
  571. start_activity() {
  572. return this.startActivity.date + " " + this.startActivity.time;
  573. },
  574. properties() {
  575. const entries = this.propertiesList.map((p) => [p.id, this[p.code]]);
  576. return Object.fromEntries(entries);
  577. },
  578. newsItem() {
  579. return {
  580. author: this.author,
  581. activity: this.activity,
  582. dont_send_to_rss: this.dont_send_to_rss,
  583. hash_tags: this.hash_tags,
  584. title: this.title,
  585. text: this.content,
  586. alias: this.generatedAlias,
  587. description: this.description,
  588. news_category_id: this.category.id,
  589. start_activity: this.start_activity,
  590. detail_image_id: this.img.image_id ? this.img.image_id : "",
  591. preview_image_id: this.previewImg.image_id
  592. ? this.previewImg.image_id
  593. : "",
  594. source: this.source,
  595. slider_id: this.sliderId.length ? this.sliderId : "",
  596. section_id: this.sectionId.id ? this.sectionId.id : "",
  597. meta_description: this.metaDescription,
  598. show_in_stories: this.showInStories,
  599. module_id: this.moduleId.id ? this.moduleId.id : "",
  600. meta_key: this.metaKeys,
  601. allow_comments: this.allowComments,
  602. show_author: this.showAuthor,
  603. sorting: this.sorting,
  604. lightning: this.lightning,
  605. story_file_id: this.storieImg ? this.storieImg.id : "",
  606. video: this.video,
  607. link: this.externalLink,
  608. properties: this.properties,
  609. verified: this.verified,
  610. files: this.files.map((file) => file.id),
  611. };
  612. },
  613. treeCategories() {
  614. if (!this.categories) return [];
  615. return nest(this.categories);
  616. },
  617. authorsList() {
  618. return this.authors;
  619. },
  620. },
  621. async beforeRouteLeave (to, from, next) {
  622. const out = window.confirm("Вы уверены, что хотите покинуть страницу?");
  623. if(!out) return;
  624. next();
  625. },
  626. };
  627. </script>
  628. <style lang="less">
  629. .tree-select {
  630. margin-bottom: 15px;
  631. }
  632. .v-application .error {
  633. margin-top: -5px;
  634. opacity: 0.8;
  635. background: transparent !important;
  636. }
  637. .green {
  638. border: 1px solid green;
  639. }
  640. input {
  641. background: none !important;
  642. border: none !important;
  643. }
  644. .message {
  645. width: 400px;
  646. height: 100px;
  647. text-align: center;
  648. display: flex;
  649. align-items: center;
  650. justify-content: center;
  651. box-shadow: 1px 1px 1px 1px;
  652. background: white;
  653. &-box {
  654. display: flex;
  655. flex-direction: column;
  656. align-items: flex-end;
  657. right: 20px;
  658. bottom: 20px;
  659. position: fixed;
  660. justify-content: flex-end;
  661. }
  662. }
  663. </style>