Browse Source

feat: save

gemercheung 7 months ago
parent
commit
cb76bb5255

+ 0 - 8
packages/backend/src/modules/permission/permission.service.ts

@@ -1,11 +1,3 @@
-/**********************************
- * @Author: Ronnie Zhang
- * @LastEditor: Ronnie Zhang
- * @LastEditTime: 2023/12/07 20:26:42
- * @Email: zclzone@outlook.com
- * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
- **********************************/
-
 import { BadRequestException, Injectable } from '@nestjs/common';
 import { CreatePermissionDto, UpdatePermissionDto } from './dto';
 import { InjectRepository } from '@nestjs/typeorm';

+ 0 - 7
packages/backend/src/modules/role/role.service.ts

@@ -1,10 +1,3 @@
-/**********************************
- * @Author: Ronnie Zhang
- * @LastEditor: Ronnie Zhang
- * @LastEditTime: 2023/12/07 20:28:20
- * @Email: zclzone@outlook.com
- * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
- **********************************/
 
 import { BadRequestException, Injectable } from '@nestjs/common';
 import {

+ 9 - 7
packages/backend/src/modules/web/web.controller.ts

@@ -1,17 +1,19 @@
-import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
+import { Controller, Get, Post, Body, Patch, Param, Delete, Headers } from '@nestjs/common';
 import { WebService } from './web.service';
 import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
 
 @ApiTags('website(展示端)')
 @Controller('web')
 export class WebController {
-  constructor(
-    private readonly webService: WebService,
-
-  ) {}
+  constructor(private readonly webService: WebService) {}
 
   @Get('menu')
-  getMenus() {
-    return this.webService.findMenuTree();
+  getMenus(@Headers('locale') locale?: string) {
+    return this.webService.findMenuTree(locale);
+  }
+
+  @Get('article/:id')
+  getArticleDetail(@Param('id') id: string, @Headers('locale') locale?: string) {
+    return this.webService.findArticleDetail(+id, locale);
   }
 }

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

@@ -4,9 +4,10 @@ import { WebController } from './web.controller';
 import { Category } from '@/modules/category/category.entity';
 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])],
+  imports: [TypeOrmModule.forFeature([Menu, Category, Article])],
   controllers: [WebController],
   providers: [WebService],
 })

+ 22 - 1
packages/backend/src/modules/web/web.service.ts

@@ -3,16 +3,19 @@ import { InjectRepository } from '@nestjs/typeorm';
 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';
 
 @Injectable()
 export class WebService {
   constructor(
     @InjectRepository(Menu)
     private menuRepo: Repository<Menu>,
+    @InjectRepository(Article)
+    private articleRepo: Repository<Article>,
     private readonly sharedService: SharedService,
   ) {}
 
-  async findMenuTree() {
+  async findMenuTree(locale?: string): Promise<Menu[]> {
     const menus = await this.menuRepo.find({
       where: {
         enable: true,
@@ -36,4 +39,22 @@ export class WebService {
     });
     return this.sharedService.handleTree(menus);
   }
+
+  async findArticleDetail(id: number, locale?: string) {
+    const article = await this.articleRepo.findOne({
+      where: { id, translations: { locale: locale } },
+      relations: { user: true, translations: true, category: true },
+      select: {
+        user: {
+          id: true,
+          username: true,
+        },
+        category: {
+          id: true,
+          title: true,
+        },
+      },
+    });
+    return article.translate(locale);
+  }
 }

+ 0 - 8
packages/frontend/src/views/login/api.js

@@ -1,11 +1,3 @@
-/**********************************
- * @Author: Ronnie Zhang
- * @LastEditor: Ronnie Zhang
- * @LastEditTime: 2023/12/05 21:28:30
- * @Email: zclzone@outlook.com
- * Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
- **********************************/
-
 import { request } from '@/utils'
 
 export default {

+ 4 - 3
packages/frontend/src/views/menu/list.vue

@@ -71,7 +71,7 @@
         </n-form-item>
         <n-form-item
           label="分类" path="categoryId" :rule="{
-            required: false,
+            required: modalForm.level === 0 ? false : true,
             type: 'number',
             trigger: ['change', 'blur'],
             message: '请输入分类',
@@ -156,6 +156,7 @@
 
 <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'
@@ -294,7 +295,7 @@ async function handleEnable(row) {
     row.enableLoading = false
     $message.success('操作成功')
     $table.value?.handleSearch()
-  }
+  }
   catch (error) {
     console.error(error)
     row.enableLoading = false
@@ -346,7 +347,7 @@ async function handleFormEdit(data = {}) {
       status: 'finished',
       url: modalForm.value.cover,
     }]
-  }
+  }
   else {
     previewFileList.value = []
   }

+ 3 - 0
packages/web/components.d.ts

@@ -7,8 +7,11 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    Footer: typeof import('./src/components/footer.vue')['default']
+    Header: typeof import('./src/components/header.vue')['default']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SubHeader: typeof import('./src/components/subHeader.vue')['default']
   }
 }

