gemercheung 7 mesi fa
parent
commit
26ddd9e069

+ 2 - 0
packages/backend/package.json

@@ -28,6 +28,8 @@
     "bcryptjs": "^2.4.3",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.0",
+    "date-fns": "^4.1.0",
+    "dayjs": "^1.11.13",
     "express-session": "^1.17.3",
     "moment": "^2.30.1",
     "mysql2": "^3.6.3",

+ 3 - 1
packages/backend/src/app.module.ts

@@ -12,7 +12,9 @@ import { LoggerModule } from 'nestjs-pino';
 import { MenuModule } from './modules/menu/menu.module';
 import { HttpLoggerMiddleware } from './common/middleware/httpOperationMiddleware';
 import { OperationLogModule } from './modules/operation-log/operation-log.module';
+import { MenuService } from './modules/menu/menu.service';
 import { OperationLogService } from './modules/operation-log/operation-log.service';
+
 @Module({
   imports: [
     LoggerModule.forRoot({
@@ -43,7 +45,7 @@ import { OperationLogService } from './modules/operation-log/operation-log.servi
 
     MenuModule,
 
-    OperationLogModule,
+    OperationLogModule.forRoot(),
   ],
   controllers: [],
   providers: [],

+ 16 - 6
packages/backend/src/common/middleware/httpOperationMiddleware.ts

@@ -1,24 +1,34 @@
-import { MenuService } from '@/modules/menu/menu.service';
+import { CreateOperationLogDto } from '@/modules/operation-log/dto/create-operation-log.dto';
 import { OperationLogService } from '@/modules/operation-log/operation-log.service';
 import { Inject, Injectable, Logger, NestMiddleware } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
 import { NextFunction, Request, Response } from 'express';
 
 @Injectable()
 export class HttpLoggerMiddleware implements NestMiddleware {
   constructor(
-    // @Inject(OperationLogService) private readonly operationLogService: OperationLogService,
+    @Inject(OperationLogService) private readonly operationLogService: OperationLogService,
   ) {}
 
   private logger = new Logger();
 
-  use(request: Request, response: Response, next: NextFunction): void {
+  use(request: any, response: Response, next: NextFunction): void {
     const { ip, method, originalUrl } = request;
-    const ips = request.ip || request.connection.remoteAddress;
-
     response.on('finish', () => {
       const msg = `${ip} ${method} ${originalUrl}`;
       this.logger.log(msg);
-      console.log('msg', msg);
+      if (request.user) {
+        // console.log('userId', request.user.userId);
+        const logDto: CreateOperationLogDto = {
+          title: msg,
+          requestUrl: originalUrl,
+          ip: ip,
+          method: method,
+          userId: request.user.userId,
+        };
+        this.operationLogService.create(logDto);
+        // console.log('operationLogService');
+      }
     });
 
     next();

+ 9 - 0
packages/backend/src/common/utils/index.ts

@@ -0,0 +1,9 @@
+import * as dayjs from 'dayjs';
+import * as customParseFormat from 'dayjs/plugin/customParseFormat.js';
+import * as utc from 'dayjs/plugin/utc.js';
+import * as timezone from 'dayjs/plugin/timezone.js';
+dayjs.extend(customParseFormat);
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+export { dayjs };

+ 20 - 3
packages/backend/src/modules/operation-log/dto/create-operation-log.dto.ts

@@ -15,8 +15,25 @@ export class CreateOperationLogDto {
   @ApiProperty()
   @IsString()
   @IsNotEmpty({ message: '标题不能为空' })
-  @Length(1, 200, {
-    message: `用户名长度必须大于$constraint1到$constraint2之间,当前传递的值是$value`,
-  })
   title: string;
+
+  @ApiProperty()
+  @IsString()
+  @IsOptional()
+  requestUrl?: string;
+
+  @ApiProperty()
+  @IsString()
+  @IsOptional()
+  ip?: string;
+
+  @ApiProperty()
+  @IsString()
+  @IsOptional()
+  method?: string;
+
+  @ApiProperty()
+  @IsNumber()
+  @IsNotEmpty({ message: 'userId不能为空' })
+  userId: number;
 }

+ 34 - 0
packages/backend/src/modules/operation-log/dto/query.dto.ts

@@ -0,0 +1,34 @@
+import { ApiProperty, ApiBody, PartialType } from '@nestjs/swagger';
+import { Exclude } from 'class-transformer';
+import {
+  Allow,
+  IsArray,
+  IsBoolean,
+  IsNotEmpty,
+  IsNumber,
+  IsOptional,
+  IsString,
+  Length,
+} from 'class-validator';
+
+export class QueryOperationLogDto {
+  @ApiProperty({ required: false })
+  @Allow()
+  pageSize?: number;
+
+  @ApiProperty({ required: false })
+  @Allow()
+  pageNo?: number;
+
+  @ApiProperty({ required: false })
+  @Allow()
+  title?: string;
+
+  @ApiProperty({ required: false })
+  @Allow()
+  enable?: boolean;
+
+  @ApiProperty({ required: false })
+  @Allow()
+  times?: [string, string];
+}

+ 16 - 6
packages/backend/src/modules/operation-log/entities/operation-log.entity.ts

@@ -1,9 +1,12 @@
+import { User } from '@/modules/user/user.entity';
 import {
   Column,
   Entity,
   PrimaryGeneratedColumn,
   CreateDateColumn,
   UpdateDateColumn,
+  OneToOne,
+  JoinColumn,
 } from 'typeorm';
 
 @Entity()
@@ -12,10 +15,7 @@ export class OperationLog {
   id: number;
 
   @Column({ unique: false, default: '' })
-  title: string;
-
-  @Column({ unique: false, default: '' })
-  userName: string;
+  method: string;
 
   @Column({ unique: false, default: '' })
   requestUrl: string;
@@ -26,6 +26,16 @@ export class OperationLog {
   @CreateDateColumn()
   createTime: Date;
 
-  @UpdateDateColumn()
-  updateTime: Date;
+  // @UpdateDateColumn()
+  // updateTime: Date;
+
+  @OneToOne(() => User, {
+    createForeignKeyConstraints: false,
+    onDelete: 'CASCADE',
+  })
+  @JoinColumn()
+  user: User;
+
+  @Column({ nullable: true })
+  userId: number;
 }

+ 13 - 19
packages/backend/src/modules/operation-log/operation-log.controller.ts

@@ -1,7 +1,8 @@
-import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
+import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
 import { OperationLogService } from './operation-log.service';
 import { CreateOperationLogDto } from './dto/create-operation-log.dto';
 import { UpdateOperationLogDto } from './dto/update-operation-log.dto';
+import { QueryOperationLogDto } from './dto/query.dto';
 
 @Controller('operation-log')
 export class OperationLogController {
@@ -12,23 +13,16 @@ export class OperationLogController {
     return this.operationLogService.create(createOperationLogDto);
   }
 
-  @Get()
-  findAll() {
-    return this.operationLogService.findAll();
-  }
-
-  @Get(':id')
-  findOne(@Param('id') id: string) {
-    return this.operationLogService.findOne(+id);
-  }
-
-  @Patch(':id')
-  update(@Param('id') id: string, @Body() updateOperationLogDto: UpdateOperationLogDto) {
-    return this.operationLogService.update(+id, updateOperationLogDto);
-  }
-
-  @Delete(':id')
-  remove(@Param('id') id: string) {
-    return this.operationLogService.remove(+id);
+  @Get('page')
+  findPagination(@Query() queryDto: QueryOperationLogDto) {
+    return this.operationLogService.findPagination(queryDto);
   }
+  // @Patch(':id')
+  // update(@Param('id') id: string, @Body() updateOperationLogDto: UpdateOperationLogDto) {
+  //   return this.operationLogService.update(+id, updateOperationLogDto);
+  // }
+  // @Delete(':id')
+  // remove(@Param('id') id: string) {
+  //   return this.operationLogService.remove(+id);
+  // }
 }

+ 10 - 2
packages/backend/src/modules/operation-log/operation-log.module.ts

@@ -1,4 +1,4 @@
-import { Module } from '@nestjs/common';
+import { DynamicModule, Module } from '@nestjs/common';
 import { OperationLogService } from './operation-log.service';
 import { OperationLogController } from './operation-log.controller';
 import { TypeOrmModule } from '@nestjs/typeorm';
@@ -8,4 +8,12 @@ import { OperationLog } from './entities/operation-log.entity';
   controllers: [OperationLogController],
   providers: [OperationLogService],
 })
-export class OperationLogModule {}
+export class OperationLogModule {
+  static forRoot(): DynamicModule {
+    return {
+      module: OperationLogModule,
+      providers: [OperationLogService],
+      exports: [OperationLogService],
+    };
+  }
+}

+ 45 - 2
packages/backend/src/modules/operation-log/operation-log.service.ts

@@ -3,14 +3,19 @@ import { CreateOperationLogDto } from './dto/create-operation-log.dto';
 import { UpdateOperationLogDto } from './dto/update-operation-log.dto';
 import { InjectRepository } from '@nestjs/typeorm';
 import { OperationLog } from './entities/operation-log.entity';
-import { Repository } from 'typeorm';
+import { Like, MoreThan, Repository } from 'typeorm';
+import { QueryOperationLogDto } from './dto/query.dto';
+import { Between } from 'typeorm';
+import { dayjs } from '../../common/utils';
+import { format } from 'date-fns';
+import { Dayjs } from 'dayjs';
 
 @Injectable()
 export class OperationLogService {
   constructor(
     @InjectRepository(OperationLog)
     private operationLogRepo: Repository<OperationLog>,
-  ) {}
+  ) { }
 
   create(createOperationLogDto: CreateOperationLogDto) {
     return this.operationLogRepo.save(createOperationLogDto);
@@ -31,4 +36,42 @@ export class OperationLogService {
   remove(id: number) {
     return `This action removes a #${id} operationLog`;
   }
+
+  async findPagination(query: QueryOperationLogDto) {
+    const pageSize = query.pageSize || 10;
+    const pageNo = query.pageNo || 1;
+    let from, to;
+    if (query.times) {
+      from = dayjs
+        .unix(Number(query.times[0]) / 1000)
+        // .utc()
+        // .tz('Asia/Shanghai')
+        .format('YYYY-MM-DD HH:mm:ss');
+      to = dayjs
+        .unix(Number(query.times[1]) / 1000)
+        // .utc()
+        // .tz('Asia/Shanghai')
+        .format('YYYY-MM-DD HH:mm:ss');
+    }
+
+    const [data, total] = await this.operationLogRepo.findAndCount({
+      relations: { user: true },
+      where: {
+        //  User: Like(`%${query.title || ''}%`),
+        userId: MoreThan(0),
+        createTime: query.times ? Between(from, to) : undefined,
+      },
+      order: {
+        // title: 'ASC',
+        createTime: 'DESC',
+      },
+      take: pageSize,
+      skip: (pageNo - 1) * pageSize,
+    });
+    // console.log('data', data)
+    const pageData = data.map((item) => {
+      return { ...item };
+    });
+    return { pageData, total };
+  }
 }

+ 1 - 1
packages/frontend/src/views/menu/list.vue

@@ -150,7 +150,7 @@ const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete,
     doCreate: api.create,
     doDelete: api.delete,
     doUpdate: api.update,
-    initForm: { enable: true },
+    initForm: { enable: true, isPublish: true },
     refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
   })
 

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

@@ -0,0 +1,8 @@
+import { request } from '@/utils'
+
+export default {
+  create: data => request.post('/operation-log', data),
+  read: (params = {}) => request.get('/operation-log/page', { params }),
+  update: data => request.patch(`/operation-log/${data.id}`, data),
+  delete: id => request.delete(`/operation-log/${id}`),
+}

+ 150 - 0
packages/frontend/src/views/opts/index.vue

@@ -0,0 +1,150 @@
+<template>
+  <CommonPage>
+    <template #action>
+      <NButton type="primary" @click="handleAdd()">
+        <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.useName" type="text" placeholder="请输入用户名" clearable>
+          <template #password-visible-icon />
+        </n-input>
+      </MeQueryItem>
+
+      <MeQueryItem label="时间段" :label-width="80">
+        <n-date-picker v-model:value="queryItems.times" type="datetimerange" :update-value-on-close="true" clearable style="width:340px" />
+      </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="title" :rule="{
+            required: true,
+            message: '请输入分类名',
+            trigger: ['input', 'blur'],
+          }"
+        >
+          <n-input v-model:value="modalForm.title" />
+        </n-form-item>
+        <n-form-item label="备注" path="remark">
+          <n-input v-model:value="modalForm.remark" />
+        </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 { useUserStore } from '@/store/index.js'
+import { formatDateTime } from '@/utils'
+import { NButton, NSwitch } from 'naive-ui'
+import { onMounted, watchEffect } from 'vue'
+import api from './api'
+
+defineOptions({ name: 'OperationLog' })
+
+const router = useRouter()
+const { userId } = useUserStore()
+const $table = ref(null)
+/** QueryBar筛选参数(可选) */
+const queryItems = ref({})
+
+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),
+  })
+onMounted(() => {
+  $table.value?.handleSearch()
+})
+watchEffect(() => {
+
+})
+const columns = [
+
+  { title: '请求URL', key: 'requestUrl' },
+  { title: '请求方式', key: 'method' },
+  { title: 'ip', key: 'ip' },
+  { title: '用户名', key: 'user.username' },
+  {
+    title: '操作时间',
+    key: 'createTime',
+    render: row => h('span', formatDateTime(row.createTime)),
+  },
+  // {
+  //   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
+  }
+}
+</script>

+ 11 - 0
pnpm-lock.yaml

@@ -49,6 +49,12 @@ importers:
       class-validator:
         specifier: ^0.14.0
         version: 0.14.1
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.13
       express-session:
         specifier: ^1.17.3
         version: 1.18.1
@@ -3652,6 +3658,9 @@ packages:
   date-fns@3.6.0:
     resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
 
+  date-fns@4.1.0:
+    resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
   dateformat@2.2.0:
     resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==}
 
@@ -11134,6 +11143,8 @@ snapshots:
 
   date-fns@3.6.0: {}
 
+  date-fns@4.1.0: {}
+
   dateformat@2.2.0: {}
 
   dayjs@1.11.13: {}