This commit is contained in:
Your Name 2025-06-15 22:37:47 +08:00
parent 45d2f36b37
commit 51341068da
72 changed files with 4550 additions and 515 deletions

View File

@ -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) {

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.BookUncheckedCreateInput> =
z.any();
const BookWhereInputSchema: ZodType<Prisma.BookWhereInput> = z.any();
const BookSelectSchema: ZodType<Prisma.BookSelect> = z.any();
const BookUpdateArgsSchema: ZodType<Prisma.BookUpdateArgs> = z.any();
const BookFindFirstArgsSchema: ZodType<Prisma.BookFindFirstArgs> = z.any();
const BookFindManyArgsSchema: ZodType<Prisma.BookFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.BookDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.BorrowRecordUncheckedCreateInput> =
z.any();
const BorrowRecordWhereInputSchema: ZodType<Prisma.BorrowRecordWhereInput> =
z.any();
const BorrowRecordSelectSchema: ZodType<Prisma.BorrowRecordSelect> = z.any();
const BorrowRecordUpdateArgsSchema: ZodType<Prisma.BorrowRecordUpdateArgs> =
z.any();
const BorrowRecordFindFirstArgsSchema: ZodType<Prisma.BorrowRecordFindFirstArgs> =
z.any();
const BorrowRecordFindManyArgsSchema: ZodType<Prisma.BorrowRecordFindManyArgs> =
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);
}),
});
}

View File

@ -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<Prisma.BorrowRecordDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.CommentUncheckedCreateInput> =
z.any();
const CommentWhereInputSchema: ZodType<Prisma.CommentWhereInput> = z.any();
const CommentSelectSchema: ZodType<Prisma.CommentSelect> = z.any();
const CommentUpdateArgsSchema: ZodType<Prisma.CommentUpdateArgs> = z.any();
const CommentFindFirstArgsSchema: ZodType<Prisma.CommentFindFirstArgs> =
z.any();
const CommentFindManyArgsSchema: ZodType<Prisma.CommentFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.CommentDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.DeviceUncheckedCreateInput> =
z.any();
const DeviceWhereInputSchema: ZodType<Prisma.DeviceWhereInput> = z.any();
const DeviceSelectSchema: ZodType<Prisma.DeviceSelect> = z.any();
const DeviceUpdateArgsSchema: ZodType<Prisma.DeviceUpdateArgs> = z.any();
const DeviceFindFirstArgsSchema: ZodType<Prisma.DeviceFindFirstArgs> = z.any();
const DeviceFindManyArgsSchema: ZodType<Prisma.DeviceFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.DeviceDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.LibraryUncheckedCreateInput> =
z.any();
const LibraryWhereInputSchema: ZodType<Prisma.LibraryWhereInput> = z.any();
const LibrarySelectSchema: ZodType<Prisma.LibrarySelect> = z.any();
const LibraryUpdateArgsSchema: ZodType<Prisma.LibraryUpdateArgs> = z.any();
const LibraryFindFirstArgsSchema: ZodType<Prisma.LibraryFindFirstArgs> =
z.any();
const LibraryFindManyArgsSchema: ZodType<Prisma.LibraryFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.LibraryDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.ReaderUncheckedCreateInput> =
z.any();
const ReaderWhereInputSchema: ZodType<Prisma.ReaderWhereInput> = z.any();
const ReaderSelectSchema: ZodType<Prisma.ReaderSelect> = z.any();
const ReaderUpdateArgsSchema: ZodType<Prisma.ReaderUpdateArgs> = z.any();
const ReaderFindFirstArgsSchema: ZodType<Prisma.ReaderFindFirstArgs> = z.any();
const ReaderFindManyArgsSchema: ZodType<Prisma.ReaderFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.ReaderDelegate> {
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,
});
}
}

View File

@ -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) {}
}

View File

@ -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 {}

View File

@ -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<Prisma.UserUncheckedCreateInput> =
z.any();
const UserWhereInputSchema: ZodType<Prisma.UserWhereInput> = z.any();
const UserSelectSchema: ZodType<Prisma.UserSelect> = z.any();
const UserUpdateArgsSchema: ZodType<Prisma.UserUpdateArgs> = z.any();
const UserFindFirstArgsSchema: ZodType<Prisma.UserFindFirstArgs> = z.any();
const UserFindManyArgsSchema: ZodType<Prisma.UserFindManyArgs> = 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);
}),
});
}

View File

@ -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<Prisma.UserDelegate> {
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,
});
}
}

View File

@ -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]) => {

View File

@ -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],

View File

@ -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;

View File

@ -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,

View File

@ -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,
}
});

View File

