ali 2 лет назад
Родитель
Сommit
0fdfad4cff
31 измененных файлов с 1499 добавлено и 48 удалено
  1. 4 0
      src/api/APIGateway.js
  2. 64 0
      src/api/models/ContentTopic.js
  3. 57 0
      src/api/models/ContentTopicCategory.js
  4. 1 0
      src/api/models/MediaHashtag.js
  5. 25 5
      src/components/FormBuilderCustumComponents/ContentCategorySelector.vue
  6. 85 0
      src/components/FormBuilderCustumComponents/Hashtags/HashtagItem.vue
  7. 133 0
      src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue
  8. 7 0
      src/components/Template/SideBar/AdminPanelSideBar.vue
  9. 61 29
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  10. 110 0
      src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue
  11. 21 0
      src/components/Widgets/Content/AdminContentTopicCreate/OptionPanel.vue
  12. 137 0
      src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue
  13. 21 0
      src/components/Widgets/Content/AdminContentTopicList/OptionPanel.vue
  14. 112 0
      src/components/Widgets/Content/AdminContentTopicShow/AdminContentTopicShow.vue
  15. 21 0
      src/components/Widgets/Content/AdminContentTopicShow/OptionPanel.vue
  16. 2 13
      src/components/Widgets/ContentCategory/AdminContentCategoryCreate/AdminContentCategoryCreate.vue
  17. 109 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryCreate/AdminContentTopicCategoryCreate.vue
  18. 21 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryCreate/OptionPanel.vue
  19. 148 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryList/AdminContentTopicCategoryList.vue
  20. 21 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryList/OptionPanel.vue
  21. 143 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryShow/AdminContentTopicCategoryShow.vue
  22. 21 0
      src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryShow/OptionPanel.vue
  23. 1 1
      src/components/Widgets/Store/StorePackage/AdminStorePackageList/AdminStorePackageList.vue
  24. 1 0
      src/css/Theme/Controls/select.scss
  25. 40 0
      src/models/ContentTopic.js
  26. 23 0
      src/models/ContentTopicCategory.js
  27. 23 0
      src/pages/Admin/ContentTopic/Show.vue
  28. 23 0
      src/pages/Admin/ContentTopicCategory/Create.vue
  29. 23 0
      src/pages/Admin/ContentTopicCategory/List.vue
  30. 23 0
      src/pages/Admin/ContentTopicCategory/Show.vue
  31. 18 0
      src/router/routes/Admin.js

+ 4 - 0
src/api/APIGateway.js

@@ -34,6 +34,7 @@ import PostCategoryAPI from 'src/api/models/PostCategory.js'
 import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
 import RegistrationAPI from 'src/api/models/Registration.js'
 import UnitCategoryAPI from 'src/api/models/UnitCategory.js'
+import ContentTopicAPI from 'src/api/models/ContentTopic.js'
 import StoreCategoryAPI from 'src/api/models/StoreCategory.js'
 import MediaCategoryAPI from 'src/api/models/MediaCategory.js'
 import NoticeReceiverAPI from 'src/api/models/NoticeReceiver.js'
@@ -48,6 +49,7 @@ import ClassroomLeadersAPI from 'src/api/models/ClassroomLeaders.js'
 import DonationCategoryApi from 'src/api/models/DonationCategory.js'
 import TeamRegistrationsAPI from 'src/api/models/TeamRegistrations.js'
 import EventRegistrationApi from 'src/api/models/EventRegistration.js'
+import ContentTopicCategoryAPI from 'src/api/models/ContentTopicCategory.js'
 import ClassroomRegistrationAPI from 'src/api/models/ClassroomRegistration.js'
 import ProjectAttendanceSheetsAPI from 'src/api/models/ProjectAttendanceSheets.js'
 import SessionAttendanceSheetsAPI from 'src/api/models/SessionAttendanceSheets.js'
