gemercheung 7 ماه پیش
والد
کامیت
6a05cf8b2a

+ 6 - 0
packages/backend/src/modules/category/category.controller.ts

@@ -30,6 +30,12 @@ export class CategoryController {
   getAllCategories(@Query() getAllCategoryDto: GetAllCategoryDto) {
     return this.categoryService.findAll(getAllCategoryDto);
   }
+
+  @Get('tree')
+  getAllCategoriesTree(@Query() getAllCategoryDto: GetAllCategoryDto) {
+    return this.categoryService.findAllWithTree(getAllCategoryDto);
+  }
+
   @Get('page')
   findPagination(@Query() queryDto: GetCategoryDto) {
     return this.categoryService.findPagination(queryDto);

+ 9 - 0
packages/backend/src/modules/category/category.service.ts

@@ -3,11 +3,14 @@ import { Category } from './category.entity';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Like, Repository } from 'typeorm';
 import { CreateCategoryDto, GetAllCategoryDto, GetCategoryDto, UpdateCategoryDto } from './dto';
+import { SharedService } from '@/shared/shared.service';
+
 @Injectable()
 export class CategoryService {
   constructor(
     @InjectRepository(Category)
     private categoryRepo: Repository<Category>,
+    private readonly sharedService: SharedService,
   ) {}
 
   async create(createCategoryDto: CreateCategoryDto) {
@@ -19,6 +22,11 @@ export class CategoryService {
     return this.categoryRepo.find({ where: query });
   }
 
+  async findAllWithTree(query: GetAllCategoryDto) {
+    const categories = await this.categoryRepo.find({ where: query });
+    return this.sharedService.handleTree(categories);
+  }
+
   async findPagination(query: GetCategoryDto) {
     const pageSize = query.pageSize || 10;
     const pageNo = query.pageNo || 1;
@@ -27,6 +35,7 @@ export class CategoryService {
         title: Like(`%${query.title || ''}%`),
         enable: query.enable || undefined,
       },
+      // children: true
       relations: { parent: true, user: true },
       order: {
         // title: 'ASC',

+ 6 - 1
packages/backend/src/modules/web/web.controller.ts

@@ -1,6 +1,6 @@
 import { Controller, Get, Post, Body, Patch, Param, Delete, Headers } from '@nestjs/common';
 import { WebService } from './web.service';
-import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
+import { ApiTags } from '@nestjs/swagger';
 
 @ApiTags('website(展示端)')
 @Controller('web')
@@ -21,4 +21,9 @@ export class WebController {
   getArticleCount(@Param('id') id: string) {
     return this.webService.setArticleCount(+id);
   }
+
+  @Get('category/:id')
+  getAllCategoriesList(@Param('id') id: string) {
+    return this.webService.getCategory(+id);
+  }
 }

+ 1 - 0
packages/backend/src/modules/web/web.module.ts

@@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 import { Menu } from '@/modules/menu/menu.entity';
 import { Article } from '@/modules/article/article.entity';
 
+
 @Module({
   imports: [TypeOrmModule.forFeature([Menu, Category, Article])],
   controllers: [WebController],

+ 11 - 0
packages/backend/src/modules/web/web.service.ts

@@ -4,6 +4,7 @@ import { Like, Repository } from 'typeorm';
 import { Menu } from '@/modules/menu/menu.entity';
 import { SharedService } from '@/shared/shared.service';
 import { Article } from '@/modules/article/article.entity';
+import { Category } from '@/modules/category/category.entity';
 
 @Injectable()
 export class WebService {
@@ -12,6 +13,8 @@ export class WebService {
     private menuRepo: Repository<Menu>,
     @InjectRepository(Article)
     private articleRepo: Repository<Article>,
+    @InjectRepository(Category)
+    private categoryRepo: Repository<Category>,
     private readonly sharedService: SharedService,
   ) {}
 
@@ -69,4 +72,12 @@ export class WebService {
     await this.articleRepo.save(article);
     return true;
   }
+
+  async getCategory(id: number) {
+    const category = await this.categoryRepo.findOne({
+      where: { id },
+      relations: { children: true },
+    });
+    return category;
+  }
 }

+ 7 - 2
packages/frontend/src/views/article/add.vue

@@ -29,8 +29,13 @@
             message: '请输入文章分类',
           }"
         >
-          <n-select
-            v-model:value="modalForm.categoryId" :options="allCategory" clearable filterable tag
+          <n-tree-select
+            v-model:value="modalForm.categoryId"
+            :options="allCategory"
+            label-field="title"
+            key-field="id"
+            placeholder="根分类"
+            clearable
             style="max-width: 300px;"
           />
         </n-form-item>

+ 16 - 12
packages/frontend/src/views/article/edit.vue

@@ -7,43 +7,47 @@
     </template>
 
     <div class="editor-wrap">
-      <n-form
+      <n-form
         ref="modalFormRef" class="form wh-full" label-placement="left" label-align="left" :label-width="80"
-        :model="modalForm"
+        :model="modalForm"
       >
-        <n-form-item
+        <n-form-item
           label="文章名称" path="title" :rule="{
             required: true,
             message: '请输入文章名称',
             trigger: ['input', 'blur'],
-          }"
+          }"
         >
           <n-input v-model:value="modalForm.title" />
         </n-form-item>
 
-        <n-form-item
+        <n-form-item
           label="文章分类" path="categoryId" :rule="{
             required: true,
             type: 'number',
             trigger: ['change', 'blur'],
             message: '请输入文章分类',
-          }"
+          }"
         >
-          <n-select
-            v-model:value="modalForm.categoryId" :options="allCategory" clearable filterable tag
-            style="max-width: 300px;"
+          <n-tree-select
+            v-model:value="modalForm.categoryId"
+            :options="allCategory"
+            label-field="title"
+            key-field="id"
+            placeholder="根分类"
+            clearable
           />
         </n-form-item>
 
         <n-tabs type="line" animated>
           <template v-for="(lang, index) in langs" :key="lang">
             <n-tab-pane :name="lang" :tab="langLabel[lang]" :index="index">
-              <n-form-item
+              <n-form-item
                 label="文章名称" path="title" :rule="{
                   required: true,
                   message: '请输入文章名称',
                   trigger: ['input', 'blur'],
-                }"
+                }"
               >
                 <n-input v-model:value="modalForm.translations[index].title" />
               </n-form-item>