@ -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<Car | undefined>();
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 (
<div className="p-6">
<SearchBar
searchText={searchText}
onSearchTextChange={setSearchText}
onSearch={handleSearch}
onAdd={handleAdd}
onImportSuccess={refetch}
data={data || []}
/>
<CarTable
data={data || []}
loading={isLoading}
onEdit={(car) => {
setEditingCar(car);
setModalOpen(true);
}}
onDelete={(ids) => deleteMutation.mutate({ ids })}
/>
<CarModal
open={modalOpen}
editingCar={editingCar}
clubList={clubList || []} // 传递俱乐部列表
loading={createMutation.isLoading || updateMutation.isLoading}
onOk={handleModalOk}
onCancel={() => setModalOpen(false)}
/>
</div>
);
};
export default CarPage;

View File

@ -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<CarModalProps> = ({
open,
loading,
editingCar,
clubList,
onOk,
onCancel,
}) => {
const [form] = Form.useForm();
return (
<Modal
title={editingCar ? '编辑车辆' : '新增车辆'}
open={open}
onOk={() => form.submit()}
onCancel={onCancel}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={editingCar}
onFinish={(values) => {
// 确保数据格式正确
const formData = {
name: values.name,
model: values.model,
number: values.number,
clubId: values.clubId,
};
onOk(formData);
}}
>
<Form.Item
name="name"
label="车辆名称"
rules={[{ required: true, message: '请输入车辆名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="model"
label="车型"
rules={[{ required: true, message: '请输入车型' }]}
>
<Input />
</Form.Item>
<Form.Item
name="number"
label="车辆编号"
rules={[{ required: true, message: '请输入车辆编号' }]}
>
<Input />
</Form.Item>
<Form.Item
name="clubId"
label="所属俱乐部"
rules={[{ required: true, message: '请选择所属俱乐部' }]}
>
<Select
allowClear
placeholder="请选择俱乐部"
>
{clubList?.map(club => (
<Select.Option key={club.id} value={club.id}>
{club.name} {/* 使用 club 关联的 name */}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -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<CarTableProps> = ({
data,
loading,
onEdit,
onDelete,
}) => {
const columns: ColumnsType<Car> = [
{
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) => (
<Space size="middle">
<Button type="link" onClick={() => onEdit(record)}>
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => onDelete([record.id])}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
/>
);
};

View File

@ -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<ImportExportButtonsProps> = ({
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 (
<div className="flex space-x-2">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleImport}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>
</div>
);
};

View File

@ -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<SearchBarProps> = ({
searchText,
onSearchTextChange,
onSearch,
onAdd,
onImportSuccess,
// data,
}) => {
return (
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索车辆名称及相关信息"
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
onPressEnter={onSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={onSearch}></Button>
</Space>
<Space>
{/* <ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/> */}
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button>
</Space>
</div>
);
};

View File

@ -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;
}

View File

@ -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<Staff | null>(null);
const [form] = Form.useForm();
const pageSize = 10;
// API 调用
const { data, isLoading, refetch } = api.staff.findManyWithPagination.useQuery<PaginatedResponse>({
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 (
<div className="h-screen w-full p-6"> {/* 修改这里,使用 h-screen 而不是 h-full */}
<h1 className="text-2xl font-bold mb-4 text-center"></h1>
<SearchBar
searchText={searchText}
onSearchTextChange={setSearchText}
onSearch={handleSearch}
onAdd={handleAdd}
onImportSuccess={handleImportSuccess}
data={data?.items || []}
/>
<StaffTable
data={data?.items || []}
total={data?.total || 0}
currentPage={currentPage}
pageSize={pageSize}
isLoading={isLoading}
onPageChange={handlePageChange}
onEdit={handleEdit}
onDelete={handleDelete}
/>
<StaffModal
visible={isModalVisible}
editingStaff={editingStaff}
form={form}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
/>
</div>
);
};
export default TestPage;

View File

@ -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<ImportExportButtonsProps> = ({
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 (
<div className="flex space-x-2">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleImport}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>
</div>
);
};

View File

@ -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<SearchBarProps> = ({
searchText,
onSearchTextChange,
onSearch,
onAdd,
onImportSuccess,
data,
}) => {
return (
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索员工姓名或用户名"
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
onPressEnter={onSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={onSearch}></Button>
</Space>
<Space>
<ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button>
</Space>
</div>
);
};

View File

@ -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<StaffModalProps> = ({
visible,
editingStaff,
form,
onOk,
onCancel,
}) => {
return (
<Modal
title={editingStaff ? "编辑员工" : "添加员工"}
open={visible}
onOk={onOk}
onCancel={onCancel}
>
<Form form={form} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="showname"
label="名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="absent"
label="是否在位"
valuePropName="checked"
>
<Switch
checkedChildren="在位"
unCheckedChildren="不在位"
/>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -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<StaffTableProps> = ({
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) => (
<Tag color={absent ? 'success' : 'error'}>
{absent ? '在位' : '不在位'}
</Tag>
),
},
{
title: '操作',
key: 'action',
render: (_: any, record: Staff) => (
<Space size="middle">
<Button
type="link"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
>
</Button>
<Button
type="link"
icon={<DeleteOutlined />}
danger
onClick={() => onDelete(record.id)}
>
</Button>
</Space>
),
},
];
// 修改样式定义
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 (
<div style={containerStyle}>
<div style={tableContainerStyle}>
<Table
dataSource={data}
columns={columns}
rowKey="id"
pagination={false}
loading={isLoading}
style={tableStyle}
/>
</div>
<div className="mt-6 flex justify-center items-center">
<div className="bg-white px-6 py-3 rounded-lg shadow-sm flex items-center space-x-6">
<Pagination
current={currentPage}
total={total}
pageSize={pageSize}
onChange={onPageChange}
showTotal={(total) => (
<span className="text-gray-600">
<span className="font-medium text-gray-900">{total}</span>
</span>
)}
showSizeChanger={false}
/>
<div className="h-6 w-px bg-gray-200" />
<div className="flex items-center space-x-2">
<Input
size="small"
style={{ width: 60 }}
value={jumpPage}
onChange={(e) => setJumpPage(e.target.value)}
onPressEnter={handleJumpPage}
placeholder="页码"
className="text-center"
/>
<Button
size="small"
type="primary"
onClick={handleJumpPage}
>
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,11 @@
export interface Staff {
id: string;
username: string;
showname: string;
absent: boolean;
}
export interface PaginatedResponse {
items: Staff[];
total: number;
}

View File

@ -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<Staff | null>(null);
const [form] = Form.useForm();
const pageSize = 10;
// 修改查询逻辑
const { data, isLoading, refetch } = api.staff.findManyWithPagination.useQuery<PaginatedResponse>({
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 (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4 text-center">培训情况记录</h1>
{/* 搜索和添加按钮 */}
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索员工姓名或用户名"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onPressEnter={handleSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={handleSearch}>搜索</Button>
</Space>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
添加员工
</Button>
</div>
{/* 修改表格,添加操作列 */}
<div className="overflow-x-auto">
<table className="min-w-full bg-white shadow-md rounded-lg">
<thead>
<tr className="bg-gray-100 border-b">
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">用户名</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">名称</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">是否在位</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{(data?.items || []).map((staff) => (
<tr key={staff.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.username}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.showname}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.absent ? '在位' : '不在位'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<Space>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(staff)}
size="small"
>
编辑
</Button>
<Button
type="primary"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(staff.id)}
size="small"
>
删除
</Button>
</Space>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 编辑/添加模态框 */}
<Modal
title={editingStaff ? "编辑员工" : "添加员工"}
open={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
>
<Form form={form} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="showname"
label="名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="absent"
label="是否在位"
valuePropName="checked"
>
<Switch
checkedChildren="在位"
unCheckedChildren="不在位"
/>
</Form.Item>
</Form>
</Modal>
<div className="mt-4 flex justify-center">
<Pagination
current={currentPage}
total={data?.total || 0}
pageSize={pageSize}
onChange={handlePageChange}
showTotal={(total) => `共 ${total} 条记录`}
showSizeChanger={false}
showQuickJumper
/>
</div>
</div>
);
};
export default TestPage;

View File

@ -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<Game | undefined>();
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 (
<div>
<div style={{ marginBottom: 16 }}>
<Space>
<span>: {stats.avgTime.toFixed(2)}</span>
<span>: {stats.maxScore.toFixed(2)}</span>
<span>: {stats.totalSorties}</span>
</Space>
</div>
<Table
columns={sortieColumns}
dataSource={record.sorties}
pagination={false}
rowKey="id"
/>
</div>
);
};
return (
<>
{contextHolder}
<div className="p-6">
<SearchBar
searchText={searchText}
onSearchTextChange={setSearchText}
onSearch={handleSearch}
onAdd={handleAdd}
onImportSuccess={refetch}
data={data || []}
/>
<GameTable
data={data || []}
loading={isLoading}
onEdit={(driver) => {
setEditingGame(driver);
setModalOpen(true);
}}
onDelete={(ids) => deleteMutation.mutate({ ids })}
/>
<GameModal
open={modalOpen}
editingGame={editingGame}
clubList={clubList || []} // 传递俱乐部列表
loading={createMutation.isLoading || updateMutation.isLoading}
onOk={handleModalOk}
onCancel={() => setModalOpen(false)}
/>
</div>
</>
);
};
export default GamePage;

View File

@ -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<GameModalProps> = ({
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 (
<Modal
title={editingGame ? '编辑比赛' : '新增比赛'}
open={open}
onOk={() => form.submit()}
onCancel={onCancel}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
onFinish={(values) => {
const formData = {
name: values.name,
startTime: values.startTime ? values.startTime.toISOString() : null,
clubIds: values.clubIds || [],
};
onOk(formData);
}}
>
<Form.Item
name="name"
label="比赛名称"
rules={[{ required: true, message: '请输入比赛名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="startTime"
label="比赛开始时间"
rules={[{ required: true, message: '请选择比赛开始时间' }]}
>
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="clubIds"
label="参与俱乐部"
rules={[{ required: true, message: '请选择参与俱乐部', type: 'array' }]}
>
<Select
mode="multiple"
allowClear
placeholder="请选择俱乐部"
>
{clubList?.map(club => (
<Select.Option key={club.id} value={club.id}>
{club.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -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<GameTableProps> = ({
data,
loading,
onEdit,
onDelete,
}) => {
// 主表格列定义
const columns: ColumnsType<Game> = [
{
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) => (
<Space size="middle">
<Button type="link" onClick={() => onEdit(record)}>
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => onDelete([record.id])}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
// 子表格列定义(车次信息)
const expandedRowRender = (record: Game) => {
const sortieColumns: ColumnsType<Sortie> = [
{
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 (
<Table
columns={sortieColumns}
dataSource={record.sorties}
pagination={false}
rowKey="id"
/>
);
};
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
expandable={{
expandedRowRender,
defaultExpandAllRows: false,
}}
/>
);
};

View File

@ -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<ImportExportButtonsProps> = ({
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 (
<div className="flex space-x-2">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleImport}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>
</div>
);
};

View File

@ -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<SearchBarProps> = ({
searchText,
onSearchTextChange,
onSearch,
onAdd,
onImportSuccess,
// data,
}) => {
return (
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索比赛名称"
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
onPressEnter={onSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={onSearch}></Button>
</Space>
<Space>
{/* <ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/> */}
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button>
</Space>
</div>
);
};

View File

@ -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;
}

View File

@ -57,6 +57,36 @@ const items = [
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem(
"图书馆",
"/library",
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem(
"读者",
"/reader",
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem(
"图书",
"/book",
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem(
"借阅记录",
"/borrowRecord",
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem(
"系统设置",

View File

@ -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<Library | undefined>();
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 (
<div className="p-6">
<SearchBar
searchText={searchText}
onSearchTextChange={setSearchText}
onSearch={handleSearch}
onAdd={handleAdd}
onImportSuccess={refetch}
data={data || []}
/>
<LibraryTable
data={data || []}
loading={isLoading}
onEdit={(library) => {
setEditingLibrary(library);
setModalOpen(true);
}}
onDelete={(ids) => deleteMutation.mutate({ ids })}
/>
<LibraryModal
open={modalOpen}
editingLibrary={editingLibrary}
libraryList={data || []}
loading={createMutation.isLoading || updateMutation.isLoading}
onOk={handleModalOk}
onCancel={handleCancelModal}
/>
</div>
);
};
export default LibraryPage;

View File

@ -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<ImportExportButtonsProps> = ({
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 (
<div className="flex space-x-2">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleImport}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>
</div>
);
};

View File

@ -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<LibraryModalProps> = ({
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 (
<Modal
title={editingLibrary ? '编辑图书馆' : '新增图书馆'}
open={open}
onOk={() => form.submit()}
onCancel={onCancel}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={editingLibrary}
onFinish={(values) => {
// 确保数据格式正确
const formData = {
name: values.name,
address: values.address,
description: values.description,
};
onOk(formData);
}}
>
<Form.Item
name="name"
label="图书馆名称"
rules={[{ required: true, message: '请输入图书馆名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="address"
label="地址"
rules={[{ required: true, message: '请输入图书馆地址' }]}
>
<Input />
</Form.Item>
<Form.Item
name="description"
label="描述"
rules={[{ required: true, message: '请输入俱乐部描述' }]}
>
<Input.TextArea />
</Form.Item>
{/* <Form.Item
name="parentId"
label="上级俱乐部"
>
<Select
allowClear
disabled={editingLibrary?.id ? libraryList?.some(c => c.id === editingLibrary.id) : false}
>
{libraryList?.filter(library => library.id !== editingLibrary?.id).map(library => (
<Select.Option key={library.id} value={library.id}>
{library.name}
</Select.Option>
))}
</Select>
</Form.Item> */}
</Form>
</Modal>
);
};

View File

@ -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<LibraryTableProps> = ({
data,
loading,
onEdit,
onDelete,
}) => {
const columns: ColumnsType<Library> = [
{
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) => (
<Space size="middle">
<Button type="link" onClick={() => onEdit(record)}>
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => onDelete([record.id])}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
/>
);
};

View File

@ -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<SearchBarProps> = ({
searchText,
onSearchTextChange,
onSearch,
onAdd,
onImportSuccess,
data,
}) => {
return (
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索图书馆"
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
onPressEnter={onSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={onSearch}></Button>
</Space>
<Space>
<ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button>
</Space>
</div>
);
};

View File

@ -0,0 +1,9 @@
export interface Library {
id: string;
name: string;
address: string;
description : string;
createdAt: Date;
updatedAt: Date;
deletedAt?: Date | null;
}

View File

@ -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<Reader | undefined>();
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 (
<div className="p-6">
<SearchBar
searchText={searchText}
onSearchTextChange={setSearchText}
onSearch={handleSearch}
onAdd={handleAdd}
onImportSuccess={refetch}
data={data || []}
/>
<ReaderTable
data={data || []}
loading={isLoading}
onEdit={(reader) => {
setEditingReader(reader);
setModalOpen(true);
}}
onDelete={(ids) => deleteMutation.mutate({ ids })}
/>
<ReaderModal
open={modalOpen}
editingReader={editingReader}
libraryList={libraryList || []} // 传递俱乐部列表
loading={createMutation.isLoading || updateMutation.isLoading}
onOk={handleModalOk}
onCancel={() => setModalOpen(false)}
/>
</div>
);
};
export default ReaderPage;

View File

@ -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<ImportExportButtonsProps> = ({
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 (
<div className="flex space-x-2">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleImport}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>
</div>
);
};

View File

@ -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<ReaderModalProps> = ({
open,
loading,
editingReader,
libraryList,
onOk,
onCancel,
}) => {
const [form] = Form.useForm();
return (
<Modal
title={editingReader ? '编辑读者' : '新增读者'}
open={open}
onOk={() => form.submit()}
onCancel={onCancel}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={editingReader}
onFinish={(values) => {
// 确保数据格式正确
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);
}}
>
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input />
</Form.Item>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="gender"
label="性别"
rules={[{ required: true, message: '请输入性别' }]}
>
<Input />
</Form.Item>
<Form.Item
name="age"
label="年龄"
rules={[{ required: true, message: '请输入年龄' }]}
>
<Input />
</Form.Item>
<Form.Item
name="relax"
label="联系方式"
>
<Input.TextArea />
</Form.Item>
<Form.Item
name="libraryId"
label="所属俱乐部"
rules={[{ required: true, message: '请选择所属图书馆' }]}
>
<Select
allowClear
placeholder="请选择图书馆"
>
{libraryList?.map(library => (
<Select.Option key={library.id} value={library.id}>
{library.name} {/* 使用 club 关联的 name */}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -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<ReaderTableProps> = ({
data,
loading,
onEdit,
onDelete,
}) => {
const columns: ColumnsType<Reader> = [
{
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) => (
<Space size="middle">
<Button type="link" onClick={() => onEdit(record)}>
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => onDelete([record.id])}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
/>
);
};

View File

@ -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<SearchBarProps> = ({
searchText,
onSearchTextChange,
onSearch,
onAdd,
onImportSuccess,
// data,
}) => {
return (
<div className="mb-4 flex justify-between">
<Space>
<Input
placeholder="搜索读者"
value={searchText}
onChange={(e) => onSearchTextChange(e.target.value)}
onPressEnter={onSearch}
prefix={<SearchOutlined />}
/>
<Button type="primary" onClick={onSearch}></Button>
</Space>
<Space>
{/* <ImportExportButtons
onImportSuccess={onImportSuccess}
data={data}
/> */}
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
</Button>
</Space>
</div>
);
};

View File

@ -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;
}

View File

@ -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: <TestPage></TestPage>,
},
{
path: "/library",
element: <LibraryPage></LibraryPage>,
},
{
path: "/reader",
element: <ReaderPage></ReaderPage>,
},
{
path: "/book",
element: <BookPage></BookPage>,
},
{
path: "/borrowRecord",
element: <BorrowRecordPage></BorrowRecordPage>,
},
],
},
],

View File

@ -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;

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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 创建权限