Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
c4abf0fa68
|
@ -4,16 +4,19 @@ import { AppConfigService } from './app-config.service';
|
||||||
import { z, ZodType } from 'zod';
|
import { z, ZodType } from 'zod';
|
||||||
import { Prisma } from '@nice/common';
|
import { Prisma } from '@nice/common';
|
||||||
import { RealtimeServer } from '@server/socket/realtime/realtime.server';
|
import { RealtimeServer } from '@server/socket/realtime/realtime.server';
|
||||||
const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> = z.any()
|
const AppConfigUncheckedCreateInputSchema: ZodType<Prisma.AppConfigUncheckedCreateInput> =
|
||||||
const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any()
|
z.any();
|
||||||
const AppConfigDeleteManyArgsSchema: ZodType<Prisma.AppConfigDeleteManyArgs> = z.any()
|
const AppConfigUpdateArgsSchema: ZodType<Prisma.AppConfigUpdateArgs> = z.any();
|
||||||
const AppConfigFindFirstArgsSchema: ZodType<Prisma.AppConfigFindFirstArgs> = z.any()
|
const AppConfigDeleteManyArgsSchema: ZodType<Prisma.AppConfigDeleteManyArgs> =
|
||||||
|
z.any();
|
||||||
|
const AppConfigFindFirstArgsSchema: ZodType<Prisma.AppConfigFindFirstArgs> =
|
||||||
|
z.any();
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppConfigRouter {
|
export class AppConfigRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly appConfigService: AppConfigService,
|
private readonly appConfigService: AppConfigService,
|
||||||
private readonly realtimeServer: RealtimeServer
|
private readonly realtimeServer: RealtimeServer,
|
||||||
) {}
|
) {}
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
|
@ -25,23 +28,24 @@ export class AppConfigRouter {
|
||||||
update: this.trpc.protectProcedure
|
update: this.trpc.protectProcedure
|
||||||
.input(AppConfigUpdateArgsSchema)
|
.input(AppConfigUpdateArgsSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.appConfigService.update(input);
|
return await this.appConfigService.update(input);
|
||||||
}),
|
}),
|
||||||
deleteMany: this.trpc.protectProcedure.input(AppConfigDeleteManyArgsSchema).mutation(async ({ input }) => {
|
deleteMany: this.trpc.protectProcedure
|
||||||
return await this.appConfigService.deleteMany(input)
|
.input(AppConfigDeleteManyArgsSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await this.appConfigService.deleteMany(input);
|
||||||
}),
|
}),
|
||||||
findFirst: this.trpc.protectProcedure.input(AppConfigFindFirstArgsSchema).
|
findFirst: this.trpc.protectProcedure
|
||||||
query(async ({ input }) => {
|
.input(AppConfigFindFirstArgsSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
return await this.appConfigService.findFirst(input)
|
return await this.appConfigService.findFirst(input);
|
||||||
}),
|
}),
|
||||||
clearRowCache: this.trpc.protectProcedure.mutation(async () => {
|
clearRowCache: this.trpc.protectProcedure.mutation(async () => {
|
||||||
return await this.appConfigService.clearRowCache()
|
return await this.appConfigService.clearRowCache();
|
||||||
}),
|
}),
|
||||||
getClientCount: this.trpc.protectProcedure.query(() => {
|
getClientCount: this.trpc.protectProcedure.query(() => {
|
||||||
return this.realtimeServer.getClientCount()
|
return this.realtimeServer.getClientCount();
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import { db, ObjectType, Prisma } from '@nice/common';
|
||||||
db,
|
|
||||||
ObjectType,
|
|
||||||
Prisma,
|
|
||||||
} from '@nice/common';
|
|
||||||
|
|
||||||
|
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
import { deleteByPattern } from '@server/utils/redis/utils';
|
import { deleteByPattern } from '@server/utils/redis/utils';
|
||||||
|
@ -12,10 +7,10 @@ import { deleteByPattern } from '@server/utils/redis/utils';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppConfigService extends BaseService<Prisma.AppConfigDelegate> {
|
export class AppConfigService extends BaseService<Prisma.AppConfigDelegate> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db, "appConfig");
|
super(db, 'appConfig');
|
||||||
}
|
}
|
||||||
async clearRowCache() {
|
async clearRowCache() {
|
||||||
await deleteByPattern("row-*")
|
await deleteByPattern('row-*');
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import {
|
import { ObjectType, RoleMapMethodSchema } from '@nice/common';
|
||||||
ObjectType,
|
|
||||||
RoleMapMethodSchema,
|
|
||||||
} from '@nice/common';
|
|
||||||
import { RoleMapService } from './rolemap.service';
|
import { RoleMapService } from './rolemap.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -67,5 +64,10 @@ export class RoleMapRouter {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return this.roleMapService.getStaffsNotMap(input);
|
return this.roleMapService.getStaffsNotMap(input);
|
||||||
}),
|
}),
|
||||||
|
getStaffIdsByRoleNames: this.trpc.procedure
|
||||||
|
.input(RoleMapMethodSchema.getStaffIdsByRoleNames)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return this.roleMapService.getStaffIdsByRoleNames(input);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class RoleMapService extends RowModelService {
|
||||||
) {
|
) {
|
||||||
const { roleId, domainId } = request;
|
const { roleId, domainId } = request;
|
||||||
// Base conditions
|
// Base conditions
|
||||||
let condition = super.createGetRowsFilters(request, staff);
|
const condition = super.createGetRowsFilters(request, staff);
|
||||||
if (isFieldCondition(condition)) return;
|
if (isFieldCondition(condition)) return;
|
||||||
// Adding conditions based on parameters existence
|
// Adding conditions based on parameters existence
|
||||||
if (roleId) {
|
if (roleId) {
|
||||||
|
@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService {
|
||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getRowDto(
|
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
||||||
row: any,
|
|
||||||
staff?: UserProfile,
|
|
||||||
): Promise<any> {
|
|
||||||
if (!row.id) return row;
|
if (!row.id) return row;
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService {
|
||||||
data: roleMaps,
|
data: roleMaps,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const wrapResult = Promise.all(result.map(async item => {
|
const wrapResult = Promise.all(
|
||||||
|
result.map(async (item) => {
|
||||||
const staff = await db.staff.findMany({
|
const staff = await db.staff.findMany({
|
||||||
include: { department: true },
|
include: { department: true },
|
||||||
where: {
|
where: {
|
||||||
id: item.objectId
|
id: item.objectId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
return { ...item, staff }
|
return { ...item, staff };
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
return wrapResult;
|
return wrapResult;
|
||||||
}
|
}
|
||||||
async addRoleForObjects(
|
async addRoleForObjects(
|
||||||
|
@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService {
|
||||||
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
||||||
return { items, totalCount };
|
return { items, totalCount };
|
||||||
}
|
}
|
||||||
async getStaffsNotMap(data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>) {
|
async getStaffsNotMap(
|
||||||
|
data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>,
|
||||||
|
) {
|
||||||
const { domainId, roleId } = data;
|
const { domainId, roleId } = data;
|
||||||
let staffs = await db.staff.findMany({
|
let staffs = await db.staff.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -280,6 +281,35 @@ export class RoleMapService extends RowModelService {
|
||||||
);
|
);
|
||||||
return staffs;
|
return staffs;
|
||||||
}
|
}
|
||||||
|
async getStaffIdsByRoleNames(
|
||||||
|
data: z.infer<typeof RoleMapMethodSchema.getStaffIdsByRoleNames>,
|
||||||
|
) {
|
||||||
|
const { roleNames } = data;
|
||||||
|
const roles = await db.role.findMany({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
in: roleNames,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const roleMaps = await db.roleMap.findMany({
|
||||||
|
where: {
|
||||||
|
roleId: {
|
||||||
|
in: roles.map((role) => role.id),
|
||||||
|
},
|
||||||
|
objectType: ObjectType.STAFF,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
objectId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const staffIds = roleMaps.map((roleMap) => roleMap.objectId);
|
||||||
|
return staffIds;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 更新角色映射
|
* 更新角色映射
|
||||||
* @param data 包含更新信息的数据
|
* @param data 包含更新信息的数据
|
||||||
|
@ -300,7 +330,9 @@ export class RoleMapService extends RowModelService {
|
||||||
* @param data 包含角色ID和域ID的数据
|
* @param data 包含角色ID和域ID的数据
|
||||||
* @returns 角色映射详情,包含部门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 { roleId, domainId } = data;
|
||||||
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,17 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||||
import { ResourceService } from './resource.service';
|
import { ResourceService } from './resource.service';
|
||||||
import { z, ZodType } from 'zod';
|
import { z, ZodType } from 'zod';
|
||||||
const ResourceCreateArgsSchema: ZodType<Prisma.ResourceCreateArgs> = z.any()
|
const ResourceCreateArgsSchema: ZodType<Prisma.ResourceCreateArgs> = z.any();
|
||||||
const ResourceCreateManyInputSchema: ZodType<Prisma.ResourceCreateManyInput> = z.any()
|
const ResourceCreateManyInputSchema: ZodType<Prisma.ResourceCreateManyInput> =
|
||||||
const ResourceDeleteManyArgsSchema: ZodType<Prisma.ResourceDeleteManyArgs> = z.any()
|
z.any();
|
||||||
const ResourceFindManyArgsSchema: ZodType<Prisma.ResourceFindManyArgs> = z.any()
|
const ResourceDeleteManyArgsSchema: ZodType<Prisma.ResourceDeleteManyArgs> =
|
||||||
const ResourceFindFirstArgsSchema: ZodType<Prisma.ResourceFindFirstArgs> = z.any()
|
z.any();
|
||||||
const ResourceWhereInputSchema: ZodType<Prisma.ResourceWhereInput> = z.any()
|
const ResourceFindManyArgsSchema: ZodType<Prisma.ResourceFindManyArgs> =
|
||||||
const ResourceSelectSchema: ZodType<Prisma.ResourceSelect> = z.any()
|
z.any();
|
||||||
|
const ResourceFindFirstArgsSchema: ZodType<Prisma.ResourceFindFirstArgs> =
|
||||||
|
z.any();
|
||||||
|
const ResourceWhereInputSchema: ZodType<Prisma.ResourceWhereInput> = z.any();
|
||||||
|
const ResourceSelectSchema: ZodType<Prisma.ResourceSelect> = z.any();
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceRouter {
|
export class ResourceRouter {
|
||||||
|
@ -24,7 +28,8 @@ export class ResourceRouter {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.resourceService.create(input, { staff });
|
return await this.resourceService.create(input, { staff });
|
||||||
}),
|
}),
|
||||||
createMany: this.trpc.protectProcedure.input(z.array(ResourceCreateManyInputSchema))
|
createMany: this.trpc.protectProcedure
|
||||||
|
.input(z.array(ResourceCreateManyInputSchema))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
|
|
||||||
|
@ -56,12 +61,14 @@ export class ResourceRouter {
|
||||||
return await this.resourceService.findMany(input);
|
return await this.resourceService.findMany(input);
|
||||||
}),
|
}),
|
||||||
findManyWithCursor: this.trpc.protectProcedure
|
findManyWithCursor: this.trpc.protectProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
cursor: z.any().nullish(),
|
cursor: z.any().nullish(),
|
||||||
take: z.number().nullish(),
|
take: z.number().nullish(),
|
||||||
where: ResourceWhereInputSchema.nullish(),
|
where: ResourceWhereInputSchema.nullish(),
|
||||||
select: ResourceSelectSchema.nullish()
|
select: ResourceSelectSchema.nullish(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.resourceService.findManyWithCursor(input);
|
return await this.resourceService.findManyWithCursor(input);
|
||||||
|
|
|
@ -187,6 +187,13 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
visitType: VisitType.LIKE,
|
visitType: VisitType.LIKE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (args.where.type === VisitType.HATE) {
|
||||||
|
EventBus.emit('updateVisitCount', {
|
||||||
|
objectType: ObjectType.POST,
|
||||||
|
id: args?.where?.postId as string,
|
||||||
|
visitType: VisitType.HATE,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return superDetele;
|
return superDetele;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { db, PostState, PostType, VisitType } from '@nice/common';
|
import { db, PostState, PostType, VisitType } from '@nice/common';
|
||||||
export async function updatePostViewCount(id: string, type: VisitType) {
|
export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
|
console.log('updatePostViewCount', type);
|
||||||
const totalViews = await db.visit.aggregate({
|
const totalViews = await db.visit.aggregate({
|
||||||
_sum: {
|
_sum: {
|
||||||
views: true,
|
views: true,
|
||||||
|
@ -19,7 +20,6 @@ export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (type === VisitType.LIKE) {
|
} else if (type === VisitType.LIKE) {
|
||||||
console.log('totalViews._sum.view', totalViews._sum.views);
|
|
||||||
await db.post.update({
|
await db.post.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
|
@ -28,5 +28,14 @@ export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
likes: totalViews._sum.views || 0, // Use 0 if no visits exist
|
likes: totalViews._sum.views || 0, // Use 0 if no visits exist
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (type === VisitType.HATE) {
|
||||||
|
await db.post.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
hates: totalViews._sum.views || 0, // Use 0 if no visits exist
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { updatePostViewCount } from '../models/post/utils';
|
||||||
const logger = new Logger('QueueWorker');
|
const logger = new Logger('QueueWorker');
|
||||||
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
||||||
await updatePostViewCount(job.data.id, job.data.type);
|
await updatePostViewCount(job.data.id, job.data.type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,7 @@ import {
|
||||||
Term,
|
Term,
|
||||||
} from '@nice/common';
|
} from '@nice/common';
|
||||||
import EventBus from '@server/utils/event-bus';
|
import EventBus from '@server/utils/event-bus';
|
||||||
import {
|
import { capitalizeFirstLetter, DevDataCounts, getCounts } from './utils';
|
||||||
|
|
||||||
capitalizeFirstLetter,
|
|
||||||
DevDataCounts,
|
|
||||||
getCounts,
|
|
||||||
} from './utils';
|
|
||||||
import { StaffService } from '@server/models/staff/staff.service';
|
import { StaffService } from '@server/models/staff/staff.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GenDevService {
|
export class GenDevService {
|
||||||
|
@ -26,7 +21,7 @@ export class GenDevService {
|
||||||
deptStaffRecord: Record<string, Staff[]> = {};
|
deptStaffRecord: Record<string, Staff[]> = {};
|
||||||
terms: Record<TaxonomySlug, Term[]> = {
|
terms: Record<TaxonomySlug, Term[]> = {
|
||||||
[TaxonomySlug.CATEGORY]: [],
|
[TaxonomySlug.CATEGORY]: [],
|
||||||
[TaxonomySlug.TAG]: []
|
[TaxonomySlug.TAG]: [],
|
||||||
};
|
};
|
||||||
depts: Department[] = [];
|
depts: Department[] = [];
|
||||||
domains: Department[] = [];
|
domains: Department[] = [];
|
||||||
|
@ -47,7 +42,6 @@ export class GenDevService {
|
||||||
await this.generateDepartments(3, 6);
|
await this.generateDepartments(3, 6);
|
||||||
await this.generateTerms(1, 3);
|
await this.generateTerms(1, 3);
|
||||||
await this.generateStaffs(4);
|
await this.generateStaffs(4);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
|
@ -164,8 +158,8 @@ export class GenDevService {
|
||||||
showname: username,
|
showname: username,
|
||||||
username: username,
|
username: username,
|
||||||
deptId: dept.id,
|
deptId: dept.id,
|
||||||
domainId: domain.id
|
domainId: domain.id,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
// Update both deptStaffRecord and staffs array
|
// Update both deptStaffRecord and staffs array
|
||||||
this.deptStaffRecord[dept.id].push(staff);
|
this.deptStaffRecord[dept.id].push(staff);
|
||||||
|
@ -190,7 +184,7 @@ export class GenDevService {
|
||||||
name,
|
name,
|
||||||
isDomain: currentDepth === 1 ? true : false,
|
isDomain: currentDepth === 1 ? true : false,
|
||||||
parentId,
|
parentId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return department;
|
return department;
|
||||||
}
|
}
|
||||||
|
@ -208,7 +202,9 @@ export class GenDevService {
|
||||||
throw new Error(`Taxonomy with slug ${taxonomySlug} not found`);
|
throw new Error(`Taxonomy with slug ${taxonomySlug} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Creating terms for taxonomy: ${taxonomy.name} (${taxonomy.slug})`);
|
this.logger.log(
|
||||||
|
`Creating terms for taxonomy: ${taxonomy.name} (${taxonomy.slug})`,
|
||||||
|
);
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
const createTermTree = async (
|
const createTermTree = async (
|
||||||
parentId: string | null,
|
parentId: string | null,
|
||||||
|
@ -223,7 +219,7 @@ export class GenDevService {
|
||||||
taxonomyId: taxonomy!.id,
|
taxonomyId: taxonomy!.id,
|
||||||
domainId: domain?.id,
|
domainId: domain?.id,
|
||||||
parentId,
|
parentId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.terms[taxonomySlug].push(newTerm);
|
this.terms[taxonomySlug].push(newTerm);
|
||||||
await createTermTree(newTerm.id, currentDepth + 1);
|
await createTermTree(newTerm.id, currentDepth + 1);
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { WebSocketModule } from '@server/socket/websocket.module';
|
||||||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||||
import { TransformModule } from '@server/models/transform/transform.module';
|
import { TransformModule } from '@server/models/transform/transform.module';
|
||||||
|
|
||||||
|
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
@ -30,6 +32,7 @@ import { TransformModule } from '@server/models/transform/transform.module';
|
||||||
PostModule,
|
PostModule,
|
||||||
VisitModule,
|
VisitModule,
|
||||||
WebSocketModule,
|
WebSocketModule,
|
||||||
|
ResourceModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
@ -13,10 +13,10 @@ import { VisitRouter } from '@server/models/visit/visit.router';
|
||||||
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
||||||
import { TransformRouter } from '@server/models/transform/transform.router';
|
import { TransformRouter } from '@server/models/transform/transform.router';
|
||||||
import { RoleRouter } from '@server/models/rbac/role.router';
|
import { RoleRouter } from '@server/models/rbac/role.router';
|
||||||
|
import { ResourceRouter } from '../models/resource/resource.router';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
|
|
||||||
logger = new Logger(TrpcRouter.name);
|
logger = new Logger(TrpcRouter.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
|
@ -31,10 +31,10 @@ export class TrpcRouter {
|
||||||
private readonly app_config: AppConfigRouter,
|
private readonly app_config: AppConfigRouter,
|
||||||
private readonly message: MessageRouter,
|
private readonly message: MessageRouter,
|
||||||
private readonly visitor: VisitRouter,
|
private readonly visitor: VisitRouter,
|
||||||
|
private readonly resource: ResourceRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
appRouter = this.trpc.router({
|
appRouter = this.trpc.router({
|
||||||
transform: this.transform.router,
|
transform: this.transform.router,
|
||||||
|
@ -48,6 +48,7 @@ export class TrpcRouter {
|
||||||
message: this.message.router,
|
message: this.message.router,
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
visitor: this.visitor.router,
|
visitor: this.visitor.router,
|
||||||
|
resource: this.resource.router,
|
||||||
});
|
});
|
||||||
wss: WebSocketServer = undefined;
|
wss: WebSocketServer = undefined;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { TrpcRouter } from "./trpc.router";
|
import { TrpcRouter } from './trpc.router';
|
||||||
|
|
||||||
export type AppRouter = TrpcRouter[`appRouter`];
|
export type AppRouter = TrpcRouter[`appRouter`];
|
||||||
|
|
|
@ -1,32 +1,25 @@
|
||||||
import {
|
import { AppConfigSlug, BaseSetting, RolePerms } from "@nice/common";
|
||||||
AppConfigSlug,
|
|
||||||
BaseSetting,
|
|
||||||
RolePerms,
|
|
||||||
} from "@nice/common";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import {
|
import { Button, Form, Input, message, theme } from "antd";
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
theme,
|
|
||||||
} from "antd";
|
|
||||||
import { useAppConfig } from "@nice/client";
|
import { useAppConfig } from "@nice/client";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { useForm } from "antd/es/form/Form";
|
import { useForm } from "antd/es/form/Form";
|
||||||
import { api } from "@nice/client"
|
import { api } from "@nice/client";
|
||||||
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||||
|
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
||||||
|
|
||||||
export default function BaseSettingPage() {
|
export default function BaseSettingPage() {
|
||||||
const { update, baseSetting } = useAppConfig();
|
const { update, baseSetting } = useAppConfig();
|
||||||
const utils = api.useUtils()
|
const utils = api.useUtils();
|
||||||
const [form] = useForm()
|
const [form] = useForm();
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const { data: clientCount } = api.app_config.getClientCount.useQuery(undefined, {
|
const { data: clientCount } = api.app_config.getClientCount.useQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
refetchInterval: 3000,
|
refetchInterval: 3000,
|
||||||
refetchIntervalInBackground: true
|
refetchIntervalInBackground: true,
|
||||||
})
|
}
|
||||||
|
);
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false);
|
const [isFormChanged, setIsFormChanged] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { user, hasSomePermissions } = useAuth();
|
const { user, hasSomePermissions } = useAuth();
|
||||||
|
@ -34,31 +27,27 @@ export default function BaseSettingPage() {
|
||||||
setIsFormChanged(true);
|
setIsFormChanged(true);
|
||||||
}
|
}
|
||||||
function onResetClick() {
|
function onResetClick() {
|
||||||
if (!form)
|
if (!form) return;
|
||||||
return
|
|
||||||
if (!baseSetting) {
|
if (!baseSetting) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue(baseSetting);
|
form.setFieldsValue(baseSetting);
|
||||||
|
|
||||||
}
|
}
|
||||||
setIsFormChanged(false);
|
setIsFormChanged(false);
|
||||||
}
|
}
|
||||||
function onSaveClick() {
|
function onSaveClick() {
|
||||||
if (form)
|
if (form) form.submit();
|
||||||
form.submit();
|
|
||||||
}
|
}
|
||||||
async function onSubmit(values: BaseSetting) {
|
async function onSubmit(values: BaseSetting) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await update.mutateAsync({
|
await update.mutateAsync({
|
||||||
where: {
|
where: {
|
||||||
slug: AppConfigSlug.BASE_SETTING,
|
slug: AppConfigSlug.BASE_SETTING,
|
||||||
},
|
},
|
||||||
data: { meta: JSON.stringify(values) }
|
data: { meta: JSON.stringify(values) },
|
||||||
});
|
});
|
||||||
setIsFormChanged(false);
|
setIsFormChanged(false);
|
||||||
message.success("已保存");
|
message.success("已保存");
|
||||||
|
@ -70,7 +59,6 @@ export default function BaseSettingPage() {
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (baseSetting && form) {
|
if (baseSetting && form) {
|
||||||
|
|
||||||
form.setFieldsValue(baseSetting);
|
form.setFieldsValue(baseSetting);
|
||||||
}
|
}
|
||||||
}, [baseSetting, form]);
|
}, [baseSetting, form]);
|
||||||
|
@ -101,7 +89,6 @@ export default function BaseSettingPage() {
|
||||||
!hasSomePermissions(RolePerms.MANAGE_BASE_SETTING)
|
!hasSomePermissions(RolePerms.MANAGE_BASE_SETTING)
|
||||||
}
|
}
|
||||||
onFinish={onSubmit}
|
onFinish={onSubmit}
|
||||||
|
|
||||||
onFieldsChange={handleFieldsChange}
|
onFieldsChange={handleFieldsChange}
|
||||||
layout="vertical">
|
layout="vertical">
|
||||||
{/* <div
|
{/* <div
|
||||||
|
@ -127,6 +114,17 @@ export default function BaseSettingPage() {
|
||||||
<Input></Input>
|
<Input></Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-2 grid grid-cols-8 gap-2 border-b">
|
||||||
|
<Form.Item
|
||||||
|
label="网站logo"
|
||||||
|
name={["appConfig", "logo"]}>
|
||||||
|
<AvatarUploader
|
||||||
|
style={{
|
||||||
|
width: 192,
|
||||||
|
height: 108,
|
||||||
|
}}></AvatarUploader>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
{/* <div
|
{/* <div
|
||||||
className="p-2 border-b flex items-center justify-between"
|
className="p-2 border-b flex items-center justify-between"
|
||||||
style={{
|
style={{
|
||||||
|
@ -171,7 +169,8 @@ export default function BaseSettingPage() {
|
||||||
清除行模型缓存
|
清除行模型缓存
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{<div
|
{
|
||||||
|
<div
|
||||||
className="p-2 border-b text-primary flex justify-between items-center"
|
className="p-2 border-b text-primary flex justify-between items-center"
|
||||||
style={{
|
style={{
|
||||||
fontSize: token.fontSize,
|
fontSize: token.fontSize,
|
||||||
|
@ -179,9 +178,12 @@ export default function BaseSettingPage() {
|
||||||
}}>
|
}}>
|
||||||
<span>app在线人数</span>
|
<span>app在线人数</span>
|
||||||
<div>
|
<div>
|
||||||
{clientCount && clientCount > 0 ? `${clientCount}人在线` : '无人在线'}
|
{clientCount && clientCount > 0
|
||||||
|
? `${clientCount}人在线`
|
||||||
|
: "无人在线"}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -134,12 +134,12 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入密码" },
|
{ required: true, message: "请输入密码" },
|
||||||
{ min: 8, message: "密码至少需要8个字符" },
|
{ min: 8, message: "密码至少需要8个字符" },
|
||||||
{
|
// {
|
||||||
pattern:
|
// pattern:
|
||||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
// /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
||||||
message:
|
// message:
|
||||||
"密码必须包含大小写字母、数字和特殊字符",
|
// "密码必须包含大小写字母、数字和特殊字符",
|
||||||
},
|
// },
|
||||||
]}>
|
]}>
|
||||||
<Input.Password placeholder="密码" />
|
<Input.Password placeholder="密码" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -150,13 +150,13 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
label="证件号"
|
label="证件号"
|
||||||
noStyle
|
noStyle
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入证件号" },
|
{ message: "请输入证件号" },
|
||||||
{
|
{
|
||||||
pattern: /^\d{5,12}$/,
|
pattern: /^\d{5,12}$/,
|
||||||
message: "请输入有效的证件号(5-12位数字)",
|
message: "请输入有效的证件号(5-12位数字)",
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Input placeholder="证件号(可选)" />
|
<Input placeholder="证件号" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item noStyle name={"email"}>
|
<Form.Item noStyle name={"email"}>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default function LetterEditorPage() {
|
||||||
const termId = searchParams.get("termId");
|
const termId = searchParams.get("termId");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen rounded-xl shadow-elegant border-2 border-white bg-gradient-to-b from-slate-100 to-slate-50 ">
|
<div className="shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
||||||
<WriteHeader></WriteHeader>
|
<WriteHeader></WriteHeader>
|
||||||
<LetterFormProvider receiverId={receiverId} termId={termId}>
|
<LetterFormProvider receiverId={receiverId} termId={termId}>
|
||||||
<LetterBasicForm />
|
<LetterBasicForm />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import DepartmentSelect from "@web/src/components/models/department/department-s
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { SearchOutlined } from "@ant-design/icons";
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
import WriteHeader from "./WriteHeader";
|
import WriteHeader from "./WriteHeader";
|
||||||
|
import { ObjectType, RoleName } from "@nice/common";
|
||||||
|
|
||||||
export default function WriteLetterPage() {
|
export default function WriteLetterPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
@ -18,12 +19,23 @@ export default function WriteLetterPage() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const { getTerm } = useTerm();
|
const { getTerm } = useTerm();
|
||||||
|
const { data: enabledStaffIds, isLoading: roleMapIsLoading } =
|
||||||
|
api.rolemap.getStaffIdsByRoleNames.useQuery({
|
||||||
|
roleNames: [RoleName.Leader, RoleName.Organization],
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const { data, isLoading, error } =
|
const { data, isLoading, error } =
|
||||||
api.staff.findManyWithPagination.useQuery({
|
api.staff.findManyWithPagination.useQuery(
|
||||||
|
{
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
where: {
|
where: {
|
||||||
|
id: enabledStaffIds
|
||||||
|
? {
|
||||||
|
in: enabledStaffIds,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
deptId: selectedDept,
|
deptId: selectedDept,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
|
@ -36,13 +48,22 @@ export default function WriteLetterPage() {
|
||||||
contains: searchQuery,
|
contains: searchQuery,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
path: ["rank"], // 指定 JSON 字段的路径
|
||||||
|
string_contains: searchQuery, // 对 rank 字段进行模糊搜索
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: "desc",
|
order: "asc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !roleMapIsLoading,
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const resetPage = useCallback(() => {
|
const resetPage = useCallback(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { env } from "@web/src/env";
|
||||||
import { message, Progress, Spin, theme } from "antd";
|
import { message, Progress, Spin, theme } from "antd";
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
|
import { Avatar } from "antd/lib";
|
||||||
|
|
||||||
export interface AvatarUploaderProps {
|
export interface AvatarUploaderProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -16,6 +17,7 @@ interface UploadingFile {
|
||||||
progress: number;
|
progress: number;
|
||||||
status: "uploading" | "done" | "error";
|
status: "uploading" | "done" | "error";
|
||||||
fileId?: string;
|
fileId?: string;
|
||||||
|
url?: string;
|
||||||
fileKey?: string;
|
fileKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
const [file, setFile] = useState<UploadingFile | null>(null);
|
const [file, setFile] = useState<UploadingFile | null>(null);
|
||||||
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
const [previewUrl, setPreviewUrl] = useState<string>(value || "");
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
@ -56,7 +59,9 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
progress: 100,
|
progress: 100,
|
||||||
status: "done",
|
status: "done",
|
||||||
fileId: result.fileId,
|
fileId: result.fileId,
|
||||||
|
url: result?.url,
|
||||||
}));
|
}));
|
||||||
|
setPreviewUrl(result?.url);
|
||||||
resolve(result.fileId);
|
resolve(result.fileId);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -65,7 +70,6 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
file?.fileKey
|
file?.fileKey
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`);
|
|
||||||
onChange?.(fileId);
|
onChange?.(fileId);
|
||||||
message.success("头像上传成功");
|
message.success("头像上传成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -98,9 +102,9 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
/>
|
/>
|
||||||
{previewUrl ? (
|
{previewUrl ? (
|
||||||
<img
|
<Avatar
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
alt="Avatar"
|
shape="square"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -4,14 +4,16 @@ import {
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Upload, message, Progress, Button } from "antd";
|
import { Upload, Progress, Button } from "antd";
|
||||||
import type { UploadFile } from "antd";
|
import type { UploadFile } from "antd";
|
||||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export interface TusUploaderProps {
|
export interface TusUploaderProps {
|
||||||
value?: string[];
|
value?: string[];
|
||||||
onChange?: (value: string[]) => void;
|
onChange?: (value: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadingFile {
|
interface UploadingFile {
|
||||||
name: string;
|
name: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
|
@ -32,98 +34,82 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
fileId,
|
fileId,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
|
// 恢复使用 uploadResults 状态跟踪最新结果
|
||||||
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
|
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
|
||||||
|
|
||||||
const handleRemoveFile = useCallback(
|
const handleRemoveFile = useCallback(
|
||||||
(fileId: string) => {
|
(fileId: string) => {
|
||||||
setCompletedFiles((prev) =>
|
setCompletedFiles((prev) =>
|
||||||
prev.filter((f) => f.fileId !== fileId)
|
prev.filter((f) => f.fileId !== fileId)
|
||||||
);
|
);
|
||||||
const newResults = uploadResults.filter((id) => id !== fileId);
|
// 使用函数式更新保证获取最新状态
|
||||||
setUploadResults(newResults);
|
setUploadResults((prev) => {
|
||||||
onChange?.(newResults);
|
const newValue = prev.filter((id) => id !== fileId);
|
||||||
|
onChange?.(newValue); // 同步更新父组件
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[uploadResults, onChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleBeforeUpload = useCallback(
|
||||||
async (fileList: UploadFile | UploadFile[]) => {
|
(file: File) => {
|
||||||
const files = Array.isArray(fileList) ? fileList : [fileList];
|
const fileKey = `${file.name}-${Date.now()}`;
|
||||||
console.log("文件", files);
|
|
||||||
|
|
||||||
if (!files.every((f) => f instanceof File)) {
|
setUploadingFiles((prev) => [
|
||||||
message.error("无效的文件格式");
|
...prev,
|
||||||
return false;
|
{
|
||||||
}
|
name: file.name,
|
||||||
|
|
||||||
const newFiles: UploadingFile[] = files.map((f) => ({
|
|
||||||
name: f.name,
|
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: "uploading" as const,
|
status: "uploading",
|
||||||
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
|
fileKey,
|
||||||
}));
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
setUploadingFiles((prev) => [...prev, ...newFiles]);
|
|
||||||
|
|
||||||
const newUploadResults: string[] = [];
|
|
||||||
try {
|
|
||||||
for (const [index, f] of files.entries()) {
|
|
||||||
if (!f) {
|
|
||||||
throw new Error(`文件 ${f.name} 无效`);
|
|
||||||
}
|
|
||||||
const fileKey = newFiles[index].fileKey!;
|
|
||||||
const fileId = await new Promise<string>(
|
|
||||||
(resolve, reject) => {
|
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
f as File,
|
file,
|
||||||
(result) => {
|
(result) => {
|
||||||
console.log("上传成功:", result);
|
|
||||||
const completedFile = {
|
|
||||||
name: f.name,
|
|
||||||
progress: 100,
|
|
||||||
status: "done" as const,
|
|
||||||
fileId: result.fileId,
|
|
||||||
};
|
|
||||||
setCompletedFiles((prev) => [
|
setCompletedFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
completedFile,
|
{
|
||||||
|
name: file.name,
|
||||||
|
progress: 100,
|
||||||
|
status: "done",
|
||||||
|
fileId: result.fileId,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setUploadingFiles((prev) =>
|
setUploadingFiles((prev) =>
|
||||||
prev.filter(
|
prev.filter((f) => f.fileKey !== fileKey)
|
||||||
(file) => file.fileKey !== fileKey
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
resolve(result.fileId);
|
|
||||||
|
// 正确的状态更新方式
|
||||||
|
setUploadResults((prev) => {
|
||||||
|
const newValue = [...prev, result.fileId];
|
||||||
|
onChange?.(newValue); // 传递值而非函数
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error("上传错误:", error);
|
console.error("上传错误:", error);
|
||||||
reject(error);
|
toast.error(
|
||||||
|
`上传失败: ${
|
||||||
|
error instanceof Error ? error.message : "未知错误"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
setUploadingFiles((prev) =>
|
||||||
|
prev.map((f) =>
|
||||||
|
f.fileKey === fileKey
|
||||||
|
? { ...f, status: "error" }
|
||||||
|
: f
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
fileKey
|
fileKey
|
||||||
);
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
newUploadResults.push(fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = Array.from(
|
|
||||||
new Set([...uploadResults, ...newUploadResults])
|
|
||||||
);
|
|
||||||
setUploadResults(newValue);
|
|
||||||
onChange?.(newValue);
|
|
||||||
message.success(`${files.length} 个文件上传成功`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("上传错误详情:", error);
|
|
||||||
message.error(
|
|
||||||
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
|
|
||||||
);
|
|
||||||
setUploadingFiles((prev) =>
|
|
||||||
prev.map((f) => ({ ...f, status: "error" }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[uploadResults, onChange, handleFileUpload]
|
[handleFileUpload, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -132,8 +118,8 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
name="files"
|
name="files"
|
||||||
multiple
|
multiple
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
style={{ background: "white", borderStyle: "solid" }}
|
style={{ background: "transparent", borderStyle: "none" }}
|
||||||
beforeUpload={handleChange}>
|
beforeUpload={handleBeforeUpload}>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
</p>
|
</p>
|
||||||
|
@ -141,64 +127,58 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
点击或拖拽文件到此区域进行上传
|
点击或拖拽文件到此区域进行上传
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-hint">支持单个或批量上传文件</p>
|
<p className="ant-upload-hint">支持单个或批量上传文件</p>
|
||||||
{/* 正在上传的文件 */}
|
|
||||||
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
|
{/* 上传状态展示 */}
|
||||||
<div className=" px-2 py-0 border rounded bg-white mt-1 ">
|
<div className="px-2 py-0 rounded mt-1">
|
||||||
|
{/* 上传中的文件 */}
|
||||||
{uploadingFiles.map((file) => (
|
{uploadingFiles.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.fileKey}
|
key={file.fileKey}
|
||||||
className="flex flex-col gap-1">
|
className="flex flex-col gap-1 mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-sm">{file.name}</div>
|
<span className="text-sm">{file.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Progress
|
<Progress
|
||||||
className="flex-1 w-full"
|
|
||||||
percent={
|
percent={
|
||||||
file.status === "done"
|
file.status === "done"
|
||||||
? 100
|
? 100
|
||||||
: Math.round(
|
: Math.round(
|
||||||
uploadProgress?.[
|
uploadProgress?.[
|
||||||
file?.fileKey
|
file.fileKey!
|
||||||
] || 0
|
] || 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
status={
|
status={
|
||||||
file.status === "error"
|
file.status === "error"
|
||||||
? "exception"
|
? "exception"
|
||||||
: file.status === "done"
|
|
||||||
? "success"
|
|
||||||
: "active"
|
: "active"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{completedFiles.length > 0 &&
|
|
||||||
completedFiles.map((file, index) => (
|
{/* 已完成的文件 */}
|
||||||
|
{completedFiles.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={file.fileId}
|
||||||
className="flex items-center justify-between gap-2">
|
className="flex items-center justify-between gap-2 mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircleOutlined className="text-green-500" />
|
<CheckCircleOutlined className="text-green-500" />
|
||||||
<div className="text-sm">
|
<span className="text-sm">{file.name}</span>
|
||||||
{file.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // 阻止事件冒泡
|
e.stopPropagation();
|
||||||
if (file.fileId) {
|
if (file.fileId)
|
||||||
handleRemoveFile(file.fileId); // 只删除文件
|
handleRemoveFile(file.fileId);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,8 +15,12 @@ export const Header = memo(function Header() {
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="">
|
<div className="">
|
||||||
<span className="text-xl font-bold">首长机关信箱</span>
|
<span className="text-xl font-bold">
|
||||||
<p className=" text-sm text-secondary-50">聆怀若水,应语如风;纾难化困,践诺成春</p>
|
首长机关信箱
|
||||||
|
</span>
|
||||||
|
<p className=" text-sm text-secondary-50">
|
||||||
|
聆怀若水,应语如风;纾难化困,践诺成春
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow max-w-2xl">
|
<div className="flex-grow max-w-2xl">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
@ -34,7 +38,6 @@ focus:outline-none focus:ring-2
|
||||||
focus:ring-[#8EADD4] focus:ring-offset-2
|
focus:ring-[#8EADD4] focus:ring-offset-2
|
||||||
focus:ring-offset-[#13294B]"
|
focus:ring-offset-[#13294B]"
|
||||||
aria-label="Login">
|
aria-label="Login">
|
||||||
|
|
||||||
<UserOutlined
|
<UserOutlined
|
||||||
className="h-5 w-5 transition-transform
|
className="h-5 w-5 transition-transform
|
||||||
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function Navigation({ className }: NavigationProps) {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mt-4 rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
|
"mt-4 rounded-t-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<div className="flex flex-col md:flex-row items-stretch">
|
<div className="flex flex-col md:flex-row items-stretch">
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { PostDto, PostStateLabels } from "@nice/common";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import PostLikeButton from "./detail/PostHeader/PostLikeButton";
|
import PostLikeButton from "./detail/PostHeader/PostLikeButton";
|
||||||
import { LetterBadge } from "./LetterBadge";
|
import { LetterBadge } from "./LetterBadge";
|
||||||
|
import PostHateButton from "./detail/PostHeader/PostHateButton";
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
interface LetterCardProps {
|
interface LetterCardProps {
|
||||||
|
@ -48,7 +49,9 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserOutlined className="text-primary text-base" />
|
<UserOutlined className="text-primary text-base" />
|
||||||
<Text className="text-primary font-medium">
|
<Text className="text-primary font-medium">
|
||||||
{letter.author?.showname || "匿名用户"}
|
{letter?.meta?.signature ||
|
||||||
|
letter.author?.showname ||
|
||||||
|
"匿名用户"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -116,6 +119,7 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<span className="mr-1">浏览量</span>
|
<span className="mr-1">浏览量</span>
|
||||||
{letter.views}
|
{letter.views}
|
||||||
</Button>
|
</Button>
|
||||||
|
<PostHateButton post={letter as any}></PostHateButton>
|
||||||
<PostLikeButton post={letter as any}></PostLikeButton>
|
<PostLikeButton post={letter as any}></PostLikeButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,10 +6,17 @@ import { Avatar } from "antd";
|
||||||
import { useVisitor } from "@nice/client";
|
import { useVisitor } from "@nice/client";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { PostDetailContext } from "./context/PostDetailContext";
|
import { PostDetailContext } from "./context/PostDetailContext";
|
||||||
import { CheckCircleOutlined, CheckOutlined, LikeFilled, LikeOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
LikeFilled,
|
||||||
|
LikeOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import PostLikeButton from "./PostHeader/PostLikeButton";
|
import PostLikeButton from "./PostHeader/PostLikeButton";
|
||||||
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
||||||
import PostResources from "./PostResources";
|
import PostResources from "./PostResources";
|
||||||
|
import PostHateButton from "./PostHeader/PostHateButton";
|
||||||
|
import PostSendButton from "./PostHeader/PostSendButton";
|
||||||
|
|
||||||
export default function PostCommentCard({
|
export default function PostCommentCard({
|
||||||
post,
|
post,
|
||||||
|
@ -30,14 +37,18 @@ export default function PostCommentCard({
|
||||||
src={post.author?.avatar}
|
src={post.author?.avatar}
|
||||||
size={50}
|
size={50}
|
||||||
name={!post.author?.avatar && post.author?.showname}
|
name={!post.author?.avatar && post.author?.showname}
|
||||||
ip={post?.meta?.ip}></CustomAvatar>
|
randomString={
|
||||||
|
post?.meta?.signature || post?.meta?.ip
|
||||||
|
}></CustomAvatar>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className={`flex-1 min-w-0 `}>
|
<div className={`flex-1 min-w-0 `}>
|
||||||
<div className="flex flex-1 justify-between ">
|
<div className="flex flex-1 justify-between ">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="flex font-medium text-slate-900">
|
<span className="flex font-medium text-slate-900">
|
||||||
{post.author?.showname || "匿名用户"}
|
{post?.meta?.signature ||
|
||||||
|
post.author?.showname ||
|
||||||
|
"匿名用户"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex text-sm text-slate-500 ">
|
<span className="flex text-sm text-slate-500 ">
|
||||||
{dayjs(post?.createdAt).format(
|
{dayjs(post?.createdAt).format(
|
||||||
|
@ -54,9 +65,15 @@ export default function PostCommentCard({
|
||||||
{/* 添加有帮助按钮 */}
|
{/* 添加有帮助按钮 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-center items-center gap-2">
|
<div className="flex justify-center items-center gap-2">
|
||||||
<span className=" text-sm text-slate-500">{`#${index + 1}`}</span>
|
{isReceiverComment && (
|
||||||
|
<PostSendButton
|
||||||
|
post={post}></PostSendButton>
|
||||||
|
)}
|
||||||
|
<PostHateButton
|
||||||
|
post={post}></PostHateButton>
|
||||||
<PostLikeButton
|
<PostLikeButton
|
||||||
post={post}></PostLikeButton>
|
post={post}></PostLikeButton>
|
||||||
|
<span className=" text-sm text-slate-500">{`#${index + 1}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Button, Tabs } from "antd";
|
import { Button, Input, Tabs } from "antd";
|
||||||
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
||||||
import { PostDetailContext } from "./context/PostDetailContext";
|
import { PostDetailContext } from "./context/PostDetailContext";
|
||||||
import { usePost } from "@nice/client";
|
import { useEntity } from "@nice/client";
|
||||||
import { PostType } from "@nice/common";
|
import { PostType } from "@nice/common";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { isContentEmpty } from "./utils";
|
import { isContentEmpty } from "./utils";
|
||||||
import { SendOutlined } from "@ant-design/icons";
|
import { SendOutlined } from "@ant-design/icons";
|
||||||
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
import { useEntity } from '../../../../../../../packages/client/src/api/hooks/useEntity';
|
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
export default function PostCommentEditor() {
|
export default function PostCommentEditor() {
|
||||||
const { post } = useContext(PostDetailContext);
|
const { post } = useContext(PostDetailContext);
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [isPreview, setIsPreview] = useState(false);
|
const [signature, setSignature] = useState<string | undefined>(undefined);
|
||||||
const [fileIds, setFileIds] = useState<string[]>([]);
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||||
// const { create } = usePost();
|
|
||||||
const { create } = useEntity("post")
|
const { create } = useEntity("post")
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -39,11 +38,14 @@ export default function PostCommentEditor() {
|
||||||
fileId: id,
|
fileId: id,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
signature,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success("发布成功!");
|
toast.success("发布成功!");
|
||||||
setContent("");
|
setContent("");
|
||||||
|
setFileIds([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("发布失败,请稍后重试");
|
toast.error("发布失败,请稍后重试");
|
||||||
console.error("Error posting comment:", error);
|
console.error("Error posting comment:", error);
|
||||||
|
@ -62,11 +64,9 @@ export default function PostCommentEditor() {
|
||||||
<QuillEditor
|
<QuillEditor
|
||||||
value={content}
|
value={content}
|
||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
|
|
||||||
className="bg-transparent"
|
className="bg-transparent"
|
||||||
theme="snow"
|
theme="snow"
|
||||||
minRows={6}
|
minRows={6}
|
||||||
|
|
||||||
modules={{
|
modules={{
|
||||||
toolbar: [
|
toolbar: [
|
||||||
["bold", "italic", "strike"],
|
["bold", "italic", "strike"],
|
||||||
|
@ -79,26 +79,39 @@ export default function PostCommentEditor() {
|
||||||
["clean"],
|
["clean"],
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tab="附件" key="2">
|
<TabPane tab="附件" key="2">
|
||||||
|
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
|
||||||
<TusUploader
|
<TusUploader
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
console.log("ids", value);
|
||||||
setFileIds(value);
|
setFileIds(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{!isContentEmpty(content) && (
|
{!isContentEmpty(content) && (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<CustomAvatar randomString={signature}></CustomAvatar>
|
||||||
|
<Input
|
||||||
|
maxLength={10}
|
||||||
|
style={{
|
||||||
|
width: 150,
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSignature(e.target.value);
|
||||||
|
}}
|
||||||
|
showCount
|
||||||
|
placeholder="签名"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
size="large"
|
|
||||||
disabled={isContentEmpty(content)}
|
disabled={isContentEmpty(content)}
|
||||||
className="flex items-center space-x-2 bg-primary"
|
className="flex items-center space-x-2 bg-primary"
|
||||||
icon={<SendOutlined />}>
|
icon={<SendOutlined />}>
|
||||||
|
|
|
@ -26,16 +26,15 @@ export default function Header() {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
||||||
<Space className="mr-4">
|
<Space className="mr-4">
|
||||||
|
|
||||||
<span className="text-white">发件人</span>
|
<span className="text-white">发件人</span>
|
||||||
<Text className="text-white" strong>
|
<Text className="text-white" strong>
|
||||||
{post?.author?.showname || "匿名用户"}
|
{post?.meta?.signature ||
|
||||||
|
post?.author?.showname ||
|
||||||
|
"匿名用户"}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Space className="mr-4">
|
<Space className="mr-4">
|
||||||
|
|
||||||
<span className="text-white">收件人 </span>
|
<span className="text-white">收件人 </span>
|
||||||
|
|
||||||
{post?.receivers?.map((receiver, index) => (
|
{post?.receivers?.map((receiver, index) => (
|
||||||
|
@ -43,27 +42,23 @@ export default function Header() {
|
||||||
strong
|
strong
|
||||||
className="text-white"
|
className="text-white"
|
||||||
key={`${index}`}>
|
key={`${index}`}>
|
||||||
{receiver?.meta?.rank} {receiver?.showname || '匿名用户'}
|
{receiver?.meta?.rank}{" "}
|
||||||
|
{receiver?.showname || "匿名用户"}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{/* Date Info Badge */}
|
{/* Date Info Badge */}
|
||||||
<Space className="mr-4">
|
<Space className="mr-4">
|
||||||
|
|
||||||
|
|
||||||
<span className="text-white">创建于</span>
|
<span className="text-white">创建于</span>
|
||||||
<Text className="text-white">
|
<Text className="text-white">
|
||||||
|
|
||||||
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
|
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
{/* Last Updated Badge */}
|
{/* Last Updated Badge */}
|
||||||
<Space className="mr-4">
|
<Space className="mr-4">
|
||||||
|
|
||||||
<span className="text-white">最后更新于</span>
|
<span className="text-white">最后更新于</span>
|
||||||
<Text className="text-white">
|
<Text className="text-white">
|
||||||
|
|
||||||
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
|
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { PostDto, VisitType } from "@nice/common";
|
||||||
|
import { useVisitor } from "@nice/client";
|
||||||
|
import { Button, Tooltip } from "antd";
|
||||||
|
import { DislikeFilled, DislikeOutlined } from "@ant-design/icons";
|
||||||
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
|
||||||
|
export default function PostHateButton({ post }: { post: PostDto }) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { hate, unHate } = useVisitor();
|
||||||
|
function hateThisPost() {
|
||||||
|
if (!post?.hated) {
|
||||||
|
post.hates += 1;
|
||||||
|
post.hated = true;
|
||||||
|
hate.mutateAsync({
|
||||||
|
data: {
|
||||||
|
visitorId: user?.id || null,
|
||||||
|
postId: post.id,
|
||||||
|
type: VisitType.HATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
post.hates -= 1;
|
||||||
|
post.hated = false;
|
||||||
|
unHate.mutateAsync({
|
||||||
|
where: {
|
||||||
|
visitorId: user?.id || null,
|
||||||
|
postId: post.id,
|
||||||
|
type: VisitType.HATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title={post?.hated ? "取消点踩" : "点踩"}
|
||||||
|
type={post?.hated ? "primary" : "default"}
|
||||||
|
style={{
|
||||||
|
backgroundColor: post?.hated ? "#ff4d4f" : "#fff",
|
||||||
|
borderColor: post?.hated ? "transparent" : "",
|
||||||
|
color: post?.hated ? "#fff" : "#000",
|
||||||
|
boxShadow: "none", // 去除阴影
|
||||||
|
}}
|
||||||
|
shape="round"
|
||||||
|
icon={post?.hated ? <DislikeFilled /> : <DislikeOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
hateThisPost();
|
||||||
|
}}>
|
||||||
|
<span className="mr-1">不满意</span>
|
||||||
|
{post?.hates || 0}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -35,16 +35,16 @@ export default function PostLikeButton({ post }: { post: PostDto }) {
|
||||||
title={post?.liked ? "取消点赞" : "点赞"}
|
title={post?.liked ? "取消点赞" : "点赞"}
|
||||||
type={post?.liked ? "primary" : "default"}
|
type={post?.liked ? "primary" : "default"}
|
||||||
shape="round"
|
shape="round"
|
||||||
|
style={{
|
||||||
|
boxShadow: "none", // 去除阴影
|
||||||
|
}}
|
||||||
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
likeThisPost()
|
likeThisPost();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<span className="mr-1">有帮助</span>
|
<span className="mr-1">有帮助</span>
|
||||||
{post?.likes}
|
{post?.likes}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { PostDto, VisitType } from "@nice/common";
|
||||||
|
import { useVisitor } from "@nice/client";
|
||||||
|
import { Button, Tooltip } from "antd";
|
||||||
|
import {
|
||||||
|
DislikeFilled,
|
||||||
|
DislikeOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
|
||||||
|
export default function PostSendButton({ post }: { post: PostDto }) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { hate, unHate } = useVisitor();
|
||||||
|
function sendPost() {
|
||||||
|
if (post?.authorId) {
|
||||||
|
window.open(`/editor?receiverId=${post?.authorId}`, "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title="给他写信"
|
||||||
|
type="default"
|
||||||
|
shape="round"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
sendPost();
|
||||||
|
}}
|
||||||
|
icon={<SendOutlined />}>
|
||||||
|
<span className="mr-1">给他写信</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { Button, Tooltip } from "antd/lib";
|
||||||
import { PostDetailContext } from "../context/PostDetailContext";
|
import { PostDetailContext } from "../context/PostDetailContext";
|
||||||
import PostLikeButton from "./PostLikeButton";
|
import PostLikeButton from "./PostLikeButton";
|
||||||
import PostResources from "../PostResources";
|
import PostResources from "../PostResources";
|
||||||
|
import PostHateButton from "./PostHateButton";
|
||||||
export function StatsSection() {
|
export function StatsSection() {
|
||||||
const { post } = useContext(PostDetailContext);
|
const { post } = useContext(PostDetailContext);
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ export function StatsSection() {
|
||||||
<Button type="default" title="回复数" shape="round" icon={<CommentOutlined />}>
|
<Button type="default" title="回复数" shape="round" icon={<CommentOutlined />}>
|
||||||
<span className="mr-1">回复数</span>{post?.commentsCount}
|
<span className="mr-1">回复数</span>{post?.commentsCount}
|
||||||
</Button>
|
</Button>
|
||||||
|
<PostHateButton post={post}></PostHateButton>
|
||||||
<PostLikeButton post={post}></PostLikeButton>
|
<PostLikeButton post={post}></PostLikeButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,9 @@ export default function PostResources({ post }: { post: PostDto }) {
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{resource.meta.size &&
|
{resource.meta.size &&
|
||||||
formatFileSize(resource.meta.size)}
|
formatFileSize(
|
||||||
|
resource.meta.size
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,7 +65,7 @@ export function LetterBasicForm() {
|
||||||
name="content"
|
name="content"
|
||||||
rules={[{ required: true, message: "请输入内容" }]}
|
rules={[{ required: true, message: "请输入内容" }]}
|
||||||
required={false}>
|
required={false}>
|
||||||
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
|
<div className="rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
|
||||||
<QuillEditor
|
<QuillEditor
|
||||||
maxLength={10000}
|
maxLength={10000}
|
||||||
placeholder="请输入内容"
|
placeholder="请输入内容"
|
||||||
|
@ -79,7 +79,7 @@ export function LetterBasicForm() {
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tab="附件" key="2">
|
<TabPane tab="附件" key="2">
|
||||||
<Form.Item name="resources" required={false}>
|
<Form.Item name="resources" required={false}>
|
||||||
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
|
<div className="rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
|
||||||
<TusUploader
|
<TusUploader
|
||||||
onChange={(resources) =>
|
onChange={(resources) =>
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
|
@ -99,6 +99,17 @@ export function LetterBasicForm() {
|
||||||
是否公开
|
是否公开
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<div className="flex gap-2 ">
|
||||||
|
<Form.Item name={["meta", "signature"]}>
|
||||||
|
<Input
|
||||||
|
maxLength={10}
|
||||||
|
style={{
|
||||||
|
width: 150,
|
||||||
|
}}
|
||||||
|
showCount
|
||||||
|
placeholder="签名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => form.submit()}
|
onClick={() => form.submit()}
|
||||||
|
@ -108,6 +119,7 @@ export function LetterBasicForm() {
|
||||||
发送信件
|
发送信件
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,14 +4,11 @@ import { RoleEditorContext } from "./role-editor";
|
||||||
import RoleForm from "./role-form";
|
import RoleForm from "./role-form";
|
||||||
|
|
||||||
export default function RoleModal() {
|
export default function RoleModal() {
|
||||||
const {
|
const { roleForm, editRoleId, roleModalOpen, setRoleModalOpen } =
|
||||||
roleForm,
|
useContext(RoleEditorContext);
|
||||||
editRoleId,
|
|
||||||
roleModalOpen, setRoleModalOpen
|
|
||||||
} = useContext(RoleEditorContext);
|
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
roleForm.submit()
|
roleForm.submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => setRoleModalOpen(false);
|
const handleCancel = () => setRoleModalOpen(false);
|
||||||
|
@ -23,8 +20,7 @@ export default function RoleModal() {
|
||||||
open={roleModalOpen}
|
open={roleModalOpen}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
// confirmLoading={loading}
|
// confirmLoading={loading}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}>
|
||||||
>
|
|
||||||
<RoleForm></RoleForm>
|
<RoleForm></RoleForm>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,14 +6,14 @@ import multiavatar from "@multiavatar/multiavatar";
|
||||||
interface CustomAvatarProps extends Omit<AvatarProps, "children"> {
|
interface CustomAvatarProps extends Omit<AvatarProps, "children"> {
|
||||||
src?: string;
|
src?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
ip?: string;
|
randomString?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CustomAvatar({
|
export function CustomAvatar({
|
||||||
src,
|
src,
|
||||||
name,
|
name,
|
||||||
className = "",
|
className = "",
|
||||||
ip,
|
randomString,
|
||||||
...props
|
...props
|
||||||
}: CustomAvatarProps) {
|
}: CustomAvatarProps) {
|
||||||
// 获取名字的第一个字符,如果没有名字则显示"匿"
|
// 获取名字的第一个字符,如果没有名字则显示"匿"
|
||||||
|
@ -39,7 +39,7 @@ export function CustomAvatar({
|
||||||
const avatarSrc =
|
const avatarSrc =
|
||||||
src || (name && name !== "匿名用户")
|
src || (name && name !== "匿名用户")
|
||||||
? src
|
? src
|
||||||
: generateAvatarFromIp(ip || "default");
|
: generateAvatarFromIp(randomString || "default");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
|
import { env } from "../env";
|
||||||
|
|
||||||
// useTusUpload.ts
|
// useTusUpload.ts
|
||||||
interface UploadProgress {
|
interface UploadProgress {
|
||||||
|
@ -27,7 +28,16 @@ export function useTusUpload() {
|
||||||
}
|
}
|
||||||
return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/");
|
return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/");
|
||||||
};
|
};
|
||||||
|
const getResourceUrl = (url: string) => {
|
||||||
|
const parts = url.split("/");
|
||||||
|
const uploadIndex = parts.findIndex((part) => part === "upload");
|
||||||
|
if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) {
|
||||||
|
throw new Error("Invalid upload URL format");
|
||||||
|
}
|
||||||
|
const resUrl = `http://${env.SERVER_IP}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`;
|
||||||
|
console.log(resUrl);
|
||||||
|
return resUrl;
|
||||||
|
};
|
||||||
const handleFileUpload = async (
|
const handleFileUpload = async (
|
||||||
file: File,
|
file: File,
|
||||||
onSuccess: (result: UploadResult) => void,
|
onSuccess: (result: UploadResult) => void,
|
||||||
|
@ -52,7 +62,7 @@ export function useTusUpload() {
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
filetype: file.type,
|
filetype: file.type,
|
||||||
size: file.size as any
|
size: file.size as any,
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
const progress = Number(
|
const progress = Number(
|
||||||
|
@ -67,13 +77,14 @@ export function useTusUpload() {
|
||||||
try {
|
try {
|
||||||
if (upload.url) {
|
if (upload.url) {
|
||||||
const fileId = getFileId(upload.url);
|
const fileId = getFileId(upload.url);
|
||||||
|
const url = getResourceUrl(upload.url);
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadProgress((prev) => ({
|
setUploadProgress((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[fileKey]: 100,
|
[fileKey]: 100,
|
||||||
}));
|
}));
|
||||||
onSuccess({
|
onSuccess({
|
||||||
url: upload.url,
|
url,
|
||||||
fileId,
|
fileId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,15 @@ import { AppConfigSlug, BaseSetting } from "@nice/common";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
export function useAppConfig() {
|
export function useAppConfig() {
|
||||||
const utils = api.useUtils()
|
const utils = api.useUtils();
|
||||||
const [baseSetting, setBaseSetting] = useState<BaseSetting | undefined>();
|
const [baseSetting, setBaseSetting] = useState<BaseSetting | undefined>();
|
||||||
|
|
||||||
const { data, isLoading }: { data: any; isLoading: boolean } =
|
const { data, isLoading }: { data: any; isLoading: boolean } =
|
||||||
api.app_config.findFirst.useQuery({
|
api.app_config.findFirst.useQuery({
|
||||||
where: { slug: AppConfigSlug.BASE_SETTING }
|
where: { slug: AppConfigSlug.BASE_SETTING },
|
||||||
});
|
});
|
||||||
const handleMutationSuccess = useCallback(() => {
|
const handleMutationSuccess = useCallback(() => {
|
||||||
utils.app_config.invalidate()
|
utils.app_config.invalidate();
|
||||||
}, [utils]);
|
}, [utils]);
|
||||||
|
|
||||||
// Use the generic success handler in mutations
|
// Use the generic success handler in mutations
|
||||||
|
@ -28,7 +28,6 @@ export function useAppConfig() {
|
||||||
if (data?.meta) {
|
if (data?.meta) {
|
||||||
setBaseSetting(JSON.parse(data?.meta));
|
setBaseSetting(JSON.parse(data?.meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading]);
|
||||||
const splashScreen = useMemo(() => {
|
const splashScreen = useMemo(() => {
|
||||||
return baseSetting?.appConfig?.splashScreen;
|
return baseSetting?.appConfig?.splashScreen;
|
||||||
|
@ -36,8 +35,10 @@ export function useAppConfig() {
|
||||||
const devDept = useMemo(() => {
|
const devDept = useMemo(() => {
|
||||||
return baseSetting?.appConfig?.devDept;
|
return baseSetting?.appConfig?.devDept;
|
||||||
}, [baseSetting]);
|
}, [baseSetting]);
|
||||||
|
const logo = useMemo(() => {
|
||||||
|
return baseSetting?.appConfig?.logo;
|
||||||
|
}, [baseSetting]);
|
||||||
return {
|
return {
|
||||||
|
|
||||||
create,
|
create,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
update,
|
update,
|
||||||
|
@ -45,5 +46,6 @@ export function useAppConfig() {
|
||||||
splashScreen,
|
splashScreen,
|
||||||
devDept,
|
devDept,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
logo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ export function useVisitor() {
|
||||||
const create = api.visitor.create.useMutation({
|
const create = api.visitor.create.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
utils.visitor.invalidate();
|
utils.visitor.invalidate();
|
||||||
|
|
||||||
// utils.post.invalidate();
|
// utils.post.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -171,5 +172,7 @@ export function useVisitor() {
|
||||||
deleteStar,
|
deleteStar,
|
||||||
like,
|
like,
|
||||||
unLike,
|
unLike,
|
||||||
|
hate,
|
||||||
|
unHate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,3 +201,10 @@ export const PostStateLabels = {
|
||||||
[PostState.PROCESSING]: "处理中",
|
[PostState.PROCESSING]: "处理中",
|
||||||
[PostState.RESOLVED]: "已完成",
|
[PostState.RESOLVED]: "已完成",
|
||||||
};
|
};
|
||||||
|
export enum RoleName {
|
||||||
|
Basic = "基层", // 基层
|
||||||
|
Organization = "机关", // 机关
|
||||||
|
Leader = "领导", // 领导
|
||||||
|
DomainAdmin = "域管理员", // 域管理员
|
||||||
|
RootAdmin = "根管理员", // 根管理员
|
||||||
|
}
|
||||||
|
|
|
@ -320,6 +320,9 @@ export const RoleMapMethodSchema = {
|
||||||
domainId: z.string().nullish(),
|
domainId: z.string().nullish(),
|
||||||
roleId: z.string().nullish(),
|
roleId: z.string().nullish(),
|
||||||
}),
|
}),
|
||||||
|
getStaffIdsByRoleNames: z.object({
|
||||||
|
roleNames: z.array(z.string()),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
export const RoleMethodSchema = {
|
export const RoleMethodSchema = {
|
||||||
create: z.object({
|
create: z.object({
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
content: true,
|
content: true,
|
||||||
views: true,
|
views: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
|
hates: true,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
resources: true,
|
resources: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
|
|
@ -39,11 +39,11 @@ export type StaffDto = Staff & {
|
||||||
domain?: Department;
|
domain?: Department;
|
||||||
department?: Department;
|
department?: Department;
|
||||||
meta?: {
|
meta?: {
|
||||||
photoUrl?: string
|
photoUrl?: string;
|
||||||
office?: string
|
office?: string;
|
||||||
email?: string
|
email?: string;
|
||||||
rank?: string
|
rank?: string;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
export interface AuthDto {
|
export interface AuthDto {
|
||||||
token: string;
|
token: string;
|
||||||
|
@ -182,6 +182,7 @@ export type ResourceDto = Resource & {
|
||||||
export type PostDto = Post & {
|
export type PostDto = Post & {
|
||||||
readed: boolean;
|
readed: boolean;
|
||||||
liked: boolean;
|
liked: boolean;
|
||||||
|
hated: boolean;
|
||||||
readedCount: number;
|
readedCount: number;
|
||||||
commentsCount: number;
|
commentsCount: number;
|
||||||
terms: TermDto[];
|
terms: TermDto[];
|
||||||
|
@ -216,6 +217,7 @@ export interface BaseSetting {
|
||||||
appConfig?: {
|
appConfig?: {
|
||||||
splashScreen?: string;
|
splashScreen?: string;
|
||||||
devDept?: string;
|
devDept?: string;
|
||||||
|
logo?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export interface PostMeta {
|
export interface PostMeta {
|
||||||
|
|
Loading…
Reference in New Issue