gemercheung 7 месяцев назад
Родитель
Сommit
cb310a46ba

+ 5 - 0
packages/backend/src/modules/web/web.controller.ts

@@ -31,4 +31,9 @@ export class WebController {
   getArticleWithSearch(@Query('key') key: string, @Headers('locale') locale?: string) {
     return this.webService.searchArticles(key, locale);
   }
+
+  @Get('articleWithCate/:id')
+  getArticlesByCate(@Param('id') id: string) {
+    return this.webService.getArticlesByCate(+id);
+  }
 }

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

@@ -128,4 +128,38 @@ export class WebService {
     });
     return articles.map((article) => article.translate(lang));
   }
+
+  async findCateAllChildIds(parentId: number) {
+    const childIds: number[] = [];
+    // 递归查找所有子菜单的 id
+    const findChildren = async (id: number) => {
+      const children = await this.categoryRepo.find({ where: { parentId: id } });
+      for (const child of children) {
+        childIds.push(child.id);
+        await findChildren(child.id); // 递归查找子菜单的子菜单
+      }
+    };
+    await findChildren(parentId);
+    return childIds;
+  }
+
+  async getArticlesByCate(id: number, locale?: string) {
+    const lang = this.sharedService.handleValidLang(locale);
+
+    // 保留
+    // const category = await this.categoryRepo.findOne({
+    //   where: { id: id },
+    //   relations: { children: true },
+    // });
+    // const ids = await this.findCateAllChildIds(category.id);
+    // ids.push(category.id);
+
+    const articles = await this.articleRepo.find({
+      where: {
+        categoryId: id,
+      },
+      relations: { translations: true },
+    });
+    return articles.map((article) => article.translate(lang));
+  }
 }

+ 6 - 1
packages/web/src/api/article.ts

