gemercheung 7 ay önce
ebeveyn
işleme
6bb35868f5

+ 2 - 0
.gitignore

@@ -1 +1,3 @@
 node_modules
+dist
+.vscode

+ 13 - 0
package.json

@@ -0,0 +1,13 @@
+{
+  "name": "helpercenter",
+  "version": "1.0.0",
+  "description": "helpercenter",
+  "dependencies": {
+  },
+  "devDependencies": {},
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "gemercheung@gmail.com",
+  "license": "ISC"
+}

+ 0 - 3
packages/.gitignore

@@ -1,3 +0,0 @@
-node_modules
-dist
-.vscode

+ 22 - 2
packages/backend/src/modules/article/article.controller.ts

@@ -1,8 +1,18 @@
 import { JwtGuard } from '@/common/guards';
-import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
+import {
+  Body,
+  Controller,
+  Delete,
+  Get,
+  Param,
+  Patch,
+  Post,
+  Query,
+  UseGuards,
+} from '@nestjs/common';
 import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
 import { ArticleService } from './article.service';
-import { CreateArticleDto, GetArticleDto, QueryArticleDto } from './dto';
+import { CreateArticleDto, GetArticleDto, QueryArticleDto, UpdateArticleDto } from './dto';
 
 @Controller('article')
 @ApiTags('article')
@@ -25,4 +35,14 @@ export class ArticleController {
   findPagination(@Query() queryDto: QueryArticleDto) {
     return this.articleService.findPagination(queryDto);
   }
+
+  @Delete(':id')
+  remove(@Param('id') id: string) {
+    return this.articleService.remove(+id);
+  }
+
+  @Patch(':id')
+  update(@Param('id') id: string, @Body() updateCategoryDto: UpdateArticleDto) {
+    return this.articleService.update(+id, updateCategoryDto);
+  }
 }

+ 2 - 2
packages/backend/src/modules/article/article.entity.ts