@@ -89,6 +91,7 @@ export const APIGateway = {
   registration: new RegistrationAPI(),
   postCategory: new PostCategoryAPI(),
   unitCategory: new UnitCategoryAPI(),
+  contentTopic: new ContentTopicAPI(),
   mediaHashtag: new MediaHashtagAPI(),
   storeCategory: new StoreCategoryAPI(),
   mediaCategory: new MediaCategoryAPI(),
@@ -104,6 +107,7 @@ export const APIGateway = {
   donationCategory: new DonationCategoryApi(),
   teamRegistrations: new TeamRegistrationsAPI(),
   eventRegistration: new EventRegistrationApi(),
+  contentTopicCategory: new ContentTopicCategoryAPI(),
   classroomRegistration: new ClassroomRegistrationAPI(),
   projectAttendanceSheets: new ProjectAttendanceSheetsAPI(),
   sessionAttendanceSheets: new SessionAttendanceSheetsAPI()

+ 64 - 0
src/api/models/ContentTopic.js

@@ -0,0 +1,64 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { ContentTopic, ContentTopicList } from 'src/models/ContentTopic.js'
+
+export default class ContentTopicAPI extends APIRepository {
+  constructor() {
+    super('content-topic', appApi)
+    this.APIAdresses = {
+      base: '/cma/topics',
+      byId: (id) => '/cma/topics/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new ContentTopic(response.data) },
+      post: (response) => { return new ContentTopic(response.data) },
+      put: (response) => { return new ContentTopic(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        content: null, // Number
+        creator: null, // Number
+        category: null, // Number
+        tile: null, // String
+        category__parent__parent: null, // Number
+        category__in: null, // Number[]
+        hashtags__id__in: null, // Number[]
+        hashtags__in: null, // Number[]
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ContentTopicList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 57 - 0
src/api/models/ContentTopicCategory.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { ContentTopicCategory, ContentTopicCategoryList } from 'src/models/ContentTopicCategory.js'
+
+export default class ContentTopicCategoryAPI extends APIRepository {
+  constructor() {
+    super('content-topic-category', appApi)
+    this.APIAdresses = {
+      base: '/cma/topics/categories',
+      byId: (id) => '/cma/topics/categories/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new ContentTopicCategory(response.data) },
+      post: (response) => { return new ContentTopicCategory(response.data) },
+      put: (response) => { return new ContentTopicCategory(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        parent__isnull: null, // String (true, false)
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new ContentTopicCategoryList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 1 - 0
src/api/models/MediaHashtag.js

@@ -27,6 +27,7 @@ export default class MediaHashtagAPI extends APIRepository {
       api: this.api,
       request: this.APIAdresses.base,
       data: this.getNormalizedSendData({
+        search: null, // String
         page: 1 // Number
       }, data),
       resolveCallback: (response) => {

+ 25 - 5
src/components/FormBuilderCustumComponents/ContentCategorySelector.vue

@@ -4,6 +4,9 @@
       <span v-if="categoryType === 'content'">
         انتخاب دسته محتوا
       </span>
+      <span v-if="categoryType === 'contentTopic'">
+        انتخاب دسته موضوع
+      </span>
       <span v-else-if="categoryType === 'store'">
         انتخاب دسته فروشگاه
       </span>
@@ -44,6 +47,7 @@
 import { APIGateway } from 'src/api/APIGateway.js'
 import { StoreCategoryList } from 'src/models/StoreCategory.js'
 import { ContentCategoryList } from 'src/models/ContentCategory.js'
+import { ContentTopicCategoryList } from 'src/models/ContentTopicCategory.js'
 
 export default {
   name: 'ContentCategorySelector',
@@ -58,11 +62,18 @@ export default {
     },
     categoryType: {
       type: String,
-      default: 'content'
+      default: 'content' // 'content', 'contentTopic', 'store'
     }
   },
   emits: ['update:value'],
   data () {
+    let categories = new ContentCategoryList()
+    if (this.categoryType === 'store') {
+      categories = new StoreCategoryList()
+    }
+    if (this.categoryType === 'contentTopic') {
+      categories = new ContentTopicCategoryList()
+    }
     return {
       mainCategory: null,
       subCategory: null,
@@ -70,7 +81,7 @@ export default {
       mainCategoryOptions: [],
       subCategoryOptions: [],
       bakhshCategoryOptions: [],
-      categories: this.categoryType === 'content' ? new ContentCategoryList() : new StoreCategoryList()
+      categories
     }
   },
   watch: {
@@ -140,14 +151,23 @@ export default {
     },
     getCategories () {
       this.categories.laoding = true
-      const apiGateway = this.categoryType === 'content' ? APIGateway.contentCategory.index({ parent__isnull: 'true', per_page: 99999 })
-        : APIGateway.storeCategory.index({ parent__isnull: 'true', per_page: 99999 })
+      const args = { parent__isnull: 'true', per_page: 99999 }
+
+      let apiGateway = APIGateway.contentCategory.index(args)
+      if (this.categoryType === 'store') {
+        apiGateway = APIGateway.storeCategory.index(args)
+      }
+      if (this.categoryType === 'contentTopic') {
+        apiGateway = APIGateway.contentTopicCategory.index(args)
+      }
 
       apiGateway
         .then(categories => {
           if (this.categoryType === 'content') {
             this.categories = new ContentCategoryList(categories.list)
-          } else {
+          } else if (this.categoryType === 'contentTopic') {
+            this.categories = new ContentTopicCategoryList(categories.list)
+          } else if (this.categoryType === 'store') {
             this.categories = new StoreCategoryList(categories.list)
           }
 

+ 85 - 0
src/components/FormBuilderCustumComponents/Hashtags/HashtagItem.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="HashtagItam">
+    <div class="title ellipsis">
+      {{ hashtag.title }}
+    </div>
+    <div class="actions">
+      <q-btn v-if="canDelete"
+             class="deleteBtn"
+             icon="clear"
+             outline
+             dense
+             round
+             color="red"
+             size="sm"
+             @click="onDelete" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HashtagItam',
+  props: {
+    hashtag: {
+      type: Object,
+      default: () => {
+        return {
+          title: ''
+        }
+      }
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:value', 'onView', 'onDelete'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView () {
+      this.$emit('onView')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.HashtagItam {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 50px;
+  padding: 8px 12px 8px 12px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    font-size: 15px;
+    letter-spacing: 0;
+    color: #212121;
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+      width: 28px;
+      height: 28px;
+      border-radius: 100%;
+      &.viewBtn {
+      }
+      &.deleteBtn {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+</style>

+ 133 - 0
src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="ContentMedias">
+    <q-select v-model="selectedHashtags"
+              use-input
+              hide-selected
+              map-options
+              multiple
+              option-value="id"
+              option-label="title"
+              input-debounce="300"
+              label="کلیدواژه"
+              :loading="fetchLoading"
+              :options="filteredHashtags"
+              @update:model-value="onChangeSelected"
+              @filter="filterFn">
+      <template v-slot:no-option>
+        <span v-if="fetchLoading">
+          کمی صبر کنید...
+        </span>
+        <span v-else>
+          موردی یافت نشد
+        </span>
+      </template>
+    </q-select>
+    <div class="selected-hastags">
+      <hashtag-item v-for="(hashtag, hashtagIndex) in selectedHashtags"
+                    :key="hashtagIndex"
+                    :hashtag="hashtag"
+                    :can-delete="!readonly"
+                    @onDelete="onDeleteHashtag(hashtagIndex)" />
+    </div>
+  </div>
+</template>
+
+<script>
+import HashtagItem from './HashtagItem.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'HashtagSelector',
+  components: { HashtagItem },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      selectedHashtags: [],
+      filteredHashtags: [],
+      fetchLoading: false
+    }
+  },
+  watch: {
+    value: {
+      handler () {
+        if (this.value && this.value.length > 0 && this.value[0].id) {
+          this.selectedHashtags = this.value
+          this.emitUpdateValue()
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    onChangeSelected () {
+      this.emitUpdateValue()
+    },
+    getHashtags (search) {
+      return new Promise((resolve, reject) => {
+        APIGateway.mediaHashtag.index({ search })
+          .then((hashtagList) => {
+            resolve(hashtagList.list.list)
+          })
+          .catch(() => {
+            reject()
+          })
+      })
+    },
+    filterFn (val, update) {
+      if (!val) {
+        this.filteredHashtags = []
+        return
+      }
+
+      update(() => {
+        this.fetchLoading = true
+        this.getHashtags(val)
+          .then((hashtagList) => {
+            this.filteredHashtags = hashtagList
+          })
+          .catch(() => {
+            this.filteredHashtags = []
+          })
+          .finally(() => {
+            this.fetchLoading = false
+          })
+      })
+    },
+    onDeleteHashtag(hashtagIndex) {
+      this.selectedHashtags.splice(hashtagIndex, 1)
+      this.emitUpdateValue()
+    },
+    emitUpdateValue () {
+      const selectedIds = this.selectedHashtags.map(item => item.id)
+      console.log('selectedIds', selectedIds)
+      this.$emit('update:value', selectedIds)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ContentMedias {
+  width: 100%;
+  .selected-hastags {
+    margin-top: 8px;
+    display: flex;
+    justify-content: flex-start;
+    gap: 8px;
+  }
+}
+</style>

+ 7 - 0
src/components/Template/SideBar/AdminPanelSideBar.vue

@@ -139,6 +139,13 @@ export default {
               show: true,
               active: false
             },
+            {
+              title: 'دسته بندی موضوعی',
+              roles: ['superuser'],
+              routeName: 'Admin.ContentTopicCategory.List',
+              show: true,
+              active: false
+            },
             {
               title: 'درخواست ها',
               roles: ['superuser'],

+ 61 - 29
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -9,21 +9,44 @@
         >
       </q-btn>
     </div>
-    <entity-edit v-if="mounted"
-                 ref="entityEdit"
-                 v-model:value="inputs"
-                 title="اطلاعات محتوا"
-                 :api="api"
-                 :entity-id-key="entityIdKey"
-                 :entity-param-key="entityParamKey"
-                 :show-route-name="showRouteName"
-                 :show-close-button="false"
-                 :show-edit-button="false"
-                 :show-expand-button="false"
-                 :show-save-button="false"
-                 :show-reload-button="false"
-                 :redirect-after-edit="false"
-                 :after-load-input-data="afterLoadInputData" />
+    <q-card flat>
+      <q-tabs v-model="tab"
+              align="left">
+        <q-tab name="content"
+               label="جزییات محتوا" />
+        <q-tab name="contentTag"
+               label="فیش برداری" />
+      </q-tabs>
+    </q-card>
+    <q-tab-panels v-if="mounted"
+                  v-model="tab">
+      <q-tab-panel name="content"
+                   class="q-pa-none">
+        <entity-edit ref="entityEdit"
+                     v-model:value="inputs"
+                     title="اطلاعات محتوا"
+                     :api="api"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-route-name="showRouteName"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false"
+                     :redirect-after-edit="false"
+                     :after-load-input-data="afterLoadInputData" />
+      </q-tab-panel>
+      <q-tab-panel name="contentTag"
+                   class="q-pa-none">
+        <admin-content-topic-create :content="contentId"
+                                    :redirect="false"
+                                    @created="onTopicCreated" />
+        <q-separator class="q-my-md" />
+        <admin-content-topic-list :key="topicListKey"
+                                  :content-id="contentId" />
+      </q-tab-panel>
+    </q-tab-panels>
   </div>
 </template>
 
@@ -35,6 +58,8 @@ import { APIGateway } from 'src/api/APIGateway.js'
 import BtnControl from 'src/components/Control/btn.vue'
 import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
 import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+import AdminContentTopicList from 'src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue'
+import AdminContentTopicCreate from 'src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
 const ContentMediasComp = shallowRef(ContentMedias)
@@ -43,14 +68,18 @@ const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
 export default {
   name: 'AdminContentShow',
   components: {
-    EntityEdit
+    EntityEdit,
+    AdminContentTopicList,
+    AdminContentTopicCreate
   },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
       mounted: false,
       entityLoading: true,
-      api: null,
+      topicListKey: Date.now(),
+      api: APIGateway.content.APIAdresses.byId(this.$route.params.id),
+      tab: 'content',
       entityIdKey: 'id',
       entityParamKey: 'id',
       showRouteName: 'Admin.Content.Show',
@@ -61,25 +90,19 @@ export default {
         { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
         { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: () => {}, col: 'col-md-6' }
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: this.edit, col: 'col-md-6' }
       ]
     }
   },
-  created() {
-    this.api = APIGateway.content.APIAdresses.byId(this.$route.params.id)
+  computed: {
+    contentId () {
+      return parseInt(this.$route.params.id)
+    }
   },
   mounted() {
-    this.setActionBtn()
     this.mounted = true
   },
   methods: {
-    setActionBtn () {
-      this.inputs.forEach((item, index) => {
-        if (item.name === 'btn') {
-          this.inputs[index].atClick = this.edit
-        }
-      })
-    },
     afterLoadInputData () {
       this.entityLoading = false
     },
@@ -94,6 +117,9 @@ export default {
           this.$refs.entityEdit.getData()
           this.entityLoading = false
         })
+    },
+    onTopicCreated () {
+      this.topicListKey = Date.now()
     }
   }
 }
@@ -101,6 +127,12 @@ export default {
 
 <style scoped lang="scss">
 .AdminContentShow {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-start;
+    margin-bottom: 10px;
+  }
   .title {
     font-style: normal;
     font-weight: 700;

+ 110 - 0
src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="AdminContentTopicCreate"
+       :style="localOptions.style">
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="ایجاد فیش"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  props: {
+    content: {
+      type: Number,
+      default: null
+    },
+    redirect: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['created'],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.contentTopic.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentTopicCategory.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان فیش', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', categoryType: 'contentTopic', col: 'col-12' },
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' },
+        { type: 'inputEditor', name: 'text', responseKey: 'text', label: 'متن', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد فیش', placeholder: ' ', atClick: this.create, col: 'col-md-6' },
+        { type: 'hidden', name: 'content', responseKey: 'content' }
+      ]
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'content', 'value', this.content)
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(this.redirect)
+        .then(() => {
+          this.$emit('created')
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentTopicCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 137 - 0
src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="AdminContentTopicList"
+       :style="localOptions.style">
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست فیش ها"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData, showConfirmRemoveDialog}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <div class="action-column-entity-index">
+            <q-btn color="primary"
+                   :to="{name: 'Admin.ContentTopic.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+            <delete-btn @click="showConfirmRemoveDialog(inputData.props.row, 'id', getRemoveMessage(inputData.props.row))" />
+          </div>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import DeleteBtn from 'src/components/Control/DeleteBtn.vue'
+
+export default {
+  name: 'AdminContentTopicList',
+  components: { EntityIndex, DeleteBtn },
+  mixins: [mixinWidget],
+  props: {
+    contentId: {
+      type: Number,
+      default: null
+    }
+  },
+  data () {
+    return {
+      api: APIGateway.contentTopic.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'content', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'دسته',
+            align: 'left',
+            field: row => row.category_info?.title
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  created() {
+    if (this.contentId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'content', 'value', this.contentId)
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    getRemoveMessage (row) {
+      return 'آیا از حذف ' + row.title + ' اطمینان دارید؟'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentTopicList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 112 - 0
src/components/Widgets/Content/AdminContentTopicShow/AdminContentTopicShow.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="AdminContentTopicShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="مشخصات دسته بندی کلی"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import { ContentTopic } from 'src/models/ContentTopic.js'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentTopicShow',
+  components: {
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      listKey: Date.now(),
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      contentTopicCategory: new ContentTopic(),
+      api: APIGateway.contentTopic.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentTopic.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان فیش', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', categoryType: 'contentTopic', col: 'col-12' },
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' },
+        { type: 'inputEditor', name: 'text', responseKey: 'text', label: 'متن', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش فیش', placeholder: ' ', atClick: this.edit, col: 'col-md-6' },
+        { type: 'hidden', name: 'content', responseKey: 'content' },
+        { type: 'hidden', name: 'id', responseKey: 'id', value: null }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.contentTopicCategory.loading = true
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    afterLoadInputData (data) {
+      this.contentTopicCategory = new ContentTopic(data)
+      this.contentTopicCategory.loading = false
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    },
+    onCreated () {
+      this.listKey = Date.now()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicShow {
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/AdminContentTopicShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 2 - 13
src/components/Widgets/ContentCategory/AdminContentCategoryCreate/AdminContentCategoryCreate.vue

@@ -49,7 +49,7 @@ export default {
       default: null
     }
   },
-  data: () => {
+  data () {
     return {
       mounted: false,
       entityLoading: false,
@@ -63,7 +63,7 @@ export default {
         // { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-12 col-12' },
         { type: 'hidden', name: 'parent', responseKey: 'parent', value: null },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: () => {}, col: 'col-12' }
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: this.create, col: 'col-12' }
       ]
     }
   },
@@ -71,20 +71,9 @@ export default {
     FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
   },
   mounted() {
-    this.setActionBtn()
     this.mounted = true
   },
   methods: {
-    setActionBtn () {
-      this.inputs.forEach((item, index) => {
-        if (item.name === 'btn') {
-          this.inputs[index].atClick = this.onSubmit
-        }
-      })
-    },
-    onSubmit () {
-      this.create()
-    },
     create() {
       const redirect = !this.parent
       this.entityLoading = true

+ 109 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryCreate/AdminContentTopicCategoryCreate.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="AdminContentTopicCategoryCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   :title="title"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminContentTopicCategoryCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'ایجاد دسته بندی اصلی'
+    },
+    parent: {
+      type: Number,
+      default: null
+    }
+  },
+  data () {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.contentTopicCategory.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.ContentTopicCategory.Show',
+      indexRouteName: 'Admin.ContentTopicCategory.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: 'hidden', name: 'parent', responseKey: 'parent', value: null, sendNull: true },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', atClick: this.create, col: 'col-12' }
+      ]
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    create() {
+      const redirect = !this.parent
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity(redirect)
+        .then(() => {
+          this.entityLoading = false
+          this.$emit('onCreated')
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    },
+    setInputOptions (name, options) {
+      FormBuilderAssist.setAttributeByName(this.inputs, name, 'options', options)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicCategoryCreate {
+  :deep(.q-card.entity-create) {
+    .q-card__section {
+      .q-expansion-item {
+        .q-expansion-item__container {
+          .q-item {
+            display: none;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryCreate/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 148 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryList/AdminContentTopicCategoryList.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="AdminContentTopicCategoryList"
+       :style="localOptions.style">
+    <div v-if="!parent"
+         class="more-action">
+      <q-btn label="دسته موضوعی جدید"
+             color="primary"
+             :to="{name: 'Admin.ContentTopicCategory.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  :title="title"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.ContentTopicCategory.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import Assist from 'assets/js/Assist.js'
+import { EntityIndex } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'AdminContentTopicCategoryList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  props: {
+    title: {
+      type: String,
+      default: 'لیست دسته بندی های کلی'
+    },
+    parent: {
+      type: Number,
+      default: null
+    }
+  },
+  data: () => {
+    return {
+      api: APIGateway.contentTopicCategory.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'parent', value: null },
+        { type: 'hidden', name: 'parent__isnull', value: null }
+      ],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'نام دسته',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'creation_time',
+            required: true,
+            label: 'تاریخ ایجاد',
+            align: 'left',
+            field: row => Assist.miladiToShamsi(row.creation_time)
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  created() {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'parent', 'value', this.parent)
+    if (!this.parent) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'parent__isnull', 'value', 'true')
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicCategoryList {
+  :deep(.entity-index) {
+    .q-card__section {
+      .q-item {
+        display: none;
+      }
+    }
+  }
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryList/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 143 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryShow/AdminContentTopicCategoryShow.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="AdminContentTopicCategoryShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="مشخصات دسته بندی کلی"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData">
+      <template #after-form-builder>
+        <div v-if="contentCategory.id"
+             class="q-mt-lg">
+          <q-expansion-item expand-separator
+                            label="لیست دسته بندی ها">
+            <q-card>
+              <q-card-section>
+                <admin-content-topic-category-list :key="listKey"
+                                                   title="لیست دسته بندی های جزیی"
+                                                   :parent="contentCategory.id" />
+              </q-card-section>
+            </q-card>
+          </q-expansion-item>
+
+          <q-expansion-item expand-separator
+                            label="ایجاد دسته بندی">
+            <q-card>
+              <q-card-section>
+                <admin-content-topic-category-create :key="listKey"
+                                                     title="ایجاد دسته بندی جزیی"
+                                                     :parent="contentCategory.id"
+                                                     @onCreated="onCreated" />
+              </q-card-section>
+            </q-card>
+          </q-expansion-item>
+
+        </div>
+      </template>
+    </entity-edit>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import BtnControl from 'src/components/Control/btn.vue'
+import { ContentTopicCategory } from 'src/models/ContentTopicCategory.js'
+import AdminContentTopicCategoryList from 'src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryList/AdminContentTopicCategoryList.vue'
+import AdminContentTopicCategoryCreate from 'src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryCreate/AdminContentTopicCategoryCreate.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+
+export default {
+  name: 'AdminContentTopicCategoryShow',
+  components: {
+    AdminContentTopicCategoryList,
+    AdminContentTopicCategoryCreate,
+    EntityEdit
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      listKey: Date.now(),
+      replyText: null,
+      mounted: false,
+      entityLoading: true,
+      contentCategory: new ContentTopicCategory(),
+      api: APIGateway.contentTopicCategory.APIAdresses.byId(this.$route.params.id),
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Ticket.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', value: null, placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', value: null, placeholder: ' ', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'تایید', placeholder: ' ', ignoreValue: false, atClick: () => {}, col: 'col-12' },
+        { type: 'hidden', name: 'parent', responseKey: 'parent', sendNull: true, value: null },
+        { type: 'hidden', name: 'id', responseKey: 'id', value: null }
+      ]
+    }
+  },
+  computed: {
+    repliesInfo () {
+      return this.inputs.find(input => input.name === 'replies_info').value
+    }
+  },
+  created() {
+    this.contentCategory.loading = true
+  },
+  mounted() {
+    this.mounted = true
+    this.setActionBtn()
+  },
+  methods: {
+    setActionBtn () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
+    },
+    afterLoadInputData (data) {
+      this.contentCategory = new ContentTopicCategory(data)
+      this.contentCategory.loading = false
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    },
+    onCreated () {
+      this.listKey = Date.now()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentTopicCategoryShow {
+}
+</style>

+ 21 - 0
src/components/Widgets/ContentTopicCategory/AdminContentTopicCategoryShow/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 1 - 1
src/components/Widgets/Store/StorePackage/AdminStorePackageList/AdminStorePackageList.vue

@@ -85,7 +85,7 @@ export default {
   name: 'AdminStorePackageList',
   components: { EntityIndex, AdminStoreProductList },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
       tab: 'packages',
       api: APIGateway.package.APIAdresses.base,

+ 1 - 0
src/css/Theme/Controls/select.scss

@@ -2,6 +2,7 @@
   .q-field__inner {
     .q-field__control {
       .q-field__control-container {
+        padding-top: 0;
         .q-field__native {
           min-height: 48px;
           padding-left: 20px;

+ 40 - 0
src/models/ContentTopic.js

@@ -0,0 +1,40 @@
+import { Content } from 'src/models/Content.js'
+import { Collection, Model } from 'js-abstract-model'
+import { ContentHashtagList } from 'src/models/ContentHashtag.js'
+import { ContentTopicCategory } from 'src/models/ContentTopicCategory.js'
+
+class ContentTopic extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'text' },
+      { key: 'thumbnail' },
+      { key: 'content' },
+      {
+        key: 'content_info',
+        relatedModel: Content
+      },
+      { key: 'category' },
+      {
+        key: 'category_info',
+        relatedModel: ContentTopicCategory
+      },
+      { key: 'hashtags' },
+      {
+        key: 'hashtags_info',
+        relatedModel: ContentHashtagList
+      },
+      { key: 'creation_time' },
+      { key: 'last_modification_time' }
+    ])
+  }
+}
+
+class ContentTopicList extends Collection {
+  model () {
+    return ContentTopic
+  }
+}
+
+export { ContentTopic, ContentTopicList }

+ 23 - 0
src/models/ContentTopicCategory.js

@@ -0,0 +1,23 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class ContentTopicCategory extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'title' },
+      { key: 'parent' },
+      {
+        key: 'children',
+        defalut: []
+      }
+    ])
+  }
+}
+
+class ContentTopicCategoryList extends Collection {
+  model () {
+    return ContentTopicCategory
+  }
+}
+
+export { ContentTopicCategory, ContentTopicCategoryList }

+ 23 - 0
src/pages/Admin/ContentTopic/Show.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.ContentTopic.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentTopicShow' }], options: { style: { paddingTop: '' } } }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentTopicCategory/Create.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.ContentTopicCategory.Create',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentTopicCategoryCreate' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentTopicCategory/List.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.ContentTopicCategory.List',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentTopicCategoryList' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 23 - 0
src/pages/Admin/ContentTopicCategory/Show.vue

@@ -0,0 +1,23 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'Admin.ContentTopicCategory.Show',
+  mixins: [mixinPageOptions],
+  data: () => {
+    return {
+      sections: [{ data: { rows: [{ cols: [{ widgets: [{ name: 'AdminContentTopicCategoryShow' }], options: {} }], options: { className: '', height: 'auto', boxed: true, boxedWidth: 1200, gutterXSize: 'md', gutterYSize: 'md', absolute: 'none', paddingOfBoxedInFullWidth: '10px', alignment: { justifyContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignItems: { global: null, xl: null, lg: null, md: null, sm: null, xs: null }, alignContent: { global: null, xl: null, lg: null, md: null, sm: null, xs: null } }, style: { maxWidth: '1200px', width: '1200px' } } }] } }]
+    }
+  },
+  created() {
+    this.currenSections = this.sections
+  }
+}
+</script>

+ 18 - 0
src/router/routes/Admin.js

@@ -208,6 +208,14 @@ export default {
         { name: 'Admin.Content.Ticket', path: 'ticket', component: () => import('src/pages/Admin/Content/Ticket.vue'), breadcrumbs: { title: 'درخواست ها دوره های آموزشی' } }
       ]
     },
+    {
+      path: 'content-topic',
+      name: 'Admin.ContentTopic',
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        { path: ':id', name: 'Admin.ContentTopic.Show', component: () => import('src/pages/Admin/ContentTopic/Show.vue') }
+      ]
+    },
     {
       path: 'hadith',
       name: 'Admin.Hadith',
@@ -281,6 +289,16 @@ export default {
         { path: ':id', name: 'Admin.ContentCategory.Show', component: () => import('src/pages/Admin/ContentCategory/Show.vue') }
       ]
     },
+    {
+      path: 'content-topic-category',
+      name: 'Admin.ContentTopicCategory',
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        { path: '', name: 'Admin.ContentTopicCategory.List', component: () => import('src/pages/Admin/ContentTopicCategory/List.vue') },
+        { path: 'create', name: 'Admin.ContentTopicCategory.Create', component: () => import('src/pages/Admin/ContentTopicCategory/Create.vue') },
+        { path: ':id', name: 'Admin.ContentTopicCategory.Show', component: () => import('src/pages/Admin/ContentTopicCategory/Show.vue') }
+      ]
+    },
     {
       path: 'donation-category',
       name: 'Admin.DonationCategory',