+ 11 - 0
packages/web/src/api/article.ts

@@ -0,0 +1,11 @@
+import { request } from '@/utils/http'
+import type { ResultData } from '@/utils/http'
+
+export type ArticleDetailType = {
+  id: number
+  title: string
+  content: string
+
+}
+export const getArticleDetail = (id: number): Promise<ResultData<ArticleDetailType>> =>
+  request.get(`web/article/${id}`)

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

@@ -1 +1,2 @@
 export * from './menu'
+export * from './article'

BIN
packages/web/src/assets/img/banner_bg2.png


BIN
packages/web/src/assets/img/icon_webchat.png


+ 0 - 18
packages/web/src/components/HelloWorld.vue

@@ -1,18 +0,0 @@
-<script setup lang="ts">
-defineProps<{ msg: string }>()
-
-const { availableLocales, locale } = useI18n()
-
-function toggleLanguage() {
-  const currentIndex = availableLocales.indexOf(locale.value)
-  const newIndex = (currentIndex + 1) % availableLocales.length
-  locale.value = availableLocales[newIndex]
-}
-</script>
-
-<template>
-  <button type="button" @click="toggleLanguage">
-    {{ $t('Switch language') }}
-  </button>
-  <h3>{{ $t(msg) }}</h3>
-</template>

File diff suppressed because it is too large
+ 105 - 0
packages/web/src/components/footer.vue


packages/web/src/layouts/header.vue → packages/web/src/components/header.vue


+ 43 - 0
packages/web/src/components/subHeader.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="header grid min-h-[160px] w-full bg-cyan-800 flex flex-col">
+    <div class="w-full h-[100px] flex max-w-screen-xl my-0 mx-auto justify-between items-center">
+      <div class="logo">
+        <img src="../assets/img/logo_4dge_cn.png" alt="logo" />
+      </div>
+      <div class="menu">
+        <n-dropdown :options="options" @select="handleSelect">
+          <n-button type="primary" ghost>{{ locale === 'zh' ? $t('zh') : $t('en') }}</n-button>
+        </n-dropdown>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { NH1, NInput, NIcon, NDropdown, NButton } from 'naive-ui'
+import { SearchOutline } from '@vicons/ionicons5'
+
+const { t, locale } = useI18n()
+
+const options = ref([
+  {
+    label: t('zh'),
+    key: 'zh',
+  },
+  {
+    label: t('en'),
+    key: 'en',
+  },
+])
+
+const handleSelect = (key: string) => {
+  locale.value = key
+}
+</script>
+<style>
+.header {
+  background-image: url('@/assets/img/banner_bg2.png');
+  background-repeat: no-repeat;
+  background-size: cover;
+  background-position: top center;
+}
+</style>

+ 22 - 0
packages/web/src/layouts/base.vue

