Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

646 rindas
19 KiB

  1. <template>
  2. <Layout>
  3. <BRow>
  4. <BCol sm="12">
  5. <BCard no-body>
  6. <BCardHeader>
  7. <h5>ویرایش بلاگ</h5>
  8. </BCardHeader>
  9. <BCardBody>
  10. <BTabs>
  11. <BTab title="عمومی">
  12. <BRow class="mt-4">
  13. <BCol md="6">
  14. <div class="form-group">
  15. <label class="form-label">تصویر بلاگ</label>
  16. <input
  17. type="file"
  18. accept="image/*"
  19. @change="handleImageChange"
  20. class="form-control"
  21. :class="{ 'is-invalid': errors.image }"
  22. />
  23. <div v-if="imagePreview" class="mt-2">
  24. <img
  25. :src="imagePreview"
  26. alt="Image Preview"
  27. class="img-fluid rounded shadow-sm Image-Preview"
  28. />
  29. </div>
  30. <small v-if="errors.image" class="text-danger">
  31. {{ errors.image }}
  32. </small>
  33. </div>
  34. </BCol>
  35. <BCol md="6">
  36. <div class="form-group">
  37. <label class="form-label">دسته</label>
  38. <VueSelect
  39. style="--vs-min-height: 48px; --vs-border-radius: 8px"
  40. :isLoading="categorySelectorLoader"
  41. v-model="blogCat"
  42. :options="formattedCategories"
  43. :reduce="(option) => option.value"
  44. placeholder="دسته ای را انتخاب نمایید"
  45. @search="handleSearch"
  46. />
  47. </div>
  48. <small v-if="errors.blogCat" class="text-danger">
  49. {{ errors.blogCat }}
  50. </small>
  51. </BCol>
  52. <button
  53. :disabled="loadingFirstTab"
  54. @click="handlerSubmitCategory"
  55. class="btn rounded btn-primary w-auto mt-5 d-flex justify-content-center align-items-center mx-auto"
  56. >
  57. <span
  58. v-if="loadingFirstTab"
  59. class="spinner-border spinner-border-sm "
  60. role="status"
  61. />
  62. ذخیره
  63. </button>
  64. </BRow>
  65. </BTab>
  66. <BTab title="ترجمه ها">
  67. <BButton
  68. :disabled="!findLocaleTranslation"
  69. :loading="loadingDelete"
  70. @click="handlerRemoveTranslation"
  71. class="btn btn-sm rounded btn-danger d-block mt-5"
  72. style="margin-right: auto"
  73. >
  74. حذف ترجمه {{ findLocaleTranslation }}
  75. </BButton>
  76. <BRow class="g-3 mt-4">
  77. <BCol lg="6">
  78. <div class="form-group">
  79. <label class="form-label">انتخاب زبان</label>
  80. <select
  81. v-model="locale"
  82. class="form-control"
  83. placeholder="انتخاب کنید"
  84. @change="handlerChangeLocale"
  85. >
  86. <option
  87. key="fa"
  88. value="fa"
  89. >
  90. فارسی
  91. </option>
  92. <option
  93. key="en"
  94. value="en"
  95. >
  96. انگلیسی
  97. </option>
  98. <option
  99. key="ar"
  100. value="ar"
  101. >
  102. عربی
  103. </option>
  104. </select>
  105. </div>
  106. </BCol>
  107. <BCol md="6">
  108. <div class="form-group">
  109. <label class="form-label">عنوان بلاگ</label>
  110. <input
  111. type="text"
  112. v-model="title"
  113. class="form-control"
  114. placeholder="عنوان بلاگ"
  115. :class="{ 'is-invalid': errors.title }"
  116. @input="clearError('title')"
  117. />
  118. </div>
  119. <small v-if="errors.title" class="text-danger">
  120. {{ errors.title }}
  121. </small>
  122. </BCol>
  123. <BCol md="6">
  124. <div class="form-group">
  125. <label class="form-label">کلمه کلیدی</label>
  126. <input
  127. type="text"
  128. v-model="slug"
  129. class="form-control"
  130. placeholder="کلمه کلیدی بلاگ"
  131. :class="{ 'is-invalid': errors.slug }"
  132. @input="clearError('slug')"
  133. />
  134. </div>
  135. <small v-if="errors.slug" class="text-danger">
  136. {{ errors.slug }}
  137. </small>
  138. </BCol>
  139. <BCol md="6">
  140. <div class="form-group">
  141. <label class="form-label">خلاصه</label>
  142. <textarea
  143. v-model="summary"
  144. class="form-control"
  145. placeholder="خلاصه ای از بلاگ"
  146. :class="{ 'is-invalid': errors.summary }"
  147. @input="clearError('summary')"
  148. />
  149. </div>
  150. <small v-if="errors.summary" class="text-danger">
  151. {{ errors.summary }}
  152. </small>
  153. </BCol>
  154. <BCol md="6">
  155. <div class="form-group">
  156. <label class="form-label">نویسنده</label>
  157. <input
  158. type="text"
  159. v-model="author"
  160. class="form-control"
  161. placeholder="نویسنده"
  162. :class="{ 'is-invalid': errors.author }"
  163. @input="clearError('author')"
  164. />
  165. </div>
  166. <small v-if="errors.author" class="text-danger">
  167. {{ errors.author }}
  168. </small>
  169. </BCol>
  170. <BCol md="12">
  171. <div class="form-group">
  172. <label class="form-label">محتوا</label>
  173. <div
  174. @input="clearError('editorContent')"
  175. ref="editor"
  176. class="quill-editor"
  177. ></div>
  178. </div>
  179. <small v-if="errors.editorContent" class="text-danger">
  180. {{ errors.editorContent }}
  181. </small>
  182. </BCol>
  183. </BRow>
  184. <div class="d-flex justify-content-center">
  185. <button
  186. type="submit"
  187. class="btn btn-primary mt-5"
  188. @click.prevent="submitForm"
  189. :disabled="loading"
  190. >
  191. <span v-if="loading">
  192. <i class="fa fa-spinner fa-spin"></i> بارگذاری...
  193. </span>
  194. <span v-else>ویرایش</span>
  195. </button>
  196. </div>
  197. </BTab>
  198. </BTabs>
  199. </BCardBody>
  200. </BCard>
  201. </BCol>
  202. </BRow>
  203. </Layout>
  204. </template>
  205. <script>
  206. import VueSelect from "vue3-select-component";
  207. import { toast } from "vue3-toastify";
  208. import "vue3-toastify/dist/index.css";
  209. import ApiServiece from "@/services/ApiService";
  210. import { ref, onMounted, computed } from "vue";
  211. import Layout from "@/layout/custom.vue";
  212. import Quill from "quill";
  213. import "quill/dist/quill.snow.css";
  214. import { useRoute } from "vue-router";
  215. import {BTabs} from "bootstrap-vue-next";
  216. export default {
  217. name: "SAMPLE-PAGE",
  218. components: {
  219. BTabs,
  220. Layout,
  221. VueSelect,
  222. },
  223. setup() {
  224. const quillInstance = ref(null);
  225. const blog = ref();
  226. const route = useRoute();
  227. const loading = ref(false);
  228. const image = ref();
  229. const imagePreview = ref();
  230. const errors = ref({});
  231. const title = ref("");
  232. const slug = ref("");
  233. const summary = ref("");
  234. const blogCat = ref();
  235. const author = ref("");
  236. const editor = ref(null);
  237. const categorySelectorLoader = ref(false);
  238. const categories = ref([{ value: null, label: null }]);
  239. const editorContent = ref("");
  240. const locale = ref("fa");
  241. const blogTranslationId = ref();
  242. const loadingFirstTab = ref(false);
  243. const loadingDelete = ref(false);
  244. const formattedCategories = computed(() =>
  245. Array.isArray(categories.value)
  246. ? categories.value.map((category) => ({
  247. value: category?.translation?.id,
  248. label: category?.translation?.title,
  249. }))
  250. : []
  251. );
  252. const findLocaleTranslation = computed(() => {
  253. const foundTranslation = blog.value?.translations?.find(
  254. item => item?.locale === locale.value
  255. );
  256. if (foundTranslation) {
  257. switch (foundTranslation?.locale) {
  258. case "en":
  259. return "انگلیسی";
  260. case "fa":
  261. return "فارسی";
  262. case "ar":
  263. return "عربی";
  264. default:
  265. return null;
  266. }
  267. }
  268. return null;
  269. });
  270. const handleSearch = async (searchTerm) => {
  271. if (searchTerm?.length < 3) return;
  272. categorySelectorLoader.value = true;
  273. try {
  274. const response = await ApiServiece.get(
  275. `admin/blog-categories?title=${searchTerm ?? ''}`
  276. );
  277. categories.value = response.data.data;
  278. categorySelectorLoader.value = false;
  279. } catch (error) {
  280. categorySelectorLoader.value = false;
  281. categories.value = [];
  282. }
  283. };
  284. const handleImageChange = (event) => {
  285. const file = event.target.files[0];
  286. if (file) {
  287. errors.value.image = null;
  288. image.value = file;
  289. const reader = new FileReader();
  290. reader.onload = () => {
  291. imagePreview.value = reader.result;
  292. };
  293. reader.readAsDataURL(file);
  294. }
  295. };
  296. const validateForm = () => {
  297. errors.value = {};
  298. if (!title.value) errors.value.title = "وارد کردن عنوان بلاگ الزامی است";
  299. if (!slug.value)
  300. errors.value.slug = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد";
  301. if (!summary.value)
  302. errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد";
  303. if (!blogCat.value)
  304. errors.value.blogCat = "انتخاب دسته برای بلاگ ضروری می باشد";
  305. if (!author.value)
  306. errors.value.author = "وارد کردن نویسنده بلاگ ضروری می باشد";
  307. if (!editorContent.value)
  308. errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد";
  309. if (!imagePreview.value)
  310. errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد";
  311. return Object.keys(errors.value)?.length === 0;
  312. };
  313. const clearError = (field) => {
  314. errors.value[field] = "";
  315. };
  316. const getBlog = () => {
  317. ApiServiece.get(`admin/blogs/${route.params.id}`)
  318. .then((resp) => {
  319. blog.value = resp.data.data;
  320. categories.value[0].value = blog.value?.blog_category_id;
  321. categories.value[0].lable = blog.value?.blog_category?.title;
  322. title.value = blog.value?.translation?.title;
  323. slug.value = blog.value?.translation?.slug;
  324. summary.value = blog.value?.translation?.summary;
  325. imagePreview.value = blog.value?.image;
  326. blogCat.value = blog.value?.blog_category_id;
  327. author.value = blog.value?.translation?.author;
  328. locale.value = blog.value?.translation?.locale;
  329. if (editor.value) {
  330. quillInstance.value = new Quill(editor.value, {
  331. theme: "snow",
  332. modules: {
  333. toolbar: [
  334. [{ header: "1" }, { header: "2" }, { font: [] }],
  335. [{ list: "ordered" }, { list: "bullet" }],
  336. [{ align: [] }],
  337. ["bold", "italic", "underline"],
  338. ["link", "image"],
  339. [{ script: "sub" }, { script: "super" }],
  340. [{ direction: "rtl" }],
  341. ],
  342. },
  343. });
  344. quillInstance.value.root.innerHTML = blog.value?.translation?.content;
  345. editorContent.value = blog.value?.translation?.content;
  346. // ✨ Update content on change
  347. quillInstance.value.on("text-change", () => {
  348. editorContent.value = quillInstance.value.root.innerHTML;
  349. });
  350. }
  351. })
  352. .catch((err) => {
  353. console.log(err);
  354. });
  355. };
  356. onMounted( () => {
  357. getBlog();
  358. handleSearch()
  359. });
  360. const submitForm = () => {
  361. if (!validateForm()) {
  362. toast.error("لطفا فیلد های لازم را وارد نمایید", {
  363. position: "top-right",
  364. autoClose: 1000,
  365. });
  366. return;
  367. }
  368. loading.value = true;
  369. const params = {
  370. title: title.value,
  371. slug: slug.value,
  372. content: editorContent.value,
  373. author: author.value,
  374. summary: summary.value,
  375. locale: locale.value,
  376. };
  377. const existingTranslation = blog.value?.translations?.find(
  378. t => t.locale === locale.value
  379. );
  380. const url = existingTranslation ? `admin/blogs/${route.params.id}/translations/${existingTranslation?.id}` : `admin/blogs/${route.params.id}/translations`
  381. ApiServiece[existingTranslation ? 'put' : 'post'](url, params, {
  382. headers: {
  383. Authorization: `Bearer ${localStorage.getItem("token")}`,
  384. },
  385. })
  386. .then((resp) => {
  387. const updatedCategory = blog.value;
  388. updatedCategory.translations = resp?.data?.data?.translations
  389. blog.value = updatedCategory;
  390. toast.success(resp?.data?.message, {
  391. position: "top-right",
  392. autoClose: 1000,
  393. });
  394. })
  395. .catch((error) => {
  396. toast.error(error?.response?.data?.message, {
  397. position: "top-right",
  398. autoClose: 1000,
  399. })
  400. })
  401. .finally(() => {
  402. loading.value = false;
  403. });
  404. };
  405. const handlerChangeLocale = (e) => {
  406. const findLocale = blog.value?.translations?.find(item => item?.locale === e.target.value);
  407. if (findLocale) {
  408. title.value = findLocale?.title;
  409. slug.value = findLocale?.slug;
  410. summary.value = findLocale?.summary;
  411. author.value = findLocale?.author;
  412. locale.value = findLocale?.locale;
  413. blogTranslationId.value = findLocale?.id
  414. quillInstance.value.root.innerHTML = findLocale?.content
  415. } else {
  416. title.value = undefined;
  417. slug.value = undefined;
  418. summary.value = undefined;
  419. author.value = undefined;
  420. quillInstance.value.root.innerHTML = undefined
  421. }
  422. }
  423. const handlerSubmitCategory = async () => {
  424. try {
  425. loadingFirstTab.value = true;
  426. const formData = new FormData();
  427. if (image.value) {
  428. formData.append("image", image.value);
  429. }
  430. formData.append("blog_categories", blogCat.value);
  431. formData.append("_method", 'put');
  432. const { data: { success, message } } = await ApiServiece.post(
  433. `admin/blogs/${blog.value?.id}`, formData, {
  434. headers: {
  435. "content-type": "multipart",
  436. Authorization: `Bearer ${localStorage.getItem("token")}`,
  437. },
  438. });
  439. if(success) {
  440. /* categoryId.value = data?.id
  441. emit("cat-updated")*/
  442. toast.success(message)
  443. }
  444. } catch (e) {
  445. toast.error(e?.response?.data?.message)
  446. } finally {
  447. loadingFirstTab.value = false;
  448. }
  449. };
  450. const handlerRemoveTranslation = async () => {
  451. const findLocale = blog.value?.translations?.find(item => item?.locale === locale.value);
  452. if (findLocale) {
  453. try {
  454. loadingDelete.value = true;
  455. const { data: { success, message, data } } = await ApiServiece.delete(
  456. `admin/blogs/${blog.value?.id}/translations/${findLocale?.id}`
  457. )
  458. if (success) {
  459. const updatedCategory = blog.value
  460. updatedCategory.translations = data?.translations
  461. blog.value = updatedCategory
  462. toast.success(message)
  463. }
  464. } catch (e) {
  465. console.log(e)
  466. }finally {
  467. loadingDelete.value = false
  468. }
  469. }
  470. }
  471. return {
  472. title,
  473. slug,
  474. summary,
  475. editor,
  476. categories,
  477. errors,
  478. image,
  479. imagePreview,
  480. handleImageChange,
  481. formattedCategories,
  482. submitForm,
  483. clearError,
  484. editorContent,
  485. author,
  486. blogCat,
  487. loading,
  488. loadingFirstTab,
  489. handleSearch,
  490. categorySelectorLoader,
  491. locale,
  492. handlerChangeLocale,
  493. blogTranslationId,
  494. handlerSubmitCategory,
  495. findLocaleTranslation,
  496. handlerRemoveTranslation,
  497. loadingDelete,
  498. };
  499. },
  500. };
  501. </script>
  502. <style scoped>
  503. .quill-editor {
  504. height: 300px;
  505. border: 1px solid #ccc;
  506. border-radius: 5px;
  507. }
  508. .quill-editor .ql-container {
  509. font-family: "Vazir", "Arial", sans-serif;
  510. font-size: 14px;
  511. }
  512. .quill-editor .ql-editor {
  513. padding: 10px;
  514. text-align: right;
  515. }
  516. .ql-editor {
  517. direction: rtl;
  518. text-align: right;
  519. }
  520. .ql-editor::before {
  521. content: attr(placeholder);
  522. direction: rtl !important;
  523. text-align: right;
  524. }
  525. .Image-Preview {
  526. min-width: 200px;
  527. max-height: 200px;
  528. max-width: 200px;
  529. object-fit: cover;
  530. border-radius: 8px;
  531. border: 1px solid #ddd;
  532. }
  533. .delete-btn {
  534. display: flex;
  535. align-items: center;
  536. padding: 3px;
  537. font-size: 10px;
  538. background-color: #e74c3c;
  539. color: white;
  540. border: none;
  541. border-radius: 5px;
  542. cursor: pointer;
  543. transition: background-color 0.3s ease, transform 0.2s ease;
  544. gap: 8px;
  545. margin-right: 100px;
  546. }
  547. .delete-btn:hover {
  548. background-color: #c0392b;
  549. transform: scale(1.05);
  550. }
  551. .delete-btn:active {
  552. background-color: #a93226;
  553. }
  554. .delete-btn:focus {
  555. outline: none;
  556. }
  557. </style>