@@ -98,7 +102,7 @@ onMounted(async () => {
   }
 })
 const allCategory = ref([])
-categoryApi.getAll().then(({ data = [] }) => (allCategory.value = data.map(item => ({ label: item.title, value: item.id }))))
+categoryApi.getAll().then(({ data = [] }) => (allCategory.value = data))
 
 function handleEdit() {
   modalFormRef.value?.validate((errors) => {

+ 1 - 1
packages/frontend/src/views/category/api.js

@@ -5,6 +5,6 @@ export default {
   read: (params = {}) => request.get('/category/page', { params }),
   update: data => request.patch(`/category/${data.id}`, data),
   delete: id => request.delete(`/category/${id}`),
-  getAll: data => request.get('/category?enable=1', data),
+  getAll: data => request.get('/category/tree?enable=1', data),
 
 }

+ 49 - 48
packages/frontend/src/views/menu/list.vue

@@ -6,7 +6,7 @@
           编辑
         </NButton>
         <NButton type="primary" @click="handleSubAdd()">
-          <i class="i-material-symbols:add mr-4 text-18" />
+          <i class="i-material-symbols:add mr-4 text-18"/>
           新增子菜单
         </NButton>
       </n-space>
@@ -15,7 +15,7 @@
     <MeCrud ref="$table" v-model:query-items="queryItems" :scroll-x="1200" :columns="columns" :get-data="api.read">
       <MeQueryItem label="标题" :label-width="50">
         <n-input v-model:value="queryItems.title" type="text" placeholder="请输入标题名" clearable>
-          <template #password-visible-icon />
+          <template #password-visible-icon/>
         </n-input>
       </MeQueryItem>
       <MeQueryItem label="状态" :label-width="50">
@@ -37,7 +37,7 @@
             trigger: ['input', 'blur'],
           }"
         >
-          <n-input v-model:value="modalForm.title" />
+          <n-input v-model:value="modalForm.title"/>
         </n-form-item>
 
         <n-form-item
@@ -47,7 +47,7 @@
             trigger: ['input', 'blur'],
           }"
         >
-          <n-input v-model:value="modalForm.description" type="textarea" />
+          <n-input v-model:value="modalForm.description" type="textarea"/>
         </n-form-item>
         <n-form-item v-if="modalForm.level !== 0" label="封面" path="cover">
           <n-upload
@@ -77,8 +77,13 @@
             message: '请输入分类',
           }"
         >
-          <n-select
-            v-model:value="modalForm.categoryId" :options="allCategory" clearable filterable tag
+          <n-tree-select
+            v-model:value="modalForm.categoryId"
+            :options="allCategory"
+            label-field="title"
+            key-field="id"
+            placeholder="根分类"
+            clearable
           />
         </n-form-item>
 
@@ -105,7 +110,8 @@
           }"
         >
           <n-select
-            v-model:value="modalForm.articleId" :options="allArticle" clearable filterable tag
+            v-model:value="modalForm.articleId" :options="allArticle"
+            clearable filterable tag
           />
         </n-form-item>
 
@@ -113,7 +119,7 @@
           v-if="modalForm.level === 0"
           label="一行显示数" path="grid"
         >
-          <n-input-number v-model:value="modalForm.grid" style="width:100%" />
+          <n-input-number v-model:value="modalForm.grid" style="width:100%"/>
         </n-form-item>
 
         <n-form-item
@@ -126,7 +132,7 @@
             trigger: ['blur', 'change'],
           }"
         >
-          <n-input-number v-model:value="modalForm.order" />
+          <n-input-number v-model:value="modalForm.order"/>
         </n-form-item>
 
         <n-form-item label="是否显示" path="isPublish">
@@ -155,14 +161,14 @@
 </template>
 
 <script setup>
