This commit is contained in:
Rao 2025-04-21 15:50:36 +08:00
commit 758678b729
122 changed files with 12504 additions and 12145 deletions

View File

@ -1,3 +1,6 @@
{
"marscode.chatLanguage": "cn"
"marscode.chatLanguage": "cn",
"marscode.codeCompletionPro": {
"enableCodeCompletionPro": true
}
}

View File

@ -23,12 +23,12 @@ import { UploadModule } from './upload/upload.module';
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用
envFilePath: '.env'
envFilePath: '.env',
}),
ScheduleModule.forRoot(),
JwtModule.register({
global: true,
secret: env.JWT_SECRET
secret: env.JWT_SECRET,
}),
WebSocketModule,
TrpcModule,
@ -42,11 +42,13 @@ import { UploadModule } from './upload/upload.module';
MinioModule,
CollaborationModule,
RealTimeModule,
UploadModule
UploadModule,
],
providers: [{
providers: [
{
provide: APP_FILTER,
useClass: ExceptionsFilter,
}],
},
],
})
export class AppModule {}

View File

@ -14,6 +14,7 @@ import {
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nice/common';
import { AuthGuard } from './auth.guard';
@ -43,8 +44,9 @@ export class AuthController {
authorization,
};
const authResult =
await this.authService.validateFileRequest(fileRequest);
const authResult = await this.authService.validateFileRequest(
fileRequest,
);
if (!authResult.isValid) {
// 使用枚举类型进行错误处理
switch (authResult.error) {

View File

@ -20,18 +20,13 @@ export class AuthGuard implements CanActivate {
throw new UnauthorizedException();
}
try {
const payload: JwtPayload = await this.jwtService.verifyAsync(
token,
{
secret: env.JWT_SECRET
}
);
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
secret: env.JWT_SECRET,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
}

View File

@ -8,11 +8,7 @@ import { SessionService } from './session.service';
import { RoleMapModule } from '@server/models/rbac/rbac.module';
@Module({
imports: [StaffModule, RoleMapModule],
providers: [
AuthService,
TrpcService,
DepartmentService,
SessionService],
providers: [AuthService, TrpcService, DepartmentService, SessionService],
exports: [AuthService],
controllers: [AuthController],
})

View File

@ -29,16 +29,21 @@ export class SessionService {
const sessionInfo: SessionInfo = {
session_id: uuidv4(),
access_token: accessToken,
access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs,
access_token_expires_at:
Date.now() + expirationConfig.accessTokenExpirationMs,
refresh_token: refreshToken,
refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs,
refresh_token_expires_at:
Date.now() + expirationConfig.refreshTokenExpirationMs,
};
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
return sessionInfo;
}
async getSession(userId: string, sessionId: string): Promise<SessionInfo | null> {
async getSession(
userId: string,
sessionId: string,
): Promise<SessionInfo | null> {
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
return sessionData ? JSON.parse(sessionData) : null;
}

View File

@ -9,10 +9,10 @@ export interface TokenConfig {
}
export interface FileAuthResult {
isValid: boolean
userId?: string
resourceType?: string
error?: string
isValid: boolean;
userId?: string;
resourceType?: string;
error?: string;
}
export interface FileRequest {
originalUri: string;
@ -20,12 +20,12 @@ export interface FileRequest {
method: string;
queryParams: string;
host: string;
authorization: string
authorization: string;
}
export enum FileValidationErrorType {
INVALID_URI = 'INVALID_URI',
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
INVALID_TOKEN = 'INVALID_TOKEN',
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}

View File

@ -11,7 +11,7 @@ import { env } from '@server/env';
import { redis } from '@server/utils/redis/redis.service';
import EventBus from '@server/utils/event-bus';
import { RoleMapService } from '@server/models/rbac/rolemap.service';
import { Request } from "express"
import { Request } from 'express';
interface ProfileResult {
staff: UserProfile | undefined;
error?: string;
@ -22,9 +22,11 @@ interface TokenVerifyResult {
error?: string;
}
export function extractTokenFromHeader(request: Request): string | undefined {
return extractTokenFromAuthorization(request.headers.authorization)
return extractTokenFromAuthorization(request.headers.authorization);
}
export function extractTokenFromAuthorization(authorization: string): string | undefined {
export function extractTokenFromAuthorization(
authorization: string,
): string | undefined {
const [type, token] = authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
@ -40,7 +42,7 @@ export class UserProfileService {
this.jwtService = new JwtService();
this.departmentService = new DepartmentService();
this.roleMapService = new RoleMapService(this.departmentService);
EventBus.on("dataChanged", ({ type, data }) => {
EventBus.on('dataChanged', ({ type, data }) => {
if (type === ObjectType.STAFF) {
// 确保 data 是数组,如果不是则转换为数组
const dataArray = Array.isArray(data) ? data : [data];
@ -51,7 +53,6 @@ export class UserProfileService {
}
}
});
}
public getProfileCacheKey(id: string) {
return `user-profile-${id}`;
@ -175,9 +176,7 @@ export class UserProfileService {
staff.deptId
? this.departmentService.getDescendantIdsInDomain(staff.deptId)
: [],
staff.deptId
? this.departmentService.getAncestorIds([staff.deptId])
: [],
staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
this.roleMapService.getPermsForObject({
domainId: staff.domainId,
staffId: staff.id,

View File

@ -1,3 +1,4 @@
export const env: { JWT_SECRET: string } = {
JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA='
}
JWT_SECRET:
process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=',
};

View File

@ -7,6 +7,6 @@ import { RealTimeModule } from '@server/socket/realtime/realtime.module';
@Module({
imports: [RealTimeModule],
providers: [AppConfigService, AppConfigRouter, TrpcService],
exports: [AppConfigService, AppConfigRouter]
exports: [AppConfigService, AppConfigRouter],
})
export class AppConfigModule {}

View File

@ -30,6 +30,7 @@ export class BaseService<
A extends DelegateArgs<D> = DelegateArgs<D>,
R extends DelegateReturnTypes<D> = DelegateReturnTypes<D>,
> {
[x: string]: any;
protected ORDER_INTERVAL = 100;
/**
* Initializes the BaseService with the specified model.
@ -152,27 +153,27 @@ export class BaseService<
* @example
* const newUser = await service.create({ data: { name: 'John Doe' } });
*/
async create(args: A['create'], params?: any): Promise<R['create']> {
try {
if (this.enableOrder && !(args as any).data.order) {
// 查找当前最大的 order 值
const maxOrderItem = (await this.getModel(params?.tx).findFirst({
orderBy: { order: 'desc' },
})) as any;
// 设置新记录的 order 值
const newOrder = maxOrderItem
? maxOrderItem.order + this.ORDER_INTERVAL
: 1;
// 将 order 添加到创建参数中
(args as any).data.order = newOrder;
}
return this.getModel(params?.tx).create(args as any) as Promise<
R['create']
>;
} catch (error) {
this.handleError(error, 'create');
}
}
// async create(args: A['create'], params?: any): Promise<R['create']> {
// try {
// if (this.enableOrder && !(args as any).data.order) {
// // 查找当前最大的 order 值
// const maxOrderItem = (await this.getModel(params?.tx).findFirst({
// orderBy: { order: 'desc' },
// })) as any;
// // 设置新记录的 order 值
// const newOrder = maxOrderItem
// ? maxOrderItem.order + this.ORDER_INTERVAL
// : 1;
// // 将 order 添加到创建参数中
// (args as any).data.order = newOrder;
// }
// return this.getModel(params?.tx).create(args as any) as Promise<
// R['create']
// >;
// } catch (error) {
// this.handleError(error, 'create');
// }
// }
/**
* Creates multiple new records with the given data.

View File

@ -39,7 +39,6 @@ import {
export type PrismaErrorCode = keyof typeof PrismaErrorCode;
interface PrismaErrorMeta {
target?: string;
model?: string;
@ -55,12 +54,18 @@ import {
) => Error;
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
P2000: (_operation, meta) => new BadRequestException(
`The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`
P2000: (_operation, meta) =>
new BadRequestException(
`The provided value for ${
meta?.target || 'a field'
} is too long. Please use a shorter value.`,
),
P2001: (operation, meta) => new NotFoundException(
`The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`
P2001: (operation, meta) =>
new NotFoundException(
`The ${
meta?.model || 'record'
} you are trying to ${operation} could not be found.`,
),
P2002: (operation, meta) => {
@ -68,131 +73,164 @@ import {
switch (operation) {
case 'create':
return new ConflictException(
`A record with the same ${field} already exists. Please use a different value.`
`A record with the same ${field} already exists. Please use a different value.`,
);
case 'update':
return new ConflictException(
`The new value for ${field} conflicts with an existing record.`
`The new value for ${field} conflicts with an existing record.`,
);
default:
return new ConflictException(
`Unique constraint violation on ${field}.`
`Unique constraint violation on ${field}.`,
);
}
},
P2003: (operation) => new BadRequestException(
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`
P2003: (operation) =>
new BadRequestException(
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`,
),
P2006: (_operation, meta) => new BadRequestException(
`The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`
P2006: (_operation, meta) =>
new BadRequestException(
`The provided value for ${
meta?.target || 'a field'
} is invalid. Please correct it.`,
),
P2007: (operation) => new InternalServerErrorException(
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`
P2007: (operation) =>
new InternalServerErrorException(
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`,
),
P2008: (operation) => new InternalServerErrorException(
`Failed to query the database during ${operation}. Please try again later.`
P2008: (operation) =>
new InternalServerErrorException(
`Failed to query the database during ${operation}. Please try again later.`,
),
P2009: (operation) => new InternalServerErrorException(
`Invalid data fetched during ${operation}. Check query structure.`
P2009: (operation) =>
new InternalServerErrorException(
`Invalid data fetched during ${operation}. Check query structure.`,
),
P2010: () => new InternalServerErrorException(
`Invalid raw query. Ensure your query is correct and try again.`
P2010: () =>
new InternalServerErrorException(
`Invalid raw query. Ensure your query is correct and try again.`,
),
P2011: (_operation, meta) => new BadRequestException(
`The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`
P2011: (_operation, meta) =>
new BadRequestException(
`The required field ${
meta?.target || 'a field'
} is missing. Please provide it to continue.`,
),
P2012: (operation, meta) => new BadRequestException(
`Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`
P2012: (operation, meta) =>
new BadRequestException(
`Missing required relation ${
meta?.relationName || ''
}. Ensure all related data exists before ${operation}.`,
),
P2014: (operation) => {
switch (operation) {
case 'create':
return new BadRequestException(
`Cannot create record because the referenced data does not exist. Ensure related data exists.`
`Cannot create record because the referenced data does not exist. Ensure related data exists.`,
);
case 'delete':
return new BadRequestException(
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`,
);
default:
return new BadRequestException(`Foreign key constraint error.`);
}
},
P2015: () => new InternalServerErrorException(
`A record with the required ID was expected but not found. Please retry.`
P2015: () =>
new InternalServerErrorException(
`A record with the required ID was expected but not found. Please retry.`,
),
P2016: (operation) => new InternalServerErrorException(
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`
P2016: (operation) =>
new InternalServerErrorException(
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`,
),
P2017: (operation) => new InternalServerErrorException(
`Connected records were not found for ${operation}. Check related data.`
P2017: (operation) =>
new InternalServerErrorException(
`Connected records were not found for ${operation}. Check related data.`,
),
P2018: () => new InternalServerErrorException(
`The required connection could not be established. Please check relationships.`
P2018: () =>
new InternalServerErrorException(
`The required connection could not be established. Please check relationships.`,
),
P2019: (_operation, meta) => new InternalServerErrorException(
`Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`
P2019: (_operation, meta) =>
new InternalServerErrorException(
`Invalid input for ${
meta?.details || 'a field'
}. Please ensure data conforms to expectations.`,
),
P2021: (_operation, meta) => new InternalServerErrorException(
`The ${meta?.model || 'model'} was not found in the database.`
P2021: (_operation, meta) =>
new InternalServerErrorException(
`The ${meta?.model || 'model'} was not found in the database.`,
),
P2025: (operation, meta) => new NotFoundException(
`The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`
P2025: (operation, meta) =>
new NotFoundException(
`The ${
meta?.model || 'record'
} you are trying to ${operation} does not exist. It may have been deleted.`,
),
P2031: () => new InternalServerErrorException(
`Invalid Prisma Client initialization error. Please check configuration.`
P2031: () =>
new InternalServerErrorException(
`Invalid Prisma Client initialization error. Please check configuration.`,
),
P2033: (operation) => new InternalServerErrorException(
`Insufficient database write permissions for ${operation}.`
P2033: (operation) =>
new InternalServerErrorException(
`Insufficient database write permissions for ${operation}.`,
),
P2034: (operation) => new InternalServerErrorException(
`Database read-only transaction failed during ${operation}.`
P2034: (operation) =>
new InternalServerErrorException(
`Database read-only transaction failed during ${operation}.`,
),
P2037: (operation) => new InternalServerErrorException(
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`
P2037: (operation) =>
new InternalServerErrorException(
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`,
),
P1000: () => new InternalServerErrorException(
`Database authentication failed. Verify your credentials and try again.`
P1000: () =>
new InternalServerErrorException(
`Database authentication failed. Verify your credentials and try again.`,
),
P1001: () => new InternalServerErrorException(
`The database server could not be reached. Please check its availability.`
P1001: () =>
new InternalServerErrorException(
`The database server could not be reached. Please check its availability.`,
),
P1002: () => new InternalServerErrorException(
`Connection to the database timed out. Verify network connectivity and server availability.`
P1002: () =>
new InternalServerErrorException(
`Connection to the database timed out. Verify network connectivity and server availability.`,
),
P1015: (operation) => new InternalServerErrorException(
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`
P1015: (operation) =>
new InternalServerErrorException(
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`,
),
P1017: () => new InternalServerErrorException(
`Database connection failed. Ensure the database is online and credentials are correct.`
P1017: () =>
new InternalServerErrorException(
`Database connection failed. Ensure the database is online and credentials are correct.`,
),
P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
throw new Error('Function not implemented.');
}
},
};

View File

@ -1,25 +1,28 @@
import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common";
import { RowModelService } from "./row-model.service";
import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder";
import EventBus from "@server/utils/event-bus";
import supejson from "superjson-cjs"
import { deleteByPattern } from "@server/utils/redis/utils";
import { redis } from "@server/utils/redis/redis.service";
import { z } from "zod";
import { UserProfile, RowModelRequest, RowRequestSchema } from '@nice/common';
import { RowModelService } from './row-model.service';
import { isFieldCondition, LogicalCondition, SQLBuilder } from './sql-builder';
import EventBus from '@server/utils/event-bus';
import supejson from 'superjson-cjs';
import { deleteByPattern } from '@server/utils/redis/utils';
import { redis } from '@server/utils/redis/redis.service';
import { z } from 'zod';
export class RowCacheService extends RowModelService {
constructor(tableName: string, private enableCache: boolean = true) {
super(tableName)
constructor(
tableName: string,
private enableCache: boolean = true,
) {
super(tableName);
if (this.enableCache) {
EventBus.on("dataChanged", async ({ type, data }) => {
EventBus.on('dataChanged', async ({ type, data }) => {
if (type === tableName) {
const dataArray = Array.isArray(data) ? data : [data];
for (const item of dataArray) {
try {
if (item.id) {
this.invalidateRowCacheById(item.id)
this.invalidateRowCacheById(item.id);
}
if (item.parentId) {
this.invalidateRowCacheById(item.parentId)
this.invalidateRowCacheById(item.parentId);
}
} catch (err) {
console.error(`Error deleting cache for type ${tableName}:`, err);
@ -38,21 +41,15 @@ export class RowCacheService extends RowModelService {
await deleteByPattern(pattern);
}
createJoinSql(request?: RowModelRequest): string[] {
return []
return [];
}
protected async getRowRelation(args: { data: any, staff?: UserProfile }) {
protected async getRowRelation(args: { data: any; staff?: UserProfile }) {
return args.data;
}
protected async setResPermissions(
data: any,
staff?: UserProfile,
) {
return data
protected async setResPermissions(data: any, staff?: UserProfile) {
return data;
}
protected async getRowDto(
data: any,
staff?: UserProfile,
): Promise<any> {
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
// 如果没有id直接返回原数据
if (!data?.id) return data;
// 如果未启用缓存,直接处理并返回数据
@ -77,25 +74,21 @@ export class RowCacheService extends RowModelService {
private async getCachedData(
key: string,
staff?: UserProfile
staff?: UserProfile,
): Promise<any | null> {
const cachedDataStr = await redis.get(key);
if (!cachedDataStr) return null;
const cachedData = supejson.parse(cachedDataStr) as any;
if (!cachedData?.id) return null;
return staff
? this.setResPermissions(cachedData, staff)
: cachedData;
return staff ? this.setResPermissions(cachedData, staff) : cachedData;
}
private async processDataWithPermissions(
data: any,
staff?: UserProfile
staff?: UserProfile,
): Promise<any> {
// 处理权限
const permData = staff
? await this.setResPermissions(data, staff)
: data;
const permData = staff ? await this.setResPermissions(data, staff) : data;
// 获取关联数据
return this.getRowRelation({ data: permData, staff });
}

View File

@ -160,7 +160,10 @@ export abstract class RowModelService {
const { rowGroupCols, valueCols, groupKeys } = request;
return valueCols.map(
(valueCol) =>
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`,
`${valueCol.aggFunc}(${valueCol.field.replace(
'.',
'_',
)}) AS ${valueCol.field.split('.').join('_')}`,
);
}
protected createGroupingRowSelect(
@ -179,7 +182,9 @@ export abstract class RowModelService {
colsToSelect.push(
...valueCols.map(
(valueCol) =>
`${wrapperSql ? '' : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
`${wrapperSql ? '' : valueCol.aggFunc}(${
valueCol.field
}) AS ${valueCol.field.replace('.', '_')}`,
),
);
@ -286,7 +291,10 @@ export abstract class RowModelService {
protected buildAggSelect(valueCols: any[]): string[] {
return valueCols.map(
(valueCol) =>
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace(
'.',
'_',
)}`,
);
}

View File

@ -1,21 +1,38 @@
export interface FieldCondition {
field: string;
op: OperatorType
type?: "text" | "number" | "date";
op: OperatorType;
type?: 'text' | 'number' | 'date';
value?: any;
valueTo?: any;
};
export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in';
export type LogicalCondition = FieldCondition | {
}
export type OperatorType =
| 'equals'
| 'notEqual'
| 'contains'
| 'startsWith'
| 'endsWith'
| 'blank'
| 'notBlank'
| 'greaterThan'
| 'lessThanOrEqual'
| 'inRange'
| 'lessThan'
| 'greaterThan'
| 'in';
export type LogicalCondition =
| FieldCondition
| {
AND?: LogicalCondition[];
OR?: LogicalCondition[];
};
export function isFieldCondition(condition: LogicalCondition): condition is FieldCondition {
export function isFieldCondition(
condition: LogicalCondition,
): condition is FieldCondition {
return (condition as FieldCondition).field !== undefined;
}
function buildCondition(condition: FieldCondition): string {
const { field, op, value, type = "text", valueTo } = condition;
const { field, op, value, type = 'text', valueTo } = condition;
switch (op) {
case 'equals':
return `${field} = '${value}'`;
@ -28,15 +45,11 @@ function buildCondition(condition: FieldCondition): string {
case 'endsWith':
return `${field} LIKE '%${value}'`;
case 'blank':
if (type !== "date")
return `(${field} IS NULL OR ${field} = '')`;
else
return `${field} IS NULL`;
if (type !== 'date') return `(${field} IS NULL OR ${field} = '')`;
else return `${field} IS NULL`;
case 'notBlank':
if (type !== 'date')
return `${field} IS NOT NULL AND ${field} != ''`;
else
return `${field} IS NOT NULL`;
if (type !== 'date') return `${field} IS NOT NULL AND ${field} != ''`;
else return `${field} IS NOT NULL`;
case 'greaterThan':
return `${field} > '${value}'`;
case 'lessThanOrEqual':
@ -52,7 +65,9 @@ function buildCondition(condition: FieldCondition): string {
// Return a condition that is always false if value is empty or an empty array
return '1 = 0';
}
return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`;
return `${field} IN (${(value as any[])
.map((val) => `'${val}'`)
.join(', ')})`;
default:
return 'true'; // Default return for unmatched conditions
}
@ -63,18 +78,18 @@ function buildLogicalCondition(logicalCondition: LogicalCondition): string {
}
const parts: string[] = [];
if (logicalCondition.AND && logicalCondition.AND.length > 0) {
const andParts = logicalCondition.AND
.map(c => buildLogicalCondition(c))
.filter(part => part !== ''); // Filter out empty conditions
const andParts = logicalCondition.AND.map((c) =>
buildLogicalCondition(c),
).filter((part) => part !== ''); // Filter out empty conditions
if (andParts.length > 0) {
parts.push(`(${andParts.join(' AND ')})`);
}
}
// Process OR conditions
if (logicalCondition.OR && logicalCondition.OR.length > 0) {
const orParts = logicalCondition.OR
.map(c => buildLogicalCondition(c))
.filter(part => part !== ''); // Filter out empty conditions
const orParts = logicalCondition.OR.map((c) =>
buildLogicalCondition(c),
).filter((part) => part !== ''); // Filter out empty conditions
if (orParts.length > 0) {
parts.push(`(${orParts.join(' OR ')})`);
}
@ -85,12 +100,18 @@ function buildLogicalCondition(logicalCondition: LogicalCondition): string {
export class SQLBuilder {
static select(fields: string[], distinctField?: string): string {
const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : "";
return `SELECT ${distinctClause}${fields.join(", ")}`;
const distinctClause = distinctField
? `DISTINCT ON (${distinctField}) `
: '';
return `SELECT ${distinctClause}${fields.join(', ')}`;
}
static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string {
static rowNumber(
orderBy: string,
partitionBy: string | null = null,
alias: string = 'row_num',
): string {
if (!orderBy) {
throw new Error("orderBy 参数不能为空");
throw new Error('orderBy 参数不能为空');
}
let partitionClause = '';
@ -106,15 +127,15 @@ export class SQLBuilder {
static where(conditions: LogicalCondition): string {
const whereClause = buildLogicalCondition(conditions);
return whereClause ? `WHERE ${whereClause}` : "";
return whereClause ? `WHERE ${whereClause}` : '';
}
static groupBy(columns: string[]): string {
return columns.length ? `GROUP BY ${columns.join(", ")}` : "";
return columns.length ? `GROUP BY ${columns.join(', ')}` : '';
}
static orderBy(columns: string[]): string {
return columns.length ? `ORDER BY ${columns.join(", ")}` : "";
return columns.length ? `ORDER BY ${columns.join(', ')}` : '';
}
static limit(pageSize: number, offset: number = 0): string {
@ -125,14 +146,27 @@ export class SQLBuilder {
return clauses.filter(Boolean).join(' ');
}
static createFilterSql(key: string, item: any): LogicalCondition {
const conditionFuncs: Record<string, (item: { values?: any[], dateFrom?: string, dateTo?: string, filter: any, type: OperatorType, filterType: OperatorType }) => LogicalCondition> = {
const conditionFuncs: Record<
string,
(item: {
values?: any[];
dateFrom?: string;
dateTo?: string;
filter: any;
type: OperatorType;
filterType: OperatorType;
}) => LogicalCondition
> = {
text: (item) => ({ value: item.filter, op: item.type, field: key }),
number: (item) => ({ value: item.filter, op: item.type, field: key }),
date: (item) => ({ value: item.dateFrom, valueTo: item.dateTo, op: item.type, field: key }),
set: (item) => ({ value: item.values, op: "in", field: key })
}
return conditionFuncs[item.filterType](item)
date: (item) => ({
value: item.dateFrom,
valueTo: item.dateTo,
op: item.type,
field: key,
}),
set: (item) => ({ value: item.values, op: 'in', field: key }),
};
return conditionFuncs[item.filterType](item);
}
}

View File

@ -1,6 +1,6 @@
import { Controller, UseGuards } from "@nestjs/common";
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard';
import { DailyTrainService } from "./dailyTrain.service";
import { DailyTrainService } from './dailyTrain.service';
@Controller('train-content')
export class DailyTrainController {

View File

@ -5,7 +5,6 @@ import { DailyTrainController } from './dailyTrain.controller';
import { DailyTrainService } from './dailyTrain.service';
import { DailyTrainRouter } from './dailyTrain.router';
@Module({
imports: [StaffModule],
controllers: [DailyTrainController],

View File

@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { DailyTrainService } from "./dailyTrain.service";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { DailyTrainService } from './dailyTrain.service';
@Injectable()
export class DailyTrainRouter {
@ -9,8 +9,5 @@ export class DailyTrainRouter {
private readonly dailyTrainService: DailyTrainService,
) {}
router = this.trpc.router({
})
router = this.trpc.router({});
}

View File

@ -1,9 +1,8 @@
import { Injectable } from "@nestjs/common";
import { BaseService } from "../base/base.service";
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
import { DefaultArgs } from "@prisma/client/runtime/library";
import EventBus, { CrudOperation } from "@server/utils/event-bus";
import { Injectable } from '@nestjs/common';
import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable()
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
@ -11,25 +10,23 @@ export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate
super(db, ObjectType.DAILY_TRAIN, true);
}
async create(args: Prisma.DailyTrainTimeCreateArgs) {
console.log(args)
const result = await super.create(args)
this.emitDataChanged(CrudOperation.CREATED,result)
return result
console.log(args);
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
const result = await super.update(args)
this.emitDataChanged(CrudOperation.UPDATED,result)
return result
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
const result = await super.findMany(args);
return result;
}
private emitDataChanged(operation: CrudOperation, data: any) {
EventBus.emit('dataChanged', {
type: ObjectType.DAILY_TRAIN,

View File

@ -6,7 +6,12 @@ import { DepartmentController } from './department.controller';
import { DepartmentRowService } from './department.row.service';
@Module({
providers: [DepartmentService, DepartmentRouter, DepartmentRowService, TrpcService],
providers: [
DepartmentService,
DepartmentRouter,
DepartmentRowService,
TrpcService,
],
exports: [DepartmentService, DepartmentRouter],
controllers: [DepartmentController],
})

View File

@ -27,7 +27,7 @@ export class MessageController {
select: {
title: true,
content: true,
url: true
url: true,
},
});
@ -53,7 +53,7 @@ export class MessageController {
visits: {
none: {
id: staffId,
type: VisitType.READED
type: VisitType.READED,
},
},
receivers: {
@ -92,7 +92,7 @@ export class MessageController {
visits: {
none: {
id: staffId,
type: VisitType.READED
type: VisitType.READED,
},
},
receivers: {

View File

@ -3,9 +3,10 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { MessageService } from './message.service';
import { Prisma } from '@nice/common';
import { z, ZodType } from 'zod';
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any()
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any()
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any()
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> =
z.any();
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any();
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any();
@Injectable()
export class MessageRouter {
constructor(
@ -20,20 +21,21 @@ export class MessageRouter {
return await this.messageService.create({ data: input }, { staff });
}),
findManyWithCursor: this.trpc.protectProcedure
.input(z.object({
.input(
z.object({
cursor: z.any().nullish(),
take: z.number().nullish(),
where: MessageWhereInputSchema.nullish(),
select: MessageSelectSchema.nullish()
}))
select: MessageSelectSchema.nullish(),
}),
)
.query(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.messageService.findManyWithCursor(input, staff);
}),
getUnreadCount: this.trpc.protectProcedure
.query(async ({ ctx }) => {
getUnreadCount: this.trpc.protectProcedure.query(async ({ ctx }) => {
const { staff } = ctx;
return await this.messageService.getUnreadCount(staff);
})
})
}),
});
}

View File

@ -8,26 +8,28 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
constructor() {
super(db, ObjectType.MESSAGE);
}
async create(args: Prisma.MessageCreateArgs, params?: { tx?: Prisma.MessageDelegate, staff?: UserProfile }) {
async create(
args: Prisma.MessageCreateArgs,
params?: { tx?: Prisma.MessageDelegate; staff?: UserProfile },
) {
args.data!.senderId = params?.staff?.id;
args.include = {
receivers: {
select: { id: true, registerToken: true, username: true }
}
}
select: { id: true, registerToken: true, username: true },
},
};
const result = await super.create(args);
EventBus.emit("dataChanged", {
EventBus.emit('dataChanged', {
type: ObjectType.MESSAGE,
operation: CrudOperation.CREATED,
data: result
})
return result
data: result,
});
return result;
}
async findManyWithCursor(
args: Prisma.MessageFindManyArgs,
staff?: UserProfile,
) {
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
let { items } = result;
await Promise.all(
@ -46,12 +48,12 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
visits: {
none: {
visitorId: staff?.id,
type: VisitType.READED
}
}
}
})
type: VisitType.READED,
},
},
},
});
return count
return count;
}
}

View File

@ -1,9 +1,8 @@
import { Message, UserProfile, VisitType, db } from "@nice/common"
import { Message, UserProfile, VisitType, db } from '@nice/common';
export async function setMessageRelation(
data: Message,
staff?: UserProfile,
): Promise<any> {
const readed =
(await db.visit.count({
where: {
@ -13,8 +12,7 @@ export async function setMessageRelation(
},
})) > 0;
Object.assign(data, {
readed
})
readed,
});
}

View File

@ -1,6 +1,6 @@
import { Controller, UseGuards } from "@nestjs/common";
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard';
import { DailyTrainService } from "./dailyTrain.service";
import { DailyTrainService } from './dailyTrain.service';
@Controller('train-content')
export class DailyTrainController {

View File

@ -5,7 +5,6 @@ import { DailyTrainController } from './dailyTrain.controller';
import { DailyTrainService } from './dailyTrain.service';
import { DailyTrainRouter } from './dailyTrain.router';
@Module({
imports: [StaffModule],
controllers: [DailyTrainController],

View File

@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { DailyTrainService } from "./dailyTrain.service";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { DailyTrainService } from './dailyTrain.service';
@Injectable()
export class DailyTrainRouter {
@ -9,8 +9,5 @@ export class DailyTrainRouter {
private readonly dailyTrainService: DailyTrainService,
) {}
router = this.trpc.router({
})
router = this.trpc.router({});
}

View File

@ -1,9 +1,8 @@
import { Injectable } from "@nestjs/common";
import { BaseService } from "../base/base.service";
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
import { DefaultArgs } from "@prisma/client/runtime/library";
import EventBus, { CrudOperation } from "@server/utils/event-bus";
import { Injectable } from '@nestjs/common';
import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable()
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
@ -11,25 +10,23 @@ export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate
super(db, ObjectType.DAILY_TRAIN, true);
}
async create(args: Prisma.DailyTrainTimeCreateArgs) {
console.log(args)
const result = await super.create(args)
this.emitDataChanged(CrudOperation.CREATED,result)
return result
console.log(args);
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
const result = await super.update(args)
this.emitDataChanged(CrudOperation.UPDATED,result)
return result
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
const result = await super.findMany(args);
return result;
}
private emitDataChanged(operation: CrudOperation, data: any) {
EventBus.emit('dataChanged', {
type: ObjectType.DAILY_TRAIN,

View File

@ -8,7 +8,13 @@ import { DepartmentModule } from '../department/department.module';
@Module({
imports: [DepartmentModule],
providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter],
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter]
providers: [
RoleMapService,
RoleRouter,
TrpcService,
RoleService,
RoleMapRouter,
],
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter],
})
export class RoleMapModule {}

View File

@ -3,14 +3,14 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nice/common';
import { RoleService } from './role.service';
import { z, ZodType } from 'zod';
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any()
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any()
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any()
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any()
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any()
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any()
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any()
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any()
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any();
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any();
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any();
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any();
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any();
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any();
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any();
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any();
const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any();
@Injectable()
export class RoleRouter {
@ -31,7 +31,8 @@ export class RoleRouter {
const { staff } = ctx;
return await this.roleService.update(input, staff);
}),
createMany: this.trpc.protectProcedure.input(z.array(RoleCreateManyInputSchema))
createMany: this.trpc.protectProcedure
.input(z.array(RoleCreateManyInputSchema))
.mutation(async ({ ctx, input }) => {
const { staff } = ctx;
@ -41,7 +42,7 @@ export class RoleRouter {
.input(
z.object({
ids: z.array(z.string()),
data: RoleUpdateInputSchema.optional()
data: RoleUpdateInputSchema.optional(),
}),
)
.mutation(async ({ input }) => {
@ -64,23 +65,27 @@ export class RoleRouter {
return await this.roleService.findMany(input);
}),
findManyWithCursor: this.trpc.protectProcedure
.input(z.object({
.input(
z.object({
cursor: z.any().nullish(),
take: z.number().optional(),
where: RoleWhereInputSchema.optional(),
select: RoleSelectSchema.optional()
}))
select: RoleSelectSchema.optional(),
}),
)
.query(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.roleService.findManyWithCursor(input);
}),
findManyWithPagination: this.trpc.procedure
.input(z.object({
.input(
z.object({
page: z.number(),
pageSize: z.number().optional(),
where: RoleWhereInputSchema.optional(),
select: RoleSelectSchema.optional()
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
select: RoleSelectSchema.optional(),
}),
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => {
return await this.roleService.findManyWithPagination(input);
}),

View File

@ -1,45 +1,57 @@
import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common";
import { RowCacheService } from "../base/row-cache.service";
import { isFieldCondition, LogicalCondition } from "../base/sql-builder";
import { z } from "zod";
import {
db,
ObjectType,
RowModelRequest,
RowRequestSchema,
UserProfile,
} from '@nice/common';
import { RowCacheService } from '../base/row-cache.service';
import { isFieldCondition, LogicalCondition } from '../base/sql-builder';
import { z } from 'zod';
export class RoleRowService extends RowCacheService {
protected createGetRowsFilters(
request: z.infer<typeof RowRequestSchema>,
staff?: UserProfile
staff?: UserProfile,
) {
const condition = super.createGetRowsFilters(request)
if (isFieldCondition(condition))
return {}
const baseModelCondition: LogicalCondition[] = [{
const condition = super.createGetRowsFilters(request);
if (isFieldCondition(condition)) return {};
const baseModelCondition: LogicalCondition[] = [
{
field: `${this.tableName}.deleted_at`,
op: "blank",
type: "date"
}]
condition.AND = [...baseModelCondition, ...condition.AND!]
return condition
op: 'blank',
type: 'date',
},
];
condition.AND = [...baseModelCondition, ...condition.AND!];
return condition;
}
createUnGroupingRowSelect(): string[] {
return [
`${this.tableName}.id AS id`,
`${this.tableName}.name AS name`,
`${this.tableName}.system AS system`,
`${this.tableName}.permissions AS permissions`
`${this.tableName}.permissions AS permissions`,
];
}
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
if (!data.id)
return data
if (!data.id) return data;
const roleMaps = await db.roleMap.findMany({
where: {
roleId: data.id
}
})
const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId)
const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).map(roleMap => roleMap.objectId)
const depts = await db.department.findMany({ where: { id: { in: deptIds } } })
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } })
const result = { ...data, depts, staffs }
return result
roleId: data.id,
},
});
const deptIds = roleMaps
.filter((item) => item.objectType === ObjectType.DEPARTMENT)
.map((roleMap) => roleMap.objectId);
const staffIds = roleMaps
.filter((item) => item.objectType === ObjectType.STAFF)
.map((roleMap) => roleMap.objectId);
const depts = await db.department.findMany({
where: { id: { in: deptIds } },
});
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } });
const result = { ...data, depts, staffs };
return result;
}
createJoinSql(request?: RowModelRequest): string[] {
return [];

View File

@ -1,9 +1,6 @@
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import {
ObjectType,
RoleMapMethodSchema,
} from '@nice/common';
import { ObjectType, RoleMapMethodSchema } from '@nice/common';
import { RoleMapService } from './rolemap.service';
@Injectable()

View File

@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService {
return condition;
}
protected async getRowDto(
row: any,
staff?: UserProfile,
): Promise<any> {
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
if (!row.id) return row;
return row;
}
@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService {
data: roleMaps,
});
});
const wrapResult = Promise.all(result.map(async item => {
const wrapResult = Promise.all(
result.map(async (item) => {
const staff = await db.staff.findMany({
include: { department: true },
where: {
id: item.objectId
}
})
return { ...item, staff }
}))
id: item.objectId,
},
});
return { ...item, staff };
}),
);
return wrapResult;
}
async addRoleForObjects(
@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService {
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
return { items, totalCount };
}
async getStaffsNotMap(data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>) {
async getStaffsNotMap(
data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>,
) {
const { domainId, roleId } = data;
let staffs = await db.staff.findMany({
where: {
@ -300,7 +301,9 @@ export class RoleMapService extends RowModelService {
* @param data ID和域ID的数据
* @returns ID和员工ID列表
*/
async getRoleMapDetail(data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>) {
async getRoleMapDetail(
data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>,
) {
const { roleId, domainId } = data;
const res = await db.roleMap.findMany({ where: { roleId, domainId } });

View File

@ -1,23 +1,24 @@
import path, { dirname } from "path";
import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types";
import { Resource, ResourceStatus, db } from "@nice/common";
import { Logger } from "@nestjs/common";
import path, { dirname } from 'path';
import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types';
import { Resource, ResourceStatus, db } from '@nice/common';
import { Logger } from '@nestjs/common';
import fs from 'fs/promises';
export abstract class BaseProcessor implements ResourceProcessor {
constructor() {}
protected logger = new Logger(BaseProcessor.name)
protected logger = new Logger(BaseProcessor.name);
abstract process(resource: Resource): Promise<Resource>
protected createOutputDir(filepath: string, subdirectory: string = 'assets'): string {
const outputDir = path.join(
path.dirname(filepath),
subdirectory,
abstract process(resource: Resource): Promise<Resource>;
protected createOutputDir(
filepath: string,
subdirectory: string = 'assets',
): string {
const outputDir = path.join(path.dirname(filepath), subdirectory);
fs.mkdir(outputDir, { recursive: true }).catch((err) =>
this.logger.error(`Failed to create directory: ${err.message}`),
);
fs.mkdir(outputDir, { recursive: true }).catch(err => this.logger.error(`Failed to create directory: ${err.message}`));
return outputDir;
}
}
//

View File

@ -1,20 +1,20 @@
import { Resource } from "@nice/common";
import { Resource } from '@nice/common';
export interface ResourceProcessor {
process(resource: Resource): Promise<any>
process(resource: Resource): Promise<any>;
}
export interface ProcessResult {
success: boolean
resource: Resource
error?: Error
success: boolean;
resource: Resource;
error?: Error;
}
export interface BaseMetadata {
size: number
filetype: string
filename: string
extension: string
modifiedAt: Date
size: number;
filetype: string;
filename: string;
extension: string;
modifiedAt: Date;
}
/**
*
@ -37,7 +37,7 @@ export interface VideoMetadata {
duration?: number;
videoCodec?: string;
audioCodec?: string;
coverUrl?: string
coverUrl?: string;
}
/**
@ -51,5 +51,7 @@ export interface AudioMetadata {
codec?: string; // 音频编码格式
}
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
export type FileMetadata = ImageMetadata &
VideoMetadata &
AudioMetadata &
BaseMetadata;

View File

@ -1,6 +1,6 @@
import { Controller, UseGuards } from "@nestjs/common";
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard';
import { sportProjectService } from "./sportProject.service";
import { sportProjectService } from './sportProject.service';
@Controller('sportProject')
export class sportProjectController {

View File

@ -1,13 +1,16 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { sportProjectService } from "./sportProject.service";
import { z, ZodType } from "zod";
import { Prisma } from "@nice/common";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { sportProjectService } from './sportProject.service';
import { z, ZodType } from 'zod';
import { Prisma } from '@nice/common';
const SportProjectArgsSchema:ZodType<Prisma.SportProjectCreateArgs> = z.any()
const SportProjectUpdateArgsSchema:ZodType<Prisma.SportProjectUpdateArgs> = z.any()
const SportProjectFindManyArgsSchema:ZodType<Prisma.SportProjectFindManyArgs> = z.any()
const SportProjectFindFirstArgsSchema:ZodType<Prisma.SportProjectFindFirstArgs> = z.any()
const SportProjectArgsSchema: ZodType<Prisma.SportProjectCreateArgs> = z.any();
const SportProjectUpdateArgsSchema: ZodType<Prisma.SportProjectUpdateArgs> =
z.any();
const SportProjectFindManyArgsSchema: ZodType<Prisma.SportProjectFindManyArgs> =
z.any();
const SportProjectFindFirstArgsSchema: ZodType<Prisma.SportProjectFindFirstArgs> =
z.any();
@Injectable()
export class SportProjectRouter {
constructor(
@ -16,27 +19,31 @@ export class SportProjectRouter {
) {}
router = this.trpc.router({
create:this.trpc.procedure.input(SportProjectArgsSchema)
create: this.trpc.procedure
.input(SportProjectArgsSchema)
.mutation(async ({ input }) => {
console.log(input)
return this.sportProjectService.create(input)
console.log(input);
return this.sportProjectService.create(input);
}),
update:this.trpc.procedure.input(SportProjectUpdateArgsSchema)
update: this.trpc.procedure
.input(SportProjectUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.sportProjectService.update(input)
return this.sportProjectService.update(input);
}),
findMany:this.trpc.procedure.input(SportProjectFindManyArgsSchema)
findMany: this.trpc.procedure
.input(SportProjectFindManyArgsSchema)
.query(async ({ input }) => {
return this.sportProjectService.findMany(input)
return this.sportProjectService.findMany(input);
}),
softDeleteByIds:this.trpc.procedure.input(z.object({ ids: z.array(z.string()) }))
softDeleteByIds: this.trpc.procedure
.input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => {
return this.sportProjectService.softDeleteByIds(input.ids)
return this.sportProjectService.softDeleteByIds(input.ids);
}),
findFirst:this.trpc.procedure.input(SportProjectFindFirstArgsSchema)
findFirst: this.trpc.procedure
.input(SportProjectFindFirstArgsSchema)
.query(async ({ input }) => {
return this.sportProjectService.findFirst(input)
return this.sportProjectService.findFirst(input);
}),
})
});
}

View File

@ -1,8 +1,8 @@
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";
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';
interface AgeRange {
start: number | null;
@ -14,7 +14,7 @@ interface ScoreStandard {
ageRanges: AgeRange[];
scoreTable: {
[score: string]: number[];
}
};
}
@Injectable()
@ -23,30 +23,29 @@ export class sportProjectService extends BaseService<Prisma.SportProjectDelegate
super(db, ObjectType.SPORT_PROJECT, false);
}
async create(args: Prisma.SportProjectCreateArgs) {
const result = await super.create(args)
this.emitDataChanged(CrudOperation.CREATED,result)
return result
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async update(args: Prisma.SportProjectUpdateArgs) {
const result = await super.update(args)
this.emitDataChanged(CrudOperation.UPDATED,result)
return result
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
async findMany(args: Prisma.SportProjectFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findFirst(args: Prisma.SportProjectFindFirstArgs) {
const result = await super.findFirst(args)
return result
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
this.emitDataChanged(CrudOperation.DELETED, result);
return result;
}
private emitDataChanged(operation: CrudOperation, data: any) {

View File

@ -1,6 +1,6 @@
import { Controller, UseGuards } from "@nestjs/common";
import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard';
import { SportStandardService } from "./sportStandard.service";
import { SportStandardService } from './sportStandard.service';
@Controller('sportStandard')
export class SportStandardController {

View File

@ -1,21 +1,26 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { SportStandardService } from "./sportStandard.service";
import { z, ZodType } from "zod";
import { Prisma } from "@nice/common";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { SportStandardService } from './sportStandard.service';
import { z, ZodType } from 'zod';
import { Prisma } from '@nice/common';
const SportStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> = z.any()
const SportStandardUpdateArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> = z.any()
const SportStandardFindManyArgsSchema: ZodType<Prisma.SportStandardFindManyArgs> = z.any()
const SportStandardCreateStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> = z.any()
const SportStandardUpdateStandardArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> = z.any()
const SportStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> =
z.any();
const SportStandardUpdateArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> =
z.any();
const SportStandardFindManyArgsSchema: ZodType<Prisma.SportStandardFindManyArgs> =
z.any();
const SportStandardCreateStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> =
z.any();
const SportStandardUpdateStandardArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> =
z.any();
const GetScoreArgsSchema = z.object({
projectId: z.string().nonempty(),
gender: z.boolean(),
age: z.number().min(0),
performance: z.number().min(0).or(z.string()),
personType: z.string().nonempty(),
projectUnit: z.string().nonempty()
projectUnit: z.string().nonempty(),
});
interface AgeRange {
@ -39,40 +44,49 @@ export class SportStandardRouter {
// .mutation(async ({input})=>{
// return this.sportStandardService.create(input)
// }),
update: this.trpc.procedure.input(SportStandardUpdateArgsSchema)
update: this.trpc.procedure
.input(SportStandardUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.sportStandardService.update(input)
return this.sportStandardService.update(input);
}),
findMany: this.trpc.procedure.input(SportStandardFindManyArgsSchema)
findMany: this.trpc.procedure
.input(SportStandardFindManyArgsSchema)
.query(async ({ input }) => {
return this.sportStandardService.findMany(input)
return this.sportStandardService.findMany(input);
}),
createStandard: this.trpc.procedure.input(SportStandardCreateStandardArgsSchema)
createStandard: this.trpc.procedure
.input(SportStandardCreateStandardArgsSchema)
.mutation(async ({ input }) => {
const data = {
projectId: input.data.projectId,
gender: input.data.gender,
personType: input.data.personType,
ageRanges: input.data.ageRanges as any as AgeRange[],
scoreTable: input.data.scoreTable as Record
}
return this.sportStandardService.createStandard(data, input.select, input.include)
scoreTable: input.data.scoreTable as Record,
};
return this.sportStandardService.createStandard(
data,
input.select,
input.include,
);
}),
updateStandard: this.trpc.procedure.input(SportStandardUpdateStandardArgsSchema)
updateStandard: this.trpc.procedure
.input(SportStandardUpdateStandardArgsSchema)
.mutation(async ({ input }) => {
const data = {
id: input.data.id as string,
ageRanges: input.data.ageRanges as any as AgeRange[],
scoreTable: input.data.scoreTable as Record
}
return this.sportStandardService.updateStandard(data)
scoreTable: input.data.scoreTable as Record,
};
return this.sportStandardService.updateStandard(data);
}),
getScore: this.trpc.procedure.input(GetScoreArgsSchema).query(async ({ input }) => {
console.log('计算')
console.log(input)
getScore: this.trpc.procedure
.input(GetScoreArgsSchema)
.query(async ({ input }) => {
console.log('计算');
console.log(input);
return this.sportStandardService.getScore(input);
}),
})
});
}

View File

@ -1,9 +1,9 @@
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";
import { z } from "zod";
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';
import { z } from 'zod';
interface AgeRange {
start: number | null;
@ -24,7 +24,7 @@ const GetScoreArgsSchema = z.object({
age: z.number().min(0),
performance: z.number().min(0).or(z.string()),
personType: z.string().nonempty(),
projectUnit: z.string().nonempty()
projectUnit: z.string().nonempty(),
});
@Injectable()
@ -33,26 +33,25 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
super(db, ObjectType.SPORT_STANDARD, false);
}
async create(args: Prisma.SportStandardCreateArgs) {
console.log(args)
const result = await super.create(args)
this.emitDataChanged(CrudOperation.CREATED, result)
return result
console.log(args);
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async update(args: Prisma.SportStandardUpdateArgs) {
const result = await super.update(args)
this.emitDataChanged(CrudOperation.UPDATED, result)
return result
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
async findMany(args: Prisma.SportStandardFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findUnique(args: Prisma.SportStandardFindUniqueArgs) {
const result = await super.findUnique(args)
return result
const result = await super.findUnique(args);
return result;
}
private emitDataChanged(operation: CrudOperation, data: any) {
@ -72,9 +71,9 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
scoreTable: Record;
},
select?: Prisma.SportStandardSelect<DefaultArgs>,
include?: Prisma.SportStandardInclude<DefaultArgs>
include?: Prisma.SportStandardInclude<DefaultArgs>,
) {
console.log(data)
console.log(data);
this.validateAgeRanges(data.ageRanges);
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
const result = await super.create({
@ -83,62 +82,69 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
gender: data.gender,
personType: data.personType,
ageRanges: JSON.stringify(data.ageRanges),
scoreTable: JSON.stringify(data.scoreTable)
scoreTable: JSON.stringify(data.scoreTable),
},
select,
include
})
this.emitDataChanged(CrudOperation.CREATED, result)
return result
include,
});
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
private validateAgeRanges(ranges: AgeRange[]) {
// 检查年龄段是否按顺序排列且无重叠
for (let i = 0; i < ranges.length - 1; i++) {
const current = ranges[i];
const next = ranges[i + 1];
// 先按起始年龄排序
const sortedRanges = [...ranges].sort(
(a, b) => (a.start || 0) - (b.start || 0),
);
if (current.end !== next.start) {
throw new Error('年龄段必须连续且不重叠');
for (let i = 0; i < sortedRanges.length - 1; i++) {
const current = sortedRanges[i];
const next = sortedRanges[i + 1];
// 检查重叠
if ((current.end || Infinity) >= next.start) {
throw new Error(`年龄范围 ${current.label}${next.label} 重叠`);
}
// 检查连续性(允许有间隔)
if ((current.end || Infinity) + 1 > next.start) {
throw new Error(`年龄范围 ${current.label}${next.label} 不连续`);
}
}
}
private validateScoreTable(
scoreTable: Record,
expectedLength: number
) {
Object.values(scoreTable).forEach(standards => {
private validateScoreTable(scoreTable: Record, expectedLength: number) {
Object.values(scoreTable).forEach((standards) => {
if (standards.length !== expectedLength) {
throw new Error('分数表的每行数据长度必须与年龄段数量匹配');
}
});
}
async updateStandard(
data: {
async updateStandard(data: {
id: string;
ageRanges: AgeRange[];
scoreTable: Record;
}
) {
}) {
this.validateAgeRanges(data.ageRanges);
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
const result = await super.update({
where: {
id: data.id
id: data.id,
},
data: {
ageRanges: JSON.stringify(data.ageRanges),
scoreTable: JSON.stringify(data.scoreTable)
}
})
this.emitDataChanged(CrudOperation.UPDATED, result)
return result
scoreTable: JSON.stringify(data.scoreTable),
},
});
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
public SportScoreCalculator(performance: number | string, age: number, scoreStandard: ScoreStandard, projectUnit: string, ): number {
public SportScoreCalculator(
performance: number | string,
age: number,
scoreStandard: ScoreStandard,
projectUnit: string,
): number {
// 1. 找到对应的年龄段索引
const ageRangeIndex = scoreStandard.ageRanges.findIndex(range => {
const ageRangeIndex = scoreStandard.ageRanges.findIndex((range) => {
const isAboveStart = range.start === null || age > range.start;
const isBelowEnd = range.end === null || age <= range.end;
return isAboveStart && isBelowEnd;
@ -156,11 +162,15 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
const isTimeUnit = projectUnit.includes('time'); // 假设时间单位包含 '时间单位' 字符串
for (const score of scores) {
const standard = scoreStandard.scoreTable[score.toString()][ageRangeIndex];
const standard =
scoreStandard.scoreTable[score.toString()][ageRangeIndex];
if (isTimeUnit) {
// 此时的performance和standard是时间字符串
// 需要将时间字符串转换为秒
if (this.timeStringToSeconds(performance) <= this.timeStringToSeconds(standard)) {
if (
this.timeStringToSeconds(performance) <=
this.timeStringToSeconds(standard)
) {
return score;
}
} else {
@ -174,33 +184,34 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
}
async getScore(data: z.infer<typeof GetScoreArgsSchema>) {
console.log("传入的参数",data)
console.log('传入的参数', data);
const standard = await this.findUnique({
where: {
projectId_gender_personType: { // 使用复合唯一索引
projectId_gender_personType: {
// 使用复合唯一索引
projectId: data.projectId,
gender: data.gender,
personType: data.personType
}
}
})
console.log("找到的评分标准",standard)
personType: data.personType,
},
},
});
console.log('找到的评分标准', standard);
if (!standard) {
throw new Error('未找到对应的评分标准');
}
const scoreTable:Record = JSON.parse(String(standard.scoreTable))
const ageRanges:AgeRange[] = JSON.parse(String(standard.ageRanges))
const scoreTable: Record = JSON.parse(String(standard.scoreTable));
const ageRanges: AgeRange[] = JSON.parse(String(standard.ageRanges));
const scoreStandard: ScoreStandard = {
ageRanges,
scoreTable
}
console.log("评分标准",scoreStandard)
scoreTable,
};
console.log('评分标准', scoreStandard);
return this.SportScoreCalculator(
data.performance,
data.age,
scoreStandard,
data.projectUnit,
)
);
}
// 将 13:45 格式的字符串转换为秒数
private timeStringToSeconds(timeStr) {
@ -212,6 +223,8 @@ export class SportStandardService extends BaseService<Prisma.SportStandardDelega
private secondsToTimeString(seconds: number) {
const minutesPart = Math.floor(seconds / 60);
const secondsPart = seconds % 60;
return `${String(minutesPart).padStart(2, '0')}:${String(secondsPart).padStart(2, '0')}`;
return `${String(minutesPart).padStart(2, '0')}:${String(
secondsPart,
).padStart(2, '0')}`;
}
}

View File

@ -27,11 +27,14 @@ export class StaffController {
}
@Get('find-by-dept')
async findByDept(
@Query('dept-id') deptId: string,
@Query('dept-id') deptId: string | null,
@Query('domain-id') domainId: string,
) {
try {
const result = await this.staffService.findByDept({ deptId, domainId });
const result = await this.staffService.findByDept({
deptId: deptId || null,
domainId: domainId,
});
return {
data: result,
errmsg: 'success',

View File

@ -37,8 +37,8 @@ const StaffSelect = {
type: true,
categorize: true,
createdAt: true,
updatedAt: true
}
updatedAt: true,
},
},
// 获取关联的 TrainSituation 模型的基础信息
trainSituations: {
@ -57,21 +57,23 @@ const StaffSelect = {
parentId: true,
deletedAt: true,
createdAt: true,
updatedAt: true
}
}
}
updatedAt: true,
},
},
},
},
department: {
select: {
id: true,
name: true,
}
}
}
},
},
};
@Injectable()
export class StaffService extends BaseService<Prisma.StaffDelegate> {
//索引
[x: string]: any;
constructor(private readonly departmentService: DepartmentService) {
super(db, ObjectType.STAFF, true);
}
@ -83,7 +85,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
async findByDept(data: z.infer<typeof StaffMethodSchema.findByDept>) {
const { deptId, domainId } = data;
const childDepts = await this.departmentService.getDescendantIds(
deptId,
deptId || null,
true,
);
const result = await db.staff.findMany({
@ -176,30 +178,35 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
return staff;
}
}
async findSportStaffByDept(data: z.infer<typeof StaffMethodSchema.findSportStaffByDept>) {
async findSportStaffByDept(
data: z.infer<typeof StaffMethodSchema.findSportStaffByDept>,
) {
const { deptId, domainId } = data;
let queryResult;
if (!deptId) {
// deptId 为空时执行 res 的请求
queryResult = await db.staff.findMany({
select: StaffSelect
select: StaffSelect,
});
} else {
// deptId 不为空时执行 result 的请求
const childDepts = await this.departmentService.getDescendantIds(deptId, true);
const childDepts = await this.departmentService.getDescendantIds(
deptId,
true,
);
queryResult = await db.staff.findMany({
where: {
deptId: { in: childDepts },
domainId,
},
select: StaffSelect
select: StaffSelect,
});
}
// 筛选出 trainSituations 中 trainContent 的 type 为 'SPORT' 的记录
const filteredResult = queryResult.filter(staff => {
return staff.trainSituations.some(trainSituation => {
const filteredResult = queryResult.filter((staff) => {
return staff.trainSituations.some((trainSituation) => {
return trainSituation.trainContent.type === 'SPORT';
});
});

View File

@ -1,5 +1,5 @@
import { Controller, UseGuards } from "@nestjs/common";
import { TrainContentService } from "./trainContent.service";
import { Controller, UseGuards } from '@nestjs/common';
import { TrainContentService } from './trainContent.service';
import { AuthGuard } from '@server/auth/auth.guard';
@Controller('train-content')

View File

@ -5,7 +5,6 @@ import { TrainContentController } from './trainContent.controller';
import { TrainContentRouter } from './trainContent.router';
import { TrpcService } from '@server/trpc/trpc.service';
@Module({
imports: [StaffModule],
controllers: [TrainContentController],

View File

@ -1,13 +1,16 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { TrainContentService } from "./trainContent.service";
import { z, ZodType } from "zod";
import { Prisma } from "@nice/common";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { TrainContentService } from './trainContent.service';
import { z, ZodType } from 'zod';
import { Prisma } from '@nice/common';
const TrainContentArgsSchema:ZodType<Prisma.TrainContentCreateArgs> = z.any()
const TrainContentUpdateArgsSchema:ZodType<Prisma.TrainContentUpdateArgs> = z.any()
const TrainContentFindManyArgsSchema:ZodType<Prisma.TrainContentFindManyArgs> = z.any()
const TrainContentFindFirstArgsSchema:ZodType<Prisma.TrainContentFindFirstArgs> = z.any()
const TrainContentArgsSchema: ZodType<Prisma.TrainContentCreateArgs> = z.any();
const TrainContentUpdateArgsSchema: ZodType<Prisma.TrainContentUpdateArgs> =
z.any();
const TrainContentFindManyArgsSchema: ZodType<Prisma.TrainContentFindManyArgs> =
z.any();
const TrainContentFindFirstArgsSchema: ZodType<Prisma.TrainContentFindFirstArgs> =
z.any();
@Injectable()
export class TrainContentRouter {
constructor(
@ -16,22 +19,25 @@ export class TrainContentRouter {
) {}
router = this.trpc.router({
create:this.trpc.procedure.input(TrainContentArgsSchema)
create: this.trpc.procedure
.input(TrainContentArgsSchema)
.mutation(async ({ input }) => {
return this.trainContentService.create(input)
return this.trainContentService.create(input);
}),
update:this.trpc.procedure.input(TrainContentUpdateArgsSchema)
update: this.trpc.procedure
.input(TrainContentUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.trainContentService.update(input)
return this.trainContentService.update(input);
}),
findMany:this.trpc.procedure.input(TrainContentFindManyArgsSchema)
findMany: this.trpc.procedure
.input(TrainContentFindManyArgsSchema)
.query(async ({ input }) => {
return this.trainContentService.findMany(input)
return this.trainContentService.findMany(input);
}),
findFirst:this.trpc.procedure.input(TrainContentFindFirstArgsSchema)
findFirst: this.trpc.procedure
.input(TrainContentFindFirstArgsSchema)
.query(async ({ input }) => {
return this.trainContentService.findFirst(input)
return this.trainContentService.findFirst(input);
}),
})
});
}

View File

@ -1,9 +1,8 @@
import { Injectable } from "@nestjs/common";
import { BaseService } from "../base/base.service";
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
import { DefaultArgs } from "@prisma/client/runtime/library";
import EventBus, { CrudOperation } from "@server/utils/event-bus";
import { Injectable } from '@nestjs/common';
import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable()
export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> {
@ -11,26 +10,25 @@ export class TrainContentService extends BaseService<Prisma.TrainContentDelegate
super(db, ObjectType.TRAIN_CONTENT, true);
}
async create(args: Prisma.TrainContentCreateArgs) {
console.log(args)
const result = await super.create(args)
this.emitDataChanged(CrudOperation.CREATED,result)
return result
console.log(args);
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async update(args: Prisma.TrainContentUpdateArgs) {
const result = await super.update(args)
this.emitDataChanged(CrudOperation.UPDATED,result)
return result
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
async findMany(args: Prisma.TrainContentFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findFirst(args: Prisma.TrainContentFindFirstArgs) {
const result = await super.findFirst(args)
return result
const result = await super.findFirst(args);
return result;
}
private emitDataChanged(operation: CrudOperation, data: any) {

View File

@ -1,5 +1,5 @@
import { Controller, UseGuards } from "@nestjs/common";
import { TrainSituationService } from "./trainSituation.service";
import { Controller, UseGuards } from '@nestjs/common';
import { TrainSituationService } from './trainSituation.service';
import { AuthGuard } from '@server/auth/auth.guard';
@Controller('train-situation')

View File

@ -1,12 +1,15 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { TrainSituationService } from "./trainSituation.service";
import { z, ZodType } from "zod";
import { Prisma } from "@nice/common";
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { TrainSituationService } from './trainSituation.service';
import { z, ZodType } from 'zod';
import { Prisma } from '@nice/common';
const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> = z.any()
const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> = z.any()
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> = z.any()
const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> =
z.any();
const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> =
z.any();
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> =
z.any();
@Injectable()
export class TrainSituationRouter {
@ -18,29 +21,33 @@ export class TrainSituationRouter {
create: this.trpc.protectProcedure
.input(TrainSituationArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.create(input)
return this.trainSituationService.create(input);
}),
update: this.trpc.protectProcedure
.input(TrainSituationUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.update(input)
return this.trainSituationService.update(input);
}),
findMany: this.trpc.protectProcedure
.input(TrainSituationFindManyArgsSchema)
.query(async ({ input }) => {
return this.trainSituationService.findMany(input)
return this.trainSituationService.findMany(input);
}),
findManyByDepId: this.trpc.protectProcedure
.input(z.object({
.input(
z.object({
deptId: z.string().optional(),
domainId: z.string().optional(),
trainContentId: z.string().optional()
}))
trainContentId: z.string().optional(),
date: z.string().optional(),
}),
)
.query(async ({ input }) => {
return this.trainSituationService.findManyByDeptId(input)
return this.trainSituationService.findManyByDeptId(input);
}),
createManyTrainSituation: this.trpc.protectProcedure
.input(z.array(
.input(
z.array(
z.object({
staffId: z.string(),
trainContentId: z.string(),
@ -52,20 +59,22 @@ export class TrainSituationRouter {
projectId: z.string(),
gender: z.boolean(),
age: z.number(),
performance: z.number().or(z.string())
})
))
performance: z.number().or(z.string()),
}),
),
)
.mutation(async ({ input }) => {
console.log(input)
return this.trainSituationService.createManyTrainSituation(input)
console.log(input);
return this.trainSituationService.createManyTrainSituation(input);
}),
deleteSameGroupTrainSituation: this.trpc.protectProcedure
.input(z.object({
groupId:z.string()
}))
.input(
z.object({
groupId: z.string(),
}),
)
.mutation(async ({ input }) => {
return this.trainSituationService.deleteSameGroupTrainSituation(input)
})
})
return this.trainSituationService.deleteSameGroupTrainSituation(input);
}),
});
}

View File

@ -1,92 +1,137 @@
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";
import { StaffService } from "../staff/staff.service";
import { SportStandardService } from "../sport-standard/sportStandard.service";
import { uuidv4 } from "lib0/random";
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';
import { StaffService } from '../staff/staff.service';
import { SportStandardService } from '../sport-standard/sportStandard.service';
import { uuidv4 } from 'lib0/random';
@Injectable()
export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> {
constructor(private readonly staffService:StaffService,private readonly sportStandardService:SportStandardService) {
constructor(
private readonly staffService: StaffService,
private readonly sportStandardService: SportStandardService,
) {
super(db, ObjectType.TRAIN_SITUATION, false);
}
// 创建培训情况
async create(
args: Prisma.TrainSituationCreateArgs,
){
console.log(args)
const result = await super.create(args);
async create(args: Prisma.TrainSituationCreateArgs) {
console.log(args);
const result = await db.trainSituation.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
// 更新培训情况
async update(
args: Prisma.TrainSituationUpdateArgs,
){
const result = await super.update(args);
async update(args: Prisma.TrainSituationUpdateArgs) {
const result = await db.trainSituation.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
// 查找培训情况
async findMany(args: Prisma.TrainSituationFindManyArgs)
{
async findMany(args: Prisma.TrainSituationFindManyArgs) {
const result = await super.findMany(args);
return result;
}
// 查找某一单位所有人员的培训情况
async findManyByDeptId(args:{
deptId?:string,
domainId?:string,
trainContentId?:string
}):Promise<{
async findManyByDeptId(params: {
deptId?: string;
domainId?: string;
trainContentId?: string;
date?: string;
}): Promise<
{
id: string;
staffId: string;
trainContentId: string;
mustTrainTime: number;
alreadyTrainTime: number;
score: number;
}[]>
{
const staffs = await this.staffService.findByDept({deptId:args.deptId,domainId:args.domainId})
const result = await super.findMany({
where:{
staffId:{
in:staffs.map(staff=>staff.id)
},
...(args.trainContentId ? {trainContentId:args.trainContentId} : {})
}[]
> {
const { deptId, domainId, trainContentId, date } = params;
// 构建查询条件
const where: Prisma.TrainSituationWhereInput = {};
if (deptId) {
const staffs = await this.staffService.findManyByDeptId(deptId);
where.staffId = { in: staffs.map((s) => s.id) };
}
})
return result
if (trainContentId) {
where.trainContentId = trainContentId;
}
// 添加日期过滤条件
if (date) {
// 创建日期的开始时间当天的00:00:00
const startDate = new Date(date);
startDate.setHours(0, 0, 0, 0);
// 创建日期的结束时间当天的23:59:59.999
const endDate = new Date(date);
endDate.setHours(23, 59, 59, 999);
// 设置createdAt字段在指定的日期范围内
where.createdAt = {
gte: startDate,
lte: endDate,
};
// 日志输出,方便调试
console.log(
`Filtering train situations between ${startDate.toISOString()} and ${endDate.toISOString()}`,
);
}
// 查询结果并包含关联数据
const result = await super.findMany({
where,
include: {
staff: {
include: {
department: true,
position: true,
},
},
trainContent: true,
},
orderBy: {
createdAt: 'desc', // 按创建时间倒序排列,最新的记录在前
},
});
return result;
}
//async createDailyTrainTime()
async createTrainSituation(args:{
staffId?:string,
trainContentId?:string,
mustTrainTime?:number,
alreadyTrainTime?:number,
value?:string,
async createTrainSituation(
args: {
staffId?: string;
trainContentId?: string;
mustTrainTime?: number;
alreadyTrainTime?: number;
value?: string;
projectUnit?:string,
personType?:string,
projectId?:string,
gender?:boolean,
age?:number,
performance?:number|string,
},groupId?:string){
console.log("传入的参数",args)
projectUnit?: string;
personType?: string;
projectId?: string;
gender?: boolean;
age?: number;
performance?: number | string;
},
groupId?: string,
) {
console.log('传入的参数', args);
const score = await this.sportStandardService.getScore({
projectId: args.projectId,
gender: args.gender,
age: args.age,
performance: args.performance,
personType: args.personType,
projectUnit:args.projectUnit
})
console.log("计算出的分数",score)
projectUnit: args.projectUnit,
});
console.log('计算出的分数', score);
const data: Prisma.TrainSituationCreateArgs = {
data: {
staffId: args.staffId,
@ -95,43 +140,45 @@ export class TrainSituationService extends BaseService<Prisma.TrainSituationDele
alreadyTrainTime: args.alreadyTrainTime,
value: args.value,
score: score,
groupId:groupId
}
}
console.log("创建的数据",data)
groupId: groupId,
},
};
console.log('创建的数据', data);
const result = await super.create(data);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async createManyTrainSituation(args:{
staffId?:string,
trainContentId?:string,
mustTrainTime?:number,
alreadyTrainTime?:number,
value?:string,
async createManyTrainSituation(
args: {
staffId?: string;
trainContentId?: string;
mustTrainTime?: number;
alreadyTrainTime?: number;
value?: string;
projectUnit?:string,
personType?:string,
projectId?:string,
gender?:boolean,
age?:number,
performance?:number|string,
}[]){
console.log("传入的参数",args)
projectUnit?: string;
personType?: string;
projectId?: string;
gender?: boolean;
age?: number;
performance?: number | string;
}[],
) {
console.log('传入的参数', args);
const groupId = uuidv4();
args.forEach(async (item) => {
await this.createTrainSituation(item,groupId)
})
await this.createTrainSituation(item, groupId);
});
}
async deleteSameGroupTrainSituation(args: { groupId?: string }) {
const {groupId} = args
const { groupId } = args;
const result = await super.deleteMany({
where: {
groupId
}
})
this.emitDataChanged(CrudOperation.DELETED,result)
return result
groupId,
},
});
this.emitDataChanged(CrudOperation.DELETED, result);
return result;
}
// 发送数据变化事件

View File

@ -8,12 +8,7 @@ import { DepartmentModule } from '../department/department.module';
import { StaffModule } from '../staff/staff.module';
// import { TransformController } from './transform.controller';
@Module({
imports: [
DepartmentModule,
StaffModule,
TermModule,
TaxonomyModule,
],
imports: [DepartmentModule, StaffModule, TermModule, TaxonomyModule],
providers: [TransformService, TransformRouter, TrpcService],
exports: [TransformRouter, TransformService],
// controllers:[TransformController]

View File

@ -5,6 +5,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
@Module({
providers: [VisitService, VisitRouter, TrpcService],
exports: [VisitRouter]
exports: [VisitRouter],
})
export class VisitModule {}

View File

@ -1,7 +1,6 @@
import { WebSocketServer, WebSocket } from "ws";
import { Logger } from "@nestjs/common";
import { WebSocketServerConfig, WSClient, WebSocketType } from "../types";
import { WebSocketServer, WebSocket } from 'ws';
import { Logger } from '@nestjs/common';
import { WebSocketServerConfig, WSClient, WebSocketType } from '../types';
import { SocketMessage } from '@nice/common';
const DEFAULT_CONFIG: WebSocketServerConfig = {
@ -25,9 +24,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
protected readonly logger = new Logger(this.constructor.name);
protected readonly finalConfig: WebSocketServerConfig;
private userClientMap: Map<string, WSClient> = new Map();
constructor(
protected readonly config: Partial<WebSocketServerConfig> = {}
) {
constructor(protected readonly config: Partial<WebSocketServerConfig> = {}) {
this.finalConfig = {
...DEFAULT_CONFIG,
...config,
@ -39,7 +36,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
}
}
public getClientCount() {
return this.clients.size
return this.clients.size;
}
// 暴露 WebSocketServer 实例的只读访问
public get wss(): WebSocketServer | null {
@ -62,7 +59,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
this._wss = new WebSocketServer({
noServer: true,
path: this.serverPath
path: this.serverPath,
});
this.debugLog(`WebSocket server starting on path: ${this.serverPath}`);
@ -76,12 +73,12 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
this.pingIntervalId = undefined;
}
this.clients.forEach(client => client.close());
this.clients.forEach((client) => client.close());
this.clients.clear();
this.timeouts.clear();
if (this._wss) {
await new Promise(resolve => this._wss!.close(resolve));
await new Promise((resolve) => this._wss!.close(resolve));
this._wss = null;
}
@ -89,33 +86,36 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
}
public broadcast(data: SocketMessage): void {
this.clients.forEach(client =>
client.readyState === WebSocket.OPEN && client.send(JSON.stringify(data))
this.clients.forEach(
(client) =>
client.readyState === WebSocket.OPEN &&
client.send(JSON.stringify(data)),
);
}
public sendToUser(id: string, data: SocketMessage) {
const message = JSON.stringify(data);
const client = this.userClientMap.get(id);
client?.send(message)
client?.send(message);
}
public sendToUsers(ids: string[], data: SocketMessage) {
const message = JSON.stringify(data);
ids.forEach(id => {
ids.forEach((id) => {
const client = this.userClientMap.get(id);
client?.send(message);
});
}
public sendToRoom(roomId: string, data: SocketMessage) {
const message = JSON.stringify(data);
this.clients.forEach(client => {
this.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
client.send(message)
client.send(message);
}
})
});
}
protected getRoomClientsCount(roomId?: string): number {
if (!roomId) return 0;
return Array.from(this.clients).filter(client => client.roomId === roomId).length;
return Array.from(this.clients).filter((client) => client.roomId === roomId)
.length;
}
public handleConnection(ws: WSClient): void {
@ -161,7 +161,10 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
ws.on('pong', () => this.handlePong(ws))
.on('close', () => this.handleDisconnection(ws))
.on('error', (error) => {
this.logger.error(`[${this.serverType}] client error on path ${this.serverPath}:`, error);
this.logger.error(
`[${this.serverType}] client error on path ${this.serverPath}:`,
error,
);
this.handleDisconnection(ws);
});
}
@ -178,19 +181,19 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
private startPingInterval(): void {
this.pingIntervalId = setInterval(
() => this.pingClients(),
this.finalConfig.pingInterval
this.finalConfig.pingInterval,
);
}
private pingClients(): void {
this.clients.forEach(ws => {
this.clients.forEach((ws) => {
if (!ws.isAlive) return this.handleDisconnection(ws);
ws.isAlive = false;
ws.ping();
const timeout = setTimeout(
() => !ws.isAlive && this.handleDisconnection(ws),
this.finalConfig.pingTimeout
this.finalConfig.pingTimeout,
);
this.timeouts.set(ws, timeout);
});
@ -200,6 +203,8 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
if (!this._wss) return;
this._wss
.on('connection', (ws: WSClient) => this.handleConnection(ws))
.on('error', (error) => this.logger.error(`Server error on path ${this.serverPath}:`, error));
.on('error', (error) =>
this.logger.error(`Server error on path ${this.serverPath}:`, error),
);
}
}

View File

@ -8,12 +8,13 @@ import http from 'http';
import { parseInt as libParseInt } from 'lib0/number';
import { WSSharedDoc } from './ws-shared-doc';
/**
* URL配置,
* null
*/
const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null;
const CALLBACK_URL = process.env.CALLBACK_URL
? new URL(process.env.CALLBACK_URL)
: null;
/**
* ,
@ -25,7 +26,9 @@ const CALLBACK_TIMEOUT = libParseInt(process.env.CALLBACK_TIMEOUT || '5000');
*
* CALLBACK_OBJECTS中解析JSON格式的配置
*/
const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {};
const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS
? JSON.parse(process.env.CALLBACK_OBJECTS)
: {};
/**
* URL是否已配置的标志
@ -37,10 +40,13 @@ export const isCallbackSet = !!CALLBACK_URL;
*/
interface DataToSend {
room: string; // 房间/文档标识
data: Record<string, {
data: Record<
string,
{
type: string; // 数据类型
content: any; // 数据内容
}>;
}
>;
}
/**
@ -59,25 +65,29 @@ type OriginType = any;
* @param origin -
* @param doc -
*/
export const callbackHandler = (update: UpdateType, origin: OriginType, doc: WSSharedDoc): void => {
export const callbackHandler = (
update: UpdateType,
origin: OriginType,
doc: WSSharedDoc,
): void => {
// 获取文档名称作为房间标识
const room = doc.name;
// 初始化要发送的数据对象
const dataToSend: DataToSend = {
room,
data: {}
data: {},
};
// 获取所有需要监听的共享对象名称
const sharedObjectList = Object.keys(CALLBACK_OBJECTS);
// 遍历所有共享对象,获取它们的最新内容
sharedObjectList.forEach(sharedObjectName => {
sharedObjectList.forEach((sharedObjectName) => {
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName];
dataToSend.data[sharedObjectName] = {
type: sharedObjectType,
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON()
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON(),
};
});
@ -106,8 +116,8 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(dataString)
}
'Content-Length': Buffer.byteLength(dataString),
},
};
// 创建HTTP请求
@ -137,14 +147,24 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
* @param doc -
* @returns
*/
const getContent = (objName: string, objType: string, doc: WSSharedDoc): any => {
const getContent = (
objName: string,
objType: string,
doc: WSSharedDoc,
): any => {
// 根据对象类型返回相应的共享对象
switch (objType) {
case 'Array': return doc.getArray(objName);
case 'Map': return doc.getMap(objName);
case 'Text': return doc.getText(objName);
case 'XmlFragment': return doc.getXmlFragment(objName);
case 'XmlElement': return doc.getXmlElement(objName);
default: return {};
case 'Array':
return doc.getArray(objName);
case 'Map':
return doc.getMap(objName);
case 'Text':
return doc.getText(objName);
case 'XmlFragment':
return doc.getXmlFragment(objName);
case 'XmlElement':
return doc.getXmlElement(objName);
default:
return {};
}
};

View File

@ -3,6 +3,6 @@ import { YjsServer } from './yjs.server';
@Module({
providers: [YjsServer],
exports: [YjsServer]
exports: [YjsServer],
})
export class CollaborationModule {}

View File

@ -2,4 +2,3 @@ export interface ConnectionOptions {
docName: string;
gc: boolean;
}

View File

@ -1,17 +1,28 @@
import { readSyncMessage } from '@nice/common';
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common';
import {
applyAwarenessUpdate,
Awareness,
encodeAwarenessUpdate,
removeAwarenessStates,
writeSyncStep1,
writeUpdate,
} from '@nice/common';
import * as encoding from 'lib0/encoding';
import * as decoding from 'lib0/decoding';
import * as Y from "yjs"
import * as Y from 'yjs';
import { debounce } from 'lodash';
import { getPersistence, setPersistence } from './persistence';
import { callbackHandler, isCallbackSet } from './callback';
import { WebSocket } from "ws";
import { WebSocket } from 'ws';
import { YMessageType } from '@nice/common';
import { WSClient } from '../types';
export const docs = new Map<string, WSSharedDoc>();
export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000');
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000');
export const CALLBACK_DEBOUNCE_WAIT = parseInt(
process.env.CALLBACK_DEBOUNCE_WAIT || '2000',
);
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(
process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000',
);
export const getYDoc = (docname: string, gc = true): WSSharedDoc => {
return docs.get(docname) || createYDoc(docname, gc);
};
@ -27,7 +38,9 @@ export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => {
return;
}
try {
conn.send(m, {}, err => { err != null && closeConn(doc, conn) });
conn.send(m, {}, (err) => {
err != null && closeConn(doc, conn);
});
} catch (e) {
closeConn(doc, conn);
}
@ -36,14 +49,12 @@ export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
if (doc.conns.has(conn)) {
const controlledIds = doc.conns.get(conn) as Set<number>;
doc.conns.delete(conn);
removeAwarenessStates(
doc.awareness,
Array.from(controlledIds),
null
);
removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
if (doc.conns.size === 0 && getPersistence() !== null) {
getPersistence()?.writeState(doc.name, doc).then(() => {
getPersistence()
?.writeState(doc.name, doc)
.then(() => {
doc.destroy();
});
docs.delete(doc.name);
@ -52,7 +63,11 @@ export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
conn.close();
};
export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => {
export const messageListener = (
conn: WSClient,
doc: WSSharedDoc,
message: Uint8Array,
) => {
try {
const encoder = encoding.createEncoder();
const decoder = decoding.createDecoder(message);
@ -71,7 +86,7 @@ export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8
applyAwarenessUpdate(
doc.awareness,
decoding.readVarUint8Array(decoder),
conn
conn,
);
// console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`)
break;
@ -83,7 +98,12 @@ export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8
}
};
const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr: any) => {
const updateHandler = (
update: Uint8Array,
_origin: any,
doc: WSSharedDoc,
_tr: any,
) => {
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, YMessageType.Sync);
writeUpdate(encoder, update);
@ -91,7 +111,8 @@ const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr:
doc.conns.forEach((_, conn) => send(doc, conn, message));
};
let contentInitializor: (ydoc: Y.Doc) => Promise<void> = (_ydoc) => Promise.resolve();
let contentInitializor: (ydoc: Y.Doc) => Promise<void> = (_ydoc) =>
Promise.resolve();
export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise<void>) => {
contentInitializor = f;
};
@ -110,22 +131,29 @@ export class WSSharedDoc extends Y.Doc {
this.awareness = new Awareness(this);
this.awareness.setLocalState(null);
const awarenessUpdateHandler = ({
const awarenessUpdateHandler = (
{
added,
updated,
removed
removed,
}: {
added: number[],
updated: number[],
removed: number[]
}, conn: WebSocket) => {
added: number[];
updated: number[];
removed: number[];
},
conn: WebSocket,
) => {
const changedClients = added.concat(updated, removed);
if (changedClients.length === 0) return
if (changedClients.length === 0) return;
if (conn !== null) {
const connControlledIDs = this.conns.get(conn) as Set<number>;
if (connControlledIDs !== undefined) {
added.forEach(clientID => { connControlledIDs.add(clientID); });
removed.forEach(clientID => { connControlledIDs.delete(clientID); });
added.forEach((clientID) => {
connControlledIDs.add(clientID);
});
removed.forEach((clientID) => {
connControlledIDs.delete(clientID);
});
}
}
@ -133,7 +161,7 @@ export class WSSharedDoc extends Y.Doc {
encoding.writeVarUint(encoder, YMessageType.Awareness);
encoding.writeVarUint8Array(
encoder,
encodeAwarenessUpdate(this.awareness, changedClients)
encodeAwarenessUpdate(this.awareness, changedClients),
);
const buff = encoding.toUint8Array(encoder);
@ -146,11 +174,12 @@ export class WSSharedDoc extends Y.Doc {
this.on('update', updateHandler as any);
if (isCallbackSet) {
this.on('update', debounce(
callbackHandler as any,
CALLBACK_DEBOUNCE_WAIT,
{ maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
) as any);
this.on(
'update',
debounce(callbackHandler as any, CALLBACK_DEBOUNCE_WAIT, {
maxWait: CALLBACK_DEBOUNCE_MAXWAIT,
}) as any,
);
}
this.whenInitialized = contentInitializor(this);

View File

@ -1,25 +1,36 @@
import { Injectable } from "@nestjs/common";
import { WebSocketType, WSClient } from "../types";
import { BaseWebSocketServer } from "../base/base-websocket-server";
import { encoding } from "lib0";
import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common";
import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc";
import { Injectable } from '@nestjs/common';
import { WebSocketType, WSClient } from '../types';
import { BaseWebSocketServer } from '../base/base-websocket-server';
import { encoding } from 'lib0';
import {
YMessageType,
writeSyncStep1,
encodeAwarenessUpdate,
} from '@nice/common';
import {
getYDoc,
closeConn,
WSSharedDoc,
messageListener,
send,
} from './ws-shared-doc';
@Injectable()
export class YjsServer extends BaseWebSocketServer {
public get serverType(): WebSocketType {
return WebSocketType.YJS;
}
public override handleConnection(
connection: WSClient
): void {
super.handleConnection(connection)
public override handleConnection(connection: WSClient): void {
super.handleConnection(connection);
try {
connection.binaryType = 'arraybuffer';
const doc = this.initializeDocument(connection, connection.roomId, true);
this.setupConnectionHandlers(connection, doc);
this.sendInitialSync(connection, doc);
} catch (error: any) {
this.logger.error(`Error in handleNewConnection: ${error.message}`, error.stack);
this.logger.error(
`Error in handleNewConnection: ${error.message}`,
error.stack,
);
connection.close();
}
}
@ -31,7 +42,10 @@ export class YjsServer extends BaseWebSocketServer {
return doc;
}
private setupConnectionHandlers(connection: WSClient, doc: WSSharedDoc): void {
private setupConnectionHandlers(
connection: WSClient,
doc: WSSharedDoc,
): void {
connection.on('message', (message: ArrayBuffer) => {
this.handleMessage(connection, doc, message);
});
@ -39,9 +53,14 @@ export class YjsServer extends BaseWebSocketServer {
this.handleClose(doc, connection);
});
connection.on('error', (error) => {
this.logger.error(`WebSocket error for doc ${doc.name}: ${error.message}`, error.stack);
this.logger.error(
`WebSocket error for doc ${doc.name}: ${error.message}`,
error.stack,
);
closeConn(doc, connection);
this.logger.warn(`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`);
this.logger.warn(
`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`,
);
});
}
@ -49,14 +68,24 @@ export class YjsServer extends BaseWebSocketServer {
try {
closeConn(doc, connection);
} catch (error: any) {
this.logger.error(`Error closing connection: ${error.message}`, error.stack);
this.logger.error(
`Error closing connection: ${error.message}`,
error.stack,
);
}
}
private handleMessage(connection: WSClient, doc: WSSharedDoc, message: ArrayBuffer): void {
private handleMessage(
connection: WSClient,
doc: WSSharedDoc,
message: ArrayBuffer,
): void {
try {
messageListener(connection, doc, new Uint8Array(message));
} catch (error: any) {
this.logger.error(`Error handling message: ${error.message}`, error.stack);
this.logger.error(
`Error handling message: ${error.message}`,
error.stack,
);
}
}
private sendInitialSync(connection: WSClient, doc: any): void {
@ -77,7 +106,10 @@ export class YjsServer extends BaseWebSocketServer {
encoding.writeVarUint(encoder, YMessageType.Awareness);
encoding.writeVarUint8Array(
encoder,
encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys()))
encodeAwarenessUpdate(
doc.awareness,
Array.from(awarenessStates.keys()),
),
);
send(doc, connection, encoding.toUint8Array(encoder));
}

View File

@ -1,9 +1,8 @@
import { Module } from '@nestjs/common';
import { RealtimeServer } from './realtime.server';
@Module({
providers: [RealtimeServer],
exports: [ RealtimeServer]
exports: [RealtimeServer],
})
export class RealTimeModule {}

View File

@ -1,23 +1,35 @@
import { Injectable, OnModuleInit } from "@nestjs/common";
import { WebSocketType } from "../types";
import { BaseWebSocketServer } from "../base/base-websocket-server";
import EventBus, { CrudOperation } from "@server/utils/event-bus";
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
import { Injectable, OnModuleInit } from '@nestjs/common';
import { WebSocketType } from '../types';
import { BaseWebSocketServer } from '../base/base-websocket-server';
import EventBus, { CrudOperation } from '@server/utils/event-bus';
import {
ObjectType,
SocketMsgType,
MessageDto,
PostDto,
PostType,
} from '@nice/common';
@Injectable()
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
export class RealtimeServer
extends BaseWebSocketServer
implements OnModuleInit
{
onModuleInit() {
EventBus.on("dataChanged", ({ data, type, operation }) => {
EventBus.on('dataChanged', ({ data, type, operation }) => {
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
const receiverIds = (data as Partial<MessageDto>).receivers.map(
(receiver) => receiver.id,
);
this.sendToUsers(receiverIds, {
type: SocketMsgType.NOTIFY,
payload: { objectType: ObjectType.MESSAGE },
});
}
if (type === ObjectType.POST) {
const post = data as Partial<PostDto>
const post = data as Partial<PostDto>;
}
})
});
}
public get serverType(): WebSocketType {
return WebSocketType.REALTIME;

View File

@ -1,16 +1,16 @@
import { WebSocketServer, WebSocket } from "ws";
import { WebSocketServer, WebSocket } from 'ws';
// 类型定义
export enum WebSocketType {
YJS = "yjs",
REALTIME = "realtime"
YJS = 'yjs',
REALTIME = 'realtime',
}
export interface WebSocketServerConfig {
path?: string;
pingInterval?: number;
pingTimeout?: number;
debug?: boolean
debug?: boolean;
}
export interface ServerInstance {
@ -23,7 +23,7 @@ export interface ServerInstance {
export interface WSClient extends WebSocket {
isAlive?: boolean;
type?: WebSocketType;
userId?: string
origin?: string
roomId?: string
userId?: string;
origin?: string;
roomId?: string;
}

View File

@ -1,9 +1,9 @@
import { Injectable, Logger } from "@nestjs/common";
import { Server } from "http";
import { WSClient } from "./types";
import { RealtimeServer } from "./realtime/realtime.server";
import { YjsServer } from "./collaboration/yjs.server";
import { BaseWebSocketServer } from "./base/base-websocket-server";
import { Injectable, Logger } from '@nestjs/common';
import { Server } from 'http';
import { WSClient } from './types';
import { RealtimeServer } from './realtime/realtime.server';
import { YjsServer } from './collaboration/yjs.server';
import { BaseWebSocketServer } from './base/base-websocket-server';
@Injectable()
export class WebSocketService {
@ -11,14 +11,14 @@ export class WebSocketService {
private readonly servers: BaseWebSocketServer[] = [];
constructor(
private realTimeServer: RealtimeServer,
private yjsServer: YjsServer
private yjsServer: YjsServer,
) {
this.servers.push(this.realTimeServer)
this.servers.push(this.yjsServer)
this.servers.push(this.realTimeServer);
this.servers.push(this.yjsServer);
}
public async initialize(httpServer: Server): Promise<void> {
try {
await Promise.all(this.servers.map(server => server.start()));
await Promise.all(this.servers.map((server) => server.start()));
this.setupUpgradeHandler(httpServer);
} catch (error) {
this.logger.error('Failed to initialize:', error);
@ -36,7 +36,7 @@ export class WebSocketService {
const urlParams = new URLSearchParams(url.search);
const roomId = urlParams.get('roomId');
const userId = urlParams.get('userId');
const server = this.servers.find(server => {
const server = this.servers.find((server) => {
const serverPathClean = server.serverPath.replace(/\/$/, '');
const pathnameClean = pathname.replace(/\/$/, '');
return serverPathClean === pathnameClean;
@ -48,8 +48,8 @@ export class WebSocketService {
server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => {
ws.userId = userId;
ws.origin = request.url
ws.roomId = roomId
ws.origin = request.url;
ws.roomId = roomId;
server.wss!.emit('connection', ws, request);
});
} catch (error) {

View File

@ -52,8 +52,8 @@ export class GenDevService {
await this.generateStaffs(4);
//await this.generateTerms(2, 6);
//await this.generateCourses(8);
await this.generateTrainContent(2,6)
await this.generateTrainSituations()
await this.generateTrainContent(2, 6);
await this.generateTrainSituations();
} catch (err) {
this.logger.error(err);
}
@ -88,7 +88,9 @@ export class GenDevService {
this.domains.forEach((domain) => {
this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id);
this.logger.log(
`Domain: ${domain.name} has ${this.domainDepts[domain.id].length} child departments`,
`Domain: ${domain.name} has ${
this.domainDepts[domain.id].length
} child departments`,
);
});
this.logger.log(`Completed: Generated ${this.depts.length} departments.`);
@ -104,7 +106,9 @@ export class GenDevService {
if (currentDepth > maxDepth) return;
for (let i = 0; i < count; i++) {
const deptName = `${parentId?.slice(0, 6) || '根'}公司${currentDepth}-${i}`;
const deptName = `${
parentId?.slice(0, 6) || '根'
}${currentDepth}-${i}`;
const newDept = await this.createDepartment(
deptName,
parentId,
@ -190,7 +194,9 @@ export class GenDevService {
});
for (const cate of cates) {
for (let i = 0; i < countPerCate; i++) {
const randomTitle = `${titleList[Math.floor(Math.random() * titleList.length)]} ${Math.random().toString(36).substring(7)}`;
const randomTitle = `${
titleList[Math.floor(Math.random() * titleList.length)]
} ${Math.random().toString(36).substring(7)}`;
const randomLevelId =
levels[Math.floor(Math.random() * levels.length)].id;
const randomDeptId =
@ -224,7 +230,9 @@ export class GenDevService {
this.deptStaffRecord[dept.id] = [];
}
for (let i = 0; i < countPerDept; i++) {
const username = `${dept.name}-S${staffsGenerated.toString().padStart(4, '0')}`;
const username = `${dept.name}-S${staffsGenerated
.toString()
.padStart(4, '0')}`;
const staff = await this.staffService.create({
data: {
showname: username,
@ -328,7 +336,9 @@ export class GenDevService {
) => {
if (currentDepth > depth) return;
for (let i = 0; i < nodesPerLevel; i++) {
const name = `${taxonomySlug}-${domain?.name || 'public'}-${currentDepth}-${counter++} `;
const name = `${taxonomySlug}-${
domain?.name || 'public'
}-${currentDepth}-${counter++} `;
const newTerm = await this.termService.create({
data: {
name,
@ -349,13 +359,13 @@ export class GenDevService {
private async createTrainContent(
type: string,
title: string,
parentId:string|null
parentId: string | null,
) {
const trainContent = await db.trainContent.create({
data: {
type,
title,
parentId
parentId,
},
});
return trainContent;
@ -363,11 +373,19 @@ export class GenDevService {
// 生成培训内容
private async generateTrainContent(depth: number = 3, count: number = 6) {
if (this.counts.trainContentCount !== 0) return;
const totalTrainContent = this.calculateTotalTrainContent(depth,count)
this.logger.log("Start generating train content...")
await this.generateSubTrainContent(null,1,depth,count,totalTrainContent)
this.trainContents = await db.trainContent.findMany()
this.logger.log(`Completed: Generated ${this.trainContents.length} departments.`);
const totalTrainContent = this.calculateTotalTrainContent(depth, count);
this.logger.log('Start generating train content...');
await this.generateSubTrainContent(
null,
1,
depth,
count,
totalTrainContent,
);
this.trainContents = await db.trainContent.findMany();
this.logger.log(
`Completed: Generated ${this.trainContents.length} departments.`,
);
}
// 生成培训内容子内容
private async generateSubTrainContent(
@ -378,25 +396,27 @@ export class GenDevService {
total: number,
) {
if (currentDepth > maxDepth) return;
const contentType = [TrainContentType.SUBJECTS,TrainContentType.COURSE]
const contentType = [TrainContentType.SUBJECTS, TrainContentType.COURSE];
for (let i = 0; i < count; i++) {
const trainContentTitle = `${parentId?.slice(0,6) || '根'}公司${currentDepth}-${i}`
const trainContentTitle = `${
parentId?.slice(0, 6) || '根'
}${currentDepth}-${i}`;
const newTrainContent = await this.createTrainContent(
contentType[currentDepth - 1],
trainContentTitle,
parentId
)
parentId,
);
this.trainContentGeneratedCount++;
this.logger.log(
`Generated ${this.trainContentGeneratedCount}/${total} train contents`
)
`Generated ${this.trainContentGeneratedCount}/${total} train contents`,
);
await this.generateSubTrainContent(
newTrainContent.id,
currentDepth + 1,
maxDepth,
count,
total
)
total,
);
}
}
private calculateTotalTrainContent(depth: number, count: number): number {
@ -407,7 +427,6 @@ export class GenDevService {
return total;
}
private async createTrainSituation(staffId: string, trainContentId: string) {
const trainSituation = await db.trainSituation.create({
data: {
@ -416,37 +435,35 @@ export class GenDevService {
mustTrainTime: Math.floor(Math.random() * 100),
alreadyTrainTime: Math.floor(Math.random() * 100),
score: Math.floor(Math.random() * 100),
}
})
return trainSituation
},
});
return trainSituation;
}
private async generateTrainSituations(probability: number = 0.1) {
this.logger.log("Start generating train situations...")
this.logger.log('Start generating train situations...');
const allTrainContents = await db.trainContent.findMany();
// 这里相当于两次遍历 找到没有parentID的即是
const leafNodes = allTrainContents.filter((item)=>item.parentId !== null)
console.log(leafNodes.length)
const staffs = await db.staff.findMany()
const leafNodes = allTrainContents.filter((item) => item.parentId !== null);
console.log(leafNodes.length);
const staffs = await db.staff.findMany();
let situationCount = 0
const totalPossibleSituations = leafNodes.length * staffs.length
let situationCount = 0;
const totalPossibleSituations = leafNodes.length * staffs.length;
for (const staff of staffs) {
for (const leaf of leafNodes) {
if (Math.random() < probability) {
await this.createTrainSituation(staff.id,leaf.id)
situationCount++
await this.createTrainSituation(staff.id, leaf.id);
situationCount++;
if (situationCount % 100 === 0) {
this.logger.log(
`Generated ${situationCount} train situations`
);
this.logger.log(`Generated ${situationCount} train situations`);
}
}
}
}
this.logger.log(
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`,
);
}
}

View File

@ -9,8 +9,15 @@ import { DepartmentModule } from '@server/models/department/department.module';
import { TermModule } from '@server/models/term/term.module';
@Module({
imports: [MinioModule, AuthModule, AppConfigModule, StaffModule, DepartmentModule, TermModule],
imports: [
MinioModule,
AuthModule,
AppConfigModule,
StaffModule,
DepartmentModule,
TermModule,
],
providers: [InitService, GenDevService],
exports: [InitService]
exports: [InitService],
})
export class InitModule {}

View File

@ -122,16 +122,16 @@ export class InitService {
const existingTrainContent = await db.trainContent.findFirst({
where: {
type: 'SPORTS',
title: '体能考核'
}
title: '体能考核',
},
});
if (!existingTrainContent) {
await db.trainContent.create({
data: {
type: 'SPORTS',
title: '体能考核'
}
})
title: '体能考核',
},
});
} else {
this.logger.log('Sport train already exists');
}

View File

@ -5,6 +5,6 @@ import { MessageModule } from '@server/models/message/message.module';
@Module({
imports: [MessageModule],
providers: [ReminderService],
exports: [ReminderService]
exports: [ReminderService],
})
export class ReminderModule {}

View File

@ -75,7 +75,5 @@ export class ReminderService {
*/
async remindDeadline() {
this.logger.log('开始检查截止日期以发送提醒。');
}
}

View File

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { InitModule } from '@server/tasks/init/init.module';
import { ReminderModule } from "@server/tasks/reminder/reminder.module"
import { ReminderModule } from '@server/tasks/reminder/reminder.module';
@Module({
imports: [InitModule, ReminderModule],
providers: [TasksService]
providers: [TasksService],
})
export class TasksModule {}

View File

@ -11,7 +11,7 @@ export class TasksService implements OnModuleInit {
constructor(
private readonly schedulerRegistry: SchedulerRegistry,
private readonly initService: InitService,
private readonly reminderService: ReminderService
private readonly reminderService: ReminderService,
) {}
async onModuleInit() {
@ -30,11 +30,17 @@ export class TasksService implements OnModuleInit {
await this.reminderService.remindDeadline();
this.logger.log('Reminder successfully processed');
} catch (reminderErr) {
this.logger.error('Error occurred while processing reminder', reminderErr);
this.logger.error(
'Error occurred while processing reminder',
reminderErr,
);
}
});
this.schedulerRegistry.addCronJob('remindDeadline', handleRemindJob as any);
this.schedulerRegistry.addCronJob(
'remindDeadline',
handleRemindJob as any,
);
this.logger.log('Start remind cron job');
handleRemindJob.start();
} catch (cronJobErr) {

View File

@ -40,7 +40,7 @@ import { SportProjectModule } from '@server/models/sport-project/sportProject.mo
TrainContentModule,
TrainSituationModule,
DailyTrainModule,
SportProjectModule
SportProjectModule,
],
controllers: [],
providers: [TrpcService, TrpcRouter, Logger],

View File

@ -40,7 +40,7 @@ export class TrpcRouter {
private readonly trainSituation: TrainSituationRouter,
private readonly dailyTrain: DailyTrainRouter,
private readonly sportStandard: SportStandardRouter,
private readonly sportProject:SportProjectRouter
private readonly sportProject: SportProjectRouter,
) {}
getRouter() {
return;
@ -62,7 +62,7 @@ export class TrpcRouter {
trainSituation: this.trainSituation.router,
dailyTrain: this.dailyTrain.router,
sportStandard: this.sportStandard.router,
sportProject:this.sportProject.router
sportProject: this.sportProject.router,
});
wss: WebSocketServer = undefined;

View File

@ -18,8 +18,9 @@ export class TrpcService {
ip: string;
}> {
const token = opts.req.headers.authorization?.split(' ')[1];
const staff =
await UserProfileService.instance.getUserProfileByToken(token);
const staff = await UserProfileService.instance.getUserProfileByToken(
token,
);
const ip = getClientIp(opts.req);
return {
staff: staff.staff,

View File

@ -3,6 +3,6 @@ import { MinioService } from './minio.service';
@Module({
providers: [MinioService],
exports: [MinioService]
exports: [MinioService],
})
export class MinioModule {}

View File

@ -1,4 +1,4 @@
import { redis } from "./redis.service";
import { redis } from './redis.service';
export async function deleteByPattern(pattern: string) {
try {

View File

@ -1,36 +1,40 @@
import { createReadStream } from "fs";
import { createInterface } from "readline";
import { createReadStream } from 'fs';
import { createInterface } from 'readline';
import { db } from '@nice/common';
import * as tus from "tus-js-client";
import * as tus from 'tus-js-client';
import ExcelJS from 'exceljs';
export function truncateStringByByte(str, maxBytes) {
let byteCount = 0;
let index = 0;
while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) {
while (
index < str.length &&
byteCount + new TextEncoder().encode(str[index]).length <= maxBytes
) {
byteCount += new TextEncoder().encode(str[index]).length;
index++;
}
return str.substring(0, index) + (index < str.length ? "..." : "");
return str.substring(0, index) + (index < str.length ? '...' : '');
}
export async function loadPoliciesFromCSV(filePath: string) {
const policies = {
p: [],
g: []
g: [],
};
const stream = createReadStream(filePath);
const rl = createInterface({
input: stream,
crlfDelay: Infinity
crlfDelay: Infinity,
});
// Updated regex to handle commas inside parentheses as part of a single field
const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
const regex =
/(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
for await (const line of rl) {
// Ignore empty lines and comments
if (line.trim() && !line.startsWith("#")) {
if (line.trim() && !line.startsWith('#')) {
const parts = [];
let match;
while ((match = regex.exec(line)) !== null) {
@ -61,10 +65,10 @@ export function uploadFile(blob: any, fileName: string) {
metadata: {
filename: fileName,
filetype:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
onError: (error) => {
console.error("Failed because: " + error);
console.error('Failed because: ' + error);
reject(error); // 错误时,我们要拒绝 promise
},
onProgress: (bytesUploaded, bytesTotal) => {
@ -80,7 +84,6 @@ export function uploadFile(blob: any, fileName: string) {
});
}
class TreeNode {
value: string;
children: TreeNode[];
@ -91,14 +94,12 @@ class TreeNode {
}
addChild(childValue: string): TreeNode {
let newChild = undefined
if (this.children.findIndex(child => child.value === childValue) === -1) {
let newChild = undefined;
if (this.children.findIndex((child) => child.value === childValue) === -1) {
newChild = new TreeNode(childValue);
this.children.push(newChild)
this.children.push(newChild);
}
return this.children.find(child => child.value === childValue)
return this.children.find((child) => child.value === childValue);
}
}
function buildTree(data: string[][]): TreeNode {
@ -111,12 +112,9 @@ function buildTree(data: string[][]): TreeNode {
}
}
return root;
} catch (error) {
console.error(error);
}
catch (error) {
console.error(error)
}
}
export function printTree(node: TreeNode, level: number = 0): void {
const indent = ' '.repeat(level);
@ -133,9 +131,12 @@ export async function generateTreeFromFile(file: Buffer): Promise<TreeNode> {
const data: string[][] = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) { // Skip header row if any
const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString());
data.push(rowData.map(value => value.trim()));
if (rowNumber > 1) {
// Skip header row if any
const rowData: string[] = (row.values as string[])
.slice(2)
.map((cell) => (cell || '').toString());
data.push(rowData.map((value) => value.trim()));
}
});
// Fill forward values

View File

@ -11,7 +11,7 @@ export class ZodValidationPipe implements PipeTransform {
} catch (error: any) {
throw new BadRequestException('Validation failed', {
cause: error,
description: error.errors
description: error.errors,
});
}
}

View File

@ -3,3 +3,5 @@ VITE_APP_SERVER_PORT=3000
VITE_APP_FILE_PORT=80
VITE_APP_VERSION=0.3.0
VITE_APP_APP_NAME=MOOC

View File

@ -14,7 +14,7 @@
VITE_APP_FILE_PORT: "$FILE_PORT",
};
</script>
<title>$APP_NAME</title>
<title>trainng_DATA</title>
</head>
<body>

View File

@ -40,3 +40,38 @@
.ag-root-wrapper {
border: 0px;
}
.assessment-standard-table .ant-table {
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-thead > tr > th {
background-color: #f9fafb;
font-weight: 500;
color: #374151;
text-align: center;
padding: 12px 8px;
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-tbody > tr > td {
text-align: center;
padding: 12px 8px;
border: 1px solid #e5e7eb;
}
.assessment-standard-table .ant-table-tbody > tr:hover > td {
background-color: #f9fafb;
}
.assessment-standard-table .ant-input {
text-align: center;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 4px 8px;
width: 80px;
}
.assessment-standard-table .ant-input:focus {
border-color: #9ca3af;
box-shadow: none;
}

View File

@ -6,23 +6,51 @@ export default function AssessmentModal() {
<div>
<Modal
title="添加年龄范围"
visible={isAgeModalVisible}
open={isAgeModalVisible}
onOk={ageForm.submit}
onCancel={handleAgeCancel}
>
<Form form={ageForm} onFinish={handleAgeOk}>
<Form.Item name="start" label="起始年龄" rules={[{ required: true }]}>
<Form.Item
name="start"
label="起始年龄"
rules={[
{ required: true },
({ getFieldValue }) => ({
validator(_, value) {
const end = getFieldValue('end');
if (end && value > end) {
return Promise.reject(new Error('起始年龄不能大于结束年龄'));
}
return Promise.resolve();
},
}),
]}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item name="end" label="结束年龄">
<Form.Item
name="end"
label="结束年龄"
rules={[
({ getFieldValue }) => ({
validator(_, value) {
const start = getFieldValue('start');
if (value && start > value) {
return Promise.reject(new Error('结束年龄不能小于起始年龄'));
}
return Promise.resolve();
},
}),
]}
>
<InputNumber min={0} />
</Form.Item>
</Form>
</Modal>
<Modal
title="添加分数与对应标准"
visible={isScoreModalVisible}
open={isScoreModalVisible}
onOk={scoreForm.submit}
onCancel={handleScoreCancel}
>

View File

@ -8,7 +8,7 @@ import React, {
// import { useDebounce } from "use-debounce";
import { Form, FormInstance } from 'antd';
import { api } from "@nice/client";
import { TaxonomySlug } from "packages/common/dist";
import toast from "react-hot-toast";
interface AssessmentStandardContextType {
form: FormInstance;
@ -58,21 +58,36 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
const showAgeModal = () => {
setIsAgeModalVisible(true);
};
// 处理年龄范围模态框确定
const handleAgeOk = (values: any) => {
console.log('values', values)
const { start, end } = values;
if (end && start == end) {
toast.error("年龄范围不能相同");
return;
}
//年龄校验
const isOverlap = ageRanges.some(range => {
if (end) {
return (start >= range.start && start <= (range.end || Infinity)) ||
(end >= range.start && end <= (range.end || Infinity)) ||
(start <= range.start && end >= (range.end || Infinity));
} else {
return start >= range.start && start <= (range.end || Infinity);
}
});
if (isOverlap) {
toast.error("年龄范围不能与已存在的范围重叠");
return;
}
const newRange = {
start,
end,
label: end ? `${start}-${end}` : `${start}岁以上`,
};
setAgeRanges([...ageRanges, newRange]);
setIsAgeModalVisible(false);
};
// 处理年龄范围模态框取消
const handleAgeCancel = () => {
setIsAgeModalVisible(false);
@ -82,7 +97,6 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
const showScoreModal = () => {
setIsScoreModalVisible(true);
};
// 处理分数标准模态框确定
const handleScoreOk = async (values: any) => {
const { score, standards } = values;
@ -98,6 +112,7 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
const handleScoreCancel = () => {
setIsScoreModalVisible(false);
};
return (
<AssessmentStandardContext.Provider
value={{

View File

@ -1,9 +1,9 @@
import { Button, Form, Input, Select, Skeleton } from "antd";
import { Button, Form, Input, Select, Skeleton, Pagination } from "antd";
import { useAssessmentStandardContext } from "./assessment-standard-provider";
import { api, useSport } from "@nice/client";
import toast from "react-hot-toast";
import create from "@ant-design/icons/lib/components/IconFont";
import { useState } from 'react';
const { Search } = Input;
export default function SportCreateContent() {
const { form, sportProjectList, sportProjectLoading } = useAssessmentStandardContext();
const { createSportProject, softDeleteByIds } = useSport();
@ -13,6 +13,9 @@ export default function SportCreateContent() {
title: "体能考核"
}
})
const [searchValue, setSearchValue] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const handleCreateProject = async () => {
if (form.getFieldsValue().createProjectName && form.getFieldsValue().unit) {
await createSportProject.mutateAsync({
@ -37,16 +40,20 @@ export default function SportCreateContent() {
}
}
const handleDeleteProject = async (id: string) => {
console.log(id)
// console.log(id)
await softDeleteByIds.mutateAsync({
ids: [id]
} as any)
toast.success("删除项目成功")
}
const handleSearch = (value) => {
setSearchValue(value);
setCurrentPage(1);
};
return (
<>
<Form form={form} layout="vertical">
<div className="flex items-center space-x-4 w-1/2">
<div className="flex items-center space-x-4 w-1/3">
<Form.Item label="创建项目" name="createProjectName">
<Input placeholder="请输入创建的项目名称" className="mr-2" />
</Form.Item>
@ -64,16 +71,48 @@ export default function SportCreateContent() {
</div>
{sportProjectLoading ?
<Skeleton></Skeleton> :
<div className='w-1/3 my-3 max-h-48 overflow-y-auto'>
<div className='flex my-3 max-h-48 overflow-y-auto'>
{sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => (
<div key={item.id} className='w-full flex justify-between p-4 mt-2 bg-white rounded-md'>
<div className='font-bold'>{item.name}{item.unit}</div>
<span className='text-red-500 cursor-pointer' onClick={() => handleDeleteProject(item.id)}></span>
<div
key={item.id}
className='w-full flex justify-between items-center p-4 my-3 bg-white rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-shadow duration-200'
>
<div className='flex items-center'>
<span className='h-4 w-1 bg-blue-500 rounded-full mr-3'></span>
<div className='font-medium text-gray-800'>
{item.name}
<span className='text-gray-500 text-sm ml-2'>{item.unit}</span>
</div>
</div>
<button
className='text-gray-400 hover:text-red-500 transition-colors duration-200 flex items-center gap-1 px-2 py-1 rounded-md hover:bg-red-50'
onClick={() => handleDeleteProject(item.id)}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span></span>
</button>
</div>
))}
</div>
}
</Form>
{/* <Search
placeholder="搜索..."
allowClear
value={searchValue}
onChange={(e) => {
if (!e.target.value) {
setSearchValue('');
setCurrentPage(1);
} else {
setSearchValue(e.target.value);
}
}}
onSearch={handleSearch}
style={{ width: 200 }}
/> */}
</>
)
}

View File

@ -3,6 +3,7 @@ import { useAssessmentStandardContext } from "./assessment-standard-provider";
import toast from "react-hot-toast";
import { api, useSport } from "@nice/client";
import { useEffect, useMemo } from "react";
import '../../../App.css'
export default function StandardCreateContent() {
const {form,sportProjectList,ageRanges,records,showAgeModal,showScoreModal,setRecords,setAgeRanges,isStandardCreate,setIsStandardCreate} = useAssessmentStandardContext();
const { createSportStandard,updateSportStandard } = useSport();
@ -28,6 +29,9 @@ export default function StandardCreateContent() {
dataIndex: 'score',
key: 'score',
width: 100,
render: (score: number) => (
<span className="font-medium text-gray-700">{score}</span>
)
},
...ageRanges.map((range, index) => ({
title: range.label,
@ -35,7 +39,6 @@ export default function StandardCreateContent() {
key: `standard[${index}]`,
render: (_: any, record: any) => (
<Input
style={{ width: '80px' }}
value={record.standards[index]}
onChange={(e) => {
const inputValue = e.target.value;
@ -67,6 +70,25 @@ export default function StandardCreateContent() {
}
}
const createStandard = async ()=>{
// 新增年龄范围校验
const sortedRanges = [...ageRanges].sort((a, b) => a.start - b.start);
for (let i = 0; i < sortedRanges.length - 1; i++) {
const current = sortedRanges[i];
const next = sortedRanges[i + 1];
// 检查是否有重叠
if ((current.end || Infinity) >= next.start) {
toast.error(`年龄范围 ${current.label}${next.label} 重叠`);
return;
}
// 检查是否有间隔
if ((current.end || Infinity) + 1 !== next.start) {
toast.error(`年龄范围 ${current.label}${next.label} 不连续`);
return;
}
}
const result = await createSportStandard.mutateAsync({
data: {
projectId: form.getFieldsValue().projectId,
@ -80,6 +102,23 @@ export default function StandardCreateContent() {
toast.success("保存标准成功")
}
const updateStandard = async ()=>{
// 新增年龄范围校验
const sortedRanges = [...ageRanges].sort((a, b) => a.start - b.start);
for (let i = 0; i < sortedRanges.length - 1; i++) {
const current = sortedRanges[i];
const next = sortedRanges[i + 1];
if ((current.end || Infinity) >= next.start) {
toast.error(`年龄范围 ${current.label}${next.label} 重叠`);
return;
}
if ((current.end || Infinity) + 1 !== next.start) {
toast.error(`年龄范围 ${current.label}${next.label} 不连续`);
return;
}
}
const result = await updateSportStandard.mutateAsync({
data: {
id: data[0].id,
@ -101,31 +140,36 @@ export default function StandardCreateContent() {
});
useEffect(() => {
if (data && data.length) {
setIsStandardCreate(false)
const records: {
score: number;
standards: number[];
}[] = Object.entries(JSON.parse(String(data[0].scoreTable))).map(([score, standards]) => ({
setIsStandardCreate(false);
const records = Object.entries(JSON.parse(String(data[0].scoreTable))).map(([score, standards]) => ({
score: Number(score),
standards: standards as number[]
}));
setAgeRanges(JSON.parse(String(data[0].ageRanges)))
setRecords(records)
setAgeRanges(JSON.parse(String(data[0].ageRanges)));
setRecords(records);
} else {
// 如果没有找到数据,重置状态
setIsStandardCreate(true);
setAgeRanges([]);
setRecords([]);
}
}, [data])
}, [data, projectId, gender, personType]); // 添加更多依赖项
return (
<Form form={form} layout="vertical">
<Space size="large" className="my-6">
<Form.Item label="项目" name="projectId">
<Select
style={{ width: 200 }}
placeholder="选择考核项目"
options={sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => ({ value: item.id, label: `${item.name}${item.unit}` })) || []}
onChange={() => {
// 重置状态,强制重新加载
setIsStandardCreate(true);
setAgeRanges([]);
setRecords([]);
}}
/>
</Form.Item>
<Form.Item label="性别" name="gender">
<Select
style={{ width: 120 }}
@ -136,7 +180,6 @@ export default function StandardCreateContent() {
]}
/>
</Form.Item>
<Form.Item label="人员类型" name="personType">
<Select
style={{ width: 160 }}
@ -151,14 +194,19 @@ export default function StandardCreateContent() {
<Button onClick={showScoreModal} className="mt-9 ml-2"></Button>
<Button type="primary" onClick={handleSave} className='mt-9 ml-2'></Button>
</Space>
<div className="mt-4">
<Table
columns={columns}
dataSource={records}
bordered
pagination={false}
bordered
rowKey="score"
className="assessment-standard-table"
size="middle"
scroll={{ x: 'max-content' }}
rowClassName={() => "bg-white"}
/>
</div>
</Form>
)
}

View File

@ -0,0 +1,7 @@
export default function CommonAssessmentPage() {
return (
<div>
<h1>CommonAssessmentPage</h1>
</div>
)
}

View File

@ -0,0 +1,141 @@
import { api } from "@nice/client";
import { useMainContext } from "../layout/MainProvider";
import toast from "react-hot-toast";
import { Form, Modal, Select, Radio } from "antd";
import { useState } from "react";
export default function DailyModal() {
const { form, visible, setVisible, editingRecord, setEditingRecord } = useMainContext();
const [submitting, setSubmitting] = useState(false);
// 获取更新和创建的mutation
const updateMutation = api.trainSituation.update.useMutation();
const createMutation = api.trainSituation.create.useMutation();
// 获取员工列表
const { data: staffs } = api.staff.findMany.useQuery({
where: {
deletedAt: null
},
include: {
position: true,
},
});
// console.log('wd', staffs);
// 获取训练内容列表
const { data: trainContents } = api.trainContent.findMany.useQuery({});
const handleOk = async () => {
try {
console.log('handleOk 开始执行');
const values = await form.validateFields();
console.log('表单验证通过,值:', values);
setSubmitting(true);
// 根据 TrainSituation 模型构建数据
const trainData = {
staffId: values.staffId,
trainContentId: values.trainContentId,
mustTrainTime: 1, // 预期训练时间
alreadyTrainTime: values.isPresent ? 1 : 0, // 实际训练时间
value: values.content, // 训练内容描述
score: values.isPresent ? 1 : 0,
};
if (editingRecord?.id) {
await updateMutation.mutateAsync({
where: { id: editingRecord.id },
data: trainData
});
} else {
await createMutation.mutateAsync({
data: trainData
});
}
console.log('操作成功');
toast.success("保存成功");
setVisible(false);
setEditingRecord(null);
form.resetFields();
} catch (error) {
console.error("保存失败:", error);
toast.error("保存失败");
} finally {
setSubmitting(false);
}
};
const handleCancel = () => {
setVisible(false);
setEditingRecord(null);
form.resetFields();
};
// useEffect(() => {
// if (visible && editingRecord) {
// form.setFieldsValue(editingRecord);
// }
// }, [visible, editingRecord]);
return (
<Modal
title="每日训练填报"
open={visible}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={submitting}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="staffId"
label="人员"
rules={[{ required: true, message: '请选择人员' }]}
>
<Select
placeholder="请选择人员"
options={staffs?.map(staff => ({
label: `${staff.showname || staff.username || '未知'} ${staff.positionId || '无职务'})}`,
value: staff.id
}))}
/>
</Form.Item>
{/* <Form.Item
name="position"
label="职务"
>
<Input disabled />
</Form.Item> */}
<Form.Item
name="trainContentId"
label="训练科目"
rules={[{ required: true, message: '请选择训练科目' }]}
>
<Select
placeholder="请选择训练科目"
options={trainContents?.map(item => ({
label: item.title,
value: item.id
}))}
/>
</Form.Item>
{/* <Form.Item
name="content"
label="训练内容"
rules={[{ required: true, message: '请填写训练内容' }]}
>
<Input.TextArea rows={4} placeholder="请输入今日训练内容" />
</Form.Item> */}
<Form.Item
name="isPresent"
label="在位情况"
rules={[{ required: true, message: '请选择在位情况' }]}
initialValue={true}
>
<Radio.Group>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
}

View File

@ -0,0 +1,76 @@
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Button, Form, Input, DatePicker } from "antd";
import { useCallback, useEffect } from "react";
import _ from "lodash";
import { useMainContext } from "../layout/MainProvider";
import DailyTable from "./DailyTable";
import DailyModal from "./DailyModal";
import dayjs from "dayjs";
export default function DailyReport() {
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
useEffect(() => {
setFormValue({
staffId: "",
trainContentId: "",
content: "",
isPresent: true,
date: dayjs().format('YYYY-MM-DD')
});
},);
const handleNew = () => {
form.setFieldsValue(formValue);
setVisible(true);
};
const handleSearch = useCallback(
_.debounce((value: string) => {
setSearchValue(value);
}, 500),
[]
);
const handleDateChange = (date) => {
if (!date) return;
// 更新当前选择的日期
const newFormValue = { ...formValue, date: date.format('YYYY-MM-DD') };
setFormValue(newFormValue);
};
return (
<div className="p-2 min-h-screen bg-gradient-to-br">
<Form>
<div className="p-4 h-full flex flex-col">
<div className="max-w-full mx-auto flex-1 flex flex-col">
<div className="flex justify-between mb-4 space-x-4 items-center">
<div className="text-2xl"></div>
<DatePicker
defaultValue={dayjs()}
onChange={handleDateChange}
allowClear={false}
className="w-40"
/>
<div className="relative w-1/3">
<Input
placeholder="输入姓名搜索"
onChange={(e) => handleSearch(e.target.value)}
className="pl-10 w-full border"
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5" />
</div>
<Button
type="primary"
onClick={handleNew}
className="font-bold py-2 px-4 rounded"
>
</Button>
</div>
<DailyTable></DailyTable>
<DailyModal></DailyModal>
</div>
</div>
</Form>
</div>
);
}

View File

@ -0,0 +1,143 @@
import { Button, Table, Modal } from "antd";
import { api } from "@nice/client";
import { useMainContext } from "../layout/MainProvider";
import dayjs from "dayjs";
import { useState } from "react";
// 定义 editRecord 的类型接口
interface EditRecord {
id: string;
staffId?: string;
staffName?: string;
position?: string;
trainContentId?: string;
content?: string;
isPresent: boolean;
}
export default function DailyTable() {
const { form, setVisible, searchValue, formValue } = useMainContext();
const [editingRecord, setEditingRecord] = useState<EditRecord | null>(null);
const { data: trainSituations, isLoading, refetch } =
api.trainSituation.findMany.useQuery({
where: {
...(searchValue ? {
staff: {
showname: {
contains: searchValue
}
}
} : {})
},
include: {
staff: {
include: {
position: true,
department: true
}
},
trainContent: true,
}
});
// console.log('data', trainSituations);
// 基于日期过滤数据(如果后端不支持日期过滤)
const filteredTrainSituations = trainSituations?.filter(situation => {
// 检查是否有日期条件
if (!formValue.date) return true;
// 将记录日期转换为YYYY-MM-DD格式
const situationDate = dayjs(situation.createdAt).format('YYYY-MM-DD');
return situationDate === formValue.date;
}) || [];
const columns = [
{
title: "人员姓名",
dataIndex: ["staff", "showname"],
key: "showname",
render: (name, record) => name || record.staff?.showname || "未知"
},
{
title: "单位",
dataIndex: ["staff", "department", "name"],
key: "dept",
render: (name) => name || "未知单位"
},
{
title: "职务",
dataIndex: ["staff", "position", "type"],
key: "position",
render: (type, record) => type || "无职务"
},
{
title: "训练内容",
dataIndex: ["trainContent", "title"],
key: "trainContent",
render: (title, record) => title || record.value || "未知内容"
},
{
title: "在位情况",
dataIndex: "alreadyTrainTime",
key: "isPresent",
render: (alreadyTrainTime) => alreadyTrainTime > 0 ? "在位" : "不在位"
},
{
title: "操作",
key: "action",
render: (_, record) => (
<div className="flex space-x-2">
<Button
type="primary"
key={record.id}
onClick={() => handleEdit(record)}
>
</Button>
</div>
),
}
];
const handleEdit = (record: any) => {
console.log(record);
const editRecord: EditRecord = {
id: record.id,
staffId: record.staff?.id,
staffName: record.staff?.showname,
position: record.staff?.position?.type,
trainContentId: record.trainContent?.id,
content: record.value,
isPresent: record.alreadyTrainTime > 0,
};
setEditingRecord(editRecord);
form.setFieldsValue(editRecord);
setVisible(true);
refetch();
};
return (
<>
{isLoading ? (
<div>...</div>
) : (
<Table
columns={columns}
dataSource={filteredTrainSituations}
rowKey="id"
tableLayout="fixed"
pagination={{
position: ["bottomCenter"],
className: "flex justify-center mt-4",
pageSize: 10,
}}
/>
)}
</>
);
}

View File

@ -0,0 +1,6 @@
import React from 'react';
import { Button, Table, Modal } from "antd";
import { api } from "@nice/client";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import dayjs from "dayjs";

View File

@ -1,10 +1,78 @@
import DailyContext from "@web/src/components/models/trainPlan/TrainPlanContext";
import DailyLayout from "@web/src/components/models/trainPlan/TrainPlanLayout";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Button, Form, Input, DatePicker } from "antd";
import { useCallback, useEffect } from "react";
import _ from "lodash";
import { useMainContext } from "../layout/MainProvider";
import DailyTable from "./DailyTable";
import DailyModal from "./DailyModal";
import dayjs from "dayjs";
export default function DailyPage(){
return <>
<DailyContext>
<DailyLayout></DailyLayout>
</DailyContext>
</>
export default function DailyReport() {
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
useEffect(() => {
setFormValue({
staffId: "",
trainContentId: "",
content: "",
isPresent: true,
date: dayjs().format('YYYY-MM-DD')
});
}, []);
const handleNew = () => {
form.setFieldsValue(formValue);
setVisible(true);
};
const handleSearch = useCallback(
_.debounce((value: string) => {
setSearchValue(value);
}, 500),
[]
);
const handleDateChange = (date) => {
if (!date) return;
// 更新当前选择的日期
const newFormValue = { ...formValue, date: date.format('YYYY-MM-DD') };
setFormValue(newFormValue);
};
return (
<div className="p-2 min-h-screen bg-gradient-to-br">
<Form>
<div className="p-4 h-full flex flex-col">
<div className="max-w-full mx-auto flex-1 flex flex-col">
<div className="flex justify-between mb-4 space-x-4 items-center">
<div className="text-2xl"></div>
<DatePicker
defaultValue={dayjs()}
onChange={handleDateChange}
allowClear={false}
className="w-40"
/>
<div className="relative w-1/3">
<Input
placeholder="输入姓名搜索"
onChange={(e) => handleSearch(e.target.value)}
className="pl-10 w-full border"
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5" />
</div>
<Button
type="primary"
onClick={handleNew}
className="font-bold py-2 px-4 rounded"
>
</Button>
</div>
<DailyTable></DailyTable>
<DailyModal></DailyModal>
</div>
</div>
</Form>
</div>
);
}

View File

@ -1,34 +1,642 @@
import { Button } from "antd"
import { api } from "@nice/client"
import React from "react"
import { useSport } from "@nice/client"
export default function Dashboard() {
const { createSportStandard } = useSport()
const handleCreateSportStandard = async () => {
const res = await createSportStandard.mutateAsync({
data: {
projectId: "cm8o6jzp908bp846bv513aqvo",
gender: true,
personType: "STAFF",
ageRanges: [
{ start: null, end: 24, label: "24岁以下" },
{ start: 24, end: 27, label: "25-27岁" },
{ start: 27, end: 30, label: "28-30岁" },
{ start: 30, end: null, label: "30岁以上" }
import React, { useState } from 'react';
import { Card, Row, Col, Divider } from 'antd';
import ReactEcharts from 'echarts-for-react';
const Dashboard = () => {
// 在这里获取你的项目数据
const [data, setData] = useState({
projectInfo: {
name: '训练管理系统',
type: '软件开发',
status: '进行中',
department: '技术研发中心',
client: '软件小组',
supervisor: '项目监理公司',
contractor: '开发团队',
planStartDate: '2023-10-01',
planEndDate: '2024-03-31',
actualStartDate: '2023-10-05',
actualEndDate: '待定'
},
attendance: {
personnel: { present: 15, absent: 5, leave: 2 }
},
training: {
// 参训内容数据
contents: [
{ value: 8, name: '岗位训练' },
{ value: 6, name: '共同科目' },
{ value: 4, name: '通用四项' },
{ value: 2, name: '其他' }
],
scoreTable: {
"100": [85, 81, 79, 77],
"95": [74, 70, 68, 66],
"90": [65, 61, 59, 57]
// 参训时长数据
hours: [
{ value: 25, name: '已完成' },
{ value: 10, name: '进行中' },
{ value: 5, name: '未开始' }
]
},
materials: {
planned: [10, 15, 20],
actual: [5, 8, 12]
}
});
// 人员出勤率图表配置 - 优化样式
const personnelAttendanceOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}人 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 8,
itemHeight: 8,
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '人员出勤率',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff',
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.attendance.personnel.present,
name: '在位',
itemStyle: { color: '#3DD598' }
},
{
value: data.attendance.personnel.absent,
name: '不在位',
itemStyle: { color: '#FFB076' }
},
{
value: data.attendance.personnel.leave,
name: '请假',
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 参训内容饼图配置
const trainingContentsOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}项 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '参训内容',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff'
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.training.contents[0].value,
name: data.training.contents[0].name,
itemStyle: { color: '#4FDFFF' }
},
{
value: data.training.contents[1].value,
name: data.training.contents[1].name,
itemStyle: { color: '#FFCA28' }
},
{
value: data.training.contents[2].value,
name: data.training.contents[2].name,
itemStyle: { color: '#3DD598' }
},
{
value: data.training.contents[3].value,
name: data.training.contents[3].name,
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 参训时长饼图配置
const trainingHoursOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
formatter: '{a} <br/>{b}: {c}小时 ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
textStyle: { color: '#fff', fontSize: 14 }
},
series: [{
name: '参训时长',
type: 'pie',
radius: ['50%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderWidth: 1,
borderColor: 'rgba(32,120,160,0.8)',
},
label: {
show: true,
position: 'inside',
formatter: '{d}%',
fontSize: 10,
color: '#fff'
},
labelLine: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: [
{
value: data.training.hours[0].value,
name: data.training.hours[0].name,
itemStyle: { color: '#4FDFFF' }
},
{
value: data.training.hours[1].value,
name: data.training.hours[1].name,
itemStyle: { color: '#FFB076' }
},
{
value: data.training.hours[2].value,
name: data.training.hours[2].name,
itemStyle: { color: '#FC5A5A' }
}
]
}]
};
// 任务完成情况柱状图配置
const materialsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff' },
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['上半年', '下半年'],
right: 10,
top: 10,
textStyle: {
color: '#fff',
fontSize: 10
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '22%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['任务A', '任务B', '任务C'],
axisLine: {
lineStyle: {
color: 'rgba(255,255,255,0.3)'
}
},
axisLabel: {
color: '#fff',
fontSize: 10
}
},
yAxis: {
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,0.3)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255,255,255,0.1)',
type: 'dashed'
}
},
axisLabel: {
color: '#fff',
fontSize: 10
}
},
series: [
{
name: '上半年',
type: 'bar',
barWidth: '25%',
itemStyle: {
color: '#4FDFFF'
},
emphasis: {
itemStyle: {
color: '#6ab0f0'
}
},
data: data.materials.planned
},
{
name: '下半年',
type: 'bar',
barWidth: '25%',
itemStyle: {
color: '#FFCA28'
},
emphasis: {
itemStyle: {
color: '#FFB75C'
}
},
data: data.materials.actual
}
]
};
// 项目进度甘特图配置
const ganttChartOption = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: function (params) {
if (params.seriesName === '实际完成' || params.seriesName === '计划完成') {
return `${params.name}<br/>${params.seriesName}: ${params.value[0]} ~ ${params.value[1]}`;
}
return params.name;
},
backgroundColor: 'rgba(0,21,41,0.8)',
borderColor: 'rgba(32,120,160,0.8)',
textStyle: { color: '#fff'}
},
legend: {
data: ['计划完成', '实际完成', '超时完成'],
top: 0,
right: 10,
textStyle: { color: '#fff', fontSize: 12 },
itemWidth: 15,
itemHeight: 10
},
grid: {
top: 30,
bottom: 30,
left: 100,
right: 20
},
xAxis: {
type: 'time',
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } },
axisLabel: {
color: '#fff',
fontSize: 10,
formatter: '{yyyy}.{MM}.{dd}',
rotate: 45,
margin: 8,
align: 'center',
interval: 'auto',
hideOverlap: true
},
splitLine: {
show: true,
lineStyle: { color: 'rgba(255,255,255,0.1)', type: 'dashed' }
},
min: '2019-02-01',
max: '2019-04-30'
},
yAxis: {
type: 'category',
data: ['岗位训练', '共同科目', '通用四项'],
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } },
axisLabel: {
color: '#fff',
fontSize: 12,
margin: 20
},
inverse: true
},
series: [
{
name: '计划完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#FFCA28' },
barWidth: 20,
data: [
{
name: '岗位训练',
value: ['2019-03-08', '2019-04-17']
},
{
name: '共同科目',
value: ['2019-02-20', '2019-03-07']
},
{
name: '通用四项',
value: ['2019-02-05', '2019-02-26']
}
]
},
{
name: '实际完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#4FDFFF' },
barWidth: 20,
data: [
{
},
{
name: '共同科目',
value: ['2019-02-20', '2019-03-05']
},
{}
]
},
{
name: '超时完成',
type: 'bar',
stack: 'same',
itemStyle: { color: '#FC5A5A' },
barWidth: 20,
data: [
// 空数据
{},
{},
{
name: '通用四项',
value: ['2019-02-26', '2019-02-27']
}
]
},
// 添加当前日期标记
{
name: '当前日期',
type: 'line',
markLine: {
symbol: ['none', 'none'],
label: {
show: false
},
lineStyle: {
color: '#fff',
type: 'dashed',
width: 1
},
data: [
{
xAxis: '2019-02-27'
}
]
}
}
} as any)
console.log(res)
}
]
};
// 六边形背景SVG
const hexagonBg = `
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="hexagons" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(5)">
<path d="M 0,25 12.5,0 37.5,0 50,25 37.5,50 12.5,50 Z"
stroke="rgba(32,120,160,0.2)" stroke-width="0.5" fill="none"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#hexagons)" />
</svg>
`;
// 卡片通用样式
const cardStyle = {
backgroundColor: 'rgba(0,21,41,0.7)',
color: 'white',
border: '1px solid rgba(32,120,160,0.8)',
boxShadow: '0 0 15px rgba(0,100,200,0.3)'
};
const headerStyle = {
color: '#4FDFFF',
borderBottom: '1px solid rgba(32,120,160,0.8)',
fontSize: '16px',
fontWeight: 'bold'
};
return (
<div >
<Button type="primary" onClick={() => handleCreateSportStandard()}></Button>
<div style={{
padding: '20px',
minHeight: '100vh',
background: 'linear-gradient(135deg, #001529 0%, #003366 100%)',
position: 'relative',
overflow: 'hidden'
}}>
{/* 六边形背景 */}
<div dangerouslySetInnerHTML={{ __html: hexagonBg }} style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 0
}} />
<div style={{ position: 'relative', zIndex: 1 }}>
<h1 style={{
textAlign: 'center',
color: '#4FDFFF',
marginBottom: '20px',
textShadow: '0 0 10px rgba(79,223,255,0.5)'
}}></h1>
<div style={{
position: 'absolute',
top: '20px',
left: '20px',
fontSize: '14px',
color: '#fff'
}}>
{new Date().toLocaleString()} {['日', '一', '二', '三', '四', '五', '六'][new Date().getDay()]}
</div>
)
}
<Row gutter={[16, 16]}>
<Col span={8}>
<Card
title="项目概况"
bordered={false}
style={{...cardStyle,height:'100%'}}
headStyle={headerStyle}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* <div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div> */}
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>: </span>
<span>{data.projectInfo.name}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>: </span>
<span>{data.projectInfo.client}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>:</span>
<span>{data.projectInfo.planStartDate}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}>:</span>
<span>{data.projectInfo.planEndDate}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ width: '100px' }}></span>
</div>
</div>
</Card>
</Col>
<Col span={16}>
<Card
title="项目进度甘特图"
bordered={false}
style={{...cardStyle,height:'100%'}}
headStyle={headerStyle}
>
<ReactEcharts option={ganttChartOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
<Col span={8}>
<Card
title="人员出勤率"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={personnelAttendanceOption} style={{ height: '220px' }} />
</Card>
</Col>
<Col span={8}>
<Card
title="参训内容"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={trainingContentsOption} style={{ height: '220px' }} />
</Card>
</Col>
<Col span={8}>
<Card
title="参训时长"
bordered={false}
style={cardStyle}
headStyle={headerStyle}
>
<ReactEcharts option={trainingHoursOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
<Col span={24}>
<Card
title="任务完成情况"
bordered={false}
style={{ height: '300px', backgroundColor: '#0c2135', color: 'white' }}
headStyle={{ color: 'white', borderBottom: '1px solid #1890ff' }}
>
<ReactEcharts option={materialsOption} style={{ height: '220px' }} />
</Card>
</Col>
</Row>
</div>
</div>
);
};
export default Dashboard;

View File

@ -107,8 +107,7 @@ const NavigationMenu: React.FC = () => {
location.pathname.startsWith("/assessment/") ||
location.pathname === "/plan/weekplan" ||
location.pathname === "/plan/monthplan"
)
{
) {
setSelectedKeys([location.pathname]);
setOpenKeys([location.pathname.split('/').slice(0, 2).join('/')]);
} else if (
@ -123,7 +122,7 @@ const NavigationMenu: React.FC = () => {
}, [location.pathname]);
const hit = (pathname: string): string[] => {
for (let p in children2Parent) {
for (const p in children2Parent) {
if (pathname.search(p) >= 0) {
return children2Parent[p];
}
@ -132,7 +131,7 @@ const NavigationMenu: React.FC = () => {
};
const openKeyMerge = (pathname: string): string[] => {
let newOpenKeys = hit(pathname);
const newOpenKeys = hit(pathname);
for (let i = 0; i < openKeys.length; i++) {
let isIn = false;
for (let j = 0; j < newOpenKeys.length; j++) {
@ -173,14 +172,14 @@ const NavigationMenu: React.FC = () => {
// }, [items, permissions]);
// const checkMenuPermissions = (items: any, permissions: any) => {
// let menus: any = [];
// const menus: any = [];
// if (permissions.length === 0) {
// setActiveMenus(menus);
// return;
// }
// for (let i in items) {
// let menuItem = items[i];
// for (const i in items) {
// const menuItem = items[i];
// // 一级菜单=>没有子菜单&配置了权限
// if (menuItem.children === null) {
// if (
@ -192,10 +191,10 @@ const NavigationMenu: React.FC = () => {
// menus.push(menuItem);
// continue;
// }
// let children = [];
// const children = [];
// for (let j in menuItem.children) {
// let childrenItem = menuItem.children[j];
// for (const j in menuItem.children) {
// const childrenItem = menuItem.children[j];
// if (
// typeof permissions[childrenItem.permission] !== "undefined" ||

View File

@ -0,0 +1,7 @@
export default function MonthPlanPage() {
return (
<div>
<h1>MonthPlanPage</h1>
</div>
)
}

View File

@ -3,7 +3,7 @@ import * as XLSX from 'xlsx'
import { Table, Select, Pagination } from 'antd'
import type { ColumnsType, ColumnType } from 'antd/es/table'
import { UploadOutlined } from '@ant-design/icons'
import React from 'react';
interface TableData {
key: string
[key: string]: any

View File

@ -0,0 +1,7 @@
export default function PositionAssessmentPage() {
return (
<div>
<h1>PositionAssessmentPage</h1>
</div>
)
}

View File

@ -1,7 +1,6 @@
import { Button, Table, Modal, Form, Upload, Skeleton } from "antd";
import { useEffect, useState, } from "react";
import toast from "react-hot-toast";
import _ from "lodash";
import * as XLSX from 'xlsx';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { useSportPageContext } from "./sportPageProvider";
@ -70,12 +69,6 @@ export default function SportPage() {
dataIndex: "unit",
key: "unit",
},
...sportProjectList?.filter(item=>item.deletedAt === null).map((item) => ({
title: item.name,
dataIndex: item.name,
key: item.name,
editable: true,
})),
{
title: "BMI",
dataIndex: "bodyType",
@ -148,37 +141,35 @@ export default function SportPage() {
// 导入成绩
const handleImport = (file) => {
const reader = new FileReader();
// const reader = new FileReader();
// reader.onload = (e) => {
// try {
// const data = new Uint8Array(e.target.result);
// const workbook = XLSX.read(data, { type: 'array' });
// // 获取第一个工作表
// const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// // 转换为JSON
// const jsonData = XLSX.utils.sheet_to_json(worksheet);
// // 添加id字段
// const importedData = jsonData.map((item, index) => ({
// id: sportsData.length + index + 1,
// ...item,
// // 确保totalScore是计算所有项目成绩的和
// totalScore: calculateTotalScore(item)
// }));
// // 更新数据
// setSportsData([...sportsData, ...importedData]);
// toast.success(`成功导入${importedData.length}条记录`);
// } catch (error) {
// console.error('导入失败:', error);
// toast.error("导入失败,请检查文件格式");
// }
// };
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet);
// 添加id字段
const importedData = jsonData.map((item, index) => ({
id: sportsData.length + index + 1,
...item,
// 确保totalScore是计算所有项目成绩的和
totalScore: calculateTotalScore(item)
}));
// reader.readAsArrayBuffer(file);
// 更新数据
setSportsData([...sportsData, ...importedData]);
toast.success(`成功导入${importedData.length}条记录`);
} catch (error) {
console.error('导入失败:', error);
toast.error("导入失败,请检查文件格式");
}
};
reader.readAsArrayBuffer(file);
// 防止自动上传
return false;
// // 防止自动上传
// return false;
};
return (
@ -212,7 +203,6 @@ export default function SportPage() {
</Button>
</div>
</div>
{staffsWithScoreLoading ? (
<Skeleton></Skeleton>
) : (

View File

@ -51,7 +51,7 @@ export default function SportPageModal() {
<Modal
title="添加考核成绩"
onOk={newScoreForm.submit}
visible={isNewScoreModalVisible}
open={isNewScoreModalVisible}
onCancel={handleNewScoreCancel}
>
<Form

View File

@ -18,7 +18,6 @@ export default function StaffModal() {
}
},
}
});
// useEffect(() => {
// traincontents?.forEach((situation)=>{
@ -102,7 +101,7 @@ export default function StaffModal() {
<>
<Modal
title="编辑员工信息"
visible={visible}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
>

View File

@ -1,6 +1,6 @@
import TrainPlanWrite from "./TrainPlanWrite";
export default function DailyLayout(){
return (
<div className="w-full h-[calc(100vh-100px)]">

View File

@ -1,3 +1,3 @@
export default function TrainPlanWrite(){
return <div>TrainPlanWrite</div>
return <div>test</div>
}

View File

@ -17,6 +17,9 @@ import AdminLayout from "../components/layout/admin/AdminLayout";
import { adminRoute } from "./admin-route";
import SportPage from "../app/main/sport/page";
import { SportPageProvider } from "../app/main/sport/sportPageProvider";
import PositionAssessmentPage from "../app/main/positionassessment/page";
import CommonAssessmentPage from "../app/main/commonassessment/page";
import MonthPlanPage from "../app/main/plan/monthplan/page";
interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
@ -25,7 +28,6 @@ interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
breadcrumb?: string;
}
export interface CustomNonIndexRouteObject extends NonIndexRouteObject {
name?: string;
children?: CustomRouteObject[];
@ -68,7 +70,7 @@ export const routes: CustomRouteObject[] = [
},
{
path: "monthplan",
element:<DailyPage></DailyPage>
element: <MonthPlanPage></MonthPlanPage>
}
]
},
@ -81,11 +83,11 @@ export const routes: CustomRouteObject[] = [
children: [
{
path: "positionassessment",
element:<DailyPage></DailyPage>
element: <PositionAssessmentPage></PositionAssessmentPage>
},
{
path: "commonassessment",
element:<DailyPage></DailyPage>
element: <CommonAssessmentPage></CommonAssessmentPage>
},
{
path: "sportsassessment",
@ -100,10 +102,8 @@ export const routes: CustomRouteObject[] = [
element: <AdminLayout></AdminLayout>,
children: adminRoute.children,
}
],
},
],
},

Some files were not shown because too many files have changed in this diff Show More