Compare commits
4 Commits
45d2f36b37
...
40ce147949
| Author | SHA1 | Date |
|---|---|---|
|
|
40ce147949 | |
|
|
fe542c59e4 | |
|
|
ab4ba55721 | |
|
|
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.BOOK, 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,96 @@
|
|||
import { Injectable, BadRequestException } 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) {
|
||||
// 验证读者和图书是否属于同一个图书馆
|
||||
await this.validateLibraryMatch(
|
||||
args.data.readerId as string,
|
||||
args.data.bookId as string,
|
||||
);
|
||||
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args: Prisma.BorrowRecordUpdateArgs) {
|
||||
// 如果更新了读者或图书,需要验证
|
||||
if (args.data.readerId || args.data.bookId) {
|
||||
const record = await db.borrowRecord.findUnique({
|
||||
where: { id: args.where.id as string },
|
||||
select: { readerId: true, bookId: true },
|
||||
});
|
||||
|
||||
const readerId = args.data.readerId
|
||||
? typeof args.data.readerId === 'string'
|
||||
? args.data.readerId
|
||||
: record.readerId
|
||||
: record.readerId;
|
||||
|
||||
const bookId = args.data.bookId
|
||||
? typeof args.data.bookId === 'string'
|
||||
? args.data.bookId
|
||||
: record.bookId
|
||||
: record.bookId;
|
||||
|
||||
await this.validateLibraryMatch(readerId, bookId);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 验证读者和图书是否属于同一个图书馆
|
||||
private async validateLibraryMatch(readerId: string, bookId: string) {
|
||||
const reader = await db.reader.findUnique({
|
||||
where: { id: readerId },
|
||||
select: { libraryId: true },
|
||||
});
|
||||
|
||||
const book = await db.book.findUnique({
|
||||
where: { id: bookId },
|
||||
select: { libraryId: true },
|
||||
});
|
||||
|
||||
if (!reader || !book) {
|
||||
throw new BadRequestException('读者或图书不存在');
|
||||
}
|
||||
|
||||
if (reader.libraryId !== book.libraryId) {
|
||||
throw new BadRequestException('读者只能借阅所属图书馆的书籍');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,85 @@
|
|||
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,
|
||||
// private readonly termService: TermService, // 添加 TermService
|
||||
) {}
|
||||
|
||||
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);
|
||||
}),
|
||||
// 获取读者分类
|
||||
getReaderTypes: this.trpc.procedure.query(async () => {
|
||||
return this.readerService.getReaderTypes();
|
||||
}),
|
||||
|
||||
// 为读者分配分类
|
||||
assignReaderType: this.trpc.procedure
|
||||
.input(
|
||||
z.object({
|
||||
readerId: z.string(),
|
||||
termId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.readerService.assignReaderType(
|
||||
input.readerId,
|
||||
input.termId,
|
||||
);
|
||||
}),
|
||||
|
||||
// 移除读者分类
|
||||
removeReaderType: this.trpc.procedure
|
||||
.input(
|
||||
z.object({
|
||||
readerId: z.string(),
|
||||
termId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.readerService.removeReaderType(
|
||||
input.readerId,
|
||||
input.termId,
|
||||
);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取读者分类
|
||||
async getReaderTypes() {
|
||||
const taxonomy = await db.taxonomy.findFirst({
|
||||
where: { slug: 'reader_type' },
|
||||
include: {
|
||||
terms: {
|
||||
where: { deletedAt: null },
|
||||
orderBy: { order: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
return taxonomy?.terms || [];
|
||||
}
|
||||
|
||||
// 为读者分配分类
|
||||
async assignReaderType(readerId: string, termId: string) {
|
||||
return this.update({
|
||||
where: { id: readerId },
|
||||
data: {
|
||||
terms: {
|
||||
connect: { id: termId },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 移除读者分类
|
||||
async removeReaderType(readerId: string, termId: string) {
|
||||
return this.update({
|
||||
where: { id: readerId },
|
||||
data: {
|
||||
terms: {
|
||||
disconnect: { id: termId },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { Reader } from './../../../../../node_modules/.prisma/client/index.d';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DepartmentService } from '@server/models/department/department.service';
|
||||
import { AppConfigService } from '@server/models/app-config/app-config.service';
|
||||
|
|
@ -54,11 +55,58 @@ export class GenDevService {
|
|||
//await this.generateCourses(8);
|
||||
await this.generateTrainContent(2, 6);
|
||||
await this.generateTrainSituations();
|
||||
await this.initializeReaderType();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
EventBus.emit('genDataEvent', { type: 'end' });
|
||||
}
|
||||
|
||||
public async initializeReaderType() {
|
||||
this.logger.log('初始化读者分类');
|
||||
|
||||
// 查找是否存在系统类型分类
|
||||
let readerTypeTaxonomy = await db.taxonomy.findFirst({
|
||||
where: { slug: 'reader_type' }, // 通过 slug 查找
|
||||
});
|
||||
|
||||
// 如果不存在,则创建新的分类
|
||||
if (!readerTypeTaxonomy) {
|
||||
readerTypeTaxonomy = await db.taxonomy.create({
|
||||
data: {
|
||||
name: '读者类型', // 分类名称
|
||||
slug: 'reader_type', // 唯一标识符
|
||||
objectType: ['reader'], // 关联对象类型为 device
|
||||
},
|
||||
});
|
||||
}
|
||||
// 2. 创建具体的读者分类
|
||||
const readerTypes = [
|
||||
{ name: '长借读者', description: '可长期借阅图书的读者' },
|
||||
{ name: '短借读者', description: '只能短期借阅图书的读者' },
|
||||
];
|
||||
|
||||
// 检查并创建分类
|
||||
for (const type of readerTypes) {
|
||||
const existingTerm = await db.term.findFirst({
|
||||
where: {
|
||||
name: type.name,
|
||||
taxonomyId: readerTypeTaxonomy.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingTerm) {
|
||||
await db.term.create({
|
||||
data: {
|
||||
name: type.name,
|
||||
description: type.description,
|
||||
taxonomyId: readerTypeTaxonomy.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateCounts() {
|
||||
this.counts = await getCounts();
|
||||
Object.entries(this.counts).forEach(([key, value]) => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export interface DevDataCounts {
|
|||
termCount: number;
|
||||
courseCount: number;
|
||||
}
|
||||
|
||||
//查询数据库并返回各种实体的当前计数
|
||||
export async function getCounts(): Promise<DevDataCounts> {
|
||||
const counts = {
|
||||
deptCount: await db.department.count(),
|
||||
|
|
@ -27,9 +29,13 @@ export async function getCounts(): Promise<DevDataCounts> {
|
|||
};
|
||||
return counts;
|
||||
}
|
||||
|
||||
//将字符串的首字母大写
|
||||
export function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
//这个函数生成指定数量的随机图片链接
|
||||
export function getRandomImageLinks(count: number = 5): string[] {
|
||||
const baseUrl = 'https://picsum.photos/200/300?random=';
|
||||
const imageLinks: string[] = [];
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export class ReminderService {
|
|||
* @param totalDays 总天数
|
||||
* @returns 提醒时间点数组
|
||||
*/
|
||||
//从开始日期距离截止日期还有一半的天数时提醒一次,然后每1/2的1/2的天数提醒一次,直到距离截止日期还有一天提醒一次
|
||||
|
||||
generateReminderTimes(totalDays: number): number[] {
|
||||
// 如果总天数小于3天则不需要提醒
|
||||
if (totalDays < 3) return [];
|
||||
|
|
|
|||
|
|
@ -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,14 @@ 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';
|
||||
import { UserModule } from '@server/models/user/user.module';
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
|
|
@ -39,6 +48,13 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
|
|||
TrainSituationModule,
|
||||
DailyTrainModule,
|
||||
SystemLogModule,
|
||||
LibraryModule,
|
||||
DeviceModule,
|
||||
ReaderModule,
|
||||
BookModule,
|
||||
BorrowRecordModule,
|
||||
CommentModule,
|
||||
UserModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [TrpcService, TrpcRouter, Logger],
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ 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';
|
||||
import { UserRouter } from '@server/models/user/user.router';
|
||||
@Injectable()
|
||||
export class TrpcRouter {
|
||||
logger = new Logger(TrpcRouter.name);
|
||||
|
|
@ -40,6 +46,13 @@ 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,
|
||||
private readonly userRouter: UserRouter,
|
||||
) {}
|
||||
getRouter() {
|
||||
return;
|
||||
|
|
@ -61,6 +74,13 @@ 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,
|
||||
user: this.userRouter.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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Form, Input, Button, message, Row, Col } from "antd";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../providers/auth-provider";
|
||||
import DepartmentSelect from "../components/models/department/department-select";
|
||||
import SineWave from "../components/animation/sine-wave";
|
||||
const LoginPage: React.FC = () => {
|
||||
const [showLogin, setShowLogin] = useState(true);
|
||||
const [registerLoading, setRegisterLoading] = useState(false);
|
||||
const { login, isAuthenticated, signup } = useAuth();
|
||||
const loginFormRef = useRef<any>(null);
|
||||
const registerFormRef = useRef<any>(null);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const onFinishLogin = async (values: any) => {
|
||||
try {
|
||||
const { username, password } = values;
|
||||
await login(username, password);
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message || "账号或密码错误!");
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishRegister = async (values: any) => {
|
||||
setRegisterLoading(true);
|
||||
const { username, password, deptId, officerId, showname } = values;
|
||||
try {
|
||||
await signup({ username, password, deptId, officerId, showname });
|
||||
message.success("注册成功!");
|
||||
setShowLogin(true);
|
||||
// loginFormRef.current.submit();
|
||||
} catch (err: any) {
|
||||
message.error(err?.response?.data?.message);
|
||||
} finally {
|
||||
setRegisterLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const redirectUrl = params.get("redirect_url") || "/";
|
||||
navigate(redirectUrl, { replace: true });
|
||||
}
|
||||
}, [isAuthenticated, location]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center items-center h-screen w-full bg-gray-200"
|
||||
style={
|
||||
{
|
||||
// backgroundImage: `url(${backgroundUrl})`,
|
||||
// backgroundSize: "cover",
|
||||
}
|
||||
}>
|
||||
<div
|
||||
className="flex items-center transition-all hover:bg-white overflow-hidden border-2 border-white bg-gray-50 shadow-elegant rounded-xl "
|
||||
style={{ width: 800, height: 600 }}>
|
||||
<div
|
||||
className={`transition-all h-full flex-1 text-white p-10 flex items-center justify-center bg-primary`}>
|
||||
{showLogin ? (
|
||||
<div className="flex flex-col">
|
||||
<SineWave width={300} height={200} />
|
||||
<div className="text-2xl my-4">没有账号?</div>
|
||||
<div className="my-4 font-thin text-sm">
|
||||
点击注册一个属于你自己的账号吧!
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowLogin(false)}
|
||||
className="hover:translate-y-1 my-8 p-2 text-center rounded-xl border-white border hover:bg-white hover:text-primary hover:shadow-xl hover:cursor-pointer transition-all">
|
||||
注册
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="text-2xl my-4">注册小贴士</div>
|
||||
<div className="my-4 font-thin text-sm">
|
||||
请认真填写用户信息哦!
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowLogin(true)}
|
||||
className="hover:translate-y-1 my-8 rounded-xl text-center border-white border p-2 hover:bg-white hover:text-primary hover:shadow-xl hover:cursor-pointer transition-all">
|
||||
返回登录
|
||||
</div>
|
||||
<SineWave width={300} height={200} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 py-10 px-10">
|
||||
{showLogin ? (
|
||||
<>
|
||||
<div className="text-center text-2xl text-primary select-none">
|
||||
<span className="px-2">登录</span>
|
||||
</div>
|
||||
<Form
|
||||
ref={loginFormRef}
|
||||
onFinish={onFinishLogin}
|
||||
layout="vertical"
|
||||
requiredMark="optional"
|
||||
size="large">
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="账号"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入账号",
|
||||
},
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
},
|
||||
]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<div className="flex items-center justify-center">
|
||||
<Button type="primary" htmlType="submit">
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-center text-2xl text-primary">
|
||||
注册
|
||||
</div>
|
||||
<Form
|
||||
requiredMark="optional"
|
||||
ref={registerFormRef}
|
||||
onFinish={onFinishRegister}
|
||||
layout="vertical"
|
||||
size="large">
|
||||
<Form.Item
|
||||
name="deptId"
|
||||
label="所属单位"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请选择单位",
|
||||
},
|
||||
]}>
|
||||
<DepartmentSelect
|
||||
domain={true}></DepartmentSelect>
|
||||
</Form.Item>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="账号"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入账号",
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
max: 15,
|
||||
message:
|
||||
"账号长度为 2 到 15 个字符",
|
||||
},
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="showname"
|
||||
label="姓名"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入姓名",
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
max: 15,
|
||||
message:
|
||||
"姓名长度为 2 到 15 个字符",
|
||||
},
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name="officerId"
|
||||
label="证件号"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: /^\d{5,12}$/,
|
||||
message:
|
||||
"请输入正确的证件号(数字格式)",
|
||||
},
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: "密码长度不能小于 6 位",
|
||||
},
|
||||
]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="repeatPass"
|
||||
label="确认密码"
|
||||
dependencies={["password"]}
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请再次输入密码",
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue(
|
||||
"password"
|
||||
) === value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"两次输入的密码不一致"
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
loading={registerLoading}
|
||||
type="primary"
|
||||
htmlType="submit">
|
||||
注册
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
|
|
@ -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,153 @@
|
|||
import { api } from '@nice/client';
|
||||
import { message, Form, Modal } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { BookTable } from './components/BookTable';
|
||||
import { BookModal } from './components/BookModal';
|
||||
import { Book, Library } from './components/types';
|
||||
|
||||
export const BookPage = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingBook, setEditingBook] = useState<Book | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
// 查询数据
|
||||
const { data, isLoading, refetch, isError, error } = api.book.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: searchText ? [
|
||||
{ bookName: { contains: searchText } },
|
||||
{ isbn: { contains: searchText } },
|
||||
{ author: { contains: searchText } },
|
||||
{ library: { name: { contains: searchText } } },
|
||||
{ terms: { some:{ name: { contains: searchText } } }},
|
||||
] : undefined
|
||||
},
|
||||
include: {
|
||||
library: true,
|
||||
terms:true,
|
||||
},
|
||||
});
|
||||
|
||||
// 独立查询所有图书馆数据
|
||||
const { data: libraryList, isLoading: librarysLoading } = api.library.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
if (isError && error) {
|
||||
message.error('获取数据失败:' + error.message);
|
||||
}
|
||||
|
||||
// 创建数据
|
||||
const createMutation = api.book.create.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('创建成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('创建失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新数据
|
||||
const updateMutation = api.book.update.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('更新成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('更新失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除数据
|
||||
const deleteMutation = api.book.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('删除成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('删除失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// 处理添加
|
||||
const handleAdd = () => {
|
||||
setEditingBook(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleModalOk = (values: any) => {
|
||||
if (editingBook) {
|
||||
updateMutation.mutate({
|
||||
where: { id: editingBook.id },
|
||||
data: {
|
||||
bookName: values.bookName,
|
||||
isbn: values.isbn,
|
||||
author: values.author,
|
||||
libraryId: values.libraryId,
|
||||
// 更新terms关联
|
||||
terms: values.termIds ? {
|
||||
set: values.termIds.map((id: string) => ({ id }))
|
||||
} : undefined
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createMutation.mutate({
|
||||
|
||||
bookName: values.bookName,
|
||||
isbn: values.isbn,
|
||||
author: values.author,
|
||||
libraryId: values.libraryId,
|
||||
// 更新terms关联
|
||||
terms: values.termIds ? {
|
||||
connect: values.termIds.map((id: string) => ({ id }))
|
||||
} : undefined
|
||||
|
||||
});
|
||||
} console.log(values);
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onSearchTextChange={setSearchText}
|
||||
onSearch={handleSearch}
|
||||
onAdd={handleAdd}
|
||||
onImportSuccess={refetch}
|
||||
/>
|
||||
|
||||
<BookTable
|
||||
data={(data || []) as Book[]}
|
||||
loading={isLoading}
|
||||
onEdit={(book) => {
|
||||
setEditingBook(book);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
onDelete={(ids) => deleteMutation.mutate({ ids })}
|
||||
/>
|
||||
|
||||
<BookModal
|
||||
open={modalOpen}
|
||||
editingBook={editingBook}
|
||||
libraryList={(libraryList || []) as Library[]} // 传递俱乐部列表
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookPage;
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import { Modal, Form, Input, Select } from 'antd';
|
||||
import { Book, Library } from './types';
|
||||
import React, { useEffect } from 'react';
|
||||
import TermSelect from '@web/src/components/models/term/term-select';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface BookModalProps {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
editingBook?: Book;
|
||||
libraryList?: Library[];
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const BookModal: React.FC<BookModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingBook,
|
||||
libraryList,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 查询图书分类的taxonomy
|
||||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||||
|
||||
// type: 'book'
|
||||
});
|
||||
|
||||
// 当编辑图书时,设置初始值
|
||||
//bug,需要再新增时初始化表单
|
||||
useEffect(() => {
|
||||
if (editingBook && open) {
|
||||
form.setFieldsValue({
|
||||
...editingBook,
|
||||
termIds: editingBook.terms?.map(term => term.id) || []
|
||||
});
|
||||
} else if (!editingBook && open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [editingBook, open, form]);
|
||||
|
||||
// 找到图书分类的taxonomy
|
||||
const bookTaxonomy = taxonomies?.find(tax => tax.objectType.includes('book'));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editingBook ? '编辑图书' : '新增图书'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={editingBook}
|
||||
onFinish={(values) => {
|
||||
// 确保数据格式正确
|
||||
const formData = {
|
||||
bookName: values.bookName,
|
||||
isbn: values.isbn,
|
||||
author: values.author,
|
||||
termIds: values.termIds, // 添加图书分类ID
|
||||
libraryId: values.libraryId,
|
||||
};
|
||||
onOk(formData);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="bookName"
|
||||
label="书名"
|
||||
rules={[{ required: true, message: '请输入书名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isbn"
|
||||
label="图书编码"
|
||||
rules={[{ required: true, message: '请输入图书编码' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="author"
|
||||
label="作者"
|
||||
rules={[{ required: true, message: '请输入作者' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{bookTaxonomy && (
|
||||
<Form.Item
|
||||
name="termIds"
|
||||
label="图书分类"
|
||||
>
|
||||
<TermSelect
|
||||
multiple
|
||||
placeholder="请选择图书分类"
|
||||
taxonomyId={bookTaxonomy.id}
|
||||
/>
|
||||
</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} {/* 使用数据库中的 name 字段 */}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { Table, Space, Button, Popconfirm, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Book } from './types';
|
||||
|
||||
import React from 'react';
|
||||
interface BookTableProps {
|
||||
data: Book[];
|
||||
loading?: boolean;
|
||||
onEdit: (record: Book) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export const BookTable: React.FC<BookTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const columns: ColumnsType<Book> = [
|
||||
{
|
||||
title: '图书名称',
|
||||
dataIndex: 'bookName',
|
||||
key: 'bookName',
|
||||
},
|
||||
{
|
||||
title: '图书编码',
|
||||
dataIndex: 'isbn',
|
||||
key: 'isbn',
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: 'author',
|
||||
key: 'author',
|
||||
},
|
||||
{
|
||||
title: '图书分类',
|
||||
key: 'terms',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.terms?.map(term => (
|
||||
<Tag color="blue" key={term.id}>
|
||||
{term.name}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
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,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,25 @@
|
|||
export interface Book{
|
||||
id : string ;
|
||||
bookName : string;
|
||||
isbn : string;
|
||||
author : string;
|
||||
libraryId : string;
|
||||
library? : Library;
|
||||
terms? : Term[];
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
|
||||
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Term {
|
||||
id: string;
|
||||
name: string;
|
||||
taxonomyId?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { api } from '@nice/client';
|
||||
import { message, Form } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { BorrowRecordTable } from './components/BorrowRecordTable';
|
||||
import { BorrowRecordModal } from './components/BorrowRecordModal';
|
||||
import { BorrowRecord } from './components/types';
|
||||
|
||||
export const BorrowRecordPage = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingRecord, setEditingRecord] = useState<BorrowRecord | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 查询借阅记录数据
|
||||
const { data, isLoading, refetch, isError, error } = api.borrowRecord.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
libraryId: selectedLibraryId || undefined,
|
||||
OR: searchText ? [
|
||||
{ book: { bookName: { contains: searchText } } },
|
||||
{ book: { isbn: { contains: searchText } } },
|
||||
{ reader: { name: { contains: searchText } } },
|
||||
{ reader: { username: { contains: searchText } } },
|
||||
] : undefined
|
||||
},
|
||||
include: {
|
||||
library: true,
|
||||
reader: true,
|
||||
book: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (isError && error) {
|
||||
message.error('获取数据失败:' + error.message);
|
||||
}
|
||||
|
||||
// 创建借阅记录
|
||||
const createMutation = api.borrowRecord.create.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('创建成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('创建失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新借阅记录
|
||||
const updateMutation = api.borrowRecord.update.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('更新成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('更新失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除借阅记录
|
||||
const deleteMutation = api.borrowRecord.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('删除成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('删除失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// 处理添加
|
||||
const handleAdd = () => {
|
||||
setEditingRecord(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleModalOk = (values: any) => {
|
||||
if (editingRecord) {
|
||||
updateMutation.mutate({
|
||||
where: { id: editingRecord.id },
|
||||
data: {
|
||||
giveTime: values.giveTime,
|
||||
backTime: values.backTime,
|
||||
isbackTime: values.isbackTime,
|
||||
libraryId: values.libraryId,
|
||||
readerId: values.readerId,
|
||||
bookId: values.bookId,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createMutation.mutate({
|
||||
giveTime: values.giveTime,
|
||||
backTime: values.backTime,
|
||||
isbackTime: values.isbackTime,
|
||||
libraryId: values.libraryId,
|
||||
readerId: values.readerId,
|
||||
bookId: values.bookId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">图书借阅记录管理</h1>
|
||||
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onSearchTextChange={setSearchText}
|
||||
onSearch={handleSearch}
|
||||
onAdd={handleAdd}
|
||||
libraryId={selectedLibraryId}
|
||||
onLibraryChange={setSelectedLibraryId}
|
||||
/>
|
||||
|
||||
<BorrowRecordTable
|
||||
data={(data || []) as BorrowRecord[]}
|
||||
loading={isLoading}
|
||||
onEdit={(record) => {
|
||||
setEditingRecord(record);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
onDelete={(ids) => deleteMutation.mutate({ ids })}
|
||||
/>
|
||||
|
||||
<BorrowRecordModal
|
||||
open={modalOpen}
|
||||
editingRecord={editingRecord}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BorrowRecordPage;
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import { Modal, Form, DatePicker, Select, Alert } from 'antd';
|
||||
import { BorrowRecord, Library, Reader, Book } from './types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '@nice/client';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface BorrowRecordModalProps {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
editingRecord?: BorrowRecord;
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const BorrowRecordModal: React.FC<BorrowRecordModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingRecord,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
||||
const [selectedReaderId, setSelectedReaderId] = useState<string>('');
|
||||
|
||||
// 查询所有图书馆
|
||||
const { data: libraryList } = api.library.findMany.useQuery({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
// 根据选择的图书馆筛选读者
|
||||
const { data: readerList } = api.reader.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
libraryId: selectedLibraryId || undefined
|
||||
},
|
||||
include: { library: true }
|
||||
}, {
|
||||
enabled: !!selectedLibraryId
|
||||
});
|
||||
|
||||
// 根据选择的图书馆筛选图书
|
||||
const { data: bookList } = api.book.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
libraryId: selectedLibraryId || undefined
|
||||
},
|
||||
include: { library: true }
|
||||
}, {
|
||||
enabled: !!selectedLibraryId
|
||||
});
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
if (editingRecord && open) {
|
||||
setSelectedLibraryId(editingRecord.libraryId);
|
||||
setSelectedReaderId(editingRecord.readerId);
|
||||
|
||||
form.setFieldsValue({
|
||||
...editingRecord,
|
||||
giveTime: dayjs(editingRecord.giveTime),
|
||||
backTime: dayjs(editingRecord.backTime),
|
||||
isbackTime: editingRecord.isbackTime ? dayjs(editingRecord.isbackTime) : null,
|
||||
});
|
||||
} else if (!editingRecord && open) {
|
||||
form.resetFields();
|
||||
setSelectedLibraryId('');
|
||||
setSelectedReaderId('');
|
||||
}
|
||||
}, [editingRecord, open, form]);
|
||||
|
||||
// 处理图书馆选择变化
|
||||
const handleLibraryChange = (value: string) => {
|
||||
setSelectedLibraryId(value);
|
||||
// 清空读者和图书的选择
|
||||
form.setFieldsValue({ readerId: undefined, bookId: undefined });
|
||||
};
|
||||
|
||||
// 处理读者选择变化
|
||||
const handleReaderChange = (value: string) => {
|
||||
setSelectedReaderId(value);
|
||||
|
||||
// 如果选择了读者,自动设置图书馆为读者所属图书馆
|
||||
if (value && readerList) {
|
||||
const reader = readerList.find(r => r.id === value);
|
||||
if (reader && reader.libraryId) {
|
||||
setSelectedLibraryId(reader.libraryId);
|
||||
form.setFieldsValue({ libraryId: reader.libraryId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editingRecord ? '编辑借阅记录' : '新增借阅记录'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
const formData = {
|
||||
...values,
|
||||
giveTime: values.giveTime.toDate(),
|
||||
backTime: values.backTime.toDate(),
|
||||
isbackTime: values.isbackTime ? values.isbackTime.toDate() : new Date(0),
|
||||
};
|
||||
onOk(formData);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="libraryId"
|
||||
label="图书馆"
|
||||
rules={[{ required: true, message: '请选择图书馆' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择图书馆"
|
||||
onChange={handleLibraryChange}
|
||||
>
|
||||
{libraryList?.map(library => (
|
||||
<Select.Option key={library.id} value={library.id}>
|
||||
{library.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="readerId"
|
||||
label="借阅人"
|
||||
rules={[{ required: true, message: '请选择借阅人' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择借阅人"
|
||||
onChange={handleReaderChange}
|
||||
disabled={!selectedLibraryId}
|
||||
>
|
||||
{readerList?.map(reader => (
|
||||
<Select.Option key={reader.id} value={reader.id}>
|
||||
{reader.name} ({reader.username})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{!selectedLibraryId && (
|
||||
<Alert
|
||||
message="请先选择图书馆"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="bookId"
|
||||
label="借阅图书"
|
||||
rules={[{ required: true, message: '请选择借阅图书' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择图书"
|
||||
disabled={!selectedLibraryId}
|
||||
>
|
||||
{bookList?.map(book => (
|
||||
<Select.Option key={book.id} value={book.id}>
|
||||
{book.bookName} (ISBN: {book.isbn})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="giveTime"
|
||||
label="借出时间"
|
||||
rules={[{ required: true, message: '请选择借出时间' }]}
|
||||
>
|
||||
<DatePicker showTime style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="backTime"
|
||||
label="应还时间"
|
||||
rules={[{ required: true, message: '请选择应还时间' }]}
|
||||
>
|
||||
<DatePicker showTime style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isbackTime"
|
||||
label="实际归还时间"
|
||||
>
|
||||
<DatePicker showTime style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { Button, Space, Table, Tag, Popconfirm } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { BorrowRecord } from './types';
|
||||
import React from 'react';
|
||||
|
||||
interface BorrowRecordTableProps {
|
||||
data: BorrowRecord[];
|
||||
loading?: boolean;
|
||||
onEdit: (record: BorrowRecord) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export const BorrowRecordTable: React.FC<BorrowRecordTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
console.log(data);
|
||||
|
||||
const columns: ColumnsType<BorrowRecord> = [
|
||||
{
|
||||
title: '图书名称',
|
||||
dataIndex: ['book', 'bookName'],
|
||||
key: 'bookName',
|
||||
},
|
||||
{
|
||||
title: '图书ISBN',
|
||||
dataIndex: ['book', 'isbn'],
|
||||
key: 'isbn',
|
||||
},
|
||||
{
|
||||
title: '借阅人',
|
||||
dataIndex: ['reader', 'name'],
|
||||
key: 'readerName',
|
||||
},
|
||||
{
|
||||
title: '所属图书馆',
|
||||
dataIndex: ['library', 'name'],
|
||||
key: 'libraryName',
|
||||
},
|
||||
{
|
||||
title: '借出时间',
|
||||
dataIndex: 'giveTime',
|
||||
key: 'giveTime',
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '应还时间',
|
||||
dataIndex: 'backTime',
|
||||
key: 'backTime',
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '实际归还时间',
|
||||
dataIndex: 'isbackTime',
|
||||
key: 'isbackTime',
|
||||
render: (date: string) => {
|
||||
const isbackDate = new Date(date);
|
||||
const backDate = new Date(0); // 1970年1月1日
|
||||
return isbackDate > backDate ?
|
||||
isbackDate.toLocaleString('zh-CN') :
|
||||
<Tag color="red">未归还</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
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,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,59 @@
|
|||
import { Button, Input, Space, Select } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface SearchBarProps {
|
||||
searchText: string;
|
||||
onSearchTextChange: (text: string) => void;
|
||||
onSearch: () => void;
|
||||
onAdd: () => void;
|
||||
libraryId: string;
|
||||
onLibraryChange: (libraryId: string) => void;
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
searchText,
|
||||
onSearchTextChange,
|
||||
onSearch,
|
||||
onAdd,
|
||||
libraryId,
|
||||
onLibraryChange
|
||||
}) => {
|
||||
// 查询所有图书馆
|
||||
const { data: libraryList } = api.library.findMany.useQuery({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="搜索图书或读者信息"
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchTextChange(e.target.value)}
|
||||
onPressEnter={onSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="选择图书馆"
|
||||
value={libraryId || undefined}
|
||||
onChange={onLibraryChange}
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
{libraryList?.map(library => (
|
||||
<Select.Option key={library.id} value={library.id}>
|
||||
{library.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Button type="primary" onClick={onSearch}>搜索</Button>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
||||
添加借阅记录
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
export interface BorrowRecord {
|
||||
id: string;
|
||||
giveTime: Date | string;
|
||||
backTime: Date | string;
|
||||
isbackTime: Date | string;
|
||||
libraryId: string;
|
||||
library?: Library;
|
||||
readerId: string;
|
||||
reader?: Reader;
|
||||
bookId: string;
|
||||
book?: Book;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Reader {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
libraryId: string;
|
||||
}
|
||||
|
||||
export interface Book {
|
||||
id: string;
|
||||
bookName: string;
|
||||
isbn: string;
|
||||
author: string;
|
||||
libraryId: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
items: BorrowRecord[];
|
||||
total: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import { api } from '@nice/client';
|
||||
import { message, Card, Empty, Pagination } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { CommentTable } from './components/CommentTable';
|
||||
import { CommentModal } from './components/CommentModal';
|
||||
import { Comment } from './components/types';
|
||||
|
||||
export const CommentPage = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingRecord, setEditingRecord] = useState<Comment | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
||||
const [commentType, setCommentType] = useState<'all' | 'book' | 'library'>('all');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// 构建查询条件
|
||||
const buildWhereClause = () => {
|
||||
const where: any = {
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
// 如果选择了图书馆
|
||||
if (selectedLibraryId) {
|
||||
if (commentType === 'book') {
|
||||
where.book = { libraryId: selectedLibraryId };
|
||||
} else if (commentType === 'library') {
|
||||
where.libraryId = selectedLibraryId;
|
||||
} else {
|
||||
where.OR = [
|
||||
{ libraryId: selectedLibraryId },
|
||||
{ book: { libraryId: selectedLibraryId } }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据评论类型筛选
|
||||
if (commentType === 'book' && !selectedLibraryId) {
|
||||
where.bookId = { not: null };
|
||||
} else if (commentType === 'library' && !selectedLibraryId) {
|
||||
where.libraryId = { not: null };
|
||||
}
|
||||
|
||||
// 搜索文本
|
||||
if (searchText) {
|
||||
where.OR = [
|
||||
{ content: { contains: searchText } },
|
||||
{ reader: { name: { contains: searchText } } },
|
||||
{ reader: { username: { contains: searchText } } },
|
||||
{ book: { bookName: { contains: searchText } } },
|
||||
{ library: { name: { contains: searchText } } }
|
||||
];
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
|
||||
// 查询评论数据
|
||||
const { data, isLoading, refetch, isError, error } = api.comment.findMany.useQuery({
|
||||
where: buildWhereClause(),
|
||||
include: {
|
||||
library: true,
|
||||
reader: true,
|
||||
book: {
|
||||
include: {
|
||||
library: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
if (isError && error) {
|
||||
message.error('获取数据失败:' + error.message);
|
||||
}
|
||||
|
||||
// 创建评论
|
||||
const createMutation = api.comment.create.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('评论发布成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('评论发布失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新评论
|
||||
const updateMutation = api.comment.update.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('评论更新成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('评论更新失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除评论
|
||||
const deleteMutation = api.comment.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('评论删除成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('评论删除失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
refetch();
|
||||
};
|
||||
|
||||
// 处理添加
|
||||
const handleAdd = () => {
|
||||
setEditingRecord(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleModalOk = (values: any) => {
|
||||
if (editingRecord) {
|
||||
updateMutation.mutate({
|
||||
where: { id: editingRecord.id },
|
||||
data: {
|
||||
content: values.content,
|
||||
libraryId: values.libraryId,
|
||||
readerId: values.readerId,
|
||||
bookId: values.bookId,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createMutation.mutate({
|
||||
content: values.content,
|
||||
libraryId: values.libraryId,
|
||||
readerId: values.readerId,
|
||||
bookId: values.bookId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = (page: number, size?: number) => {
|
||||
setCurrentPage(page);
|
||||
if (size) {
|
||||
setPageSize(size);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取数据总数的估计值,用于分页
|
||||
// 由于我们不能直接获取总数,这里使用当前页数据长度来估计
|
||||
// 如果当前页数据长度小于页大小,说明已经到达最后一页
|
||||
const estimatedTotal = data && data.length < pageSize
|
||||
? (currentPage - 1) * pageSize + data.length
|
||||
: (currentPage * pageSize) + 1; // 如果当前页数据等于页大小,可能还有更多数据
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">图书与图书馆评论管理</h1>
|
||||
<p className="text-center text-gray-500 mb-6">
|
||||
读者可以对任何图书馆和图书发表评论,分享阅读体验和使用感受
|
||||
</p>
|
||||
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
onSearchTextChange={setSearchText}
|
||||
onSearch={handleSearch}
|
||||
onAdd={handleAdd}
|
||||
libraryId={selectedLibraryId}
|
||||
onLibraryChange={setSelectedLibraryId}
|
||||
commentType={commentType}
|
||||
onCommentTypeChange={setCommentType}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
{data && data.length > 0 ? (
|
||||
<>
|
||||
<CommentTable
|
||||
data={(data || []) as Comment[]}
|
||||
loading={isLoading}
|
||||
onEdit={(record) => {
|
||||
setEditingRecord(record);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
onDelete={(ids) => deleteMutation.mutate({ ids })}
|
||||
/>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={estimatedTotal}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `第 ${currentPage} 页 / 共 ${Math.ceil(total / pageSize)} 页`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Empty
|
||||
description={isLoading ? "加载中..." : "暂无评论数据"}
|
||||
className="py-12"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<CommentModal
|
||||
open={modalOpen}
|
||||
editingRecord={editingRecord}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentPage;
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
import { Modal, Form, Input, Select, Radio, Alert, Divider } from 'antd';
|
||||
import { Comment, Library, Reader, Book } from './types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface CommentModalProps {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
editingRecord?: Comment;
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const CommentModal: React.FC<CommentModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingRecord,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [commentType, setCommentType] = useState<'book' | 'library'>('book');
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
||||
const [selectedReaderId, setSelectedReaderId] = useState<string>('');
|
||||
console.log(editingRecord);
|
||||
|
||||
// 查询所有图书馆
|
||||
const { data: libraryList } = api.library.findMany.useQuery({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
// 查询所有读者
|
||||
const { data: readerList } = api.reader.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 根据选择的图书馆筛选图书
|
||||
const { data: bookList } = api.book.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
libraryId: selectedLibraryId || undefined
|
||||
},
|
||||
include: { library: true }
|
||||
}, {
|
||||
enabled: !!selectedLibraryId && commentType === 'book'
|
||||
});
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
if (editingRecord && open) {
|
||||
// 判断评论类型
|
||||
const type = editingRecord.bookId ? 'book' : 'library';
|
||||
setCommentType(type);
|
||||
setSelectedLibraryId(type === 'book' ? editingRecord.book?.libraryId : editingRecord.libraryId);
|
||||
setSelectedReaderId(editingRecord.readerId);
|
||||
|
||||
form.setFieldsValue({
|
||||
content: editingRecord.content,
|
||||
readerId: editingRecord.readerId,
|
||||
commentType: type,
|
||||
libraryId: type === 'library' ? editingRecord.libraryId : undefined,
|
||||
bookId: type === 'book' ? editingRecord.bookId : undefined,
|
||||
});
|
||||
} else if (!editingRecord && open) {
|
||||
form.resetFields();
|
||||
setCommentType('book');
|
||||
setSelectedLibraryId('');
|
||||
setSelectedReaderId('');
|
||||
}
|
||||
}, [editingRecord, open, form]);
|
||||
|
||||
// 处理评论类型变化
|
||||
const handleCommentTypeChange = (e: any) => {
|
||||
const type = e.target.value;
|
||||
setCommentType(type);
|
||||
// 清空相关字段
|
||||
if (type === 'book') {
|
||||
form.setFieldsValue({ libraryId: undefined });
|
||||
} else {
|
||||
form.setFieldsValue({ bookId: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图书馆选择变化
|
||||
const handleLibraryChange = (value: string) => {
|
||||
setSelectedLibraryId(value);
|
||||
// 如果是图书评论,清空图书选择
|
||||
if (commentType === 'book') {
|
||||
form.setFieldsValue({ bookId: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editingRecord ? '编辑评论' : '添加评论'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
// 根据评论类型处理数据
|
||||
const formData = {
|
||||
...values,
|
||||
libraryId: values.commentType === 'library' ? values.libraryId : null,
|
||||
bookId: values.commentType === 'book' ? values.bookId : null,
|
||||
};
|
||||
onOk(formData);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="readerId"
|
||||
label="评论人"
|
||||
rules={[{ required: true, message: '请选择评论人' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择评论人"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
>
|
||||
{readerList?.map(reader => (
|
||||
<Select.Option key={reader.id} value={reader.id}>
|
||||
{reader.name} ({reader.username})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>评论对象</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="commentType"
|
||||
label="评论类型"
|
||||
rules={[{ required: true, message: '请选择评论类型' }]}
|
||||
initialValue="book"
|
||||
>
|
||||
<Radio.Group onChange={handleCommentTypeChange}>
|
||||
<Radio value="book">图书评论</Radio>
|
||||
<Radio value="library">图书馆评论</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{commentType === 'book' ? (
|
||||
<>
|
||||
<Form.Item
|
||||
name="libraryId"
|
||||
label="图书所属图书馆"
|
||||
rules={[{ required: true, message: '请选择图书馆' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择图书馆"
|
||||
onChange={handleLibraryChange}
|
||||
>
|
||||
{libraryList?.map(library => (
|
||||
<Select.Option key={library.id} value={library.id}>
|
||||
{library.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{!selectedLibraryId && (
|
||||
<Alert
|
||||
message="请先选择图书馆"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="bookId"
|
||||
label="评论图书"
|
||||
rules={[{ required: true, message: '请选择图书' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择图书"
|
||||
disabled={!selectedLibraryId}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
>
|
||||
{bookList?.map(book => (
|
||||
<Select.Option key={book.id} value={book.id}>
|
||||
{book.bookName} (ISBN: {book.isbn})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="libraryId"
|
||||
label="评论图书馆"
|
||||
rules={[{ required: true, message: '请选择图书馆' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择图书馆"
|
||||
onChange={handleLibraryChange}
|
||||
>
|
||||
{libraryList?.map(library => (
|
||||
<Select.Option key={library.id} value={library.id}>
|
||||
{library.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Divider>评论内容</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="content"
|
||||
label="评论内容"
|
||||
rules={[{ required: true, message: '请输入评论内容' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="请输入您的评论内容..."
|
||||
showCount
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { Button, Space, Table, Popconfirm, Avatar, Rate, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Comment } from './types';
|
||||
import React from 'react';
|
||||
import { UserOutlined, BookOutlined, HomeOutlined } from '@ant-design/icons';
|
||||
|
||||
|
||||
interface CommentTableProps {
|
||||
data: Comment[];
|
||||
loading?: boolean;
|
||||
onEdit: (record: Comment) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export const CommentTable: React.FC<CommentTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
console.log(data);
|
||||
|
||||
const columns: ColumnsType<Comment> = [
|
||||
{
|
||||
title: '评论内容',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
ellipsis:true,
|
||||
render: (content: string) => (
|
||||
<Space>
|
||||
{content}
|
||||
</Space>
|
||||
),
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '评论人',
|
||||
dataIndex: ['reader', 'name'],
|
||||
key: 'readerName',
|
||||
render: (name: string, record: Comment) => (
|
||||
<Space>
|
||||
<Avatar icon={<UserOutlined />} />
|
||||
<span>{name} ({record.reader?.username})</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '评论对象',
|
||||
key: 'commentTarget',
|
||||
render: (_, record: Comment) => {
|
||||
if (record.bookId && record.book) {
|
||||
return (
|
||||
<Space>
|
||||
<BookOutlined />
|
||||
<span>
|
||||
<Tag color="blue">{record.book.library?.name}</Tag>
|
||||
{record.book.bookName} (ISBN: {record.book.isbn})
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
} else if (record.libraryId && record.library) {
|
||||
return (
|
||||
<Space>
|
||||
<HomeOutlined />
|
||||
<span>{record.library.name}</span>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: '评论时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
sorter: (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
},
|
||||
{
|
||||
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"
|
||||
pagination={{ pageSize: 10 }}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<div className="p-4 bg-gray-50">
|
||||
{record.content}
|
||||
<div className="text-gray-500 text-sm mt-2">
|
||||
评论时间: {new Date(record.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,73 @@
|
|||
import { Button, Input, Space, Select, Radio } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface SearchBarProps {
|
||||
searchText: string;
|
||||
onSearchTextChange: (text: string) => void;
|
||||
onSearch: () => void;
|
||||
onAdd: () => void;
|
||||
libraryId: string;
|
||||
onLibraryChange: (libraryId: string) => void;
|
||||
commentType: 'all' | 'book' | 'library';
|
||||
onCommentTypeChange: (type: 'all' | 'book' | 'library') => void;
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
searchText,
|
||||
onSearchTextChange,
|
||||
onSearch,
|
||||
onAdd,
|
||||
libraryId,
|
||||
onLibraryChange,
|
||||
commentType,
|
||||
onCommentTypeChange
|
||||
}) => {
|
||||
// 查询所有图书馆
|
||||
const { data: libraryList } = api.library.findMany.useQuery({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-wrap justify-between items-center">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-2 sm:mb-0">
|
||||
<Input
|
||||
placeholder="搜索评论内容或评论人"
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchTextChange(e.target.value)}
|
||||
onPressEnter={onSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="选择图书馆"
|
||||
value={libraryId || undefined}
|
||||
onChange={onLibraryChange}
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
{libraryList?.map(library => (
|
||||
<Select.Option key={library.id} value={library.id}>
|
||||
{library.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Radio.Group
|
||||
value={commentType}
|
||||
onChange={(e) => onCommentTypeChange(e.target.value)}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value="all">全部评论</Radio.Button>
|
||||
<Radio.Button value="book">图书评论</Radio.Button>
|
||||
<Radio.Button value="library">图书馆评论</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Button type="primary" onClick={onSearch}>搜索</Button>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
||||
添加评论
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
export interface Comment {
|
||||
id: string;
|
||||
content: string;
|
||||
libraryId?: string | null;
|
||||
library?: Library;
|
||||
readerId: string;
|
||||
reader?: Reader;
|
||||
bookId?: string | null;
|
||||
book?: Book;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
address?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Reader {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
libraryId?: string;
|
||||
}
|
||||
|
||||
export interface Book {
|
||||
id: string;
|
||||
bookName: string;
|
||||
isbn: string;
|
||||
author: string;
|
||||
libraryId: string;
|
||||
library?: Library;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
items: Comment[];
|
||||
total: number;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -52,10 +52,17 @@ const items = [
|
|||
null,
|
||||
),
|
||||
getItem(
|
||||
"test",
|
||||
"/test",
|
||||
"图书管理系统",
|
||||
"/library",
|
||||
<i className="iconfont icon-icon-category" />,
|
||||
null,
|
||||
[ getItem("用户", "/li/user", null, null, null),
|
||||
getItem("图书馆", "/li/library", null, null, null),
|
||||
getItem("读者", "/li/reader", null, null, null),
|
||||
getItem("图书", "/li/book", null, null, null),
|
||||
getItem("借阅记录", "/li/borrowRecord", null, null, null),
|
||||
getItem("评论", "/li/comment", null, null, null),
|
||||
|
||||
],
|
||||
null,
|
||||
),
|
||||
getItem(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
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';
|
||||
import { useAuth } from '@web/src/providers/auth-provider';
|
||||
|
||||
export const LibraryPage = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingLibrary, setEditingLibrary] = useState<Library | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { user } = useAuth();
|
||||
|
||||
// 查询条件:管理员只能看到自己的图书馆
|
||||
const whereCondition = {
|
||||
deletedAt: null,
|
||||
OR: searchText.trim() ? [
|
||||
{ name: { contains: searchText } },
|
||||
{ address: { contains: searchText } },
|
||||
{ description: { contains: searchText } },
|
||||
] : undefined,
|
||||
...(user?.role === 'LIB_ADMIN' ? { adminId: user.id } : {})
|
||||
};
|
||||
|
||||
// 查询数据
|
||||
const { data, isLoading, refetch, isError, error } = api.library.findMany.useQuery({
|
||||
where: whereCondition,
|
||||
include: {
|
||||
admin: true,
|
||||
_count: {
|
||||
select: {
|
||||
readers: true,
|
||||
books: true,
|
||||
borrowRecords: true,
|
||||
comments: true
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 处理模态框取消
|
||||
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,
|
||||
adminId: values.adminId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createMutation.mutate({
|
||||
name: values.name,
|
||||
address: values.address,
|
||||
description: values.description,
|
||||
adminId: values.adminId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleCancelModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryPage;
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { Button, Upload, message } from 'antd';
|
||||
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { Library } from './types';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface ImportExportButtonsProps {
|
||||
onImportSuccess: () => void;
|
||||
data: Library[];
|
||||
}
|
||||
|
||||
export const ImportExportButtons: React.FC<ImportExportButtonsProps> = ({
|
||||
onImportSuccess,
|
||||
data,
|
||||
}) => {
|
||||
const createMutation = api.library.create.useMutation({
|
||||
onSuccess: () => {
|
||||
// 成功时不显示单条消息,让最终统计来显示
|
||||
},
|
||||
onError: (error) => {
|
||||
// 只有当不是名称重复错误时才显示错误信息
|
||||
if (!error.message.includes('Unique constraint failed')) {
|
||||
message.error('导入失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加恢复记录的 mutation
|
||||
const restoreMutation = api.library.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 libraryData = jsonData.map((row: any) => ({
|
||||
id: row['ID'] || undefined,
|
||||
name: row['名称'],
|
||||
address: row['地址'],
|
||||
description: row['描述'],
|
||||
adminId: row['管理员ID'] || null,
|
||||
}));
|
||||
|
||||
// 批量处理
|
||||
for (const library of libraryData) {
|
||||
try {
|
||||
await createMutation.mutateAsync(library);
|
||||
successCount++;
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('Unique constraint failed')) {
|
||||
try {
|
||||
if (!library.id) {
|
||||
errorCount++;
|
||||
console.error('恢复记录失败: 缺少唯一标识 id');
|
||||
continue;
|
||||
}
|
||||
await restoreMutation.mutateAsync({
|
||||
where: {
|
||||
id: library.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: null,
|
||||
name: library.name,
|
||||
address: library.address,
|
||||
description: library.description,
|
||||
adminId: library.adminId,
|
||||
}
|
||||
});
|
||||
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(library => ({
|
||||
'名称': library.name,
|
||||
'地址': library.address,
|
||||
'描述': library.description,
|
||||
'管理员': library.admin?.name || '',
|
||||
'管理员ID': library.adminId || '',
|
||||
'创建时间': new Date(library.createdAt).toLocaleString(),
|
||||
'更新时间': new Date(library.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,123 @@
|
|||
import { Modal, Form, Input, Select, Spin } from 'antd';
|
||||
import { Library } from './types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { api } from '@nice/client';
|
||||
|
||||
interface LibraryModalProps {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
editingLibrary?: Library;
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LibraryModal: React.FC<LibraryModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingLibrary,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 查询图书馆管理员用户
|
||||
const { data: adminUsers, isLoading: loadingAdmins } = api.user.findMany.useQuery({
|
||||
where: {
|
||||
role: 'LIB_ADMIN',
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
}, {
|
||||
enabled: open, // 只有当模态框打开时才查询
|
||||
});
|
||||
|
||||
// 核心逻辑:当模态框的打开状态或编辑对象改变时,更新表单
|
||||
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}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={editingLibrary}
|
||||
onFinish={(values) => {
|
||||
// 确保数据格式正确
|
||||
const formData = {
|
||||
name: values.name,
|
||||
address: values.address,
|
||||
description: values.description,
|
||||
adminId: values.adminId,
|
||||
};
|
||||
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 rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="adminId"
|
||||
label="图书馆管理员"
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="选择图书馆管理员"
|
||||
loading={loadingAdmins}
|
||||
notFoundContent={loadingAdmins ? <Spin size="small" /> : null}
|
||||
>
|
||||
{adminUsers?.map(admin => (
|
||||
<Select.Option key={admin.id} value={admin.id}>
|
||||
{admin.name} ({admin.username})
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import { Table, Space, Button, Popconfirm, Badge, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Library } from './types';
|
||||
import { BookOutlined, TeamOutlined, SwapOutlined, CommentOutlined } from '@ant-design/icons';
|
||||
|
||||
import React from 'react';
|
||||
interface LibraryTableProps {
|
||||
data: any[]; // 使用 any 类型,因为包含了 _count 等额外数据
|
||||
loading?: boolean;
|
||||
onEdit: (record: Library) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export const LibraryTable: React.FC<LibraryTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const columns: ColumnsType<any> = [
|
||||
{
|
||||
title: '图书馆名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
},
|
||||
{
|
||||
title: '图书馆描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '管理员',
|
||||
dataIndex: ['admin', 'name'],
|
||||
key: 'admin',
|
||||
render: (text, record) => text || '未分配',
|
||||
},
|
||||
{
|
||||
title: '统计信息',
|
||||
key: 'stats',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Tooltip title="读者数量">
|
||||
<Badge count={record._count?.readers || 0} showZero>
|
||||
<TeamOutlined style={{ fontSize: '16px' }} />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<Tooltip title="图书数量">
|
||||
<Badge count={record._count?.books || 0} showZero>
|
||||
<BookOutlined style={{ fontSize: '16px' }} />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<Tooltip title="借阅记录">
|
||||
<Badge count={record._count?.borrowRecords || 0} showZero>
|
||||
<SwapOutlined style={{ fontSize: '16px' }} />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<Tooltip title="评论">
|
||||
<Badge count={record._count?.comments || 0} showZero>
|
||||
<CommentOutlined style={{ fontSize: '16px' }} />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
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="确定要删除吗?"
|
||||
description="删除后相关的读者、图书、借阅记录和评论将无法访问此图书馆"
|
||||
onConfirm={() => onDelete([record.id])}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,21 @@
|
|||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
description: string;
|
||||
adminId?: string | null;
|
||||
admin?: {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
} | null;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
_count?: {
|
||||
readers: number;
|
||||
books: number;
|
||||
borrowRecords: number;
|
||||
comments: number;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
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,
|
||||
terms: { // 添加分类信息
|
||||
where: { deletedAt: null },
|
||||
include: {
|
||||
taxonomy: true
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
// 查询所有读者分类
|
||||
const { data: readerTypes } = api.reader.getReaderTypes.useQuery();
|
||||
|
||||
// 分配读者分类
|
||||
const assignTypeMutation = api.reader.assignReaderType.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('分类分配成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('分类分配失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除读者分类
|
||||
const removeTypeMutation = api.reader.removeReaderType.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('分类移除成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('分类移除失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
if (isError && error) {
|
||||
message.error('获取数据失败:' + error.message);
|
||||
}
|
||||
|
||||
// 处理分类变更
|
||||
const handleTypeChange = async (readerId: string, termId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
await assignTypeMutation.mutateAsync({ readerId, termId });
|
||||
} else {
|
||||
await removeTypeMutation.mutateAsync({ readerId, termId });
|
||||
}
|
||||
};
|
||||
|
||||
// 独立查询所有俱乐部数据
|
||||
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,
|
||||
terms: values.terms ? {
|
||||
connect: values.terms.map((id: string) => ({ id }))
|
||||
} : undefined
|
||||
}
|
||||
});
|
||||
} 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,
|
||||
terms: values.terms ? {
|
||||
connect: values.terms.map((id: string) => ({ id }))
|
||||
} : undefined
|
||||
|
||||
});
|
||||
} 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}
|
||||
readerTypes={readerTypes || []}
|
||||
onEdit={(reader) => {
|
||||
setEditingReader(reader);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
onDelete={(ids) => deleteMutation.mutate({ ids })}
|
||||
onTypeChange={handleTypeChange}
|
||||
/>
|
||||
|
||||
<ReaderModal
|
||||
open={modalOpen}
|
||||
editingReader={editingReader}
|
||||
libraryList={libraryList || []} // 传递俱乐部列表
|
||||
readerTypes={readerTypes || []}
|
||||
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,133 @@
|
|||
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[];
|
||||
readerTypes: any[];
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ReaderModal: React.FC<ReaderModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingReader,
|
||||
libraryList,
|
||||
readerTypes,
|
||||
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.Item
|
||||
name="terms"
|
||||
label="读者类型"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="请选择读者类型"
|
||||
options={readerTypes?.map(type => ({
|
||||
label: type.name,
|
||||
value: type.id
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import { Table, Space, Button, Popconfirm,Select } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Reader } from './types';
|
||||
|
||||
import React from 'react';
|
||||
interface ReaderTableProps {
|
||||
data: Reader[];
|
||||
loading?: boolean;
|
||||
readerTypes: any[];
|
||||
onEdit: (record: Reader) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
onTypeChange: (readerId: string, termId: string, checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const ReaderTable: React.FC<ReaderTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
readerTypes,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onTypeChange,
|
||||
|
||||
}) => {
|
||||
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: '读者类型',
|
||||
dataIndex: 'terms',
|
||||
render: (terms: any[], record: Reader) => (
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={terms?.map(t => t.id)}
|
||||
onChange={(values) => {
|
||||
// 处理分类变更
|
||||
values.forEach(termId => {
|
||||
if (!terms?.find(t => t.id === termId)) {
|
||||
onTypeChange(record.id, termId, true);
|
||||
}
|
||||
});
|
||||
terms?.forEach(term => {
|
||||
if (!values.includes(term.id)) {
|
||||
onTypeChange(record.id, term.id, false);
|
||||
}
|
||||
});
|
||||
}}
|
||||
options={readerTypes?.map(type => ({
|
||||
label: type.name,
|
||||
value: type.id
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import { useState } from 'react';
|
||||
import { UserTable } from '@web/src/app/main/user/components/UserTable';
|
||||
import { UserModal } from '@web/src/app/main/user/components/UserModal';
|
||||
import { SearchBar } from '@web/src/app/main/user/components/SearchBar';
|
||||
import { User, Library } from '@web/src/app/main/user/components/types';
|
||||
import { api } from '@nice/client';
|
||||
import { message } from 'antd';
|
||||
import { useAuth } from '@web/src/providers/auth-provider';
|
||||
import { Character } from '@nice/common'; // 或者从正确的路径导入
|
||||
|
||||
export const UserPage = () => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [roleFilter, setRoleFilter] = useState<string | null>(null);
|
||||
const { user: currentUser } = useAuth();
|
||||
|
||||
// 查询条件
|
||||
const whereCondition = {
|
||||
deletedAt: null,
|
||||
OR: searchText.trim() ? [
|
||||
{ name: { contains: searchText } },
|
||||
{ username: { contains: searchText } },
|
||||
] : undefined,
|
||||
...(roleFilter ? { role: { equals: roleFilter as Character } } : {}),
|
||||
};
|
||||
|
||||
// 查询数据
|
||||
const { data, isLoading, refetch, isError, error } = api.user.findMany.useQuery({
|
||||
where: whereCondition,
|
||||
include: {
|
||||
reader: {
|
||||
include: {
|
||||
library: true
|
||||
}
|
||||
},
|
||||
library: true
|
||||
},
|
||||
});
|
||||
|
||||
if (isError && error) {
|
||||
message.error('获取数据失败:' + error.message);
|
||||
}
|
||||
|
||||
// 查询图书馆列表
|
||||
const { data: libraryList } = api.library.findMany.useQuery({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
...(currentUser?.role === 'LIB_ADMIN' ? { adminId: currentUser.id } : {})
|
||||
}
|
||||
});
|
||||
|
||||
// 创建数据
|
||||
const createMutation = api.user.create.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('创建成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('创建失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新数据
|
||||
const updateMutation = api.user.update.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('更新成功');
|
||||
setModalOpen(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('更新失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除数据
|
||||
const deleteMutation = api.user.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
message.success('删除成功');
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('删除失败:' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// 处理添加
|
||||
const handleAdd = () => {
|
||||
setEditingUser(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 处理模态框取消
|
||||
const handleCancelModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingUser(undefined);
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleModalOk = (values: any) => {
|
||||
if (editingUser) {
|
||||
const updateData: any = {
|
||||
name: values.name,
|
||||
gender: values.gender,
|
||||
age: values.age,
|
||||
role: values.role,
|
||||
relax: values.relax,
|
||||
};
|
||||
|
||||
// 如果提供了密码,则更新密码
|
||||
if (values.password) {
|
||||
updateData.password = values.password;
|
||||
}
|
||||
|
||||
// 如果是图书馆管理员,更新关联的图书馆
|
||||
if (values.role === 'LIB_ADMIN') {
|
||||
updateData.library = {
|
||||
connect: { id: values.libraryId }
|
||||
};
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
where: { id: editingUser.id },
|
||||
data: updateData
|
||||
});
|
||||
} else {
|
||||
const createData: any = {
|
||||
name: values.name,
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
gender: values.gender,
|
||||
age: values.age,
|
||||
role: values.role,
|
||||
relax: values.relax
|
||||
};
|
||||
|
||||
// 如果是图书馆管理员,关联到图书馆
|
||||
if (values.role === 'LIB_ADMIN' && values.libraryId) {
|
||||
createData.library = {
|
||||
connect: { id: values.libraryId }
|
||||
};
|
||||
}
|
||||
|
||||
createMutation.mutate(createData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<SearchBar
|
||||
searchText={searchText}
|
||||
roleFilter={roleFilter}
|
||||
onSearchTextChange={setSearchText}
|
||||
onRoleFilterChange={setRoleFilter}
|
||||
onSearch={handleSearch}
|
||||
onAdd={handleAdd}
|
||||
onImportSuccess={refetch}
|
||||
/>
|
||||
|
||||
<UserTable
|
||||
data={(data || []) as User[]}
|
||||
loading={isLoading}
|
||||
onEdit={(user) => {
|
||||
setEditingUser(user);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
onDelete={(ids) => deleteMutation.mutate({ ids })}
|
||||
/>
|
||||
|
||||
<UserModal
|
||||
open={modalOpen}
|
||||
editingUser={editingUser}
|
||||
libraryList={(libraryList || []) as Library[]}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleCancelModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPage;
|
||||
|
|
@ -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,75 @@
|
|||
import { Button, Input, Space, Select } 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;
|
||||
roleFilter: string | null;
|
||||
onSearchTextChange: (text: string) => void;
|
||||
onRoleFilterChange: (role: string | null) => void;
|
||||
onSearch: () => void;
|
||||
onAdd: () => void;
|
||||
onImportSuccess: () => void;
|
||||
// data: Driver[];
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
searchText,
|
||||
roleFilter,
|
||||
onSearchTextChange,
|
||||
onRoleFilterChange,
|
||||
onSearch,
|
||||
onAdd,
|
||||
onImportSuccess,
|
||||
// data,
|
||||
}) => {
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '系统管理员', value: 'ADMIN' },
|
||||
{ label: '图书馆管理员', value: 'LIB_ADMIN' },
|
||||
{ label: '读者', value: 'READER' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="搜索用户名或姓名"
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchTextChange(e.target.value)}
|
||||
onPressEnter={onSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="选择角色"
|
||||
value={roleFilter || ''}
|
||||
onChange={(value) => onRoleFilterChange(value || null)}
|
||||
options={roleOptions}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<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,172 @@
|
|||
import { Modal, Form, Input, Select, InputNumber } from 'antd';
|
||||
import { User, Library } from './types';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
interface UserModalProps {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
editingUser?: User;
|
||||
libraryList?: Library[];
|
||||
onOk: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const UserModal: React.FC<UserModalProps> = ({
|
||||
open,
|
||||
loading,
|
||||
editingUser,
|
||||
libraryList,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 当编辑的用户变化时,重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
|
||||
// 如果是编辑模式,设置初始值
|
||||
if (editingUser) {
|
||||
const formValues = {
|
||||
...editingUser,
|
||||
// 不回显密码
|
||||
password: undefined,
|
||||
};
|
||||
form.setFieldsValue(formValues);
|
||||
}
|
||||
}
|
||||
}, [open, editingUser, form]);
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '系统管理员', value: 'ADMIN' },
|
||||
{ label: '图书馆管理员', value: 'LIB_ADMIN' },
|
||||
{ label: '读者', value: 'READER' }
|
||||
];
|
||||
|
||||
// 当角色变化时,重置相关字段
|
||||
const handleRoleChange = (value: string) => {
|
||||
if (value === 'LIB_ADMIN') {
|
||||
// 图书馆管理员需要选择图书馆
|
||||
form.setFieldsValue({ libraryId: undefined });
|
||||
} else {
|
||||
// 其他角色不需要选择图书馆
|
||||
form.setFieldsValue({ libraryId: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editingUser ? '编辑用户' : '新增用户'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
// 确保数据格式正确
|
||||
const formData = {
|
||||
...values,
|
||||
age: parseInt(values.age, 10),
|
||||
};
|
||||
onOk(formData);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="姓名"
|
||||
rules={[{ required: true, message: '请输入姓名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{
|
||||
required: !editingUser,
|
||||
message: '请输入密码'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder={editingUser ? '不修改请留空' : '请输入密码'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="gender"
|
||||
label="性别"
|
||||
rules={[{ required: true, message: '请输入性别' }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="男">男</Select.Option>
|
||||
<Select.Option value="女">女</Select.Option>
|
||||
<Select.Option value="其他">其他</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="age"
|
||||
label="年龄"
|
||||
rules={[{ required: true, message: '请输入年龄' }]}
|
||||
>
|
||||
<InputNumber min={1} max={120} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="relax"
|
||||
label="联系方式"
|
||||
>
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select options={roleOptions} onChange={handleRoleChange} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 仅当角色为图书馆管理员时显示图书馆选择 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.role !== currentValues.role}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('role') === 'LIB_ADMIN' ? (
|
||||
<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}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { Table, Space, Button, Popconfirm, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { User } from './types';
|
||||
|
||||
import React from 'react';
|
||||
interface UserTableProps {
|
||||
data: User[];
|
||||
loading?: boolean;
|
||||
onEdit: (record: User) => void;
|
||||
onDelete: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
export const UserTable: React.FC<UserTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
// 角色显示标签颜色映射
|
||||
const roleColors = {
|
||||
ADMIN: 'red',
|
||||
LIB_ADMIN: 'blue',
|
||||
READER: 'green'
|
||||
};
|
||||
|
||||
// 角色名称映射
|
||||
const roleLabels = {
|
||||
ADMIN: '系统管理员',
|
||||
LIB_ADMIN: '图书馆管理员',
|
||||
READER: '读者'
|
||||
};
|
||||
|
||||
const columns: ColumnsType<User> = [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
},
|
||||
{
|
||||
title: '年龄',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
},
|
||||
{
|
||||
title: '联系方式',
|
||||
dataIndex: 'relax',
|
||||
key: 'relax',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => (
|
||||
<Tag color={roleColors[role as keyof typeof roleColors]}>
|
||||
{roleLabels[role as keyof typeof roleLabels]}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '关联信息',
|
||||
key: 'relation',
|
||||
render: (_, record) => {
|
||||
if (record.role === 'LIB_ADMIN' && record.library) {
|
||||
return <span>管理图书馆: {record.library.name}</span>;
|
||||
} else if (record.role === 'READER' && record.reader) {
|
||||
return <span>读者ID: {record.reader.id}</span>;
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
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,43 @@
|
|||
|
||||
// apps/web/src/app/main/user/components/types.ts
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
password?: string; // 编辑时可能不需要修改密码
|
||||
gender: string;
|
||||
age: number;
|
||||
relax?: string | null;
|
||||
role: 'ADMIN' | 'LIB_ADMIN' | 'READER';
|
||||
reader?: Reader | null;
|
||||
library?: Library | null;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
}
|
||||
|
||||
export interface Reader {
|
||||
id: string;
|
||||
name: string;
|
||||
password: string;
|
||||
username: string;
|
||||
gender: string;
|
||||
age: number;
|
||||
relax: string;
|
||||
libraryId: string;
|
||||
library: Library;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
description: string;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
deletedAt?: Date | string | null;
|
||||
}
|
||||
|
|
@ -19,6 +19,14 @@ 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";
|
||||
import CommentPage from "../app/main/comment/Page";
|
||||
import UserPage from "../app/main/user/Page";
|
||||
import WithAuth from "../components/utils/with-auth";
|
||||
|
||||
|
||||
|
||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||
|
|
@ -78,6 +86,35 @@ export const routes: CustomRouteObject[] = [
|
|||
path: "/test",
|
||||
element: <TestPage></TestPage>,
|
||||
},
|
||||
{
|
||||
path: "/li/user",
|
||||
element: <UserPage></UserPage>,
|
||||
},
|
||||
{
|
||||
path: "/li/library",
|
||||
element: <WithAuth><LibraryPage></LibraryPage></WithAuth>,
|
||||
},
|
||||
{
|
||||
path: "/li/library",
|
||||
element: <LibraryPage></LibraryPage>,
|
||||
},
|
||||
{
|
||||
path: "/li/reader",
|
||||
element: <ReaderPage></ReaderPage>,
|
||||
},
|
||||
{
|
||||
path: "/li/book",
|
||||
element: <BookPage></BookPage>,
|
||||
},
|
||||
|
||||
{
|
||||
path: "/li/borrowRecord",
|
||||
element: <BorrowRecordPage></BorrowRecordPage>,
|
||||
},
|
||||
{
|
||||
path: "/li/comment",
|
||||
element: <CommentPage></CommentPage>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ server {
|
|||
# 监听80端口
|
||||
listen 80;
|
||||
# 服务器域名/IP地址,使用环境变量
|
||||
server_name 192.168.239.194;
|
||||
server_name 192.168.202.194;
|
||||
|
||||
# 基础性能优化配置
|
||||
# 启用tcp_nopush以优化数据发送
|
||||
|
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://192.168.239.194:3000/auth/file;
|
||||
proxy_pass http://192.168.202.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,169 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Character {
|
||||
ADMIN // 系统管理员
|
||||
LIB_ADMIN // 图书馆管理员
|
||||
READER // 读者
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String @map("name") // 必填:用户姓名
|
||||
username String @unique // 必填:登录用户名
|
||||
password String @map("password") // 必填:登录密码
|
||||
gender String @map("gender") // 必填:性别
|
||||
age Int @map("age") // 必填:年龄
|
||||
relax String? @map("relax") // 选填:备注信息
|
||||
role Character @default(READER) // 必填:用户角色,默认为读者
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
// 关联关系 - 只在一端指定 onDelete/onUpdate
|
||||
reader Reader? // 一对一关系
|
||||
library Library? @relation("LibraryAdmin") // 图书馆管理员关系
|
||||
|
||||
@@index([deletedAt])
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Library {
|
||||
id String @id @default(cuid())
|
||||
name String @map("name") // 必填:图书馆名称
|
||||
address String @map("address") // 必填:图书馆地址
|
||||
description String @map("description") // 必填:图书馆描述
|
||||
|
||||
// 关联关系 - 图书馆管理员
|
||||
admin User? @relation("LibraryAdmin", fields: [adminId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
adminId String? @unique @map("admin_id") // 选填:图书馆管理员ID,用户删除时置空
|
||||
|
||||
// 图书馆的读者 - 不在这一端指定 onDelete/onUpdate
|
||||
readers Reader[] @relation("LibraryReaders")
|
||||
|
||||
// 图书馆的图书 - 不在这一端指定 onDelete/onUpdate
|
||||
books Book[] @relation("LibraryBooks")
|
||||
|
||||
// 图书馆的借阅记录和评论 - 不在这一端指定 onDelete/onUpdate
|
||||
borrowRecords BorrowRecord[] @relation("LibraryBorrows")
|
||||
comments Comment[] @relation("LibraryComments")
|
||||
|
||||
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())
|
||||
|
||||
// 关联到用户表 - 在这一端指定 onDelete/onUpdate
|
||||
userId String @unique @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
// 关联到图书馆 - 在这一端指定 onDelete/onUpdate
|
||||
libraryId String? @map("library_id")
|
||||
library Library? @relation("LibraryReaders", fields: [libraryId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
|
||||
// 读者的借阅记录和评论 - 不在这一端指定 onDelete/onUpdate
|
||||
borrowRecords BorrowRecord[] @relation("ReaderBorrows")
|
||||
comments Comment[] @relation("ReaderComments")
|
||||
|
||||
// 读者的标签关系
|
||||
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") // 必填:ISBN编号
|
||||
bookName String @map("book_name") // 必填:图书名称
|
||||
author String @map("author") // 必填:作者
|
||||
|
||||
// 关联到图书馆 - 在这一端指定 onDelete/onUpdate
|
||||
libraryId String @map("library_id")
|
||||
library Library @relation("LibraryBooks", fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
// 图书的借阅记录和评论 - 不在这一端指定 onDelete/onUpdate
|
||||
borrowRecord BorrowRecord[] @relation("BookBorrows")
|
||||
comments Comment[] @relation("BookComments")
|
||||
|
||||
// 图书的标签关系
|
||||
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") // 必填:实际归还时间
|
||||
|
||||
// 关联到图书馆 - 在这一端指定 onDelete/onUpdate
|
||||
libraryId String? @map("library_id")
|
||||
library Library? @relation("LibraryBorrows", fields: [libraryId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
|
||||
// 关联到读者 - 在这一端指定 onDelete/onUpdate
|
||||
readerId String @map("read_id")
|
||||
reader Reader @relation("ReaderBorrows", fields: [readerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
// 关联到图书 - 在这一端指定 onDelete/onUpdate
|
||||
bookId String? @map("book_id")
|
||||
book Book? @relation("BookBorrows", fields: [bookId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
|
||||
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") // 必填:评论内容
|
||||
|
||||
// 关联到图书馆 - 在这一端指定 onDelete/onUpdate
|
||||
libraryId String? @map("library_id")
|
||||
library Library? @relation("LibraryComments", fields: [libraryId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
|
||||
// 关联到读者 - 在这一端指定 onDelete/onUpdate
|
||||
readerId String @map("read_id")
|
||||
reader Reader @relation("ReaderComments", fields: [readerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
||||
// 关联到图书 - 在这一端指定 onDelete/onUpdate
|
||||
bookId String? @map("book_id")
|
||||
book Book? @relation("BookComments", fields: [bookId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
|
||||
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 +208,8 @@ 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 +572,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 +611,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 +623,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 +662,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 +709,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 +729,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 +747,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,6 +1,8 @@
|
|||
|
||||
export enum SocketMsgType {
|
||||
NOTIFY,
|
||||
}
|
||||
|
||||
export enum PostType {
|
||||
POST = "post",
|
||||
POST_COMMENT = "post_comment",
|
||||
|
|
@ -62,7 +64,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