From 51341068da7d3d52db74d9a258daa641ad80f88a Mon Sep 17 00:00:00 2001 From: Your Name <2499342078@qq.com> Date: Sun, 15 Jun 2025 22:37:47 +0800 Subject: [PATCH] add --- apps/server/src/auth/auth.controller.ts | 5 +- .../server/src/models/book/book.controller.ts | 21 + apps/server/src/models/book/book.module.ts | 12 + apps/server/src/models/book/book.router.ts | 53 ++ apps/server/src/models/book/book.service.ts | 47 ++ .../borrowRecord/borrowRecord.controller.ts | 21 + .../borrowRecord/borrowRecord.module.ts | 12 + .../borrowRecord/borrowRecord.router.ts | 57 +++ .../borrowRecord/borrowRecord.service.ts | 47 ++ .../src/models/comment/comment.controller.ts | 22 + .../src/models/comment/comment.module.ts | 14 + .../src/models/comment/comment.router.ts | 51 ++ .../src/models/comment/comment.service.ts | 45 ++ .../src/models/device/device.controller.ts | 22 + .../server/src/models/device/device.module.ts | 14 + .../server/src/models/device/device.router.ts | 50 ++ .../src/models/device/device.service.ts | 45 ++ .../src/models/library/library.controller.ts | 21 + .../src/models/library/library.module.ts | 12 + .../src/models/library/library.router.ts | 54 ++ .../src/models/library/library.service.ts | 47 ++ .../src/models/reader/reader.controller.ts | 22 + .../server/src/models/reader/reader.module.ts | 14 + .../server/src/models/reader/reader.router.ts | 50 ++ .../src/models/reader/reader.service.ts | 45 ++ .../server/src/models/user/user.controller.ts | 21 + apps/server/src/models/user/user.module.ts | 12 + apps/server/src/models/user/user.router.ts | 53 ++ apps/server/src/models/user/user.service.ts | 47 ++ apps/server/src/tasks/init/gendev.service.ts | 20 + apps/server/src/trpc/trpc.module.ts | 15 + apps/server/src/trpc/trpc.router.ts | 19 +- apps/server/src/trpc/trpc.service.ts | 5 +- apps/web/src/app/main/Test/Page.tsx | 10 +- apps/web/src/app/main/book/Page.tsx | 146 ++++++ .../src/app/main/book/components/CarModal.tsx | 91 ++++ .../src/app/main/book/components/CarTable.tsx | 75 +++ .../book/components/ImportExportButtons.tsx | 158 ++++++ .../app/main/book/components/SearchBar.tsx | 55 ++ .../web/src/app/main/book/components/types.ts | 19 + apps/web/src/app/main/borrowRecord/Page.tsx | 163 ++++++ .../components/ImportExportButtons.tsx | 153 ++++++ .../borrowRecord/components/SearchBar.tsx | 53 ++ .../borrowRecord/components/StaffModal.tsx | 55 ++ .../borrowRecord/components/StaffTable.tsx | 152 ++++++ .../app/main/borrowRecord/components/types.ts | 11 + apps/web/src/app/main/borrowRecord/test.text | 265 ++++++++++ apps/web/src/app/main/game/Page.tsx | 175 +++++++ .../app/main/game/components/GameModal.tsx | 93 ++++ .../app/main/game/components/GameTable.tsx | 114 +++++ .../game/components/ImportExportButtons.tsx | 153 ++++++ .../app/main/game/components/SearchBar.tsx | 55 ++ .../web/src/app/main/game/components/types.ts | 30 ++ .../src/app/main/layout/NavigationMenu.tsx | 30 ++ apps/web/src/app/main/library/Page.tsx | 144 ++++++ .../components/ImportExportButtons.tsx | 158 ++++++ .../main/library/components/LibraryModal.tsx | 107 ++++ .../main/library/components/LibraryTable.tsx | 70 +++ .../app/main/library/components/SearchBar.tsx | 47 ++ .../src/app/main/library/components/types.ts | 9 + apps/web/src/app/main/reader/Page.tsx | 152 ++++++ .../reader/components/ImportExportButtons.tsx | 153 ++++++ .../main/reader/components/ReaderModal.tsx | 117 +++++ .../main/reader/components/ReaderTable.tsx | 81 +++ .../app/main/reader/components/SearchBar.tsx | 55 ++ .../src/app/main/reader/components/types.ts | 22 + apps/web/src/routes/index.tsx | 23 + config/nginx/conf.d/web.conf | 4 +- packages/common/prisma/schema.BACKUP | 475 ------------------ packages/common/prisma/schema.prisma | 209 +++++++- packages/common/prisma/schema.prisma.txt | 179 +++++++ packages/common/src/enum.ts | 9 +- 72 files changed, 4550 insertions(+), 515 deletions(-) create mode 100644 apps/server/src/models/book/book.controller.ts create mode 100644 apps/server/src/models/book/book.module.ts create mode 100644 apps/server/src/models/book/book.router.ts create mode 100644 apps/server/src/models/book/book.service.ts create mode 100644 apps/server/src/models/borrowRecord/borrowRecord.controller.ts create mode 100644 apps/server/src/models/borrowRecord/borrowRecord.module.ts create mode 100644 apps/server/src/models/borrowRecord/borrowRecord.router.ts create mode 100644 apps/server/src/models/borrowRecord/borrowRecord.service.ts create mode 100644 apps/server/src/models/comment/comment.controller.ts create mode 100644 apps/server/src/models/comment/comment.module.ts create mode 100644 apps/server/src/models/comment/comment.router.ts create mode 100644 apps/server/src/models/comment/comment.service.ts create mode 100644 apps/server/src/models/device/device.controller.ts create mode 100644 apps/server/src/models/device/device.module.ts create mode 100644 apps/server/src/models/device/device.router.ts create mode 100644 apps/server/src/models/device/device.service.ts create mode 100644 apps/server/src/models/library/library.controller.ts create mode 100644 apps/server/src/models/library/library.module.ts create mode 100644 apps/server/src/models/library/library.router.ts create mode 100644 apps/server/src/models/library/library.service.ts create mode 100644 apps/server/src/models/reader/reader.controller.ts create mode 100644 apps/server/src/models/reader/reader.module.ts create mode 100644 apps/server/src/models/reader/reader.router.ts create mode 100644 apps/server/src/models/reader/reader.service.ts create mode 100644 apps/server/src/models/user/user.controller.ts create mode 100644 apps/server/src/models/user/user.module.ts create mode 100644 apps/server/src/models/user/user.router.ts create mode 100644 apps/server/src/models/user/user.service.ts create mode 100755 apps/web/src/app/main/book/Page.tsx create mode 100644 apps/web/src/app/main/book/components/CarModal.tsx create mode 100644 apps/web/src/app/main/book/components/CarTable.tsx create mode 100644 apps/web/src/app/main/book/components/ImportExportButtons.tsx create mode 100644 apps/web/src/app/main/book/components/SearchBar.tsx create mode 100644 apps/web/src/app/main/book/components/types.ts create mode 100755 apps/web/src/app/main/borrowRecord/Page.tsx create mode 100644 apps/web/src/app/main/borrowRecord/components/ImportExportButtons.tsx create mode 100644 apps/web/src/app/main/borrowRecord/components/SearchBar.tsx create mode 100644 apps/web/src/app/main/borrowRecord/components/StaffModal.tsx create mode 100644 apps/web/src/app/main/borrowRecord/components/StaffTable.tsx create mode 100644 apps/web/src/app/main/borrowRecord/components/types.ts create mode 100644 apps/web/src/app/main/borrowRecord/test.text create mode 100755 apps/web/src/app/main/game/Page.tsx create mode 100644 apps/web/src/app/main/game/components/GameModal.tsx create mode 100644 apps/web/src/app/main/game/components/GameTable.tsx create mode 100644 apps/web/src/app/main/game/components/ImportExportButtons.tsx create mode 100644 apps/web/src/app/main/game/components/SearchBar.tsx create mode 100644 apps/web/src/app/main/game/components/types.ts create mode 100755 apps/web/src/app/main/library/Page.tsx create mode 100644 apps/web/src/app/main/library/components/ImportExportButtons.tsx create mode 100644 apps/web/src/app/main/library/components/LibraryModal.tsx create mode 100644 apps/web/src/app/main/library/components/LibraryTable.tsx create mode 100644 apps/web/src/app/main/library/components/SearchBar.tsx create mode 100644 apps/web/src/app/main/library/components/types.ts create mode 100755 apps/web/src/app/main/reader/Page.tsx create mode 100644 apps/web/src/app/main/reader/components/ImportExportButtons.tsx create mode 100644 apps/web/src/app/main/reader/components/ReaderModal.tsx create mode 100644 apps/web/src/app/main/reader/components/ReaderTable.tsx create mode 100644 apps/web/src/app/main/reader/components/SearchBar.tsx create mode 100644 apps/web/src/app/main/reader/components/types.ts delete mode 100755 packages/common/prisma/schema.BACKUP create mode 100644 packages/common/prisma/schema.prisma.txt diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts index 490e25e..de67161 100755 --- a/apps/server/src/auth/auth.controller.ts +++ b/apps/server/src/auth/auth.controller.ts @@ -43,9 +43,8 @@ export class AuthController { authorization, }; - const authResult = await this.authService.validateFileRequest( - fileRequest, - ); + const authResult = + await this.authService.validateFileRequest(fileRequest); if (!authResult.isValid) { // 使用枚举类型进行错误处理 switch (authResult.error) { diff --git a/apps/server/src/models/book/book.controller.ts b/apps/server/src/models/book/book.controller.ts new file mode 100644 index 0000000..021075d --- /dev/null +++ b/apps/server/src/models/book/book.controller.ts @@ -0,0 +1,21 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { BookService } from './book.service'; +import { Response } from 'express'; + +@Controller('book') +export class BookController { + constructor(private readonly bookService: BookService) {} +} diff --git a/apps/server/src/models/book/book.module.ts b/apps/server/src/models/book/book.module.ts new file mode 100644 index 0000000..6fd904f --- /dev/null +++ b/apps/server/src/models/book/book.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { BookService } from './book.service'; +import { BookRouter } from './book.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { BookController } from './book.controller'; + +@Module({ + providers: [BookService, BookRouter, TrpcService], + exports: [BookService, BookRouter], + controllers: [BookController], +}) +export class BookModule {} diff --git a/apps/server/src/models/book/book.router.ts b/apps/server/src/models/book/book.router.ts new file mode 100644 index 0000000..2c27f3f --- /dev/null +++ b/apps/server/src/models/book/book.router.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { BookService } from './book.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const BookUncheckedCreateInputSchema: ZodType = + z.any(); +const BookWhereInputSchema: ZodType = z.any(); +const BookSelectSchema: ZodType = z.any(); +const BookUpdateArgsSchema: ZodType = z.any(); +const BookFindFirstArgsSchema: ZodType = z.any(); +const BookFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class BookRouter { + constructor( + private readonly trpc: TrpcService, + private readonly bookService: BookService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(BookUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + return this.bookService.create({ data: input }); + }), + + update: this.trpc.procedure + .input(BookUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.bookService.update(input); + }), + + findMany: this.trpc.procedure + .input(BookFindManyArgsSchema) + .query(async ({ input }) => { + return this.bookService.findMany(input); + }), + + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.bookService.softDeleteByIds(input.ids); + }), + + findFirst: this.trpc.procedure + .input(BookFindFirstArgsSchema) + .query(async ({ input }) => { + return this.bookService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/book/book.service.ts b/apps/server/src/models/book/book.service.ts new file mode 100644 index 0000000..afc9cae --- /dev/null +++ b/apps/server/src/models/book/book.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + +@Injectable() +export class BookService extends BaseService { + constructor() { + super(db, ObjectType.LIBRARY, false); + } + + async create(args: Prisma.BookCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.BookUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.BookFindManyArgs) { + const result = await super.findMany(args); + return result; + } + + async findFirst(args: Prisma.BookFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.BOOK, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/borrowRecord/borrowRecord.controller.ts b/apps/server/src/models/borrowRecord/borrowRecord.controller.ts new file mode 100644 index 0000000..3862a45 --- /dev/null +++ b/apps/server/src/models/borrowRecord/borrowRecord.controller.ts @@ -0,0 +1,21 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { BorrowRecordService } from './borrowRecord.service'; +import { Response } from 'express'; + +@Controller('borrowRecord') +export class BorrowRecordController { + constructor(private readonly borrowRecordService: BorrowRecordService) {} +} diff --git a/apps/server/src/models/borrowRecord/borrowRecord.module.ts b/apps/server/src/models/borrowRecord/borrowRecord.module.ts new file mode 100644 index 0000000..b889022 --- /dev/null +++ b/apps/server/src/models/borrowRecord/borrowRecord.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { BorrowRecordService } from './borrowRecord.service'; +import { BorrowRecordRouter } from './borrowRecord.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { BorrowRecordController } from './borrowRecord.controller'; + +@Module({ + providers: [BorrowRecordService, BorrowRecordRouter, TrpcService], + exports: [BorrowRecordService, BorrowRecordRouter], + controllers: [BorrowRecordController], +}) +export class BorrowRecordModule {} diff --git a/apps/server/src/models/borrowRecord/borrowRecord.router.ts b/apps/server/src/models/borrowRecord/borrowRecord.router.ts new file mode 100644 index 0000000..cb1189b --- /dev/null +++ b/apps/server/src/models/borrowRecord/borrowRecord.router.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { BorrowRecordService } from './borrowRecord.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const BorrowRecordUncheckedCreateInputSchema: ZodType = + z.any(); +const BorrowRecordWhereInputSchema: ZodType = + z.any(); +const BorrowRecordSelectSchema: ZodType = z.any(); +const BorrowRecordUpdateArgsSchema: ZodType = + z.any(); +const BorrowRecordFindFirstArgsSchema: ZodType = + z.any(); +const BorrowRecordFindManyArgsSchema: ZodType = + z.any(); + +@Injectable() +export class BorrowRecordRouter { + constructor( + private readonly trpc: TrpcService, + private readonly borrowRecordService: BorrowRecordService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(BorrowRecordUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + return this.borrowRecordService.create({ data: input }); + }), + + update: this.trpc.procedure + .input(BorrowRecordUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.borrowRecordService.update(input); + }), + + findMany: this.trpc.procedure + .input(BorrowRecordFindManyArgsSchema) + .query(async ({ input }) => { + return this.borrowRecordService.findMany(input); + }), + + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.borrowRecordService.softDeleteByIds(input.ids); + }), + + findFirst: this.trpc.procedure + .input(BorrowRecordFindFirstArgsSchema) + .query(async ({ input }) => { + return this.borrowRecordService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/borrowRecord/borrowRecord.service.ts b/apps/server/src/models/borrowRecord/borrowRecord.service.ts new file mode 100644 index 0000000..cc13a98 --- /dev/null +++ b/apps/server/src/models/borrowRecord/borrowRecord.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + +@Injectable() +export class BorrowRecordService extends BaseService { + constructor() { + super(db, ObjectType.BORROWRECORD, false); + } + + async create(args: Prisma.BorrowRecordCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.BorrowRecordUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.BorrowRecordFindManyArgs) { + const result = await super.findMany(args); + return result; + } + + async findFirst(args: Prisma.BorrowRecordFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.BORROWRECORD, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/comment/comment.controller.ts b/apps/server/src/models/comment/comment.controller.ts new file mode 100644 index 0000000..8ac5c72 --- /dev/null +++ b/apps/server/src/models/comment/comment.controller.ts @@ -0,0 +1,22 @@ +// apps/server/src/models/comment/comment.controller.ts +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { CommentService } from './comment.service'; +import { Response } from 'express'; + +@Controller('comment') +export class CommentController { + constructor(private readonly commentService: CommentService) {} +} diff --git a/apps/server/src/models/comment/comment.module.ts b/apps/server/src/models/comment/comment.module.ts new file mode 100644 index 0000000..139378b --- /dev/null +++ b/apps/server/src/models/comment/comment.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CommentService } from './comment.service'; +import { CommentRouter } from './comment.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DepartmentModule } from '../department/department.module'; +import { CommentController } from './comment.controller'; + +@Module({ + imports: [DepartmentModule], + providers: [CommentService, CommentRouter, TrpcService], + exports: [CommentService, CommentRouter], + controllers: [CommentController], +}) +export class CommentModule {} diff --git a/apps/server/src/models/comment/comment.router.ts b/apps/server/src/models/comment/comment.router.ts new file mode 100644 index 0000000..c0aefad --- /dev/null +++ b/apps/server/src/models/comment/comment.router.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { CommentService } from './comment.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const CommentUncheckedCreateInputSchema: ZodType = + z.any(); +const CommentWhereInputSchema: ZodType = z.any(); +const CommentSelectSchema: ZodType = z.any(); +const CommentUpdateArgsSchema: ZodType = z.any(); +const CommentFindFirstArgsSchema: ZodType = + z.any(); +const CommentFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class CommentRouter { + constructor( + private readonly trpc: TrpcService, + private readonly commentService: CommentService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(CommentUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + console.log(input); + return this.commentService.create({ data: input }); + }), + update: this.trpc.procedure + .input(CommentUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.commentService.update(input); + }), + findMany: this.trpc.procedure + .input(CommentFindManyArgsSchema) + .query(async ({ input }) => { + return this.commentService.findMany(input); + }), + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.commentService.softDeleteByIds(input.ids); + }), + findFirst: this.trpc.procedure + .input(CommentFindFirstArgsSchema) + .query(async ({ input }) => { + return this.commentService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/comment/comment.service.ts b/apps/server/src/models/comment/comment.service.ts new file mode 100644 index 0000000..da13c0e --- /dev/null +++ b/apps/server/src/models/comment/comment.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma, UserProfile } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; +import { DefaultArgs } from '@prisma/client/runtime/library'; + +@Injectable() +export class CommentService extends BaseService { + constructor() { + super(db, ObjectType.COMMENT, false); + } + async create(args: Prisma.CommentCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.CommentUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.CommentFindManyArgs) { + const result = await super.findMany(args); + return result; + } + async findFirst(args: Prisma.CommentFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.COMMENT, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/device/device.controller.ts b/apps/server/src/models/device/device.controller.ts new file mode 100644 index 0000000..3f5c0ac --- /dev/null +++ b/apps/server/src/models/device/device.controller.ts @@ -0,0 +1,22 @@ +// apps/server/src/models/device/device.controller.ts +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { DeviceService } from './device.service'; +import { Response } from 'express'; + +@Controller('device') +export class DeviceController { + constructor(private readonly deviceService: DeviceService) {} +} diff --git a/apps/server/src/models/device/device.module.ts b/apps/server/src/models/device/device.module.ts new file mode 100644 index 0000000..0a19ff5 --- /dev/null +++ b/apps/server/src/models/device/device.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DeviceService } from './device.service'; +import { DeviceRouter } from './device.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DepartmentModule } from '../department/department.module'; +import { DeviceController } from './device.controller'; + +@Module({ + imports: [DepartmentModule], + providers: [DeviceService, DeviceRouter, TrpcService], + exports: [DeviceService, DeviceRouter], + controllers: [DeviceController], +}) +export class DeviceModule {} diff --git a/apps/server/src/models/device/device.router.ts b/apps/server/src/models/device/device.router.ts new file mode 100644 index 0000000..50dcc44 --- /dev/null +++ b/apps/server/src/models/device/device.router.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DeviceService } from './device.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const DeviceUncheckedCreateInputSchema: ZodType = + z.any(); +const DeviceWhereInputSchema: ZodType = z.any(); +const DeviceSelectSchema: ZodType = z.any(); +const DeviceUpdateArgsSchema: ZodType = z.any(); +const DeviceFindFirstArgsSchema: ZodType = z.any(); +const DeviceFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class DeviceRouter { + constructor( + private readonly trpc: TrpcService, + private readonly deviceService: DeviceService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(DeviceUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + console.log(input); + return this.deviceService.create({ data: input }); + }), + update: this.trpc.procedure + .input(DeviceUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.deviceService.update(input); + }), + findMany: this.trpc.procedure + .input(DeviceFindManyArgsSchema) + .query(async ({ input }) => { + return this.deviceService.findMany(input); + }), + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.deviceService.softDeleteByIds(input.ids); + }), + findFirst: this.trpc.procedure + .input(DeviceFindFirstArgsSchema) + .query(async ({ input }) => { + return this.deviceService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/device/device.service.ts b/apps/server/src/models/device/device.service.ts new file mode 100644 index 0000000..1f12844 --- /dev/null +++ b/apps/server/src/models/device/device.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma, UserProfile } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; +import { DefaultArgs } from '@prisma/client/runtime/library'; + +@Injectable() +export class DeviceService extends BaseService { + constructor() { + super(db, ObjectType.DEVICE, false); + } + async create(args: Prisma.DeviceCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.DeviceUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.DeviceFindManyArgs) { + const result = await super.findMany(args); + return result; + } + async findFirst(args: Prisma.DeviceFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.DEVICE, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/library/library.controller.ts b/apps/server/src/models/library/library.controller.ts new file mode 100644 index 0000000..7701354 --- /dev/null +++ b/apps/server/src/models/library/library.controller.ts @@ -0,0 +1,21 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { LibraryService } from './library.service'; +import { Response } from 'express'; + +@Controller('library') +export class LibraryController { + constructor(private readonly libraryService: LibraryService) {} +} diff --git a/apps/server/src/models/library/library.module.ts b/apps/server/src/models/library/library.module.ts new file mode 100644 index 0000000..8f4bca7 --- /dev/null +++ b/apps/server/src/models/library/library.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { LibraryService } from './library.service'; +import { LibraryRouter } from './library.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { LibraryController } from './library.controller'; + +@Module({ + providers: [LibraryService, LibraryRouter, TrpcService], + exports: [LibraryService, LibraryRouter], + controllers: [LibraryController], +}) +export class LibraryModule {} diff --git a/apps/server/src/models/library/library.router.ts b/apps/server/src/models/library/library.router.ts new file mode 100644 index 0000000..c10fcec --- /dev/null +++ b/apps/server/src/models/library/library.router.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { LibraryService } from './library.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const LibraryUncheckedCreateInputSchema: ZodType = + z.any(); +const LibraryWhereInputSchema: ZodType = z.any(); +const LibrarySelectSchema: ZodType = z.any(); +const LibraryUpdateArgsSchema: ZodType = z.any(); +const LibraryFindFirstArgsSchema: ZodType = + z.any(); +const LibraryFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class LibraryRouter { + constructor( + private readonly trpc: TrpcService, + private readonly libraryService: LibraryService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(LibraryUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + return this.libraryService.create({ data: input }); + }), + + update: this.trpc.procedure + .input(LibraryUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.libraryService.update(input); + }), + + findMany: this.trpc.procedure + .input(LibraryFindManyArgsSchema) + .query(async ({ input }) => { + return this.libraryService.findMany(input); + }), + + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.libraryService.softDeleteByIds(input.ids); + }), + + findFirst: this.trpc.procedure + .input(LibraryFindFirstArgsSchema) + .query(async ({ input }) => { + return this.libraryService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/library/library.service.ts b/apps/server/src/models/library/library.service.ts new file mode 100644 index 0000000..521f26e --- /dev/null +++ b/apps/server/src/models/library/library.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + +@Injectable() +export class LibraryService extends BaseService { + constructor() { + super(db, ObjectType.LIBRARY, false); + } + + async create(args: Prisma.LibraryCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.LibraryUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.LibraryFindManyArgs) { + const result = await super.findMany(args); + return result; + } + + async findFirst(args: Prisma.LibraryFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.LIBRARY, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/reader/reader.controller.ts b/apps/server/src/models/reader/reader.controller.ts new file mode 100644 index 0000000..7a3abf5 --- /dev/null +++ b/apps/server/src/models/reader/reader.controller.ts @@ -0,0 +1,22 @@ +// apps/server/src/models/reader/reader.controller.ts +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ReaderService } from './reader.service'; +import { Response } from 'express'; + +@Controller('reader') +export class ReaderController { + constructor(private readonly readerService: ReaderService) {} +} diff --git a/apps/server/src/models/reader/reader.module.ts b/apps/server/src/models/reader/reader.module.ts new file mode 100644 index 0000000..1598f4d --- /dev/null +++ b/apps/server/src/models/reader/reader.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ReaderService } from './reader.service'; +import { ReaderRouter } from './reader.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DepartmentModule } from '../department/department.module'; +import { ReaderController } from './reader.controller'; + +@Module({ + imports: [DepartmentModule], + providers: [ReaderService, ReaderRouter, TrpcService], + exports: [ReaderService, ReaderRouter], + controllers: [ReaderController], +}) +export class ReaderModule {} diff --git a/apps/server/src/models/reader/reader.router.ts b/apps/server/src/models/reader/reader.router.ts new file mode 100644 index 0000000..22e9b71 --- /dev/null +++ b/apps/server/src/models/reader/reader.router.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { ReaderService } from './reader.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const ReaderUncheckedCreateInputSchema: ZodType = + z.any(); +const ReaderWhereInputSchema: ZodType = z.any(); +const ReaderSelectSchema: ZodType = z.any(); +const ReaderUpdateArgsSchema: ZodType = z.any(); +const ReaderFindFirstArgsSchema: ZodType = z.any(); +const ReaderFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class ReaderRouter { + constructor( + private readonly trpc: TrpcService, + private readonly readerService: ReaderService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(ReaderUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + console.log(input); + return this.readerService.create({ data: input }); + }), + update: this.trpc.procedure + .input(ReaderUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.readerService.update(input); + }), + findMany: this.trpc.procedure + .input(ReaderFindManyArgsSchema) + .query(async ({ input }) => { + return this.readerService.findMany(input); + }), + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.readerService.softDeleteByIds(input.ids); + }), + findFirst: this.trpc.procedure + .input(ReaderFindFirstArgsSchema) + .query(async ({ input }) => { + return this.readerService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/reader/reader.service.ts b/apps/server/src/models/reader/reader.service.ts new file mode 100644 index 0000000..a688824 --- /dev/null +++ b/apps/server/src/models/reader/reader.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma, UserProfile } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; +import { DefaultArgs } from '@prisma/client/runtime/library'; + +@Injectable() +export class ReaderService extends BaseService { + constructor() { + super(db, ObjectType.READER, false); + } + async create(args: Prisma.ReaderCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.ReaderUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.ReaderFindManyArgs) { + const result = await super.findMany(args); + return result; + } + async findFirst(args: Prisma.ReaderFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.READER, + operation, + data, + }); + } +} diff --git a/apps/server/src/models/user/user.controller.ts b/apps/server/src/models/user/user.controller.ts new file mode 100644 index 0000000..b43336f --- /dev/null +++ b/apps/server/src/models/user/user.controller.ts @@ -0,0 +1,21 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UploadedFile, + UseInterceptors, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { UserService } from './user.service'; +import { Response } from 'express'; + +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} +} diff --git a/apps/server/src/models/user/user.module.ts b/apps/server/src/models/user/user.module.ts new file mode 100644 index 0000000..80e20bf --- /dev/null +++ b/apps/server/src/models/user/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserRouter } from './user.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { UserController } from './user.controller'; + +@Module({ + providers: [UserService, UserRouter, TrpcService], + exports: [UserService, UserRouter], + controllers: [UserController], +}) +export class UserModule {} diff --git a/apps/server/src/models/user/user.router.ts b/apps/server/src/models/user/user.router.ts new file mode 100644 index 0000000..2f3f8eb --- /dev/null +++ b/apps/server/src/models/user/user.router.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { UserService } from './user.service'; +import { Prisma } from '@nice/common'; +import { z, ZodType } from 'zod'; + +const UserUncheckedCreateInputSchema: ZodType = + z.any(); +const UserWhereInputSchema: ZodType = z.any(); +const UserSelectSchema: ZodType = z.any(); +const UserUpdateArgsSchema: ZodType = z.any(); +const UserFindFirstArgsSchema: ZodType = z.any(); +const UserFindManyArgsSchema: ZodType = z.any(); + +@Injectable() +export class UserRouter { + constructor( + private readonly trpc: TrpcService, + private readonly userService: UserService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(UserUncheckedCreateInputSchema) + .mutation(async ({ input }) => { + return this.userService.create({ data: input }); + }), + + update: this.trpc.procedure + .input(UserUpdateArgsSchema) + .mutation(async ({ input }) => { + return this.userService.update(input); + }), + + findMany: this.trpc.procedure + .input(UserFindManyArgsSchema) + .query(async ({ input }) => { + return this.userService.findMany(input); + }), + + softDeleteByIds: this.trpc.procedure + .input(z.object({ ids: z.array(z.string()) })) + .mutation(async ({ input }) => { + return this.userService.softDeleteByIds(input.ids); + }), + + findFirst: this.trpc.procedure + .input(UserFindFirstArgsSchema) + .query(async ({ input }) => { + return this.userService.findFirst(input); + }), + }); +} diff --git a/apps/server/src/models/user/user.service.ts b/apps/server/src/models/user/user.service.ts new file mode 100644 index 0000000..9773296 --- /dev/null +++ b/apps/server/src/models/user/user.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BaseService } from '../base/base.service'; +import { db, ObjectType, Prisma } from '@nice/common'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; + +@Injectable() +export class UserService extends BaseService { + constructor() { + super(db, ObjectType.USER, false); + } + + async create(args: Prisma.UserCreateArgs) { + const result = await super.create(args); + this.emitDataChanged(CrudOperation.CREATED, result); + return result; + } + + async update(args: Prisma.UserUpdateArgs) { + const result = await super.update(args); + this.emitDataChanged(CrudOperation.UPDATED, result); + return result; + } + + async findMany(args: Prisma.UserFindManyArgs) { + const result = await super.findMany(args); + return result; + } + + async findFirst(args: Prisma.UserFindFirstArgs) { + const result = await super.findFirst(args); + return result; + } + + async softDeleteByIds(ids: string[]) { + const result = await super.softDeleteByIds(ids); + this.emitDataChanged(CrudOperation.DELETED, result); + return result; + } + + private emitDataChanged(operation: CrudOperation, data: any) { + EventBus.emit('dataChanged', { + type: ObjectType.USER, + operation, + data, + }); + } +} diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index 503d84a..362001c 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -59,6 +59,26 @@ export class GenDevService { } EventBus.emit('genDataEvent', { type: 'end' }); } + + public async initializeReaderType() { + this.logger.log('初始化读者分类'); + + // 查找是否存在系统类型分类 + let systemTypeTaxonomy = await db.taxonomy.findFirst({ + where: { slug: 'reader_type' }, // 通过 slug 查找 + }); + + // 如果不存在,则创建新的分类 + if (!systemTypeTaxonomy) { + systemTypeTaxonomy = await db.taxonomy.create({ + data: { + name: '读者类型', // 分类名称 + slug: 'reader_type', // 唯一标识符 + objectType: ['reader'], // 关联对象类型为 device + }, + }); + } + } private async calculateCounts() { this.counts = await getCounts(); Object.entries(this.counts).forEach(([key, value]) => { diff --git a/apps/server/src/trpc/trpc.module.ts b/apps/server/src/trpc/trpc.module.ts index 13bf946..7f2d69c 100755 --- a/apps/server/src/trpc/trpc.module.ts +++ b/apps/server/src/trpc/trpc.module.ts @@ -1,3 +1,4 @@ +import { Library } from './../../../../node_modules/.prisma/client/index.d'; import { Logger, Module } from '@nestjs/common'; import { TrpcService } from './trpc.service'; import { TrpcRouter } from './trpc.router'; @@ -19,6 +20,13 @@ import { ResourceModule } from '@server/models/resource/resource.module'; import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module'; import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module'; import { SystemLogModule } from '@server/models/sys-logs/systemLog.module'; +import { LibraryModule } from '@server/models/library/library.module'; +import { DeviceModule } from '@server/models/device/device.module'; +import { ReaderModule } from '@server/models/reader/reader.module'; +import { BookModule } from '@server/models/book/book.module'; + +import { BorrowRecordModule } from '@server/models/borrowRecord/borrowRecord.module'; +import { CommentModule } from '@server/models/comment/comment.module'; @Module({ imports: [ AuthModule, @@ -39,6 +47,13 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module'; TrainSituationModule, DailyTrainModule, SystemLogModule, + LibraryModule, + DeviceModule, + ReaderModule, + BookModule, + + BorrowRecordModule, + CommentModule, ], controllers: [], providers: [TrpcService, TrpcRouter, Logger], diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index cae928c..5f34010 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -18,7 +18,12 @@ import { TrainContentRouter } from '@server/models/train-content/trainContent.ro import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router'; import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router'; import { SystemLogRouter } from '@server/models/sys-logs/systemLog.router'; - +import { DeviceRouter } from '@server/models/device/device.router'; +import { LibraryRouter } from '@server/models/library/library.router'; +import { ReaderRouter } from '@server/models/reader/reader.router'; +import { BookRouter } from '@server/models/book/book.router'; +import { BorrowRecordRouter } from '@server/models/borrowRecord/borrowRecord.router'; +import { CommentRouter } from '@server/models/comment/comment.router'; @Injectable() export class TrpcRouter { logger = new Logger(TrpcRouter.name); @@ -40,6 +45,12 @@ export class TrpcRouter { private readonly trainSituation: TrainSituationRouter, private readonly dailyTrain: DailyTrainRouter, private readonly systemLogRouter: SystemLogRouter, + private readonly deviceRouter: DeviceRouter, + private readonly libraryRouter: LibraryRouter, + private readonly readerRouter: ReaderRouter, + private readonly bookRouter: BookRouter, + private readonly borrowRecordRouter: BorrowRecordRouter, + private readonly commentRouter: CommentRouter, ) {} getRouter() { return; @@ -61,6 +72,12 @@ export class TrpcRouter { trainSituation: this.trainSituation.router, dailyTrain: this.dailyTrain.router, systemLog: this.systemLogRouter.router, + device: this.deviceRouter.router, + library: this.libraryRouter.router, + reader: this.readerRouter.router, + book: this.bookRouter.router, + borrowRecord: this.borrowRecordRouter.router, + comment: this.commentRouter.router, }); wss: WebSocketServer = undefined; diff --git a/apps/server/src/trpc/trpc.service.ts b/apps/server/src/trpc/trpc.service.ts index 1cb72f4..38024c9 100755 --- a/apps/server/src/trpc/trpc.service.ts +++ b/apps/server/src/trpc/trpc.service.ts @@ -18,9 +18,8 @@ export class TrpcService { ip: string; }> { const token = opts.req.headers.authorization?.split(' ')[1]; - const staff = await UserProfileService.instance.getUserProfileByToken( - token, - ); + const staff = + await UserProfileService.instance.getUserProfileByToken(token); const ip = getClientIp(opts.req); return { staff: staff.staff, diff --git a/apps/web/src/app/main/Test/Page.tsx b/apps/web/src/app/main/Test/Page.tsx index fc543a4..eb4853e 100755 --- a/apps/web/src/app/main/Test/Page.tsx +++ b/apps/web/src/app/main/Test/Page.tsx @@ -20,17 +20,17 @@ const TestPage: React.FC = () => { page: currentPage, pageSize: pageSize, where: { - deletedAt: null, + OR: searchText ? [ { username: { contains: searchText } }, { showname: { contains: searchText } } ] : undefined }, select: { - id: true, - username: true, - showname: true, - absent: true, + id: true, + username: true, + showname: true, + absent: true, } }); diff --git a/apps/web/src/app/main/book/Page.tsx b/apps/web/src/app/main/book/Page.tsx new file mode 100755 index 0000000..498c2ac --- /dev/null +++ b/apps/web/src/app/main/book/Page.tsx @@ -0,0 +1,146 @@ +import { api } from '@nice/client'; +import { message, Form, Modal } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { SearchBar } from './components/SearchBar'; +import { CarTable } from './components/CarTable'; +import { CarModal } from './components/CarModal'; +import { Car,Club } from './components/types'; + +export const CarPage = () => { + const [modalOpen, setModalOpen] = useState(false); + const [editingCar, setEditingCar] = useState(); + const [searchText, setSearchText] = useState(''); + const [form] = Form.useForm(); + // 查询数据 + const { data, isLoading, refetch, isError, error } = api.car.findMany.useQuery({ + where: { + deletedAt: null, + OR: searchText ? [ + { name: { contains: searchText } }, + { model: { contains: searchText } }, + {number: { contains: searchText } }, + { club: { name: { contains: searchText } } } + ] : undefined + }, + include: { + club: true, + }, + }); + + // 独立查询所有俱乐部数据 + const { data: clubList, isLoading: clubsLoading } = api.club.findMany.useQuery({ + where: { + deletedAt: null, + }, + }); + if (isError && error) { + message.error('获取数据失败:' + error.message); + } + + // 创建数据 + const createMutation = api.car.create.useMutation({ + onSuccess: () => { + message.success('创建成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('创建失败:' + error.message); + } + }); + + // 更新数据 + const updateMutation = api.car.update.useMutation({ + onSuccess: () => { + message.success('更新成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('更新失败:' + error.message); + } + }); + + // 删除数据 + const deleteMutation = api.car.softDeleteByIds.useMutation({ + onSuccess: () => { + message.success('删除成功'); + refetch(); + }, + onError: (error) => { + message.error('删除失败:' + error.message); + } + }); + + // 处理搜索 + const handleSearch = () => { + refetch(); + }; + + // 处理添加 + const handleAdd = () => { + setEditingCar(undefined); + setModalOpen(true); + }; + + // 处理表单提交 + const handleModalOk = (values: any) => { + if (editingCar) { + updateMutation.mutate({ + where: { id: editingCar.id }, + data: { + name: values.name, + model: values.model, + number: values.number, // 确保 age 是数字类型 + clubId: values.clubId, + } + }); + } else { + createMutation.mutate({ + + name: values.name, + model: values.model, + number: values.number, // 确保 age 是数字类型 + clubId: values.clubId, + + }); + }console.log(values); + + }; + + + + return ( +
+ + + { + setEditingCar(car); + setModalOpen(true); + }} + onDelete={(ids) => deleteMutation.mutate({ ids })} + /> + + setModalOpen(false)} + /> +
+ ); +}; + +export default CarPage; \ No newline at end of file diff --git a/apps/web/src/app/main/book/components/CarModal.tsx b/apps/web/src/app/main/book/components/CarModal.tsx new file mode 100644 index 0000000..2d8de17 --- /dev/null +++ b/apps/web/src/app/main/book/components/CarModal.tsx @@ -0,0 +1,91 @@ +import { Modal, Form, Input, Select } from 'antd'; +import { Car, Club } from './types'; +import React from 'react'; + +interface CarModalProps { + open: boolean; + loading?: boolean; + editingCar?: Car; + clubList?: Club[]; + onOk: (values: any) => void; + onCancel: () => void; +} + +export const CarModal: React.FC = ({ + open, + loading, + editingCar, + clubList, + onOk, + onCancel, +}) => { + const [form] = Form.useForm(); + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + > +
{ + // 确保数据格式正确 + const formData = { + name: values.name, + model: values.model, + number: values.number, + clubId: values.clubId, + }; + onOk(formData); + }} + > + + + + + + + + + + + + + + + + +
+
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/book/components/CarTable.tsx b/apps/web/src/app/main/book/components/CarTable.tsx new file mode 100644 index 0000000..fc79bb2 --- /dev/null +++ b/apps/web/src/app/main/book/components/CarTable.tsx @@ -0,0 +1,75 @@ +import { Table, Space, Button, Popconfirm } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Car } from './types'; + +import React from 'react'; +interface CarTableProps { + data: Car[]; + loading?: boolean; + onEdit: (record: Car) => void; + onDelete: (ids: string[]) => void; +} + +export const CarTable: React.FC = ({ + data, + loading, + onEdit, + onDelete, +}) => { + const columns: ColumnsType = [ + { + title: '车辆名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '车型', + dataIndex: 'model', + key: 'model', + }, + { + title: '车辆编号', + dataIndex: 'number', + key: 'number', + }, + { + title: '所属俱乐部', + dataIndex: ['club', 'name'], + key: 'clubName', + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + onDelete([record.id])} + > + + + + ), + }, + ]; + + return ( + + ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/book/components/ImportExportButtons.tsx b/apps/web/src/app/main/book/components/ImportExportButtons.tsx new file mode 100644 index 0000000..8e1007e --- /dev/null +++ b/apps/web/src/app/main/book/components/ImportExportButtons.tsx @@ -0,0 +1,158 @@ +import { Button, Upload, message } from 'antd'; +import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; +import React from 'react'; +import { Club } from './types'; +import * as XLSX from 'xlsx'; +import { api } from '@nice/client'; + +interface ImportExportButtonsProps { + onImportSuccess: () => void; + data: Club[]; +} + +export const ImportExportButtons: React.FC = ({ + onImportSuccess, + data, +}) => { + const createMutation = api.club.create.useMutation({ + onSuccess: () => { + // 成功时不显示单条消息,让最终统计来显示 + }, + onError: (error) => { + // 只有当不是名称重复错误时才显示错误信息 + if (!error.message.includes('Unique constraint failed')) { + message.error('导入失败: ' + error.message); + } + } + }); + + // 添加恢复记录的 mutation + const restoreMutation = api.club.update.useMutation({ + onSuccess: () => { + // 静默成功,不显示消息 + }, + onError: (error) => { + console.error('恢复记录失败:', error); + } + }); + + const handleImport = async (file: File) => { + try { + const reader = new FileReader(); + reader.onload = async (e) => { + const workbook = XLSX.read(e.target?.result, { type: 'binary' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + + let successCount = 0; + let restoredCount = 0; + let errorCount = 0; + + // 转换数据格式,包含 id 字段 + const clubData = jsonData.map((row: any) => ({ + id: row['ID'] || undefined, + name: row['名称'], + description: row['描述'], + parentId: row['上级俱乐部ID'] || null, + })); + + // 批量处理 + for (const club of clubData) { + try { + await createMutation.mutateAsync(club); + successCount++; + } catch (error: any) { + if (error.message.includes('Unique constraint failed')) { + try { + // 这里需要 club.id,假设 clubData 里有 id 字段,否则需要先查找 id + if (!club.id) { + errorCount++; + console.error('恢复记录失败: 缺少唯一标识 id'); + continue; + } + await restoreMutation.mutateAsync({ + where: { + id: club.id, + }, + data: { + deletedAt: null, + description: club.description, + parentId: club.parentId, + } + }); + restoredCount++; + } catch (restoreError) { + errorCount++; + console.error('恢复记录失败:', restoreError); + } + } else { + errorCount++; + console.error('创建记录失败:', error); + } + } + } + + // 显示导入结果 + if (successCount > 0 || restoredCount > 0) { + let successMessage = []; + if (successCount > 0) { + successMessage.push(`新增 ${successCount} 条`); + } + if (restoredCount > 0) { + successMessage.push(`恢复 ${restoredCount} 条`); + } + message.success(`导入完成:${successMessage.join(',')}`); + onImportSuccess(); + } + if (errorCount > 0) { + message.warning(`${errorCount} 条记录导入失败`); + } + }; + reader.readAsBinaryString(file); + } catch (error) { + message.error('文件读取失败'); + } + return false; + }; + + const handleExport = () => { + try { + const exportData = data.map(club => ({ + '名称': club.name, + '描述': club.description, + '上级俱乐部': club.parent?.name || '', + '上级俱乐部ID': club.parentId || '', + '创建时间': new Date(club.createdAt).toLocaleString(), + '更新时间': new Date(club.updatedAt).toLocaleString(), + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '俱乐部列表'); + + XLSX.writeFile(workbook, `俱乐部列表_${new Date().toLocaleDateString()}.xlsx`); + message.success('导出成功'); + } catch (error) { + message.error('导出失败: ' + (error as Error).message); + } + }; + + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/book/components/SearchBar.tsx b/apps/web/src/app/main/book/components/SearchBar.tsx new file mode 100644 index 0000000..bdf5b40 --- /dev/null +++ b/apps/web/src/app/main/book/components/SearchBar.tsx @@ -0,0 +1,55 @@ +import { Button, Input, Space } from 'antd'; +import { SearchOutlined, PlusOutlined } from '@ant-design/icons'; +import React from 'react'; +// import { ImportExportButtons } from './ImportExportButtons'; + +// Define or import the Staff type +// interface Driver { +// id: string; +// name: string; +// gender: string; +// age: number; +// clubName: string; +// } + +interface SearchBarProps { + searchText: string; + onSearchTextChange: (text: string) => void; + onSearch: () => void; + onAdd: () => void; + onImportSuccess: () => void; + // data: Driver[]; +} + +export const SearchBar: React.FC = ({ + searchText, + onSearchTextChange, + onSearch, + onAdd, + onImportSuccess, + // data, +}) => { + return ( +
+ + onSearchTextChange(e.target.value)} + onPressEnter={onSearch} + prefix={} + /> + + + + {/* */} + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/book/components/types.ts b/apps/web/src/app/main/book/components/types.ts new file mode 100644 index 0000000..ece14e8 --- /dev/null +++ b/apps/web/src/app/main/book/components/types.ts @@ -0,0 +1,19 @@ + +export interface Book{ + id : string ; + bookName : string; + isbn : string; + author : string; + clubId : string; + club : Club; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date | null; + +} + + +export interface Club { + id: string; + name: string; +} \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/Page.tsx b/apps/web/src/app/main/borrowRecord/Page.tsx new file mode 100755 index 0000000..fc543a4 --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/Page.tsx @@ -0,0 +1,163 @@ +import { api } from '@nice/client'; +import { message, Form, Modal } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { SearchBar } from './components/SearchBar'; +import { StaffTable } from './components/StaffTable'; +import { StaffModal } from './components/StaffModal'; +import { Staff, PaginatedResponse } from './components/types'; + +const TestPage: React.FC = () => { + // 状态定义 + const [searchText, setSearchText] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingStaff, setEditingStaff] = useState(null); + const [form] = Form.useForm(); + const pageSize = 10; + + // API 调用 + const { data, isLoading, refetch } = api.staff.findManyWithPagination.useQuery({ + page: currentPage, + pageSize: pageSize, + where: { + deletedAt: null, + OR: searchText ? [ + { username: { contains: searchText } }, + { showname: { contains: searchText } } + ] : undefined + }, + select: { + id: true, + username: true, + showname: true, + absent: true, + } + }); + + // 删除方法 + const deleteMutation = api.staff.softDeleteByIds.useMutation({ + onSuccess: () => { + message.success('删除成功'); + refetch(); + }, + onError: (error) => { + message.error('删除失败:' + error.message); + } + }); + + // 更新方法 + const updateMutation = api.staff.update.useMutation({ + onSuccess: () => { + message.success('更新成功'); + setIsModalVisible(false); + refetch(); + } + }); + + // 创建方法 + const createMutation = api.staff.create.useMutation({ + onSuccess: () => { + message.success('创建成功'); + setIsModalVisible(false); + refetch(); + } + }); + + // 处理函数 + const handleSearch = () => { + setCurrentPage(1); + refetch(); + }; + + const handleAdd = () => { + setEditingStaff(null); + form.resetFields(); + setIsModalVisible(true); + }; + + const handleEdit = (staff: Staff) => { + setEditingStaff(staff); + form.setFieldsValue(staff); + setIsModalVisible(true); + }; + + const handleDelete = (id: string) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除这条记录吗?', + onOk: async () => { + try { + await deleteMutation.mutateAsync({ ids: [id] }); + } catch (error) { + console.error('删除失败:', error); + } + } + }); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + refetch(); // 确保切换页面时重新获取数据 + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + if (editingStaff) { + await updateMutation.mutateAsync({ + where: { id: editingStaff.id }, + data: values + }); + } else { + await createMutation.mutateAsync({ + data: values + }); + } + } catch (error) { + message.error('操作失败:' + (error as Error).message); + } + }; + + const handleImportSuccess = () => { + refetch(); + message.success('数据已更新'); + }; + + // 初始加载 + useEffect(() => { + refetch(); + }, []); + + return ( +
{/* 修改这里,使用 h-screen 而不是 h-full */} +

培训情况记录

+ + + setIsModalVisible(false)} + /> +
+ ); +}; + +export default TestPage; \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/components/ImportExportButtons.tsx b/apps/web/src/app/main/borrowRecord/components/ImportExportButtons.tsx new file mode 100644 index 0000000..9a6ff8f --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/components/ImportExportButtons.tsx @@ -0,0 +1,153 @@ +import { Button, Upload, message } from 'antd'; +import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; +import React from 'react'; +import { Staff } from './types'; +import * as XLSX from 'xlsx'; +import { api } from '@nice/client'; + +interface ImportExportButtonsProps { + onImportSuccess: () => void; + data: Staff[]; +} + +export const ImportExportButtons: React.FC = ({ + onImportSuccess, + data, +}) => { + const createMutation = api.staff.create.useMutation({ + onSuccess: () => { + // 成功时不显示单条消息,让最终统计来显示 + }, + onError: (error) => { + // 只有当不是用户名重复错误时才显示错误信息 + if (!error.message.includes('Unique constraint failed')) { + message.error('导入失败: ' + error.message); + } + } + }); + + // 添加恢复记录的 mutation + const restoreMutation = api.staff.update.useMutation({ + onSuccess: () => { + // 静默成功,不显示消息 + }, + onError: (error) => { + console.error('恢复记录失败:', error); + } + }); + + const handleImport = async (file: File) => { + try { + const reader = new FileReader(); + reader.onload = async (e) => { + const workbook = XLSX.read(e.target?.result, { type: 'binary' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + + let successCount = 0; + let restoredCount = 0; + let errorCount = 0; + + // 转换数据格式 + const staffData = jsonData.map((row: any) => ({ + username: row['用户名'], + showname: row['名称'], + absent: row['是否在位'] === '在位', + })); + + // 批量处理 + for (const staff of staffData) { + try { + // 先尝试创建新记录 + await createMutation.mutateAsync({ + data: staff + }); + successCount++; + } catch (error: any) { + // 如果是用户名重复错误 + if (error.message.includes('Unique constraint failed')) { + try { + // 尝试恢复已删除的记录 + await restoreMutation.mutateAsync({ + where: { + username: staff.username, + }, + data: { + deletedAt: null, + showname: staff.showname, + absent: staff.absent, + } + }); + restoredCount++; + } catch (restoreError) { + errorCount++; + console.error('恢复记录失败:', restoreError); + } + } else { + errorCount++; + console.error('创建记录失败:', error); + } + } + } + + // 显示导入结果 + if (successCount > 0 || restoredCount > 0) { + let successMessage = []; + if (successCount > 0) { + successMessage.push(`新增 ${successCount} 条`); + } + if (restoredCount > 0) { + successMessage.push(`恢复 ${restoredCount} 条`); + } + message.success(`导入完成:${successMessage.join(',')}`); + onImportSuccess(); + } + if (errorCount > 0) { + message.warning(`${errorCount} 条记录导入失败`); + } + }; + reader.readAsBinaryString(file); + } catch (error) { + message.error('文件读取失败'); + } + return false; + }; + + const handleExport = () => { + try { + const exportData = data.map(staff => ({ + '用户名': staff.username, + '名称': staff.showname, + '是否在位': staff.absent ? '是' : '否' + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '员工列表'); + + XLSX.writeFile(workbook, `员工列表_${new Date().toLocaleDateString()}.xlsx`); + message.success('导出成功'); + } catch (error) { + message.error('导出失败: ' + (error as Error).message); + } + }; + + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/components/SearchBar.tsx b/apps/web/src/app/main/borrowRecord/components/SearchBar.tsx new file mode 100644 index 0000000..9f1272d --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/components/SearchBar.tsx @@ -0,0 +1,53 @@ +import { Button, Input, Space } from 'antd'; +import { SearchOutlined, PlusOutlined } from '@ant-design/icons'; +import React from 'react'; +import { ImportExportButtons } from './ImportExportButtons'; + +// Define or import the Staff type +interface Staff { + id: number; + name: string; + username: string; +} + +interface SearchBarProps { + searchText: string; + onSearchTextChange: (text: string) => void; + onSearch: () => void; + onAdd: () => void; + onImportSuccess: () => void; + data: Staff[]; +} + +export const SearchBar: React.FC = ({ + searchText, + onSearchTextChange, + onSearch, + onAdd, + onImportSuccess, + data, +}) => { + return ( +
+ + onSearchTextChange(e.target.value)} + onPressEnter={onSearch} + prefix={} + /> + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/components/StaffModal.tsx b/apps/web/src/app/main/borrowRecord/components/StaffModal.tsx new file mode 100644 index 0000000..dc03512 --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/components/StaffModal.tsx @@ -0,0 +1,55 @@ +import { Modal, Form, Input, Switch } from 'antd'; +import React from 'react'; +import { Staff } from './types'; + +interface StaffModalProps { + visible: boolean; + editingStaff: Staff | null; + form: any; + onOk: () => void; + onCancel: () => void; +} + +export const StaffModal: React.FC = ({ + visible, + editingStaff, + form, + onOk, + onCancel, +}) => { + return ( + +
+ + + + + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/components/StaffTable.tsx b/apps/web/src/app/main/borrowRecord/components/StaffTable.tsx new file mode 100644 index 0000000..3fca15f --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/components/StaffTable.tsx @@ -0,0 +1,152 @@ +import { Button, Space, Table, Tag, Input, Pagination, message } from 'antd'; +import { EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import { Staff } from './types'; + + +interface StaffTableProps { + data: Staff[]; + total: number; + currentPage: number; + pageSize: number; + isLoading?: boolean; + onPageChange: (page: number) => void; + onEdit: (staff: Staff) => void; + onDelete: (id: string) => void; +} + +export const StaffTable: React.FC = ({ + data, + total, + currentPage, + pageSize, + isLoading = false, + onPageChange, + onEdit, + onDelete, +}) => { + const [jumpPage, setJumpPage] = useState(''); + + const handleJumpPage = () => { + const page = parseInt(jumpPage); + if (!isNaN(page) && page > 0 && page <= Math.ceil(total / pageSize)) { + onPageChange(page); + setJumpPage(''); + } else { + message.error('请输入有效的页码'); + } + }; + + const columns = [ + { + title: '用户名', + dataIndex: 'username', + key: 'username', + }, + { + title: '名称', + dataIndex: 'showname', + key: 'showname', + }, + { + title: '是否在位', + dataIndex: 'absent', + key: 'absent', + render: (absent: boolean) => ( + + {absent ? '在位' : '不在位'} + + ), + }, + { + title: '操作', + key: 'action', + render: (_: any, record: Staff) => ( + + + + + ), + }, + ]; + + // 修改样式定义 + const containerStyle = { + display: 'flex', + flexDirection: 'column' as const, + height: '100%' + }; + + const tableContainerStyle = { + flex: 1, + height: 'calc(100vh - 240px)', // 减去头部、搜索栏和分页的高度 + overflow: 'hidden' + }; + + const tableStyle = { + height: '100%' + }; + + return ( +
+
+
+ +
+
+ ( + + 共 {total} 条记录 + + )} + showSizeChanger={false} + /> +
+
+ setJumpPage(e.target.value)} + onPressEnter={handleJumpPage} + placeholder="页码" + className="text-center" + /> + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/components/types.ts b/apps/web/src/app/main/borrowRecord/components/types.ts new file mode 100644 index 0000000..35a12f4 --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/components/types.ts @@ -0,0 +1,11 @@ +export interface Staff { + id: string; + username: string; + showname: string; + absent: boolean; +} + +export interface PaginatedResponse { + items: Staff[]; + total: number; +} \ No newline at end of file diff --git a/apps/web/src/app/main/borrowRecord/test.text b/apps/web/src/app/main/borrowRecord/test.text new file mode 100644 index 0000000..9fc0f42 --- /dev/null +++ b/apps/web/src/app/main/borrowRecord/test.text @@ -0,0 +1,265 @@ +import { api } from '@nice/client'; +import { Button, Input, Pagination, Space, Modal, Form, message, Switch } from 'antd'; +import { SearchOutlined, EditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import React, { useState, useRef, useEffect } from 'react'; + +// 定义 Staff 接口 +interface Staff { + id: string; + username: string; + showname: string; + absent: boolean; + // trainSituations: TrainSituation[]; +} + +interface PaginatedResponse { + items: Staff[]; + total: number; +} + +const TestPage: React.FC = () => { + const [currentPage, setCurrentPage] = useState(1); + const [searchText, setSearchText] = useState(''); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingStaff, setEditingStaff] = useState(null); + const [form] = Form.useForm(); + const pageSize = 10; + + // 修改查询逻辑 + const { data, isLoading, refetch } = api.staff.findManyWithPagination.useQuery({ + page: currentPage, + pageSize: pageSize, + where: { + deletedAt: null, + OR: searchText ? [ + { username: { contains: searchText } }, + { showname: { contains: searchText } } + ] : undefined + }, + select: { + id: true, + username: true, + showname: true, + absent: true, + } + }, { + // 移除 enabled 控制 + refetchOnWindowFocus: false, + keepPreviousData: true, + }); + + // 删除方法 + const deleteMutation = api.staff.softDeleteByIds.useMutation({ + onSuccess: () => { +message.success('删除成功'); + refetch(); + }, + onError: (error) => { + message.error('删除失败:' + error.message); + } + }); + + // 更新方法 + const updateMutation = api.staff.update.useMutation({ + onSuccess: () => { + message.success('更新成功'); + setIsModalVisible(false); + refetch(); + } + }); + + // 创建方法 + const createMutation = api.staff.create.useMutation({ + onSuccess: () => { + message.success('创建成功'); + setIsModalVisible(false); + refetch(); + } + }); + + // 修改搜索处理函数 + const handleSearch = () => { + setCurrentPage(1); + refetch(); + }; + + // 修改分页处理 + const handlePageChange = (page: number) => { + setCurrentPage(page); + refetch(); + }; + + useEffect(() => { + // 组件首次加载时执行查询 + refetch(); + }, []); + + // 处理删除的函数 + const handleDelete = async (id: string) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除这条记录吗?', + onOk: async () => { + try { + await deleteMutation.mutateAsync({ + ids: [id], + data: {} // 添加空的 data 对象 + }); + } catch (error) { + console.error('删除失败:', error); + } + } + }); + }; + + const handleEdit = (staff: Staff) => { + setEditingStaff(staff); + form.setFieldsValue(staff); + setIsModalVisible(true); + }; + + const handleAdd = () => { + setEditingStaff(null); + form.resetFields(); + setIsModalVisible(true); + }; + + const handleModalOk = () => { + form.validateFields().then(values => { + if (editingStaff) { + updateMutation.mutate({ + where: { id: editingStaff.id }, + data: values + }); + } else { + createMutation.mutate({ + data: values + }); + } + }); + }; + + return ( +
+

培训情况记录

+ + {/* 搜索和添加按钮 */} +
+ + setSearchText(e.target.value)} + onPressEnter={handleSearch} + prefix={} + /> + + + +
+ + {/* 修改表格,添加操作列 */} +
+
+ + + + + + + + + + {(data?.items || []).map((staff) => ( + + + + + + + ))} + +
用户名名称是否在位 + 操作 +
+ {staff.username} + + {staff.showname} + + {staff.absent ? '在位' : '不在位'} + + + + + +
+ + + {/* 编辑/添加模态框 */} + setIsModalVisible(false)} + > +
+ + + + + + + + + +
+
+ +
+ `共 ${total} 条记录`} + showSizeChanger={false} + showQuickJumper + /> +
+ + ); +}; + +export default TestPage; \ No newline at end of file diff --git a/apps/web/src/app/main/game/Page.tsx b/apps/web/src/app/main/game/Page.tsx new file mode 100755 index 0000000..9cd3df8 --- /dev/null +++ b/apps/web/src/app/main/game/Page.tsx @@ -0,0 +1,175 @@ +import { api } from '@nice/client'; +import { message, Form, Modal, Table, Space } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { SearchBar } from './components/SearchBar'; +import { GameTable } from './components/GameTable'; +import { GameModal } from './components/GameModal'; +import { Game } from './components/types'; + +export const GamePage = () => { + const [modalOpen, setModalOpen] = useState(false); + const [editingGame, setEditingGame] = useState(); + const [searchText, setSearchText] = useState(''); + const [messageApi, contextHolder] = message.useMessage(); + const [form] = Form.useForm(); + // 查询数据 + const { data, isLoading, refetch, isError, error } = api.game.findMany.useQuery({ + where: { + deletedAt: null, + }, + include: { + clubs: true, + sorties: { + include: { + driver: { + select: { + id: true, + name: true, + }, + }, + car: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }); + + // 独立查询所有俱乐部数据 + const { data: clubList, isLoading: clubsLoading } = api.club.findMany.useQuery({ + where: { + deletedAt: null, + }, + }); + if (isError && error) { + messageApi.error('获取数据失败:' + error.message); + } + + // 创建数据 + const createMutation = api.game.create.useMutation({ + onSuccess: () => { + messageApi.success('创建成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + messageApi.error('创建失败:' + error.message); + } + }); + + // 更新数据 + const updateMutation = api.game.update.useMutation({ + onSuccess: () => { + messageApi.success('更新成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + messageApi.error('更新失败:' + error.message); + } + }); + + // 删除数据 + const deleteMutation = api.driver.softDeleteByIds.useMutation({ + onSuccess: () => { + messageApi.success('删除成功'); + refetch(); + }, + onError: (error) => { + messageApi.error('删除失败:' + error.message); + } + }); + + // 处理搜索 + const handleSearch = () => { + refetch(); + }; + + // 处理添加 + const handleAdd = () => { + setEditingGame(undefined); + setModalOpen(true); + }; + + // 处理表单提交 + const handleModalOk = (values: any) => { + if (editingGame) { + updateMutation.mutate({ + where: { id: editingGame.id }, + data: values, + }); + } else { + createMutation.mutate({ + data: values, + }); + } + console.log(values); + }; + + // 添加统计信息 + const expandedRowRender = (record: Game) => { + const stats = { + avgTime: record.sorties.reduce((acc, curr) => acc + curr.totalTime, 0) / record.sorties.length, + maxScore: Math.max(...record.sorties.map(s => s.score)), + totalSorties: record.sorties.length, + }; + + return ( +
+
+ + 平均时间: {stats.avgTime.toFixed(2)}秒 + 最高分: {stats.maxScore.toFixed(2)} + 总车次: {stats.totalSorties} + +
+ + + ); + }; + + return ( + <> + {contextHolder} +
+ + + { + setEditingGame(driver); + setModalOpen(true); + }} + onDelete={(ids) => deleteMutation.mutate({ ids })} + /> + + setModalOpen(false)} + /> +
+ + ); +}; + +export default GamePage; \ No newline at end of file diff --git a/apps/web/src/app/main/game/components/GameModal.tsx b/apps/web/src/app/main/game/components/GameModal.tsx new file mode 100644 index 0000000..a19f675 --- /dev/null +++ b/apps/web/src/app/main/game/components/GameModal.tsx @@ -0,0 +1,93 @@ +import { Modal, Form, Input, Select, DatePicker } from 'antd'; +import { Game, Club } from './types'; +import React, { useEffect } from 'react'; +import dayjs from 'dayjs'; + +interface GameModalProps { + open: boolean; + loading?: boolean; + editingGame?: Game; + clubList?: Club[]; + onOk: (values: any) => void; + onCancel: () => void; +} + +export const GameModal: React.FC = ({ + open, + loading, + editingGame, + clubList, + onOk, + onCancel, +}) => { + const [form] = Form.useForm(); + + useEffect(() => { + if (editingGame) { + form.setFieldsValue({ + ...editingGame, + startTime: editingGame.startTime ? dayjs(editingGame.startTime) : null, + clubIds: editingGame.clubs?.map(club => club.id) || [], + }); + } else { + form.resetFields(); + } + }, [editingGame, open, form]); + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + > +
{ + const formData = { + name: values.name, + startTime: values.startTime ? values.startTime.toISOString() : null, + clubIds: values.clubIds || [], + }; + onOk(formData); + }} + > + + + + + + + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/game/components/GameTable.tsx b/apps/web/src/app/main/game/components/GameTable.tsx new file mode 100644 index 0000000..3a5b15f --- /dev/null +++ b/apps/web/src/app/main/game/components/GameTable.tsx @@ -0,0 +1,114 @@ +import { Table, Space, Button, Popconfirm } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Game, Sortie } from './types'; +import React from 'react'; + +interface GameTableProps { + data: Game[]; + loading?: boolean; + onEdit: (record: Game) => void; + onDelete: (ids: string[]) => void; +} + +export const GameTable: React.FC = ({ + data, + loading, + onEdit, + onDelete, +}) => { + // 主表格列定义 + const columns: ColumnsType = [ + { + title: '比赛名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '比赛开始时间', + dataIndex: 'startTime', + key: 'startTime', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '参与俱乐部', + dataIndex: 'clubs', + key: 'clubs', + render: (clubs: any[]) => clubs.map(club => club.name).join(', '), + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + onDelete([record.id])} + > + + + + ), + }, + ]; + + // 子表格列定义(车次信息) + const expandedRowRender = (record: Game) => { + const sortieColumns: ColumnsType = [ + { + title: '驾驶员', + dataIndex: ['driver', 'name'], + key: 'driverName', + }, + { + title: '车辆', + dataIndex: ['car', 'name'], + key: 'carName', + }, + { + title: '总时间', + dataIndex: 'totalTime', + key: 'totalTime', + render: (time: number) => `${time.toFixed(2)}秒`, + }, + { + title: '得分', + dataIndex: 'score', + key: 'score', + render: (score: number) => score.toFixed(2), + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + ]; + + return ( +
+ ); + }; + + return ( +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/game/components/ImportExportButtons.tsx b/apps/web/src/app/main/game/components/ImportExportButtons.tsx new file mode 100644 index 0000000..bdb8bf9 --- /dev/null +++ b/apps/web/src/app/main/game/components/ImportExportButtons.tsx @@ -0,0 +1,153 @@ +import { Button, Upload, message } from 'antd'; +import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; +import React from 'react'; +import { Driver } from './types'; +import * as XLSX from 'xlsx'; +import { api } from '@nice/client'; + +interface ImportExportButtonsProps { + onImportSuccess: () => void; + data: Driver[]; +} + +export const ImportExportButtons: React.FC = ({ + onImportSuccess, + data, +}) => { + const createMutation = api.driver.create.useMutation({ + onSuccess: () => { + // 成功时不显示单条消息,让最终统计来显示 + }, + onError: (error) => { + // 只有当不是用户名重复错误时才显示错误信息 + if (!error.message.includes('Unique constraint failed')) { + message.error('导入失败: ' + error.message); + } + } + }); + + // 添加恢复记录的 mutation + const restoreMutation = api.driver.update.useMutation({ + onSuccess: () => { + // 静默成功,不显示消息 + }, + onError: (error) => { + console.error('恢复记录失败:', error); + } + }); + + const handleImport = async (file: File) => { + try { + const reader = new FileReader(); + reader.onload = async (e) => { + const workbook = XLSX.read(e.target?.result, { type: 'binary' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + + let successCount = 0; + let restoredCount = 0; + let errorCount = 0; + + // 转换数据格式 + const driverData = jsonData.map((row: any) => ({ + username: row['用户名'], + showname: row['名称'], + absent: row['是否在位'] === '在位', + })); + + // 批量处理 + for (const driver of driverData) { + try { + // 先尝试创建新记录 + await createMutation.mutateAsync({ + data: driver + }); + successCount++; + } catch (error: any) { + // 如果是用户名重复错误 + if (error.message.includes('Unique constraint failed')) { + try { + // 尝试恢复已删除的记录 + await restoreMutation.mutateAsync({ + where: { + username: driver.username, + }, + data: { + deletedAt: null, + showname: driver.showname, + absent: driver.absent, + } + }); + restoredCount++; + } catch (restoreError) { + errorCount++; + console.error('恢复记录失败:', restoreError); + } + } else { + errorCount++; + console.error('创建记录失败:', error); + } + } + } + + // 显示导入结果 + if (successCount > 0 || restoredCount > 0) { + let successMessage = []; + if (successCount > 0) { + successMessage.push(`新增 ${successCount} 条`); + } + if (restoredCount > 0) { + successMessage.push(`恢复 ${restoredCount} 条`); + } + message.success(`导入完成:${successMessage.join(',')}`); + onImportSuccess(); + } + if (errorCount > 0) { + message.warning(`${errorCount} 条记录导入失败`); + } + }; + reader.readAsBinaryString(file); + } catch (error) { + message.error('文件读取失败'); + } + return false; + }; + + const handleExport = () => { + try { + const exportData = data.map(driver => ({ + '用户名': driver.username, + '名称': driver.showname, + '是否在位': driver.absent ? '是' : '否' + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '员工列表'); + + XLSX.writeFile(workbook, `员工列表_${new Date().toLocaleDateString()}.xlsx`); + message.success('导出成功'); + } catch (error) { + message.error('导出失败: ' + (error as Error).message); + } + }; + + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/game/components/SearchBar.tsx b/apps/web/src/app/main/game/components/SearchBar.tsx new file mode 100644 index 0000000..8d481e6 --- /dev/null +++ b/apps/web/src/app/main/game/components/SearchBar.tsx @@ -0,0 +1,55 @@ +import { Button, Input, Space } from 'antd'; +import { SearchOutlined, PlusOutlined } from '@ant-design/icons'; +import React from 'react'; +// import { ImportExportButtons } from './ImportExportButtons'; + +// Define or import the Staff type +// interface Driver { +// id: string; +// name: string; +// gender: string; +// age: number; +// clubName: string; +// } + +interface SearchBarProps { + searchText: string; + onSearchTextChange: (text: string) => void; + onSearch: () => void; + onAdd: () => void; + onImportSuccess: () => void; + // data: Driver[]; +} + +export const SearchBar: React.FC = ({ + searchText, + onSearchTextChange, + onSearch, + onAdd, + onImportSuccess, + // data, +}) => { + return ( +
+ + onSearchTextChange(e.target.value)} + onPressEnter={onSearch} + prefix={} + /> + + + + {/* */} + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/game/components/types.ts b/apps/web/src/app/main/game/components/types.ts new file mode 100644 index 0000000..1707bdc --- /dev/null +++ b/apps/web/src/app/main/game/components/types.ts @@ -0,0 +1,30 @@ +export interface Game { + id : string ; + name : string; + startTime : string; + clubs : Club[]; + sorties : Sortie[]; + createdAt: string; + updatedAt: string; + deletedAt?: string | null; +} + +export interface Club { + id: string; + name: string; +} + +export interface Sortie { + id: string; + totalTime: number; + score: number; + driver: { + id: string; + name: string; + }; + car: { + id: string; + name: string; + }; + createdAt: string; +} diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 27a2f1f..b3c9cc8 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -57,6 +57,36 @@ const items = [ , null, null, + ), + getItem( + "图书馆", + "/library", + , + null, + null, + ), + getItem( + "读者", + "/reader", + , + null, + null, + ), + + getItem( + "图书", + "/book", + , + null, + null, + ), + + getItem( + "借阅记录", + "/borrowRecord", + , + null, + null, ), getItem( "系统设置", diff --git a/apps/web/src/app/main/library/Page.tsx b/apps/web/src/app/main/library/Page.tsx new file mode 100755 index 0000000..3db7245 --- /dev/null +++ b/apps/web/src/app/main/library/Page.tsx @@ -0,0 +1,144 @@ + +import { useState } from 'react'; + +import { LibraryTable } from './components/LibraryTable'; +import { LibraryModal } from './components/LibraryModal'; +import { SearchBar } from './components/SearchBar'; +import { Library } from './components/types'; +import { api } from '@nice/client'; +import { message } from 'antd'; + +export const LibraryPage = () => { + const [modalOpen, setModalOpen] = useState(false); + const [editingLibrary, setEditingLibrary] = useState(); + const [searchText, setSearchText] = useState(''); + + // 查询数据 + const { data, isLoading, refetch, isError, error } = api.library.findMany.useQuery({ + where: { + deletedAt: null, + OR: searchText.trim() ? [ + { name: { contains: searchText } }, + { address: { contains: searchText } }, + { description: { contains: searchText } }, + ] : undefined + }, + include: { + + }, + }); + + if (isError && error) { + message.error('获取数据失败:' + error.message); + } + + // 创建数据 + const createMutation = api.library.create.useMutation({ + onSuccess: () => { + message.success('创建成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('创建失败:' + error.message); + } + }); + + // 更新数据 + const updateMutation = api.library.update.useMutation({ + onSuccess: () => { + message.success('更新成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('更新失败:' + error.message); + } + }); + + // 删除数据 + const deleteMutation = api.library.softDeleteByIds.useMutation({ + onSuccess: () => { + message.success('删除成功'); + refetch(); + }, + onError: (error) => { + message.error('删除失败:' + error.message); + } + }); + + // 处理搜索 + const handleSearch = () => { + refetch(); + }; + + // 处理添加 + const handleAdd = () => { + setEditingLibrary(undefined); + setModalOpen(true); + }; + + // 处理模态框取消,并确保清空 editingLibrary 状态 + const handleCancelModal = () => { + setModalOpen(false); + setEditingLibrary(undefined); // 关键:在关闭模态框时,清除编辑对象 + }; + + + // 处理表单提交 + const handleModalOk = (values: any) => { + if (editingLibrary) { + updateMutation.mutate({ + where: { id: editingLibrary.id }, + data: { + name: values.name, + address: values.address, + description: values.description, + + } + }); + } else { + createMutation.mutate({ + + name: values.name, + address: values.address, + description: values.description, + + }); + } + }; + + return ( +
+ + + { + setEditingLibrary(library); + setModalOpen(true); + }} + onDelete={(ids) => deleteMutation.mutate({ ids })} + /> + + +
+ ); +}; + +export default LibraryPage; \ No newline at end of file diff --git a/apps/web/src/app/main/library/components/ImportExportButtons.tsx b/apps/web/src/app/main/library/components/ImportExportButtons.tsx new file mode 100644 index 0000000..8e1007e --- /dev/null +++ b/apps/web/src/app/main/library/components/ImportExportButtons.tsx @@ -0,0 +1,158 @@ +import { Button, Upload, message } from 'antd'; +import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; +import React from 'react'; +import { Club } from './types'; +import * as XLSX from 'xlsx'; +import { api } from '@nice/client'; + +interface ImportExportButtonsProps { + onImportSuccess: () => void; + data: Club[]; +} + +export const ImportExportButtons: React.FC = ({ + onImportSuccess, + data, +}) => { + const createMutation = api.club.create.useMutation({ + onSuccess: () => { + // 成功时不显示单条消息,让最终统计来显示 + }, + onError: (error) => { + // 只有当不是名称重复错误时才显示错误信息 + if (!error.message.includes('Unique constraint failed')) { + message.error('导入失败: ' + error.message); + } + } + }); + + // 添加恢复记录的 mutation + const restoreMutation = api.club.update.useMutation({ + onSuccess: () => { + // 静默成功,不显示消息 + }, + onError: (error) => { + console.error('恢复记录失败:', error); + } + }); + + const handleImport = async (file: File) => { + try { + const reader = new FileReader(); + reader.onload = async (e) => { + const workbook = XLSX.read(e.target?.result, { type: 'binary' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + + let successCount = 0; + let restoredCount = 0; + let errorCount = 0; + + // 转换数据格式,包含 id 字段 + const clubData = jsonData.map((row: any) => ({ + id: row['ID'] || undefined, + name: row['名称'], + description: row['描述'], + parentId: row['上级俱乐部ID'] || null, + })); + + // 批量处理 + for (const club of clubData) { + try { + await createMutation.mutateAsync(club); + successCount++; + } catch (error: any) { + if (error.message.includes('Unique constraint failed')) { + try { + // 这里需要 club.id,假设 clubData 里有 id 字段,否则需要先查找 id + if (!club.id) { + errorCount++; + console.error('恢复记录失败: 缺少唯一标识 id'); + continue; + } + await restoreMutation.mutateAsync({ + where: { + id: club.id, + }, + data: { + deletedAt: null, + description: club.description, + parentId: club.parentId, + } + }); + restoredCount++; + } catch (restoreError) { + errorCount++; + console.error('恢复记录失败:', restoreError); + } + } else { + errorCount++; + console.error('创建记录失败:', error); + } + } + } + + // 显示导入结果 + if (successCount > 0 || restoredCount > 0) { + let successMessage = []; + if (successCount > 0) { + successMessage.push(`新增 ${successCount} 条`); + } + if (restoredCount > 0) { + successMessage.push(`恢复 ${restoredCount} 条`); + } + message.success(`导入完成:${successMessage.join(',')}`); + onImportSuccess(); + } + if (errorCount > 0) { + message.warning(`${errorCount} 条记录导入失败`); + } + }; + reader.readAsBinaryString(file); + } catch (error) { + message.error('文件读取失败'); + } + return false; + }; + + const handleExport = () => { + try { + const exportData = data.map(club => ({ + '名称': club.name, + '描述': club.description, + '上级俱乐部': club.parent?.name || '', + '上级俱乐部ID': club.parentId || '', + '创建时间': new Date(club.createdAt).toLocaleString(), + '更新时间': new Date(club.updatedAt).toLocaleString(), + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '俱乐部列表'); + + XLSX.writeFile(workbook, `俱乐部列表_${new Date().toLocaleDateString()}.xlsx`); + message.success('导出成功'); + } catch (error) { + message.error('导出失败: ' + (error as Error).message); + } + }; + + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/library/components/LibraryModal.tsx b/apps/web/src/app/main/library/components/LibraryModal.tsx new file mode 100644 index 0000000..b60396c --- /dev/null +++ b/apps/web/src/app/main/library/components/LibraryModal.tsx @@ -0,0 +1,107 @@ +import { Modal, Form, Input, Select } from 'antd'; +import { Library } from './types'; +import React ,{useEffect}from 'react'; + +interface LibraryModalProps { + open: boolean; + loading?: boolean; + editingLibrary?: Library; + libraryList?: Library[]; + onOk: (values: any) => void; + onCancel: () => void; +} + +export const LibraryModal: React.FC = ({ + open, + loading, + editingLibrary, + libraryList, + onOk, + onCancel, +}) => { + const [form] = Form.useForm(); + + + // 核心逻辑:当模态框的打开状态或编辑对象改变时,更新表单 + useEffect(() => { + if (open) { + // 当模态框打开时 + if (editingLibrary) { + // 如果有编辑中的图书馆(编辑模式),则填充表单 + form.setFieldsValue(editingLibrary); + } else { + // 如果没有编辑中的图书馆(新增模式),则清空表单 + form.resetFields(); + } + } else { + // 当模态框关闭时,也清空表单,为下一次打开(特别是新增)做准备 + form.resetFields(); + } + }, [open, editingLibrary, form]); // 依赖项:模态框的打开状态、编辑对象和表单实例 + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + > +
{ + // 确保数据格式正确 + const formData = { + name: values.name, + address: values.address, + description: values.description, + + }; + onOk(formData); + }} + > + + + + + + + + + + + + + {/* + + */} + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/library/components/LibraryTable.tsx b/apps/web/src/app/main/library/components/LibraryTable.tsx new file mode 100644 index 0000000..bd6dfc4 --- /dev/null +++ b/apps/web/src/app/main/library/components/LibraryTable.tsx @@ -0,0 +1,70 @@ +import { Table, Space, Button, Popconfirm } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Library } from './types'; + +import React from 'react'; +interface LibraryTableProps { + data: Library[]; + loading?: boolean; + onEdit: (record: Library) => void; + onDelete: (ids: string[]) => void; +} + +export const LibraryTable: React.FC = ({ + data, + loading, + onEdit, + onDelete, +}) => { + const columns: ColumnsType = [ + { + title: '图书馆名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '地址', + dataIndex: 'address', + key: 'address', + }, + { + title: '图书馆描述', + dataIndex: 'description', + key: 'description', + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + onDelete([record.id])} + > + + + + ), + }, + ]; + + return ( +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/library/components/SearchBar.tsx b/apps/web/src/app/main/library/components/SearchBar.tsx new file mode 100644 index 0000000..f580036 --- /dev/null +++ b/apps/web/src/app/main/library/components/SearchBar.tsx @@ -0,0 +1,47 @@ +import { Button, Input, Space } from 'antd'; +import { SearchOutlined, PlusOutlined } from '@ant-design/icons'; +import React from 'react'; +import { ImportExportButtons } from './ImportExportButtons'; +import { Library } from './types'; + +interface SearchBarProps { + searchText: string; + onSearchTextChange: (text: string) => void; + onSearch: () => void; + onAdd: () => void; + onImportSuccess: () => void; + data: Library[]; +} + +export const SearchBar: React.FC = ({ + searchText, + onSearchTextChange, + onSearch, + onAdd, + onImportSuccess, + data, +}) => { + return ( +
+ + onSearchTextChange(e.target.value)} + onPressEnter={onSearch} + prefix={} + /> + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/library/components/types.ts b/apps/web/src/app/main/library/components/types.ts new file mode 100644 index 0000000..44f5ce7 --- /dev/null +++ b/apps/web/src/app/main/library/components/types.ts @@ -0,0 +1,9 @@ +export interface Library { + id: string; + name: string; + address: string; + description : string; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date | null; +} diff --git a/apps/web/src/app/main/reader/Page.tsx b/apps/web/src/app/main/reader/Page.tsx new file mode 100755 index 0000000..72c31af --- /dev/null +++ b/apps/web/src/app/main/reader/Page.tsx @@ -0,0 +1,152 @@ +import { api } from '@nice/client'; +import { message, Form, Modal } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { SearchBar } from './components/SearchBar'; +import { ReaderTable } from './components/ReaderTable'; +import { ReaderModal } from './components/ReaderModal'; +import { Reader } from './components/types'; + +export const ReaderPage = () => { + const [modalOpen, setModalOpen] = useState(false); + const [editingReader, setEditingReader] = useState(); + const [searchText, setSearchText] = useState(''); + const [form] = Form.useForm(); + // 查询数据 + const { data, isLoading, refetch, isError, error } = api.reader.findMany.useQuery({ + where: { + deletedAt: null, + OR: searchText ? [ + { username: { contains: searchText } }, + { gender: { contains: searchText } }, + { age: !isNaN(Number(searchText)) ? Number(searchText) : undefined }, + { relax: { contains: searchText } }, + { library: { name: { contains: searchText } } } + ] : undefined + }, + include: { + library: true, + + }, + }); + + // 独立查询所有俱乐部数据 + const { data: libraryList, isLoading: librarysLoading } = api.library.findMany.useQuery({ + where: { + deletedAt: null, + }, + }); + if (isError && error) { + message.error('获取数据失败:' + error.message); + } + + // 创建数据 + const createMutation = api.reader.create.useMutation({ + onSuccess: () => { + message.success('创建成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('创建失败:' + error.message); + } + }); + + // 更新数据 + const updateMutation = api.reader.update.useMutation({ + onSuccess: () => { + message.success('更新成功'); + setModalOpen(false); + refetch(); + }, + onError: (error) => { + message.error('更新失败:' + error.message); + } + }); + + // 删除数据 + const deleteMutation = api.reader.softDeleteByIds.useMutation({ + onSuccess: () => { + message.success('删除成功'); + refetch(); + }, + onError: (error) => { + message.error('删除失败:' + error.message); + } + }); + + // 处理搜索 + const handleSearch = () => { + refetch(); + }; + + // 处理添加 + const handleAdd = () => { + setEditingReader(undefined); + setModalOpen(true); + }; + + // 处理表单提交 + const handleModalOk = (values: any) => { + if (editingReader) { + updateMutation.mutate({ + where: { id: editingReader.id }, + data: { + name: values.name, + password: values.password, + username: values.username, + gender: values.gender, + age: Number(values.age), // 确保 age 是数字类型 + relax: values.relax || null, + libraryId: values.libraryId, + } + }); + } else { + createMutation.mutate({ + + name: values.name, + password: values.password, + username: values.username, + gender: values.gender, + age: Number(values.age), // 确保 age 是数字类型 + relax: values.relax || null, + libraryId: values.libraryId, + + }); + } console.log(values); + + }; + + return ( +
+ + + { + setEditingReader(reader); + setModalOpen(true); + }} + onDelete={(ids) => deleteMutation.mutate({ ids })} + /> + + setModalOpen(false)} + /> +
+ ); +}; + +export default ReaderPage; \ No newline at end of file diff --git a/apps/web/src/app/main/reader/components/ImportExportButtons.tsx b/apps/web/src/app/main/reader/components/ImportExportButtons.tsx new file mode 100644 index 0000000..bdb8bf9 --- /dev/null +++ b/apps/web/src/app/main/reader/components/ImportExportButtons.tsx @@ -0,0 +1,153 @@ +import { Button, Upload, message } from 'antd'; +import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; +import React from 'react'; +import { Driver } from './types'; +import * as XLSX from 'xlsx'; +import { api } from '@nice/client'; + +interface ImportExportButtonsProps { + onImportSuccess: () => void; + data: Driver[]; +} + +export const ImportExportButtons: React.FC = ({ + onImportSuccess, + data, +}) => { + const createMutation = api.driver.create.useMutation({ + onSuccess: () => { + // 成功时不显示单条消息,让最终统计来显示 + }, + onError: (error) => { + // 只有当不是用户名重复错误时才显示错误信息 + if (!error.message.includes('Unique constraint failed')) { + message.error('导入失败: ' + error.message); + } + } + }); + + // 添加恢复记录的 mutation + const restoreMutation = api.driver.update.useMutation({ + onSuccess: () => { + // 静默成功,不显示消息 + }, + onError: (error) => { + console.error('恢复记录失败:', error); + } + }); + + const handleImport = async (file: File) => { + try { + const reader = new FileReader(); + reader.onload = async (e) => { + const workbook = XLSX.read(e.target?.result, { type: 'binary' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + + let successCount = 0; + let restoredCount = 0; + let errorCount = 0; + + // 转换数据格式 + const driverData = jsonData.map((row: any) => ({ + username: row['用户名'], + showname: row['名称'], + absent: row['是否在位'] === '在位', + })); + + // 批量处理 + for (const driver of driverData) { + try { + // 先尝试创建新记录 + await createMutation.mutateAsync({ + data: driver + }); + successCount++; + } catch (error: any) { + // 如果是用户名重复错误 + if (error.message.includes('Unique constraint failed')) { + try { + // 尝试恢复已删除的记录 + await restoreMutation.mutateAsync({ + where: { + username: driver.username, + }, + data: { + deletedAt: null, + showname: driver.showname, + absent: driver.absent, + } + }); + restoredCount++; + } catch (restoreError) { + errorCount++; + console.error('恢复记录失败:', restoreError); + } + } else { + errorCount++; + console.error('创建记录失败:', error); + } + } + } + + // 显示导入结果 + if (successCount > 0 || restoredCount > 0) { + let successMessage = []; + if (successCount > 0) { + successMessage.push(`新增 ${successCount} 条`); + } + if (restoredCount > 0) { + successMessage.push(`恢复 ${restoredCount} 条`); + } + message.success(`导入完成:${successMessage.join(',')}`); + onImportSuccess(); + } + if (errorCount > 0) { + message.warning(`${errorCount} 条记录导入失败`); + } + }; + reader.readAsBinaryString(file); + } catch (error) { + message.error('文件读取失败'); + } + return false; + }; + + const handleExport = () => { + try { + const exportData = data.map(driver => ({ + '用户名': driver.username, + '名称': driver.showname, + '是否在位': driver.absent ? '是' : '否' + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '员工列表'); + + XLSX.writeFile(workbook, `员工列表_${new Date().toLocaleDateString()}.xlsx`); + message.success('导出成功'); + } catch (error) { + message.error('导出失败: ' + (error as Error).message); + } + }; + + return ( +
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/reader/components/ReaderModal.tsx b/apps/web/src/app/main/reader/components/ReaderModal.tsx new file mode 100644 index 0000000..5b53be3 --- /dev/null +++ b/apps/web/src/app/main/reader/components/ReaderModal.tsx @@ -0,0 +1,117 @@ +import { Modal, Form, Input, Select } from 'antd'; +import { Library, Reader } from './types'; +import React from 'react'; + +interface ReaderModalProps { + open: boolean; + loading?: boolean; + editingReader?: Reader; + libraryList?: Library[]; + onOk: (values: any) => void; + onCancel: () => void; +} + +export const ReaderModal: React.FC = ({ + open, + loading, + editingReader, + libraryList, + onOk, + onCancel, +}) => { + const [form] = Form.useForm(); + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + > +
{ + // 确保数据格式正确 + const formData = { + name: values.name, + password: values.password, + username:values.username, + gender: values.gender, + age: values.age, + relax: values.relax|| null, + libraryId: values.libraryId, + }; + onOk(formData); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/reader/components/ReaderTable.tsx b/apps/web/src/app/main/reader/components/ReaderTable.tsx new file mode 100644 index 0000000..6bf4b39 --- /dev/null +++ b/apps/web/src/app/main/reader/components/ReaderTable.tsx @@ -0,0 +1,81 @@ +import { Table, Space, Button, Popconfirm } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Reader } from './types'; + +import React from 'react'; +interface ReaderTableProps { + data: Reader[]; + loading?: boolean; + onEdit: (record: Reader) => void; + onDelete: (ids: string[]) => void; +} + +export const ReaderTable: React.FC = ({ + data, + loading, + onEdit, + onDelete, +}) => { + const columns: ColumnsType = [ + { + title: '用户名', + dataIndex: 'username', + key: 'username', + }, + { + title: '性别', + dataIndex: 'gender', + key: 'gender', + }, + { + title: '年龄', + dataIndex: 'age', + key: 'age', + }, + { + title: '联系方式', + dataIndex: 'relax', + key: 'relax', + }, + { + title: '所属图书馆', + dataIndex: ['library', 'name'], + key: 'libraryName', + + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + onDelete([record.id])} + > + + + + ), + }, + ]; + + return ( +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/reader/components/SearchBar.tsx b/apps/web/src/app/main/reader/components/SearchBar.tsx new file mode 100644 index 0000000..dd00867 --- /dev/null +++ b/apps/web/src/app/main/reader/components/SearchBar.tsx @@ -0,0 +1,55 @@ +import { Button, Input, Space } from 'antd'; +import { SearchOutlined, PlusOutlined } from '@ant-design/icons'; +import React from 'react'; +// import { ImportExportButtons } from './ImportExportButtons'; + +// Define or import the Staff type +// interface Driver { +// id: string; +// name: string; +// gender: string; +// age: number; +// clubName: string; +// } + +interface SearchBarProps { + searchText: string; + onSearchTextChange: (text: string) => void; + onSearch: () => void; + onAdd: () => void; + onImportSuccess: () => void; + // data: Driver[]; +} + +export const SearchBar: React.FC = ({ + searchText, + onSearchTextChange, + onSearch, + onAdd, + onImportSuccess, + // data, +}) => { + return ( +
+ + onSearchTextChange(e.target.value)} + onPressEnter={onSearch} + prefix={} + /> + + + + {/* */} + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/main/reader/components/types.ts b/apps/web/src/app/main/reader/components/types.ts new file mode 100644 index 0000000..76f81ed --- /dev/null +++ b/apps/web/src/app/main/reader/components/types.ts @@ -0,0 +1,22 @@ + + +export interface Reader { + id : string ; + name : string; + password: string; + username : string; + gender : string; + age : number; + relax : string; + libraryId : string; + library : Library; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date | null; + +} + +export interface Library { + id: string; + name: string; +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index a52661d..bf0c695 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -19,6 +19,11 @@ import { adminRoute } from "./admin-route"; import AdminLayout from "../components/layout/admin/AdminLayout"; import SystemLogPage from "../app/main/systemlog/SystemLogPage"; import TestPage from "../app/main/Test/Page"; +import LibraryPage from "../app/main/library/Page"; +import ReaderPage from "../app/main/reader/Page"; +import BookPage from "../app/main/book/Page"; +import BorrowRecordPage from "../app/main/borrowRecord/Page"; + interface CustomIndexRouteObject extends IndexRouteObject { @@ -78,6 +83,24 @@ export const routes: CustomRouteObject[] = [ path: "/test", element: , }, + + { + path: "/library", + element: , + }, + { + path: "/reader", + element: , + }, + { + path: "/book", + element: , + }, + + { + path: "/borrowRecord", + element: , + }, ], }, ], diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index 8fda83d..2816e5b 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -2,7 +2,7 @@ server { # 监听80端口 listen 80; # 服务器域名/IP地址,使用环境变量 - server_name 192.168.239.194; + server_name 192.168.218.194; # 基础性能优化配置 # 启用tcp_nopush以优化数据发送 @@ -100,7 +100,7 @@ server { # 仅供内部使用 internal; # 代理到认证服务 - proxy_pass http://192.168.239.194:3000/auth/file; + proxy_pass http://192.168.218.194:3000/auth/file; # 请求优化:不传递请求体 proxy_pass_request_body off; diff --git a/packages/common/prisma/schema.BACKUP b/packages/common/prisma/schema.BACKUP deleted file mode 100755 index 7ae93cc..0000000 --- a/packages/common/prisma/schema.BACKUP +++ /dev/null @@ -1,475 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model Taxonomy { - id String @id @default(cuid()) - name String @unique - slug String @unique @map("slug") - deletedAt DateTime? @map("deleted_at") - createdAt DateTime @default(now()) @map("created_at") - terms Term[] - objectType String[] @map("object_type") - order Float? @map("order") - - @@index([order, deletedAt]) - @@map("taxonomy") -} - -model Term { - id String @id @default(cuid()) - name String - taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id]) - taxonomyId String? @map("taxonomy_id") - order Float? @map("order") - description String? - parentId String? @map("parent_id") - parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade) - children Term[] @relation("ChildParent") - ancestors TermAncestry[] @relation("DescendantToAncestor") - descendants TermAncestry[] @relation("AncestorToDescendant") - domainId String? @map("domain_id") - domain Department? @relation("TermDom", fields: [domainId], references: [id]) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - createdBy String? @map("created_by") - depts Department[] @relation("department_term") - hasChildren Boolean? @default(false) @map("has_children") - courses Course[] @relation("course_term") - - @@index([name]) // 对name字段建立索引,以加快基于name的查找速度 - @@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度 - @@map("term") -} - -model TermAncestry { - id String @id @default(cuid()) - ancestorId String? @map("ancestor_id") - descendantId String @map("descendant_id") - relDepth Int @map("rel_depth") - ancestor Term? @relation("AncestorToDescendant", fields: [ancestorId], references: [id]) - descendant Term @relation("DescendantToAncestor", fields: [descendantId], references: [id]) - - // 索引建议 - @@index([ancestorId]) // 针对祖先的查询 - @@index([descendantId]) // 针对后代的查询 - @@index([ancestorId, descendantId]) // 组合索引,用于查询特定的祖先-后代关系 - @@index([relDepth]) // 根据关系深度的查询 - @@map("term_ancestry") -} - -model Staff { - id String @id @default(cuid()) - showname String? @map("showname") - username String @unique @map("username") - avatar String? @map("avatar") - password String? @map("password") - phoneNumber String? @unique @map("phone_number") - - domainId String? @map("domain_id") - deptId String? @map("dept_id") - - domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) - department Department? @relation("DeptStaff", fields: [deptId], references: [id]) - order Float? - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - enabled Boolean? @default(true) - deletedAt DateTime? @map("deleted_at") - officerId String? @map("officer_id") - - watchedPost Post[] @relation("post_watch_staff") - visits Visit[] - posts Post[] - sentMsgs Message[] @relation("message_sender") - receivedMsgs Message[] @relation("message_receiver") - registerToken String? - enrollments Enrollment[] - teachedCourses CourseInstructor[] - ownedResources Resource[] - - @@index([officerId]) - @@index([deptId]) - @@index([domainId]) - @@index([username]) - @@index([order]) - @@map("staff") -} - -model Department { - id String @id @default(cuid()) - name String - order Float? - ancestors DeptAncestry[] @relation("DescendantToAncestor") - descendants DeptAncestry[] @relation("AncestorToDescendant") - parentId String? @map("parent_id") - parent Department? @relation("ChildParent", fields: [parentId], references: [id]) - children Department[] @relation("ChildParent") - domainId String? @map("domain_id") - domainTerms Term[] @relation("TermDom") - deletedAt DateTime? @map("deleted_at") - isDomain Boolean? @default(false) @map("is_domain") - domainStaffs Staff[] @relation("DomainStaff") - deptStaffs Staff[] @relation("DeptStaff") - terms Term[] @relation("department_term") - - watchedPost Post[] @relation("post_watch_dept") - hasChildren Boolean? @default(false) @map("has_children") - - @@index([parentId]) - @@index([isDomain]) - @@index([name]) - @@index([order]) - @@map("department") -} - -model DeptAncestry { - id String @id @default(cuid()) - ancestorId String? @map("ancestor_id") - descendantId String @map("descendant_id") - relDepth Int @map("rel_depth") - ancestor Department? @relation("AncestorToDescendant", fields: [ancestorId], references: [id]) - descendant Department @relation("DescendantToAncestor", fields: [descendantId], references: [id]) - - // 索引建议 - @@index([ancestorId]) // 针对祖先的查询 - @@index([descendantId]) // 针对后代的查询 - @@index([ancestorId, descendantId]) // 组合索引,用于查询特定的祖先-后代关系 - @@index([relDepth]) // 根据关系深度的查询 - @@map("dept_ancestry") -} - -model RoleMap { - id String @id @default(cuid()) - objectId String @map("object_id") - roleId String @map("role_id") - domainId String? @map("domain_id") - objectType String @map("object_type") - role Role @relation(fields: [roleId], references: [id]) - - @@index([domainId]) - @@index([objectId]) - @@map("rolemap") -} - -model Role { - id String @id @default(cuid()) - name String @unique @map("name") - permissions String[] @default([]) @map("permissions") - roleMaps RoleMap[] - system Boolean? @default(false) @map("system") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("role") -} - -model AppConfig { - id String @id @default(cuid()) - slug String @unique - title String? - description String? - meta Json? - - @@map("app_config") -} - -model Post { - // 字符串类型字段 - id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值 - type String? // 帖子类型,可为空 - title String? // 帖子标题,可为空 - content String? // 帖子内容,可为空 - domainId String? @map("domain_id") - // 日期时间类型字段 - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 - // 整数类型字段 - rating Int // 评分(1-5星) - // 关系类型字段 - authorId String? @map("author_id") - author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型 - - visits Visit[] // 访问记录,关联 Visit 模型 - - courseId String @map("course_id") - course Course @relation(fields: [courseId], references: [id]) // 关联课程,关联 Course 模型 - - parentId String? @map("parent_id") - parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型 - children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型 - - lectureId String? @map("lecture_id") - lecture Lecture? @relation(fields: [lectureId], references: [id]) // 关联讲座,关联 Lecture 模型 - resources Resource[] // 附件列表 - - watchableStaffs Staff[] @relation("post_watch_staff") // 可观看的员工列表,关联 Staff 模型 - watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 - - // 复合索引 - @@index([type, domainId]) // 类型和域组合查询 - @@index([authorId, type]) // 作者和类型组合查询 - @@index([parentId, type]) // 父级帖子和创建时间索引 - // 时间相关索引 - @@index([createdAt]) // 按创建时间倒序索引 - @@index([updatedAt]) // 按更新时间倒序索引 -} - -model Message { - id String @id @default(cuid()) - url String? - intent String? - option Json? - senderId String? @map("sender_id") - type String? - sender Staff? @relation(name: "message_sender", fields: [senderId], references: [id]) - title String? - content String? - receivers Staff[] @relation("message_receiver") - visits Visit[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - - @@index([type, createdAt]) - @@map("message") -} - -model Visit { - id String @id @default(cuid()) @map("id") - type String? - views Int @default(1) @map("views") - // sourceIP String? @map("source_ip") - // 关联关系 - visitorId String @map("visitor_id") - visitor Staff @relation(fields: [visitorId], references: [id]) - postId String? @map("post_id") - post Post? @relation(fields: [postId], references: [id]) - message Message? @relation(fields: [messageId], references: [id]) - messageId String? @map("message_id") - lecture Lecture? @relation(fields: [lectureId], references: [id], onDelete: Cascade) - lectureId String? @map("lecture_id") // 课时ID - - // 学习数据 - // progress Float? @default(0) @map("progress") // 完成进度(0-100%) - // isCompleted Boolean? @default(false) @map("is_completed") // 是否完成 - // lastPosition Int? @default(0) @map("last_position") // 视频播放位置(秒) - // totalWatchTime Int? @default(0) @map("total_watch_time") // 总观看时长(秒) - // // 时间记录 - // lastWatchedAt DateTime? @map("last_watched_at") // 最后观看时间 - createdAt DateTime @default(now()) @map("created_at") // 创建时间 - updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 - - meta Json? - - @@index([postId, type, visitorId]) - @@index([messageId, type, visitorId]) - @@map("visit") -} - -model Course { - id String @id @default(cuid()) @map("id") // 课程唯一标识符 - title String? @map("title") // 课程标题 - subTitle String? @map("sub_title") // 课程副标题(可选) - description String? @map("description") // 课程详细描述 - thumbnail String? @map("thumbnail") // 课程封面图片URL(可选) - level String? @map("level") // 课程难度等级 - // 课程内容组织结构 - terms Term[] @relation("course_term") // 课程学期 - instructors CourseInstructor[] // 课程讲师团队 - sections Section[] // 课程章节结构 - lectures Lecture[] - enrollments Enrollment[] // 学生报名记录 - reviews Post[] // 学员课程评价 - // 课程规划与目标设定 - requirements String[] @map("requirements") // 课程学习前置要求 - objectives String[] @map("objectives") // 具体的学习目标 - // 课程状态管理 - status String? @map("status") // 课程状态(如:草稿/已发布/已归档) - featured Boolean? @default(false) @map("featured") // 是否为精选推荐课程 - - // 生命周期时间戳 - createdAt DateTime? @default(now()) @map("created_at") // 创建时间 - updatedAt DateTime? @updatedAt @map("updated_at") // 最后更新时间 - publishedAt DateTime? @map("published_at") // 发布时间 - deletedAt DateTime? @map("deleted_at") // 软删除时间 - meta Json? - - // 课程统计指标 - // totalDuration Int? @default(0) @map("total_duration") // 课程总时长(分钟) - // totalLectures Int? @default(0) @map("total_lectures") // 总课时数 - // averageRating Float? @default(0) @map("average_rating") // 平均评分(1-5分) - // numberOfReviews Int? @default(0) @map("number_of_reviews") // 评价总数 - // numberOfStudents Int? @default(0) @map("number_of_students") // 学习人数 - // completionRate Float? @default(0) @map("completion_rate") // 完课率(0-100%) - // 数据库索引优化 - @@index([status]) // 课程状态索引,用于快速筛选 - @@index([level]) // 难度等级索引,用于分类查询 - @@index([featured]) // 精选标记索引,用于首页推荐 - @@map("course") -} - -model Section { - id String @id @default(cuid()) @map("id") - title String @map("title") - description String? @map("description") - objectives String[] @map("objectives") - order Float? @default(0) @map("order") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - // 关联关系 - course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) - courseId String @map("course_id") - lectures Lecture[] - meta Json? - - // totalDuration Int @default(0) @map("total_duration") - // totalLectures Int @default(0) @map("total_lectures") - @@index([courseId, order]) - @@map("section") -} - -model Lecture { - id String @id @default(cuid()) @map("id") - title String @map("title") - description String? @map("description") - content String? @map("content") - order Float? @default(0) @map("order") - duration Int @map("duration") - type String @map("type") - - videoUrl String? @map("video_url") - videoThumbnail String? @map("video_thumbnail") - publishedAt DateTime? @map("published_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - // 关联关系 - resources Resource[] - section Section? @relation(fields: [sectionId], references: [id], onDelete: Cascade) - sectionId String? @map("section_id") - course Course? @relation(fields: [courseId], references: [id], onDelete: Cascade) - courseId String? @map("course_id") - comments Post[] - visits Visit[] - - @@index([sectionId, order]) - @@index([type, publishedAt]) - @@map("lecture") -} - -model Enrollment { - id String @id @default(cuid()) @map("id") - status String @map("status") - completionRate Float @default(0) @map("completion_rate") - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - completedAt DateTime? @map("completed_at") - - // 关联关系 - student Staff @relation(fields: [studentId], references: [id]) - studentId String @map("student_id") - course Course @relation(fields: [courseId], references: [id]) - courseId String @map("course_id") - - @@unique([studentId, courseId]) - @@index([status]) - @@index([completedAt]) - @@map("enrollment") -} - -model CourseInstructor { - courseId String @map("course_id") - instructorId String @map("instructor_id") - role String @map("role") - createdAt DateTime @default(now()) @map("created_at") - order Float? @default(0) @map("order") - - course Course @relation(fields: [courseId], references: [id]) - instructor Staff @relation(fields: [instructorId], references: [id]) - - @@id([courseId, instructorId]) - @@map("course_instructor") -} - -model Resource { - id String @id @default(cuid()) @map("id") - title String? @map("title") - description String? @map("description") - type String? @map("type") - fileId String? @unique - url String? - // 元数据 - metadata Json? @map("metadata") - // 处理状态控制 - status String? - createdAt DateTime? @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - createdBy String? @map("created_by") - updatedBy String? @map("updated_by") - deletedAt DateTime? @map("deleted_at") - isPublic Boolean? @default(true) @map("is_public") - owner Staff? @relation(fields: [ownerId], references: [id]) - ownerId String? @map("owner_id") - post Post? @relation(fields: [postId], references: [id]) - postId String? @map("post_id") - lecture Lecture? @relation(fields: [lectureId], references: [id]) - lectureId String? @map("lecture_id") - - // 索引 - @@index([type]) - @@index([createdAt]) - @@map("resource") -} - -model Node { - id String @id @default(cuid()) @map("id") - title String @map("title") - description String? @map("description") - type String @map("type") - style Json? @map("style") - position Json? @map("position") - data Json? @map("data") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - // 关联关系 - sourceEdges NodeEdge[] @relation("source_node") - targetEdges NodeEdge[] @relation("target_node") - - @@map("node") -} - -model NodeEdge { - id String @id @default(cuid()) @map("id") - type String? @map("type") - label String? @map("label") - description String? @map("description") - style Json? @map("style") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - source Node @relation("source_node", fields: [sourceId], references: [id], onDelete: Cascade) - sourceId String @map("source_id") - target Node @relation("target_node", fields: [targetId], references: [id], onDelete: Cascade) - targetId String @map("target_id") - - @@unique([sourceId, targetId, type]) - @@index([sourceId]) - @@index([targetId]) - @@map("node_edge") -} diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 480cf19..e3c863e 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -10,6 +10,129 @@ datasource db { url = env("DATABASE_URL") } +enum Character { + ADMIN + CLUB_ADMIN + DRIVER + GUEST +} + + + +model User { + id String @id @default(cuid()) + username String @unique + password String + role Character @default(GUEST) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([deletedAt]) +} + +model Library { + id String @id @default(cuid()) + name String @map("name") + address String @map("address") + description String @map("description") + reader Reader[] + book Book[] + borrowRecord BorrowRecord[] + comments Comment[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + + @@index([deletedAt]) + @@map("library") +} + +model Reader { + id String @id @default(cuid()) + name String @map("name") + username String @map("use_name") + password String @map("password") + gender String @map("gender") + age Int @map("age") + relax String? @map("relax") + libraryId String? @map("library_id") + library Library? @relation(fields: [libraryId], references: [id]) + borrowRecord BorrowRecord[] + comment Comment[] + terms Term[] @relation("reader_terms") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([libraryId]) + @@index([deletedAt]) + @@map("reader") +} + +model Book { + id String @id @default(cuid()) + isbn String @map("isbn") + bookName String @map("book_name") + author String @map("author") + libraryId String @map("library_id") + library Library @relation(fields: [libraryId], references: [id]) + borrowRecord BorrowRecord[] + comments Comment[] + terms Term[] @relation("book_terms") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([libraryId]) + @@index([deletedAt]) + @@map("book") +} + + +model BorrowRecord { + id String @id @default(cuid()) + giveTime DateTime @map("give_time") + backTime DateTime @map("back_time") + isbackTime DateTime @map("isback_time") + libraryId String? @map("library_id") + library Library? @relation(fields: [libraryId], references: [id]) + readerId String @map("read_id") + reader Reader @relation(fields: [readerId], references: [id]) + bookId String @map("book_id") + book Book @relation(fields: [bookId], references: [id]) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([deletedAt]) + @@index([readerId]) + @@index([bookId]) + @@map("borrow_record") +} + +model Comment { + id String @id @default(cuid()) + content String @map("content") + libraryId String? @map("library_id") + library Library? @relation(fields: [libraryId], references: [id]) + readerId String @map("read_id") + reader Reader @relation(fields: [readerId], references: [id]) + bookId String @map("book_id") + book Book @relation(fields: [bookId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([deletedAt]) + @@index([libraryId]) + @@index([readerId]) + @@index([bookId]) + @@map("comment") +} + model Taxonomy { id String @id @default(cuid()) name String @unique @@ -45,6 +168,9 @@ model Term { depts Department[] @relation("department_term") hasChildren Boolean? @default(false) @map("has_children") posts Post[] @relation("post_term") + reader Reader[] @relation("reader_terms") + book Book[] @relation("book_terms") + @@index([name]) // 对name字段建立索引,以加快基于name的查找速度 @@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度 @@ -407,12 +533,12 @@ model Department { domainStaffs Staff[] @relation("DomainStaff") deptStaffs Staff[] @relation("DeptStaff") terms Term[] @relation("department_term") - - trainPlans TrainPlan[] @relation("TrainPlanDept") + deptDevices Device[] @relation("DeptDevice") + trainPlans TrainPlan[] @relation("TrainPlanDept") // watchedPost Post[] @relation("post_watch_dept") hasChildren Boolean? @default(false) @map("has_children") - logs SystemLog[] + logs SystemLog[] @@index([parentId]) @@index([isDomain]) @@ -446,7 +572,7 @@ model StaffFieldValue { fieldId String @map("field_id") value String? // 字段值 staff Staff @relation(fields: [staffId], references: [id]) - field StaffField @relation("StaffFieldToValue",fields: [fieldId], references: [id], onDelete: Cascade) // 添加级联删除 + field StaffField @relation("StaffFieldToValue", fields: [fieldId], references: [id], onDelete: Cascade) // 添加级联删除 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -458,22 +584,22 @@ model StaffFieldValue { model Staff { // 基础信息 - id String @id @default(cuid()) - username String? @unique @map("username") - password String? @map("password") - showname String? @map("showname") - avatar String? @map("avatar") - enabled Boolean? @default(true) - officerId String? @map("officer_id") - phoneNumber String? @map("phone_number") - age Int?@map("age") - sex String?@map("sex") + id String @id @default(cuid()) + username String? @unique @map("username") + password String? @map("password") + showname String? @map("showname") + avatar String? @map("avatar") + enabled Boolean? @default(true) + officerId String? @map("officer_id") + phoneNumber String? @map("phone_number") + age Int? @map("age") + sex String? @map("sex") // 部门关系 - domainId String? @map("domain_id") - deptId String? @map("dept_id") - domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) - department Department? @relation("DeptStaff", fields: [deptId], references: [id]) - order Float? + domainId String? @map("domain_id") + deptId String? @map("dept_id") + domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) + department Department? @relation("DeptStaff", fields: [deptId], references: [id]) + order Float? // 关联关系 trainSituations TrainSituation[] @@ -497,7 +623,9 @@ model Staff { logs SystemLog[] @relation("log_operator") // 添加自定义字段值关联 fieldValues StaffFieldValue[] - + deviceStatus String? @map("device_status") + deviceType String? @map("device_type") + @@index([officerId]) @@index([deptId]) @@index([domainId]) @@ -542,9 +670,9 @@ model ShareCode { model SystemLog { id String @id @default(cuid()) timestamp DateTime @default(now()) @map("timestamp") - level String? @map("level") // info, warning, error, debug - module String? @map("module") // 操作模块,如"人员管理" - action String? @map("action") // 具体操作,如"新增人员"、"修改人员" + level String? @map("level") // info, warning, error, debug + module String? @map("module") // 操作模块,如"人员管理" + action String? @map("action") // 具体操作,如"新增人员"、"修改人员" // 操作人信息 operatorId String? @map("operator_id") @@ -562,13 +690,14 @@ model SystemLog { afterData Json? @map("after_data") // 操作后数据 // 操作结果 - status String? @map("status") // success, failure + status String? @map("status") // success, failure errorMessage String? @map("error_message") // 如果操作失败,记录错误信息 // 关联部门 departmentId String? @map("department_id") department Department? @relation(fields: [departmentId], references: [id]) - message String? @map("message") // 完整的日志文本内容 + message String? @map("message") // 完整的日志文本内容 + // 优化索引 @@index([timestamp]) @@index([level]) @@ -579,3 +708,33 @@ model SystemLog { @@index([departmentId]) @@map("system_log") } + +model Device { + id String @id @default(cuid()) + deptId String? @map("dept_id") + department Department? @relation("DeptDevice", fields: [deptId], references: [id]) + showname String? @map("showname") + productType String? @map("product_type") + serialNumber String? @map("serial_number") + assetId String? @map("asset_id") + deviceStatus String? @map("device_status") + confidentialLabelId String? @map("confidential_label_id") + confidentialityLevel String? @map("confidentiality_level") + ipAddress String? @map("ip_address") + macAddress String? @map("mac_address") + diskSerialNumber String? @map("disk_serial_number") + storageLocation String? @map("storage_location") + responsiblePerson String? @map("responsible_person") + notes String? @map("notes") + systemType String? @map("system_type") + deviceType String? @map("device_type") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([deptId]) + @@index([systemType]) + @@index([deviceType]) + @@index([responsiblePerson]) + @@map("device") +} diff --git a/packages/common/prisma/schema.prisma.txt b/packages/common/prisma/schema.prisma.txt new file mode 100644 index 0000000..64885b9 --- /dev/null +++ b/packages/common/prisma/schema.prisma.txt @@ -0,0 +1,179 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Role { + ADMIN + CLUB_AADMIN + DRIVER + GUEST +} + +enum Gender { + MALE + FEMALE +} + +model User { + id String @id @default(cuid()) + username String @unique + password String + role Role @default(GUEST) + driver Driver? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([deletedAt]) +} + +model Club { + id String @id @default(cuid()) + name String + description String + parentId String? + parent Club? @relation("ClubHierarchy", fields: [parentId], references: [id], onDelete: Cascade) + children Club[] @relation("ClubHierarchy") + drivers Driver[] + cars Car[] + games Game[] + comments Comment[] + terms Term[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([parentId]) + @@index([deletedAt]) +} + +model Driver { + id String @id @default(cuid()) + name String? + gender Gender + age Int + bio String? + clubId String + club Club @relation(fields: [clubId], references: [id]) + sorties Sortie[] + comment Comment[] + terms Term[] + user User? @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + userId String? @unique + + @@index([clubId]) + @@index([deletedAt]) +} + +model Car { + id String @id @default(cuid()) + model String + number String + name String + clubId String + club Club @relation(fields: [clubId], references: [id]) + sorties Sortie[] + comments Comment[] + terms Term[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([clubId]) + @@index([deletedAt]) +} + +model Game { + id String @id @default(cuid()) + name String + startTime DateTime + clubs Club[] + sorties Sortie[] + comments Comment[] + terms Term[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? +} + +model Sortie { + id String @id @default(cuid()) + totalTime Float + score Float + driverId String + driver Driver @relation(fields: [driverId], references: [id]) + carId String + car Car @relation(fields: [carId], references: [id]) + gameID String + game Game @relation(fields: [gameID], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([deletedAt]) +} + +model Comment { + id String @id @default(cuid()) + content String + clubId String + club Club @relation(fields: [clubId], references: [id]) + driverId String + driver Driver @relation(fields: [driverId], references: [id]) + carId String + car Car @relation(fields: [carId], references: [id]) + gameId String + game Game @relation(fields: [gameId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([deletedAt]) + @@index([clubId]) + @@index([gameId]) + @@index([driverId]) +} + +model Taxonomy { + id String @id @default(cuid()) + name String @unique + deletedAt DateTime? + createdAt DateTime @default(now()) + terms Term[] + + @@index([deletedAt]) + @@map("taxonomy") +} + +model Term { + id String @id @default(cuid()) + name String + taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id]) + taxonomyId String? + parentId String? + parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade) + children Term[] @relation("ChildParent") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + createdBy String? + club Club[] + driver Driver[] + car Car[] + game Game[] + + @@index([name]) // 对name字段建立索引,以加快基于name的查找速度 + @@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度 + @@map("term") +} diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 897a3bb..d8f56ee 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -1,3 +1,4 @@ + export enum SocketMsgType { NOTIFY, } @@ -62,7 +63,13 @@ export enum ObjectType { TRAIN_CONTENT = "trainContent", TRAIN_SITUATION = "trainSituation", DAILY_TRAIN = "dailyTrainTime", - SYSTEM_LOG = 'system_log' + SYSTEM_LOG = 'system_log', + DEVICE = 'device', + LIBRARY = 'library', + READER = 'reader', + BOOK = 'book', + BORROWRECORD= 'borrowRecord', + USER = "user", } export enum RolePerms { // Create Permissions 创建权限