@@ -0,0 +1,22 @@
+<template>
+  <n-config-provider :theme="darkTheme" :locale="zhCN" :date-locale="dateZhCN">
+    <div class="layout w-auto h-full">
+      <sub-header></sub-header>
+      <RouterView class="min-h-2xl" />
+
+      <Footer></Footer>
+    </div>
+  </n-config-provider>
+</template>
+<script setup lang="ts">
+import { NConfigProvider } from 'naive-ui'
+import { zhCN, dateZhCN } from 'naive-ui'
+import SubHeader from '../components/subHeader.vue'
+import Footer from '../components/footer.vue'
+// import { createTheme, inputDark, datePickerDark } from 'naive-ui'
+// createTheme([inputDark, datePickerDark])
+const darkTheme = null
+const route = useRoute()
+</script>
+
+<style></style>

+ 0 - 17
packages/web/src/layouts/blank.vue

@@ -1,17 +0,0 @@
-<script setup lang="ts">
-const route = useRoute()
-</script>
-
-<template>
-  <h1>{{ route.meta.title }}xxxxx</h1>
-  <div class="navigation">
-    <RouterLink to="/">
-      <h2>Home</h2>
-    </RouterLink>
-    <RouterLink to="/about">
-      <h2>About</h2>
-    </RouterLink>
-  </div>
-
-  <RouterView />
-</template>

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

@@ -20,8 +20,8 @@
 <script setup lang="ts">
 import { NConfigProvider } from 'naive-ui'
 import { zhCN, dateZhCN } from 'naive-ui'
-import Header from './header.vue'
-import Footer from './footer.vue'
+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

+ 0 - 57
packages/web/src/layouts/footer.vue

@@ -1,57 +0,0 @@
-<template>
-  <div class="footer grid min-h-sm w-full bg-gray-800 text-white">
-    <div class="w-full h-full flex flex-col justify-between max-w-screen-xl my-0 mx-auto">
-      <div class="top w-full h-full mt-[60px] flex flex-row">
-        <div class="left flex flex-col w-auto">
-          <img src="@/assets/img/logo.png" alt="logo" class="h-[43px] w-[160px] mb-5" />
-          <span class="font-size-[16px]" > 联系我们</span>
-          <span class="font-size-[24px] my-[10px]"> 400-669-8025</span>
-          <span class="font-size-[16px] my-1"> 销售合作:sales@4dage.com</span>
-          <span class="font-size-[16px] my-1"> 媒体采访:pr@4dage.com</span>
-        </div>
-        <div class=" flex-1 middle">
-
-          xxxxx
-        </div>
-        <div>xxxxx</div>
-      </div>
-      <div class="bottom h-[82px] flex flex-row flex items-center justify-between">
-        <div class="label">Copyright © 2022 4DAGE Co., Ltd. All rights reserved.</div>
-        <div class="label icp">
-          <a
-            target="_blank"
-            href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44049102496647"
-            class="text-white min-h-[20px] inline-flex items-center justify-center mr-3 decoration-none"
-          >
-            <img
-              src="https://4dscene.4dage.com/new4dkk/v2/images/src/assets/images/baicon.d0289dc.png"
-              alt=""
-            />
-            粤公网安备 44049102496647号
-          </a>
-          <a
-            target="_blank"
-            href="https://beian.miit.gov.cn/"
-            class="text-white min-h-[20px] inline-flex items-center justify-center mr-3 decoration-none"
-            ><img
-              src="https://4dscene.4dage.com/new4dkk/v2/images/src/assets/images/baicon.d0289dc.png"
-              alt=""
-            />
-            粤ICP备14078495号
-          </a>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-<script setup lang="ts"></script>
-
-<style scoped>
-.footer {
-  background: linear-gradient(180deg, #0661c9 0%, #033063 100%);
-}
-
-.bottom {
-  border-top: 1px solid rgba(255, 255, 255, 0.1);
-}
-</style>

+ 1 - 1
packages/web/src/pages/about.vue

@@ -5,5 +5,5 @@
 <route lang="yaml">
 meta:
   title: About
-  layout: blank
+  layout: base
 </route>

+ 23 - 3
packages/web/src/pages/index.vue

@@ -11,6 +11,7 @@
                 class="show-item b-rd-3xl relative"
                 :class="{ [`style-${item.styleType}`]: true }"
                 :style="{ backgroundImage: `url(${child.cover})` }"
+                @click="handleToDoc(child)"
               >
                 <div
                   class="w-full h-full flex flex-col absolute top-0 left-0 -mx-auto justify-end overflow-hidden shadow-blueGray"
@@ -33,7 +34,11 @@
             @swiper="onSwiper"
             @slideChange="onSlideChange"
           >
-            <swiper-slide v-for="child of item.children" :key="child.id">
+            <swiper-slide
+              v-for="child of item.children"
+              :key="child.id"
+              @click="handleToDoc(child)"
+            >
               <div
                 class="cover w-full h-[400px] overflow-hidden b-rd-xl"
                 :style="{ backgroundImage: `url(${child.cover})` }"
@@ -57,6 +62,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)"
                 :class="{ [`style-${item.styleType}`]: true }"
                 class="show-item b-rd-3xl relative w-full h-full"
               >