@@ -8,6 +8,7 @@ export type ArticleDetailType = {
   createTime: string
   readCount: number
   categoryId: number
+  articleId: number
 }
 
 export type ArticleDetailMenuType = {
@@ -22,5 +23,9 @@ export const getArticleDetail = (id: number): Promise<ResultData<ArticleDetailTy
 export const getArticleCount = (id: number): Promise<ResultData<boolean>> =>
   request.get(`web/article/count/${id}`)
 
-export const getArticleSearch = (keyword: number): Promise<ResultData<boolean>> =>
+export const getArticleSearch = (keyword: number): Promise<ResultData<ArticleDetailType[]>> =>
   request.get(`web/search`, { key: keyword })
+
+export const getArticlesByCateId = (cid: number): Promise<ResultData<ArticleDetailType[]>> =>
+  request.get(`web/articleWithCate/${cid}`)
+

+ 1 - 0
packages/web/src/api/menu.ts

@@ -14,6 +14,7 @@ export type CategoryItem = {
   id: number
   title: string
   children: MenuItem[]
+  parentId: number
   remark: string
 }
 export const getMenuList = (): Promise<ResultData<MenuItem[]>> => request.get('web/menu')

+ 2 - 1
packages/web/src/layouts/base.vue

@@ -1,5 +1,5 @@
 <template>
-  <n-config-provider :theme="darkTheme" :locale="zhCN" :date-locale="dateZhCN">
+  <n-config-provider :theme="darkTheme" :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
     <div class="layout w-auto h-full">
       <sub-header></sub-header>
       <RouterView class="min-h-2xl" />
@@ -11,6 +11,7 @@
 <script setup lang="ts">
 import { NConfigProvider } from 'naive-ui'
 import { zhCN, dateZhCN } from 'naive-ui'
+import { themeOverrides } from '@/utils'
 import SubHeader from '../components/subHeader.vue'
 import Footer from '../components/footer.vue'
 // import { createTheme, inputDark, datePickerDark } from 'naive-ui'

+ 2 - 2
packages/web/src/layouts/default.vue

@@ -1,5 +1,5 @@
 <template>
-  <n-config-provider :theme="darkTheme" :locale="zhCN" :date-locale="dateZhCN">
+  <n-config-provider :theme="darkTheme" :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
     <div class="layout w-auto h-full">
       <Header></Header>
       <!--      <h1>{{ route.meta.title }}</h1>-->
@@ -20,13 +20,13 @@
 <script setup lang="ts">
 import { NConfigProvider } from 'naive-ui'
 import { zhCN, dateZhCN } from 'naive-ui'
+import { themeOverrides } from '@/utils'
 import Header from '../components/header.vue'
 import Footer from '../components/footer.vue'
 // import { createTheme, inputDark, datePickerDark } from 'naive-ui'
 // createTheme([inputDark, datePickerDark])
 const darkTheme = null
 window.$message = useMessage()
-
 </script>
 
 <style></style>

+ 6 - 8
packages/web/src/pages/index.vue

@@ -11,7 +11,7 @@
                 class="show-item b-rd-3xl relative"
                 :class="{ [`style-${item.styleType}`]: true }"
                 :style="{ backgroundImage: `url(${child.cover})` }"
-                @click="handleToDoc(child)"
+                @click="handleToDoc(child as any as ArticleDetailType)"
               >
                 <div
                   class="w-full h-full flex flex-col absolute top-0 left-0 -mx-auto justify-end overflow-hidden shadow-blueGray"
@@ -37,7 +37,7 @@
             <swiper-slide
               v-for="child of item.children"
               :key="child.id"
-              @click="handleToDoc(child)"
+              @click="handleToDoc(child as any as ArticleDetailType)"
             >
               <div
                 class="cover w-full h-[400px] overflow-hidden b-rd-xl"
@@ -53,8 +53,6 @@
                 </div>
               </div>
             </swiper-slide>
-
-            ...
           </swiper>
         </template>
 
@@ -62,7 +60,7 @@
           <n-grid x-gap="100" y-gap="100" :cols="2">
             <n-gi v-for="child of item.children" :key="child.id">
               <div
-                @click="handleToDoc(child)"
+                @click="handleToDoc(child as any as ArticleDetailType)"
                 :class="{ [`style-${item.styleType}`]: true }"
                 class="show-item b-rd-3xl relative w-full h-full"
               >
@@ -94,7 +92,7 @@
           <n-grid x-gap="100" y-gap="100" :cols="3">
             <n-gi v-for="child of item.children" :key="child.id">
               <div
-                @click="handleToDoc(child)"
+                @click="handleToDoc(child as any as ArticleDetailType)"
                 :class="{ [`style-${item.styleType}`]: true }"
                 class="show-item b-rd-3xl relative w-full h-full flex flex-col align-center items-center justify-center"
               >
@@ -124,7 +122,7 @@ layout: "default"
 </route>
 <script setup lang="ts">
 import { NH1, NGrid, NGi } from 'naive-ui'
-import { getMenuList } from '@/api'
+import { getMenuList, type ArticleDetailType } from '@/api'
 import type { MenuItem } from '@/api'
 import { Swiper, SwiperSlide } from 'swiper/vue'
 
@@ -147,7 +145,7 @@ const onSlideChange = () => {
   console.log('slide change')
 }
 
-const handleToDoc = (child: never) => {
+const handleToDoc = (child: ArticleDetailType) => {
   const { articleId, categoryId } = child
   console.log(articleId, categoryId)
   if (articleId) {

+ 89 - 15
packages/web/src/pages/showdoc/[id].vue

@@ -1,15 +1,45 @@
 <template>
   <div class="max-w-screen-xl content my-0 mx-auto text-size-base overflow-hidden">
-    <div v-if="detail" class="mt-[100px]">
+    <div v-if="detail">
+      <div class="breadcrumb my-[30px] pb-[20px] bb-1px_#EBEBEB" role="navigation">
+        <n-breadcrumb separator=">" v-if="breadcrumb">
+          <!--          {{ breadcrumb }}-->
+          <template v-for="(bread, index) in breadcrumb" :key="index">
+            <n-breadcrumb-item :clickable="false"> {{ bread.title }}</n-breadcrumb-item>
+          </template>
+        </n-breadcrumb>
+      </div>
+      <div v-if="currentCate">
+        <n-h2 class="mb-10"> {{ currentCate.title }}</n-h2>
+      </div>
       <div class="w-full flex flex-row flex-nowrap">
-        <div class="flex-basis-[240px] flex-grow-0 flex-shrink-0">
-          <n-collapse accordion class="br-1px_#EBEBEB py-[10px]">
-            <template v-for="(cate, index) in mainCategories.children" :key="index">
-              <n-collapse-item :title="cate.title" :name="cate.title">
-                <div class="px-4">{{ cate.title }}</div>
-              </n-collapse-item>
-            </template>
-          </n-collapse>
+        <div class="flex-basis-[240px] flex-grow-0 flex-shrink-0 br-1px_#EBEBEB">
+          <n-tree
+            class="left-tree"
+            v-if="mainCategories.children"
+            block-line
+            :data="mainCategories.children"
+            :default-expanded-keys="defaultExpandedKeys"
+            :node-props="nodeProps"
+            key-field="id"
+            label-field="title"
+            children-field="children"
+            selectable
+          />
+          <!--          <n-collapse class="br-1px_#EBEBEB py-[10px]">-->
+          <!--            <template v-for="(cate, index) in mainCategories.children" :key="index">-->
+          <!--              <n-collapse-item :title="cate.title" :name="cate.title">-->
+          <!--                <n-collapse v-if="cate.children">-->
+          <!--                  <template v-for="(cateChild, cIndex) in cate.children" :key="cIndex">-->
+          <!--                    <n-collapse-item :title="cateChild.title" :name="cateChild.title">-->
+          <!--                      <div class="px-6">{{ cateChild.title }}</div>-->
+          <!--                    </n-collapse-item>-->
+          <!--                  </template>-->
+          <!--                </n-collapse>-->
+          <!--                <div class="px-6" v-else>{{ cate.title }}</div>-->
+          <!--              </n-collapse-item>-->
+          <!--            </template>-->
+          <!--          </n-collapse>-->
         </div>
         <div class="flex-1 w-[calc(100%-80px)] px-[40px] mb-[120px] overflow-hidden">
           <div class="bb-1px_#EBEBEB color-[#999999]">
@@ -36,8 +66,6 @@
               >
               </n-anchor-link>
             </n-anchor>
-
-            <!--{{ mainContents }}-->
           </div>
         </div>
       </div>
@@ -62,21 +90,47 @@ import {
   getArticleDetail,
   getArticleCount,
   getCategoryTree,
+  getArticlesByCateId,
 } from '@/api'
-import { htmlToTree, createAnchorNames, dayjs } from '@/utils'
-import { NH1, NH4, NCollapse, NCollapseItem, NAnchor, NAnchorLink } from 'naive-ui'
+import { htmlToTree, createAnchorNames, dayjs, findNodeById, findBreadcrumbPath } from '@/utils'
+import { NH1, NH2, NH4, NBreadcrumb, NBreadcrumbItem, NTree, NAnchor, NAnchorLink } from 'naive-ui'
+import type { TreeOption } from 'naive-ui'
 
 const route = useRoute()
+const router = useRouter()
 
 const params = route.params as {
   id?: number
 }
-
+const breadcrumb = ref<CategoryItem[]>([])
 console.log('route', route)
 const detail = ref<ArticleDetailType | undefined>()
 const mainContents = ref<ArticleDetailMenuType[]>([])
-
+const currentCate = ref<CategoryItem | undefined>()
 const mainCategories = ref<CategoryItem[]>([])
+const defaultExpandedKeys = ref<number[]>([])
+
+const nodeProps = ({ option }: { option: TreeOption }) => {
+  return {
+    async onClick() {
+      // window.$message.info(`[Click] ${option.title},[ID] ${option.id} `)
+      const res = await getArticlesByCateId(+(option.id as string))
+      // console.log(res.data)
+      if (res.data && res.data.length > 0) {
+        const articleId = res.data[0].id
+        console.log('articleId', articleId)
+        // location.reload()
+        router.replace(`/showdoc/${articleId}`)
+        setTimeout(() => {
+          location.reload()
+        }, 500)
+      } else {
+        window.$message.info(`当前类别没有相关联的文章!`)
+      }
+    },
+  }
+}
+
 onMounted(async () => {
   setTimeout(() => {
     const html = document.querySelector('.content-html')
@@ -100,6 +154,15 @@ watchEffect(() => {
           const res = await getCategoryTree(detail.value.categoryId)
           if (res.data) {
             mainCategories.value = res.data as CategoryItem[]
+            if (mainCategories.value) {
+              currentCate.value = findNodeById(
+                [mainCategories.value],
+                detail.value.categoryId,
+              ) as unknown as CategoryItem
+              defaultExpandedKeys.value = [currentCate.value.parentId]
+              breadcrumb.value = findBreadcrumbPath([mainCategories.value], detail.value.categoryId)
+              // console.log('breadcrumb', breadcrumb.value)
+            }
           }
         }
       }
@@ -113,4 +176,15 @@ watchEffect(() => {
   width: 100% !important;
   height: auto;
 }
+
+:deep(.left-tree) {
+  --n-node-content-height: 40px !important;
+  //--n-node-color-hover: rgba(6,97,201,0.06) !important;
+  //border-right: 1px solid #e5e7eb;
+
+  .n-tree-node-wrapper {
+    width: 240px;
+    font-size: 16px;
+  }
+}
 </style>

+ 2 - 0
packages/web/src/utils/index.ts

@@ -1,3 +1,5 @@
 export * from './http'
 export * from './html'
 export * from './time'
+export * from './tree'
+export * from './themeOverides'

+ 10 - 0
packages/web/src/utils/themeOverides.ts

@@ -0,0 +1,10 @@
+import { type GlobalThemeOverrides } from 'naive-ui'
+
+export const themeOverrides: GlobalThemeOverrides = {
+  common: {
+    primaryColor: '#0661C9',
+  },
+  Button: {
+    // textColor: '#FF0000',
+  },
+}

+ 62 - 36
packages/web/src/utils/tree.ts

@@ -1,36 +1,62 @@
-interface TreeNode {
-  id: string;
-  name: string;
-  children?: TreeNode[];
-}
-export function getTopLevelMenuAndPath(tree: TreeNode[], targetId: string): TreeNode | null {
-  // 辅助函数:递归查找目标节点及其路径
-  function findPath(node: TreeNode, path: TreeNode[]): TreeNode | null {
-    // 将当前节点加入路径
-    path.push(node);
-
-    // 如果当前节点是目标节点,返回路径
-    if (node.id === targetId) {
-      return { ...node, children: path }; // 返回顶层菜单及其路径
-    }
-
-    // 如果有子节点,递归查找
-    if (node.children) {
-      for (const child of node.children) {
-        const result = findPath(child, path.slice()); // 使用 path.slice() 创建新路径副本
-        if (result) return result;
-      }
-    }
-
-    return null; // 如果当前分支没有找到目标节点,返回 null
-  }
-
-  // 遍历顶层节点,查找目标节点
-  for (const root of tree) {
-    const result = findPath(root, []);
-    if (result) return result;
-  }
-
-  return null; // 如果整个树中没有找到目标节点,返回 null
-}
-
+export interface TreeNode {
+  id: string
+  title: string
+  children?: TreeNode[]
+}
+
+// 递归查找节点的方法
+export function findNodeById(tree: TreeNode[], id: string): TreeNode | null {
+  // debugger
+  for (const node of tree) {
+    if (node.id === id) {
+      return node // 找到匹配的节点
+    }
+    if (node.children) {
+      const result = findNodeById(node.children, id) // 递归查找子节点
+      if (result) {
+        return result // 如果在子节点中找到,返回结果
+      }
+    }
+  }
+  return null // 未找到匹配的节点
+}
+
+// 递归查找面包屑路径的方法
+export function findBreadcrumbPath(
+  tree: TreeNode[],
+  targetId: string,
+  path: TreeNode[] = [],
+): TreeNode[] | null {
+  for (const node of tree) {
+    // 将当前节点加入路径
+    const currentPath = [...path, node]
+
+    // 如果当前节点是目标节点,返回当前路径
+    if (node.id === targetId) {
+      return currentPath
+    }
+
+    // 如果当前节点有子节点,递归查找
+    if (node.children) {
+      const result = findBreadcrumbPath(node.children, targetId, currentPath)
+      if (result) {
+        return result // 如果在子节点中找到目标节点,返回路径
+      }
+    }
+  }
+
+  // 未找到目标节点
+  return null
+}
+
+// 生成面包屑导航数组的方法
+export function generateBreadcrumbArray(tree: TreeNode[], targetId: string): TreeNode[] | null {
+  const path = findBreadcrumbPath(tree, targetId)
+
+  if (path) {
+    // 返回包含节点信息的面包屑导航数组
+    return path
+  }
+
+  return null // 未找到目标节点
+}