add
This commit is contained in:
parent
45d2f36b37
commit
51341068da
|
@ -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) {
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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]) => {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
export interface Staff {
|
||||
id: string;
|
||||
username: string;
|
||||
showname: string;
|
||||
absent: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
items: Staff[];
|
||||
total: number;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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(
|
||||
"系统设置",
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
description : string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt?: Date | null;
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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 创建权限
|
||||
|
|
Loading…
Reference in New Issue