@@ -88,6 +94,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)"
                 :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"
               >
@@ -95,7 +102,9 @@
 
                 <div class="font-size-[20px] font-bold my-[16px]">{{ child.title }}</div>
 
-                <div class="color-[#909090] w-[calc(100%-30px)] text-center">{{ child.description }}</div>
+                <div class="color-[#909090] w-[calc(100%-30px)] text-center">
+                  {{ child.description }}
+                </div>
               </div>
             </n-gi>
           </n-grid>
@@ -107,7 +116,7 @@
 
 <route lang="yaml">
 meta:
-title: About
+  title: About
 </route>
 
 <script setup lang="ts">
@@ -118,6 +127,7 @@ import { Swiper, SwiperSlide } from 'swiper/vue'
 
 // Import Swiper styles
 import 'swiper/css'
+import router from '@/plugins/router.ts'
 
 const list = ref<MenuItem[]>([])
 
@@ -133,6 +143,16 @@ const onSwiper = (swiper) => {
 const onSlideChange = () => {
   console.log('slide change')
 }
+
+const handleToDoc = (child: never) => {
+  const { articleId, categoryId } = child
+  console.log(articleId, categoryId)
+  if (articleId) {
+    router.push({ path: '/showdoc', query: { id: articleId } })
+  } else {
+    router.push({ path: '/showdoc', query: { cid: categoryId } })
+  }
+}
 </script>
 
 <style lang="scss" scoped>

+ 26 - 4
packages/web/src/pages/showdoc.vue

@@ -1,17 +1,39 @@
 <template>
-  <div class="max-w-screen-xl content my-0 mx-auto">
-    showdoc1
-    showdoc2
+  <div class="max-w-screen-xl content my-0 mx-auto text-size-base">
+    <template v-if="detail">
+      <n-h1> {{ detail.title }}</n-h1>
+      <div v-html="detail.content"></div>
+    </template>
   </div>
 </template>
 
 <route lang="yaml">
 meta:
-title: showdoc
+  title: showdoc
+  layout: base
 </route>
 
 <script setup lang="ts">
+import { type ArticleDetailType, getArticleDetail } from '@/api'
+import { NH1 } from 'naive-ui'
 
+const route = useRoute()
+const id = route.query.id
+
+console.log('route', route)
+
+const detail = ref<ArticleDetailType | undefined>()
+
+watchEffect(() => {
+  if (id) {
+    getArticleDetail(+id).then((data) => {
+      if (data.data) {
+        console.log(data)
+        detail.value = data.data
+      }
+    })
+  }
+})
 </script>
 
 <style lang="scss" scoped></style>

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

@@ -46,11 +46,11 @@ class RequestHttp {
      */
     this.service.interceptors.request.use(
       (config: AxiosRequestConfig) => {
-        // const token = localStorage.getItem('token') || ''
+        const locale = localStorage.getItem('locale') || 'zh'
         return {
           ...config,
           headers: {
-            // 'x-access-token': token, // 请求头中携带token信息
+            'locale': locale, // 请求头中携带token信息
           },
         }
       },