@@ -17,7 +17,7 @@ export class Article {
   @PrimaryGeneratedColumn()
   id: number;
 
-  @Column({ unique: true, length: 200 })
+  @Column({ unique: false, default: '', length: 200 })
   title: string;
 
   @Column({ default: true })
@@ -26,7 +26,7 @@ export class Article {
   @Column({ default: false })
   isShow: boolean;
 
-  @Column({ type: 'longtext' })
+  @Column({ type: 'longtext', nullable: true })
   content: string;
 
   @Column({ default: '' })

+ 12 - 3
packages/backend/src/modules/article/article.service.ts

@@ -1,8 +1,8 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
 import { Article } from './article.entity';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Like, Repository } from 'typeorm';
-import { CreateArticleDto, GetArticleDto, QueryArticleDto } from './dto';
+import { CreateArticleDto, GetArticleDto, QueryArticleDto, UpdateArticleDto } from './dto';
 @Injectable()
 export class ArticleService {
   constructor(
@@ -48,7 +48,8 @@ export class ArticleService {
       },
       relations: { user: true, category: true },
       order: {
-        title: 'DESC',
+        // title: 'ASC',
+        updateTime: 'ASC',
       },
       take: pageSize,
       skip: (pageNo - 1) * pageSize,
@@ -63,4 +64,12 @@ export class ArticleService {
     await this.articleRepo.delete(id);
     return true;
   }
+
+  async update(id: number, updateArticleDto: UpdateArticleDto) {
+    const article = await this.articleRepo.findOne({ where: { id } });
+    if (!article) throw new BadRequestException('权限不存在或者已删除');
+    const updateArticle = this.articleRepo.merge(article, updateArticleDto);
+    await this.articleRepo.save(updateArticle);
+    return true;
+  }
 }

+ 29 - 1
packages/backend/src/modules/article/dto.ts

@@ -62,4 +62,32 @@ export class GetArticleDto {
 
 export class QueryArticleDto extends GetArticleDto {}
 
-export class UpdateArticleDto extends PartialType(CreateArticleDto) {}
+export class UpdateArticleDto {
+  @ApiProperty()
+  @IsString()
+  @IsOptional()
+  @Length(1, 200, {
+    message: `用户名长度必须大于$constraint1到$constraint2之间,当前传递的值是$value`,
+  })
+  title?: string;
+
+  @ApiProperty({ required: false })
+  @IsOptional()
+  @IsString()
+  content?: string;
+
+  @ApiProperty({ required: false })
+  @IsBoolean()
+  @IsOptional()
+  enable?: boolean;
+
+  @ApiProperty({ required: false })
+  @IsOptional()
+  @IsNumber()
+  userId?: number;
+
+  @ApiProperty({ required: false })
+  @IsOptional()
+  @IsNumber()
+  categoryId?: number;
+}

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

@@ -30,6 +30,10 @@ export class CategoryController {
   getAllCategories(@Query() getCategoryDto: GetCategoryDto) {
     return this.categoryService.findAll(getCategoryDto);
   }
+  @Get('page')
+  findPagination(@Query() queryDto: GetCategoryDto) {
+    return this.categoryService.findPagination(queryDto);
+  }
 
   @Delete(':id')
   remove(@Param('id') id: string) {

+ 24 - 1
packages/backend/src/modules/category/category.service.ts

@@ -39,6 +39,28 @@ export class CategoryService {
     return { pageData, total };
   }
 
+  async findPagination(query: GetCategoryDto) {
+    const pageSize = query.pageSize || 10;
+    const pageNo = query.pageNo || 1;
+    const [data, total] = await this.categoryRepo.findAndCount({
+      where: {
+        title: Like(`%${query.title || ''}%`),
+        enable: query.enable || undefined,
+      },
+      relations: { parent: true },
+      order: {
+        // title: 'ASC',
+        updateTime: 'ASC',
+      },
+      take: pageSize,
+      skip: (pageNo - 1) * pageSize,
+    });
+    const pageData = data.map((item) => {
+      return { ...item };
+    });
+    return { pageData, total };
+  }
+
   async remove(id: number) {
     await this.categoryRepo.delete(id);
     return true;
@@ -47,7 +69,8 @@ export class CategoryService {
   async update(id: number, updateCategoryDto: UpdateCategoryDto) {
     const category = await this.categoryRepo.findOne({ where: { id } });
     if (!category) throw new BadRequestException('权限不存在或者已删除');
-    await this.categoryRepo.save(updateCategoryDto);
+    const updateCategory = this.categoryRepo.merge(category, updateCategoryDto);
+    await this.categoryRepo.save(updateCategory);
     return true;
   }
 }

+ 1 - 1
packages/backend/src/modules/category/dto.ts

@@ -25,7 +25,7 @@ export class CreateCategoryDto {
   @IsOptional()
   enable?: boolean;
 
-  @ApiProperty({ required: false })
+  @ApiProperty({ nullable: true, required: false })
   @IsNumber()
   @IsOptional()
   parentId?: number;

+ 1 - 1
packages/backend/src/modules/permission/permission.service.ts

@@ -83,6 +83,6 @@ export class PermissionService {
     const allMenu = await this.permissionRepo.find({
       where: { type: 'MENU' },
     });
-    return allMenu.some((menu) => menu.path && pathToRegexp(menu.path).test(path))
+    return allMenu.some((menu) => menu.path && pathToRegexp(menu.path).test(path));
   }
 }

+ 3 - 0
packages/frontend/eslint.config.js

@@ -11,6 +11,9 @@ export default antfu({
     'vue/block-order': 'off',
     '@typescript-eslint/no-this-alias': 'off',
     'prefer-promise-reject-errors': 'off',
+    'no-unused-vars': 'off',
+    'unused-imports/no-unused-vars': 'off',
+    'no-console': 'off',
   },
   languageOptions: {
     globals: {

+ 0 - 6
packages/frontend/package.json

@@ -51,11 +51,5 @@
     "vite-plugin-router-warn": "^1.0.0",
     "vite-plugin-vue-devtools": "^7.6.8",
     "vue3-intro-step": "^1.0.5"
-  },
-  "simple-git-hooks": {
-    "pre-commit": "pnpm lint-staged"
-  },
-  "lint-staged": {
-    "*": "eslint --fix"
   }
 }

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

@@ -18,7 +18,7 @@
 <script setup>
 import { router } from '@/router'
 import Cherry from 'cherry-markdown'
-import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
+import { NButton } from 'naive-ui'
 import { onMounted } from 'vue'
 import 'cherry-markdown/dist/cherry-markdown.css'
 
@@ -38,5 +38,5 @@ onMounted(() => {
   console.log('cherryInstance', cherryInstance)
 })
 
-function handleAdd() {}
+// function handleAdd() {}
 </script>

+ 3 - 3
packages/frontend/src/views/article/api.js

@@ -1,10 +1,10 @@
 import { request } from '@/utils'
 
 export default {
-  create: data => request.post('/role', data),
+  create: data => request.post('/article', data),
   read: (params = {}) => request.get('/article/page', { params }),
-  update: data => request.patch(`/role/${data.id}`, data),
-  delete: id => request.delete(`/role/${id}`),
+  update: data => request.patch(`/article/${data.id}`, data),
+  delete: id => request.delete(`/article/${id}`),
 
   getAllPermissionTree: () => request.get('/permission/tree'),
   getAllUsers: (params = {}) => request.get('/user', { params }),

+ 6 - 19
packages/frontend/src/views/article/index.vue

@@ -70,7 +70,7 @@
             :checked-keys="modalForm.permissionIds"
             :on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
 
-            default-expand-all checkable check-on-click
+            checkable check-on-click default-expand-all
             class="cus-scroll max-h-200 w-full"
           />
         </n-form-item>
@@ -118,8 +118,9 @@ const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete,
   })
 
 const columns = [
-  { title: '角色名', key: 'name' },
-  { title: '角色编码', key: 'code' },
+  { title: '标题名', key: 'title' },
+  { title: '分类', key: 'category.title' },
+  { title: '内容', key: 'content' },
   {
     title: '状态',
     key: 'enable',
@@ -143,8 +144,8 @@ const columns = [
   {
     title: '操作',
     key: 'actions',
-    width: 320,
-    align: 'right',
+    width: 200,
+    align: 'center',
     fixed: 'right',
     render(row) {
       return [
@@ -153,20 +154,6 @@ const columns = [
           {
             size: 'small',
             type: 'primary',
-            secondary: true,
-            onClick: () =>
-              router.push({ path: `/pms/role/user/${row.id}`, query: { roleName: row.name } }),
-          },
-          {
-            default: () => '分配用户',
-            icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
-          },
-        ),
-        h(
-          NButton,
-          {
-            size: 'small',
-            type: 'primary',
             style: 'margin-left: 12px;',
             disabled: row.code === 'SUPER_ADMIN',
             onClick: () => handleEdit(row),

+ 42 - 0
packages/frontend/src/views/category/add.vue

@@ -0,0 +1,42 @@
+<template>
+  <CommonPage show-footer>
+    <template #action>
+      <NButton type="primary" @click="router.push('article/add')">
+        <i class="i-material-symbols:add mr-4 text-18" />
+        创建新文章
+      </NButton>
+    </template>
+
+    <!-- <n-space size="large"> -->
+    <div style="min-height: 500px;">
+      <div id="markdown-container" />
+    </div>
+    <!-- </n-space> -->
+  </CommonPage>
+</template>
+
+<script setup>
+import { router } from '@/router'
+import Cherry from 'cherry-markdown'
+import { NButton } from 'naive-ui'
+import { onMounted } from 'vue'
+import 'cherry-markdown/dist/cherry-markdown.css'
+
+const articleValue = ref('')
+
+onMounted(() => {
+  const cherryInstance = new Cherry({
+    id: 'markdown-container',
+    value: articleValue.value,
+    editor: {
+      theme: 'default',
+      height: '100%',
+      defaultModel: '',
+    },
+  })
+
+  console.log('cherryInstance', cherryInstance)
+})
+
+function handleAdd() {}
+</script>

+ 13 - 0
packages/frontend/src/views/category/api.js

@@ -0,0 +1,13 @@
+import { request } from '@/utils'
+
+export default {
+  create: data => request.post('/category', data),
+  read: (params = {}) => request.get('/category/page', { params }),
+  update: data => request.patch(`/category/${data.id}`, data),
+  delete: id => request.delete(`/category/${id}`),
+
+  getAllPermissionTree: () => request.get('/permission/tree'),
+  getAllUsers: (params = {}) => request.get('/user', { params }),
+  addRoleUsers: (roleId, data) => request.patch(`/role/users/add/${roleId}`, data),
+  removeRoleUsers: (roleId, data) => request.patch(`/role/users/remove/${roleId}`, data),
+}

+ 204 - 0
packages/frontend/src/views/category/index.vue

@@ -0,0 +1,204 @@
+<template>
+  <CommonPage>
+    <template #action>
+      <NButton type="primary" @click="router.push('article/add')">
+        <i class="i-material-symbols:add mr-4 text-18" />
+        新增文章
+      </NButton>
+    </template>
+
+    <MeCrud
+      ref="$table"
+      v-model:query-items="queryItems"
+      :scroll-x="1200"
+      :columns="columns"
+      :get-data="api.read"
+    >
+      <MeQueryItem label="分类名称" :label-width="80">
+        <n-input v-model:value="queryItems.title" type="text" placeholder="请输入分类名称" clearable>
+          <template #password-visible-icon />
+        </n-input>
+      </MeQueryItem>
+      <MeQueryItem label="状态" :label-width="50">
+        <n-select
+          v-model:value="queryItems.enable"
+          clearable
+          :options="[
+            { label: '启用', value: 1 },
+            { label: '停用', value: 0 },
+          ]"
+        />
+      </MeQueryItem>
+    </MeCrud>
+
+    <MeModal ref="modalRef" width="520px">
+      <n-form
+        ref="modalFormRef"
+        label-placement="left"
+        label-align="left"
+        :label-width="80"
+        :model="modalForm"
+      >
+        <n-form-item
+          label="角色名"
+          path="name"
+          :rule="{
+            required: true,
+            message: '请输入角色名',
+            trigger: ['input', 'blur'],
+          }"
+        >
+          <n-input v-model:value="modalForm.name" />
+        </n-form-item>
+        <n-form-item
+          label="角色编码"
+          path="code"
+          :rule="{
+            required: true,
+            message: '请输入角色编码',
+            trigger: ['input', 'blur'],
+          }"
+        >
+          <n-input v-model:value="modalForm.code" :disabled="modalAction !== 'add'" />
+        </n-form-item>
+        <n-form-item label="权限" path="permissionIds">
+          <n-tree
+            key-field="id"
+            label-field="name"
+            :selectable="false"
+            :data="permissionTree"
+            :checked-keys="modalForm.permissionIds"
+            :on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
+
+            default-expand-all checkable check-on-click
+            class="cus-scroll max-h-200 w-full"
+          />
+        </n-form-item>
+        <n-form-item label="状态" path="enable">
+          <NSwitch v-model:value="modalForm.enable">
+            <template #checked>
+              启用
+            </template>
+            <template #unchecked>
+              停用
+            </template>
+          </NSwitch>
+        </n-form-item>
+      </n-form>
+    </MeModal>
+  </CommonPage>
+</template>
+
+<script setup>
+import { MeCrud, MeModal, MeQueryItem } from '@/components'
+import { useCrud } from '@/composables'
+import { NButton, NSwitch } from 'naive-ui'
+import api from './api'
+
+defineOptions({ name: 'RoleMgt' })
+
+const router = useRouter()
+
+const $table = ref(null)
+/** QueryBar筛选参数(可选) */
+const queryItems = ref({})
+
+onMounted(() => {
+  $table.value?.handleSearch()
+})
+
+const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit }
+  = useCrud({
+    name: '文章',
+    doCreate: api.create,
+    doDelete: api.delete,
+    doUpdate: api.update,
+    initForm: { enable: true },
+    refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
+  })
+
+const columns = [
+  { title: '分类名称', key: 'title' },
+  { title: '上级分类', key: 'parent.title' },
+  { title: '备注', key: 'remark' },
+  { title: '创建人', key: 'user' },
+  { title: '创建时间', key: 'createTime' },
+  {
+    title: '状态',
+    key: 'enable',
+    render: row =>
+      h(
+        NSwitch,
+        {
+          size: 'small',
+          rubberBand: false,
+          value: row.enable,
+          loading: !!row.enableLoading,
+          disabled: row.code === 'SUPER_ADMIN',
+          onUpdateValue: () => handleEnable(row),
+        },
+        {
+          checked: () => '启用',
+          unchecked: () => '停用',
+        },
+      ),
+  },
+  {
+    title: '操作',
+    key: 'actions',
+    width: 200,
+    align: 'center',
+    fixed: 'right',
+    render(row) {
+      return [
+        h(
+          NButton,
+          {
+            size: 'small',
+            type: 'primary',
+            style: 'margin-left: 12px;',
+            disabled: row.code === 'SUPER_ADMIN',
+            onClick: () => handleEdit(row),
+          },
+          {
+            default: () => '编辑',
+            icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
+          },
+        ),
+
+        h(
+          NButton,
+          {
+            size: 'small',
+            type: 'error',
+            style: 'margin-left: 12px;',
+            disabled: row.code === 'SUPER_ADMIN',
+            onClick: () => handleDelete(row.id),
+          },
+          {
+            default: () => '删除',
+            icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
+          },
+        ),
+      ]
+    },
+  },
+]
+
+async function handleEnable(row) {
+  row.enableLoading = true
+  try {
+    await api.update({ id: row.id, enable: !row.enable })
+    row.enableLoading = false
+    $message.success('操作成功')
+    $table.value?.handleSearch()
+  }
+  catch (error) {
+    console.error(error)
+    row.enableLoading = false
+  }
+}
+
+const permissionTree = ref([])
+api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
+</script>

+ 0 - 1
packages/frontend/src/views/home/index.vue

@@ -158,7 +158,6 @@ import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/compon
 import * as echarts from 'echarts/core'
 import { UniversalTransition } from 'echarts/features'
 import { CanvasRenderer } from 'echarts/renderers'
-import VChart from 'vue-echarts'
 
 const userStore = useUserStore()
 

+ 1 - 1
packages/frontend/src/views/pms/resource/components/MenuTree.vue

@@ -29,7 +29,7 @@
         key-field="code"
         label-field="name"
 
-        block-line default-expand-all
+        default-expand-all block-line
       />
     </n-space>
 

+ 0 - 11
packages/package.json

@@ -1,11 +0,0 @@
-{
-    "name": "helpercenter",
-    "version": "1.0.0",
-    "engines": {
-        "node": ">=10",
-        "pnpm": ">=3"
-    },
-    "scripts": {
-        
-    }
-}

+ 2 - 0
pnpm-lock.yaml

@@ -6,6 +6,8 @@ settings:
 
 importers:
 
+  .: {}
+
   packages/backend:
     dependencies:
       '@nestjs/common':