-import { MeCrud, MeModal, MeQueryItem } from '@/components'
-import { CommonPage } from '@/components/index.js'
-import { useCrud } from '@/composables'
-import { useUserStore } from '@/store/index.js'
-import { formatDateTime } from '@/utils'
-import { styleEnum } from '@/utils/enum.js'
-import { NButton, NImage, NSwitch } from 'naive-ui'
-import { useRoute, useRouter } from 'vue-router'
+import {MeCrud, MeModal, MeQueryItem} from '@/components'
+import {CommonPage} from '@/components/index.js'
+import {useCrud} from '@/composables'
+import {useUserStore} from '@/store/index.js'
+import {formatDateTime} from '@/utils'
+import {styleEnum} from '@/utils/enum.js'
+import {NButton, NImage, NSwitch} from 'naive-ui'
+import {useRoute, useRouter} from 'vue-router'
 import articleApi from '../article/api'
 import categoryApi from '../category/api'
 import api from './api.js'
@@ -170,7 +176,7 @@ import api from './api.js'
 const $table = ref(null)
 
 const router = useRouter()
-const { userId } = useUserStore()
+const {userId} = useUserStore()
 
 const previewFileList = ref([])
 const showModal = ref(false)
@@ -188,18 +194,18 @@ const detail = ref({
 const queryItems = ref({
   parentId: route.params.id,
 })
-const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit }
+const {modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit}
   = useCrud({
-    name: '子菜单',
-    doCreate: api.create,
-    doDelete: api.delete,
-    doUpdate: api.update,
-    initForm: { enable: true, isPublish: true, order: 0 },
-    refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
-  })
+  name: '子菜单',
+  doCreate: api.create,
+  doDelete: api.delete,
+  doUpdate: api.update,
+  initForm: {enable: true, isPublish: true, order: 0},
+  refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
+})
 
 async function getMenuDetail() {
-  const { data } = await api.getOne(route.params.id)
+  const {data} = await api.getOne(route.params.id)
   if (data) {
     console.log('data', data)
     detail.value = data
@@ -208,17 +214,17 @@ async function getMenuDetail() {
 }
 
 const columns = [
-  { title: '标题名', key: 'title', width: '200' },
-  { title: '分类', key: 'category.title' },
+  {title: '标题名', key: 'title', width: '200'},
+  {title: '分类', key: 'category.title'},
   {
     title: '封面图',
     key: 'cover',
     render: row => row.cover
       ? h(NImage, {
-          src: row.cover,
-          height: 60,
-          width: 80,
-        })
+        src: row.cover,
+        height: 60,
+        width: 80,
+      })
       : null,
   },
   {
@@ -265,7 +271,7 @@ const columns = [
           },
           {
             default: () => '编辑',
-            icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
+            icon: () => h('i', {class: 'i-material-symbols:edit-outline text-14'}),
           },
         ),
 
@@ -280,7 +286,7 @@ const columns = [
           },
           {
             default: () => '删除',
-            icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
+            icon: () => h('i', {class: 'i-material-symbols:delete-outline text-14'}),
           },
         ),
       ]
@@ -291,12 +297,11 @@ const columns = [
 async function handleEnable(row) {
   row.enableLoading = true
   try {
-    await api.update({ id: row.id, enable: !row.enable })
+    await api.update({id: row.id, enable: !row.enable})
     row.enableLoading = false
     $message.success('操作成功')
     $table.value?.handleSearch()
-  }
-  catch (error) {
+  } catch (error) {
     console.error(error)
     row.enableLoading = false
   }
@@ -315,7 +320,7 @@ onMounted(() => {
   getMenuDetail()
 })
 
-async function uploadCover({ file }) {
+async function uploadCover({file}) {
   const data = new FormData()
   data.append('file', file.file)
   const res = await api.uploadImage(data)
@@ -330,7 +335,7 @@ async function uploadCover({ file }) {
 }
 
 function handlePreview(file) {
-  const { url } = file
+  const {url} = file
   previewImageUrl.value = url
   showModal.value = true
 }
@@ -347,8 +352,7 @@ async function handleFormEdit(data = {}) {
       status: 'finished',
       url: modalForm.value.cover,
     }]
-  }
-  else {
+  } else {
     previewFileList.value = []
   }
   handleEdit(data)
@@ -364,7 +368,7 @@ function handleSubAdd() {
   modalForm.value.level = 1
   modalForm.value.cover = ''
   previewFileList.value = []
-  handleAdd({ level: 1, cover: '' })
+  handleAdd({level: 1, cover: ''})
 }
 
 function handleTopMenuEdit() {
@@ -381,11 +385,8 @@ function handleTopMenuEdit() {
 }
 
 function getAllType() {
-  categoryApi.getAll().then(({ data = [] }) => (allCategory.value = data.map(item => ({
-    label: item.title,
-    value: +item.id,
-  }))))
-  articleApi.getAll().then(({ data = [] }) => (allArticle.value = data.map(item => ({
+  categoryApi.getAll().then(({data = []}) => (allCategory.value = data))
+  articleApi.getAll().then(({data = []}) => (allArticle.value = data.map(item => ({
     label: item.title,
     value: +item.id,
   }))))