diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 100eabed..4828dedc 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -29,6 +29,7 @@ import { SurveyHistory } from './models/surveyHistory.entity'; import { ResponseSchema } from './models/responseSchema.entity'; import { Counter } from './models/counter.entity'; import { SurveyResponse } from './models/surveyResponse.entity'; +import { SurveyGroup } from './models/surveyGroup.entity'; import { ClientEncrypt } from './models/clientEncrypt.entity'; import { Word } from './models/word.entity'; import { MessagePushingTask } from './models/messagePushingTask.entity'; @@ -78,6 +79,7 @@ import { Logger } from './logger'; SurveyConf, SurveyHistory, SurveyResponse, + SurveyGroup, Counter, ResponseSchema, ClientEncrypt, diff --git a/server/src/models/surveyGroup.entity.ts b/server/src/models/surveyGroup.entity.ts new file mode 100644 index 00000000..7620042d --- /dev/null +++ b/server/src/models/surveyGroup.entity.ts @@ -0,0 +1,11 @@ +import { Entity, Column } from 'typeorm'; +import { BaseEntity } from './base.entity'; + +@Entity({ name: 'surveyGroup' }) +export class SurveyGroup extends BaseEntity { + @Column() + ownerId: string; + + @Column() + name: string; +} diff --git a/server/src/models/surveyMeta.entity.ts b/server/src/models/surveyMeta.entity.ts index 3e303470..f3daa311 100644 --- a/server/src/models/surveyMeta.entity.ts +++ b/server/src/models/surveyMeta.entity.ts @@ -37,6 +37,9 @@ export class SurveyMeta extends BaseEntity { @Column() workspaceId: string; + @Column() + groupId: string; + @Column() curStatus: { status: RECORD_STATUS; diff --git a/server/src/modules/survey/__test/surveyGroup.controller.spec.ts b/server/src/modules/survey/__test/surveyGroup.controller.spec.ts new file mode 100644 index 00000000..71ff64c8 --- /dev/null +++ b/server/src/modules/survey/__test/surveyGroup.controller.spec.ts @@ -0,0 +1,132 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SurveyGroupController } from '../controllers/surveyGroup.controller'; +import { SurveyGroupService } from '../services/surveyGroup.service'; +import { SurveyMetaService } from '../services/surveyMeta.service'; +import { HttpException } from 'src/exceptions/httpException'; +import { ObjectId } from 'mongodb'; +import { Logger } from 'src/logger'; + +jest.mock('src/guards/authentication.guard'); + +describe('SurveyGroupController', () => { + let controller: SurveyGroupController; + let service: SurveyGroupService; + + const mockService = { + create: jest.fn(), + findAll: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SurveyGroupController], + providers: [ + { + provide: SurveyMetaService, + useValue: { + countSurveyMetaByGroupId: jest.fn().mockResolvedValue(0), + }, + }, + { + provide: SurveyGroupService, + useValue: mockService, + }, + { + provide: Logger, + useValue: { + error: jest.fn(), + info: jest.fn(), + }, + }, + ], + }).compile(); + + controller = module.get(SurveyGroupController); + service = module.get(SurveyGroupService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('create', () => { + it('should create a survey group', async () => { + const result = { + _id: new ObjectId(), + name: 'Test Group', + ownerId: '123', + createdAt: new Date(), + updatedAt: new Date(), + }; // 确保这里返回的对象结构符合预期 + jest.spyOn(service, 'create').mockResolvedValue(result); + + // 创建模拟的请求对象 + const req = { + user: { + _id: '123', // 模拟的用户ID + }, + }; + + expect(await controller.create({ name: 'Test Group' }, req)).toEqual({ + code: 200, + data: { + id: result._id, + }, + }); + expect(service.create).toHaveBeenCalledWith({ + name: 'Test Group', + ownerId: req.user._id.toString(), // 这里用模拟的 req.user._id + }); + }); + }); + + describe('findAll', () => { + it('should return a list of survey groups', async () => { + const result = { total: 0, notTotal: 0, list: [], allList: [] }; + jest.spyOn(service, 'findAll').mockResolvedValue(result); + const mockReq = { user: { _id: new ObjectId() } }; + const mockQue = { curPage: 1, pageSize: 10, name: '' }; + const userId = mockReq.user._id.toString(); + expect(await controller.findAll(mockReq, mockQue)).toEqual({ + code: 200, + data: result, + }); + expect(service.findAll).toHaveBeenCalledWith(userId, '', 0, 10); + }); + }); + + describe('update', () => { + it('should update a survey group', async () => { + const updatedFields = { name: 'xxx' }; + const updatedResult = { raw: 'xxx', generatedMaps: [] }; + const id = '1'; + jest.spyOn(service, 'update').mockResolvedValue(updatedResult); + + expect(await controller.updateOne(id, updatedFields)).toEqual({ + code: 200, + ret: updatedResult, + }); + expect(service.update).toHaveBeenCalledWith(id, updatedFields); + }); + + it('should throw error on invalid parameter', async () => { + const id = '1'; + const invalidFields: any = {}; + await expect(controller.updateOne(id, invalidFields)).rejects.toThrow( + HttpException, + ); + }); + }); + + describe('remove', () => { + it('should remove a survey group', async () => { + const id = '1'; + jest.spyOn(service, 'remove').mockResolvedValue(undefined); + + expect(await controller.remove(id)).toEqual({ code: 200 }); + expect(service.remove).toHaveBeenCalledWith(id); + }); + }); +}); diff --git a/server/src/modules/survey/__test/surveyGroup.service.spec.ts b/server/src/modules/survey/__test/surveyGroup.service.spec.ts new file mode 100644 index 00000000..378601ae --- /dev/null +++ b/server/src/modules/survey/__test/surveyGroup.service.spec.ts @@ -0,0 +1,102 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SurveyGroupService } from '../services/surveyGroup.service'; +import { SurveyGroup } from 'src/models/surveyGroup.entity'; +import { SurveyMeta } from 'src/models/surveyMeta.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +describe('SurveyGroupService', () => { + let service: SurveyGroupService; + + const mockSurveyGroupRepository = { + create: jest.fn(), + save: jest.fn(), + findAndCount: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; + + const mockSurveyMetaRepository = { + updateMany: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SurveyGroupService, + { + provide: getRepositoryToken(SurveyGroup), + useValue: mockSurveyGroupRepository, + }, + { + provide: getRepositoryToken(SurveyMeta), + useValue: mockSurveyMetaRepository, + }, + ], + }).compile(); + + service = module.get(SurveyGroupService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create a survey group', async () => { + const createParams = { name: 'Test Group', ownerId: '123' }; + const mockSavedGroup = { ...createParams, id: '1' }; + + mockSurveyGroupRepository.create.mockReturnValue(mockSavedGroup); + mockSurveyGroupRepository.save.mockResolvedValue(mockSavedGroup); + + expect(await service.create(createParams)).toEqual(mockSavedGroup); + expect(mockSurveyGroupRepository.create).toHaveBeenCalledWith( + createParams, + ); + expect(mockSurveyGroupRepository.save).toHaveBeenCalledWith( + mockSavedGroup, + ); + }); + }); + + describe('findAll', () => { + it('should return survey groups', async () => { + const list = [{ id: '1', name: 'Test Group', ownerId: '123' }]; + const total = list.length; + + mockSurveyGroupRepository.findAndCount.mockResolvedValue([list, total]); + mockSurveyGroupRepository.find.mockResolvedValue(list); + + const result = await service.findAll('123', '', 0, 10); + expect(result).toEqual({ total, list, allList: list }); + expect(mockSurveyGroupRepository.findAndCount).toHaveBeenCalled(); + expect(mockSurveyGroupRepository.find).toHaveBeenCalled(); + }); + }); + + describe('update', () => { + it('should update a survey group', async () => { + const id = '1'; + const updatedFields = { name: 'Updated Test Group' }; + + await service.update(id, updatedFields); + expect(mockSurveyGroupRepository.update).toHaveBeenCalledWith(id, { + ...updatedFields, + updatedAt: expect.any(Date), + }); + }); + }); + + describe('remove', () => { + it('should remove a survey group', async () => { + const id = '1'; + await service.remove(id); + expect(mockSurveyMetaRepository.updateMany).toHaveBeenCalledWith( + { groupId: id }, + { $set: { groupId: null } }, + ); + expect(mockSurveyGroupRepository.delete).toHaveBeenCalledWith(id); + }); + }); +}); diff --git a/server/src/modules/survey/__test/surveyMeta.controller.spec.ts b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts index e70058ab..62b474e1 100644 --- a/server/src/modules/survey/__test/surveyMeta.controller.spec.ts +++ b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts @@ -77,6 +77,7 @@ describe('SurveyMetaController', () => { survey: { title: reqBody.title, remark: reqBody.remark, + groupId: null, }, }); diff --git a/server/src/modules/survey/__test/surveyMeta.service.spec.ts b/server/src/modules/survey/__test/surveyMeta.service.spec.ts index d566521b..2ebfa40c 100644 --- a/server/src/modules/survey/__test/surveyMeta.service.spec.ts +++ b/server/src/modules/survey/__test/surveyMeta.service.spec.ts @@ -86,6 +86,7 @@ describe('SurveyMetaService', () => { createMethod: params.createMethod, createFrom: params.createFrom, workspaceId: params.workspaceId, + groupId: null, }); expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey); expect(result).toEqual(newSurvey); diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index 98d32d89..383cd74f 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -78,7 +78,7 @@ export class SurveyController { throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } - const { title, remark, createMethod, createFrom } = value; + const { title, remark, createMethod, createFrom, groupId } = value; let surveyType = '', workspaceId = null; @@ -100,6 +100,7 @@ export class SurveyController { createMethod, createFrom, workspaceId, + groupId, }); await this.surveyConfService.createSurveyConf({ surveyId: surveyMeta._id.toString(), diff --git a/server/src/modules/survey/controllers/surveyGroup.controller.ts b/server/src/modules/survey/controllers/surveyGroup.controller.ts new file mode 100644 index 00000000..4852f82e --- /dev/null +++ b/server/src/modules/survey/controllers/surveyGroup.controller.ts @@ -0,0 +1,145 @@ +import { + Controller, + Get, + Post, + Delete, + Body, + Param, + UseGuards, + Request, + HttpCode, + Query, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import moment from 'moment'; +import { Authentication } from 'src/guards/authentication.guard'; +import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; +import { SurveyGroupService } from '../services/surveyGroup.service'; + +import { Logger } from 'src/logger'; +import { HttpException } from 'src/exceptions/httpException'; +import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; + +import { CreateSurveyGroupDto } from '../dto/createSurveyGroup.dto'; +import { UpdateSurveyGroupDto } from '../dto/updateSurveyGroup.dto'; +import { GetGroupListDto } from '../dto/getGroupList.dto'; + +@ApiTags('surveyGroup') +@ApiBearerAuth() +@UseGuards(Authentication) +@Controller('api/surveyGroup') +export class SurveyGroupController { + constructor( + private readonly surveyMetaService: SurveyMetaService, + private readonly SurveyGroupService: SurveyGroupService, + private readonly logger: Logger, + ) {} + @Post() + @HttpCode(200) + async create( + @Body() + reqBody: CreateSurveyGroupDto, + @Request() + req, + ) { + const { error, value } = CreateSurveyGroupDto.validate(reqBody); + if (error) { + this.logger.error(`createSurveyGroup_parameter error: ${error.message}`); + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const userId = req.user._id.toString(); + const ret = await this.SurveyGroupService.create({ + name: value.name, + ownerId: userId, + }); + return { + code: 200, + data: { + id: ret._id, + }, + }; + } + + @Get() + @HttpCode(200) + async findAll(@Request() req, @Query() queryInfo: GetGroupListDto) { + const { value, error } = GetGroupListDto.validate(queryInfo); + if (error) { + this.logger.error(`GetGroupListDto validate failed: ${error.message}`); + throw new HttpException( + `参数错误: 请联系管理员`, + EXCEPTION_CODE.PARAMETER_ERROR, + ); + } + const userId = req.user._id.toString(); + const curPage = Number(value.curPage); + const pageSize = Number(value.pageSize); + const skip = (curPage - 1) * pageSize; + const { total, list, allList } = await this.SurveyGroupService.findAll( + userId, + value.name, + skip, + pageSize, + ); + const groupIdList = list.map((item) => item._id.toString()); + const surveyTotalList = await Promise.all( + groupIdList.map((item) => { + return this.surveyMetaService.countSurveyMetaByGroupId({ + groupId: item, + }); + }), + ); + const surveyTotalMap = groupIdList.reduce((pre, cur, index) => { + const total = surveyTotalList[index]; + pre[cur] = total; + return pre; + }, {}); + const notTotal = await this.surveyMetaService.countSurveyMetaByGroupId({ + groupId: null, + }); + return { + code: 200, + data: { + total, + list: list.map((item) => { + const id = item._id.toString(); + return { + ...item, + createdAt: moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'), + surveyTotal: surveyTotalMap[id] || 0, + }; + }), + allList, + notTotal, + }, + }; + } + + @Post(':id') + @HttpCode(200) + async updateOne( + @Param('id') id: string, + @Body() + reqBody: UpdateSurveyGroupDto, + ) { + const { error, value } = UpdateSurveyGroupDto.validate(reqBody); + if (error) { + this.logger.error(`createSurveyGroup_parameter error: ${error.message}`); + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const ret = await this.SurveyGroupService.update(id, value); + return { + code: 200, + ret, + }; + } + + @Delete(':id') + @HttpCode(200) + async remove(@Param('id') id: string) { + await this.SurveyGroupService.remove(id); + return { + code: 200, + }; + } +} diff --git a/server/src/modules/survey/controllers/surveyMeta.controller.ts b/server/src/modules/survey/controllers/surveyMeta.controller.ts index 5acf41d5..d2481b0e 100644 --- a/server/src/modules/survey/controllers/surveyMeta.controller.ts +++ b/server/src/modules/survey/controllers/surveyMeta.controller.ts @@ -48,6 +48,7 @@ export class SurveyMetaController { title: Joi.string().required(), remark: Joi.string().allow(null, '').default(''), surveyId: Joi.string().required(), + groupId: Joi.string().allow(null, ''), }).validate(reqBody, { allowUnknown: true }); if (error) { @@ -57,6 +58,8 @@ export class SurveyMetaController { const survey = req.surveyMeta; survey.title = value.title; survey.remark = value.remark; + survey.groupId = + value.groupId && value.groupId !== '' ? value.groupId : null; await this.surveyMetaService.editSurveyMeta({ survey, @@ -86,7 +89,7 @@ export class SurveyMetaController { this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } - const { curPage, pageSize, workspaceId } = value; + const { curPage, pageSize, workspaceId, groupId } = value; let filter = {}, order = {}; if (value.filter) { @@ -120,6 +123,7 @@ export class SurveyMetaController { filter, order, workspaceId, + groupId, surveyIdList, }); return { diff --git a/server/src/modules/survey/dto/createSurvey.dto.ts b/server/src/modules/survey/dto/createSurvey.dto.ts index f34ea9fb..6a5e5be9 100644 --- a/server/src/modules/survey/dto/createSurvey.dto.ts +++ b/server/src/modules/survey/dto/createSurvey.dto.ts @@ -20,6 +20,9 @@ export class CreateSurveyDto { @ApiProperty({ description: '问卷创建在哪个空间下', required: false }) workspaceId?: string; + @ApiProperty({ description: '问卷创建在哪个分组下', required: false }) + groupId?: string; + static validate(data) { return Joi.object({ title: Joi.string().required(), @@ -36,6 +39,7 @@ export class CreateSurveyDto { otherwise: Joi.allow(null), }), workspaceId: Joi.string().allow(null, ''), + groupId: Joi.string().allow(null, ''), }).validate(data); } } diff --git a/server/src/modules/survey/dto/createSurveyGroup.dto.ts b/server/src/modules/survey/dto/createSurveyGroup.dto.ts new file mode 100644 index 00000000..b5697792 --- /dev/null +++ b/server/src/modules/survey/dto/createSurveyGroup.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class CreateSurveyGroupDto { + @ApiProperty({ description: '分组名称', required: true }) + name: string; + + static validate(data) { + return Joi.object({ + name: Joi.string().required(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/dto/getGroupList.dto.ts b/server/src/modules/survey/dto/getGroupList.dto.ts new file mode 100644 index 00000000..cad5e092 --- /dev/null +++ b/server/src/modules/survey/dto/getGroupList.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class GetGroupListDto { + @ApiProperty({ description: '当前页码', required: true }) + curPage: number; + + @ApiProperty({ description: '分页', required: false }) + pageSize: number; + + @ApiProperty({ description: '空间名称', required: false }) + name?: string; + + static validate(data: Partial): Joi.ValidationResult { + return Joi.object({ + curPage: Joi.number().required(), + pageSize: Joi.number().allow(null).default(10), + name: Joi.string().allow(null, '').optional(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/dto/getSurveyMetaList.dto.ts b/server/src/modules/survey/dto/getSurveyMetaList.dto.ts index a53f5974..e4d5f255 100644 --- a/server/src/modules/survey/dto/getSurveyMetaList.dto.ts +++ b/server/src/modules/survey/dto/getSurveyMetaList.dto.ts @@ -17,6 +17,9 @@ export class GetSurveyListDto { @ApiProperty({ description: '空间id', required: false }) workspaceId?: string; + @ApiProperty({ description: '分组id', required: false }) + groupId?: string; + static validate(data) { return Joi.object({ curPage: Joi.number().required(), @@ -24,6 +27,7 @@ export class GetSurveyListDto { filter: Joi.string().allow(null), order: Joi.string().allow(null), workspaceId: Joi.string().allow(null, ''), + groupId: Joi.string().allow(null, ''), }).validate(data); } } diff --git a/server/src/modules/survey/dto/updateSurveyGroup.dto.ts b/server/src/modules/survey/dto/updateSurveyGroup.dto.ts new file mode 100644 index 00000000..a2e3b3aa --- /dev/null +++ b/server/src/modules/survey/dto/updateSurveyGroup.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class UpdateSurveyGroupDto { + @ApiProperty({ description: '分组名称', required: true }) + name: string; + + static validate(data) { + return Joi.object({ + name: Joi.string().required(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/services/surveyGroup.service.ts b/server/src/modules/survey/services/surveyGroup.service.ts new file mode 100644 index 00000000..99953e28 --- /dev/null +++ b/server/src/modules/survey/services/surveyGroup.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MongoRepository } from 'typeorm'; + +import { SurveyGroup } from 'src/models/surveyGroup.entity'; +import { SurveyMeta } from 'src/models/surveyMeta.entity'; + +@Injectable() +export class SurveyGroupService { + constructor( + @InjectRepository(SurveyGroup) + private readonly SurveyGroup: MongoRepository, + @InjectRepository(SurveyMeta) + private surveyMetaRepository: MongoRepository, + ) {} + create(params: { name: string; ownerId: string }) { + const newGroup = this.SurveyGroup.create({ + ...params, + }); + return this.SurveyGroup.save(newGroup); + } + + async findAll(userId: string, name: string, skip: number, pageSize: number) { + const [list, total] = await this.SurveyGroup.findAndCount({ + skip: skip, + take: pageSize, + where: name + ? { name: { $regex: name, $options: 'i' }, ownerId: userId } + : { ownerId: userId }, + order: { + createdAt: -1, + }, + }); + const allList = await this.SurveyGroup.find({ + where: { ownerId: userId }, + select: ['_id', 'name'], + }); + return { + total, + list, + allList, + }; + } + + update(id: string, updatedFields: Partial) { + updatedFields.updatedAt = new Date(); + return this.SurveyGroup.update(id, updatedFields); + } + + async remove(id: string) { + const query = { groupId: id }; + const update = { $set: { groupId: null } }; + await this.surveyMetaRepository.updateMany(query, update); + return this.SurveyGroup.delete(id); + } +} diff --git a/server/src/modules/survey/services/surveyMeta.service.ts b/server/src/modules/survey/services/surveyMeta.service.ts index 71a160a8..dbbb4d4b 100644 --- a/server/src/modules/survey/services/surveyMeta.service.ts +++ b/server/src/modules/survey/services/surveyMeta.service.ts @@ -47,6 +47,7 @@ export class SurveyMetaService { createMethod: string; createFrom: string; workspaceId?: string; + groupId?: string; }) { const { title, @@ -57,6 +58,7 @@ export class SurveyMetaService { createFrom, userId, workspaceId, + groupId, } = params; const surveyPath = await this.getNewSurveyPath(); const newSurvey = this.surveyRepository.create({ @@ -71,6 +73,7 @@ export class SurveyMetaService { createMethod, createFrom, workspaceId, + groupId: groupId && groupId !== '' ? groupId : null, }); return await this.surveyRepository.save(newSurvey); @@ -143,10 +146,18 @@ export class SurveyMetaService { filter: Record; order: Record; workspaceId?: string; + groupId?: string; surveyIdList?: Array; }): Promise<{ data: any[]; count: number }> { - const { pageNum, pageSize, userId, username, workspaceId, surveyIdList } = - condition; + const { + pageNum, + pageSize, + userId, + username, + workspaceId, + groupId, + surveyIdList, + } = condition; const skip = (pageNum - 1) * pageSize; try { const query: Record = Object.assign( @@ -160,6 +171,15 @@ export class SurveyMetaService { if (condition.filter['curStatus.status']) { query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT; } + if (groupId && groupId !== 'all') { + query.groupId = + groupId === 'nogrouped' + ? { + $exists: true, + $eq: null, + } + : groupId; + } if (workspaceId) { query.workspaceId = workspaceId; } else { @@ -228,4 +248,21 @@ export class SurveyMetaService { }); return total; } + async countSurveyMetaByGroupId({ groupId }) { + const total = await this.surveyRepository.count({ + groupId, + $or: [ + { workspaceId: { $exists: false } }, + { workspaceId: null }, + { workspaceId: '' }, + ], + isDeleted: { + $ne: true, + }, + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }); + return total; + } } diff --git a/server/src/modules/survey/survey.module.ts b/server/src/modules/survey/survey.module.ts index 5cd48d89..ea867912 100644 --- a/server/src/modules/survey/survey.module.ts +++ b/server/src/modules/survey/survey.module.ts @@ -17,11 +17,13 @@ import { SurveyUIController } from './controllers/surveyUI.controller'; import { CollaboratorController } from './controllers/collaborator.controller'; import { DownloadTaskController } from './controllers/downloadTask.controller'; import { SessionController } from './controllers/session.controller'; +import { SurveyGroupController } from './controllers/surveyGroup.controller'; import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyHistory } from 'src/models/surveyHistory.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; +import { SurveyGroup } from 'src/models/surveyGroup.entity'; import { Word } from 'src/models/word.entity'; import { Collaborator } from 'src/models/collaborator.entity'; import { DownloadTask } from 'src/models/downloadTask.entity'; @@ -38,6 +40,7 @@ import { CounterService } from '../surveyResponse/services/counter.service'; import { FileService } from '../file/services/file.service'; import { DownloadTaskService } from './services/downloadTask.service'; import { SessionService } from './services/session.service'; +import { SurveyGroupService } from './services/surveyGroup.service'; import { Session } from 'src/models/session.entity'; @Module({ @@ -52,6 +55,7 @@ import { Session } from 'src/models/session.entity'; Counter, DownloadTask, Session, + SurveyGroup, ]), ConfigModule, SurveyResponseModule, @@ -68,6 +72,7 @@ import { Session } from 'src/models/session.entity'; CollaboratorController, DownloadTaskController, SessionController, + SurveyGroupController, ], providers: [ DataStatisticService, @@ -82,6 +87,7 @@ import { Session } from 'src/models/session.entity'; DownloadTaskService, FileService, SessionService, + SurveyGroupService, ], }) export class SurveyModule {} diff --git a/web/src/management/api/space.ts b/web/src/management/api/space.ts index 41eb4382..71794003 100644 --- a/web/src/management/api/space.ts +++ b/web/src/management/api/space.ts @@ -78,3 +78,21 @@ export const getCollaboratorPermissions = (surveyId: string) => { } }) } + +export const createGroup = ({ name }: any) => { + return axios.post('surveyGroup', { name }) +} + +export const updateGroup = ({ _id, name }: any) => { + return axios.post(`/surveyGroup/${_id}`, { name }) +} + +export const getGroupList = (params: any) => { + return axios.get('/surveyGroup', { + params + }) +} + +export const deleteGroup = (id: string) => { + return axios.delete(`/surveyGroup/${id}`) +} diff --git a/web/src/management/api/survey.js b/web/src/management/api/survey.js index 1666320a..aab5dc56 100644 --- a/web/src/management/api/survey.js +++ b/web/src/management/api/survey.js @@ -1,13 +1,14 @@ import axios from './base' -export const getSurveyList = ({ curPage, filter, order, workspaceId }) => { +export const getSurveyList = ({ curPage, filter, order, workspaceId, groupId }) => { return axios.get('/survey/getList', { params: { pageSize: 10, curPage, filter, order, - workspaceId + workspaceId, + groupId } }) } diff --git a/web/src/management/config/listConfig.js b/web/src/management/config/listConfig.js index 890fb2c8..7a326aa6 100644 --- a/web/src/management/config/listConfig.js +++ b/web/src/management/config/listConfig.js @@ -34,6 +34,25 @@ export const spaceListConfig = { } } +export const groupListConfig = { + name: { + title: '分组名称', + key: 'name', + width: 200 + }, + surveyTotal: { + title: '问卷数', + key: 'surveyTotal', + width: 150, + tip: true + }, + createdAt: { + title: '创建时间', + key: 'createdAt', + minWidth: 200 + } +} + export const fieldConfig = { type: { title: '类型', @@ -92,6 +111,16 @@ export const noSpaceSearchDataConfig = { desc: '可以更换条件查询试试', img: '/imgs/icons/list-empty.webp' } +export const noGroupDataConfig = { + title: '您还没有创建问卷分组', + desc: '赶快点击右上角立即创建问卷分组吧!', + img: '/imgs/icons/list-empty.webp' +} +export const noGroupSearchDataConfig = { + title: '没有满足该查询条件的问卷分组哦', + desc: '可以更换条件查询试试', + img: '/imgs/icons/list-empty.webp' +} export const noSearchDataConfig = { title: '没有满足该查询条件的问卷', desc: '可以更换条件查询试试', diff --git a/web/src/management/pages/create/components/CreateForm.vue b/web/src/management/pages/create/components/CreateForm.vue index 43b0c628..d5d509ac 100644 --- a/web/src/management/pages/create/components/CreateForm.vue +++ b/web/src/management/pages/create/components/CreateForm.vue @@ -26,6 +26,19 @@ />

备注仅自己可见

+ + + + + 开始创建 @@ -37,11 +50,13 @@ + + \ No newline at end of file diff --git a/web/src/management/pages/list/components/GroupModify.vue b/web/src/management/pages/list/components/GroupModify.vue new file mode 100644 index 00000000..7ccf922c --- /dev/null +++ b/web/src/management/pages/list/components/GroupModify.vue @@ -0,0 +1,119 @@ + + + + + + \ No newline at end of file diff --git a/web/src/management/pages/list/components/ModifyDialog.vue b/web/src/management/pages/list/components/ModifyDialog.vue index 1ac7da58..6940cd80 100644 --- a/web/src/management/pages/list/components/ModifyDialog.vue +++ b/web/src/management/pages/list/components/ModifyDialog.vue @@ -21,6 +21,19 @@ + + + + + @@ -66,10 +89,13 @@ import { storeToRefs } from 'pinia' import { useRouter } from 'vue-router' import BaseList from './components/BaseList.vue' import SpaceList from './components/SpaceList.vue' +import GroupList from './components/GroupList.vue' import SliderBar from './components/SliderBar.vue' import SpaceModify from './components/SpaceModify.vue' +import GroupModify from './components/GroupModify.vue' import TopNav from '@/management/components/TopNav.vue' -import { SpaceType } from '@/management/utils/workSpace' +import { MenuType } from '@/management/utils/workSpace' + import { useWorkSpaceStore } from '@/management/stores/workSpace' import { useSurveyListStore } from '@/management/stores/surveyList' import { type IWorkspace } from '@/management/utils/workSpace' @@ -78,14 +104,39 @@ const workSpaceStore = useWorkSpaceStore() const surveyListStore = useSurveyListStore() const { surveyList, surveyTotal } = storeToRefs(surveyListStore) -const { spaceMenus, workSpaceId, spaceType, workSpaceList, workSpaceListTotal } = +const { spaceMenus, workSpaceId, groupId, menuType, workSpaceList, workSpaceListTotal, groupList, groupListTotal } = storeToRefs(workSpaceStore) const router = useRouter() +const tableTitle = computed(() => { + if(menuType.value === MenuType.PersonalGroup && !groupId.value) { + return '我的空间' + } else if (menuType.value === MenuType.SpaceGroup && !workSpaceId.value) { + return '团队空间' + } else { + return currentTeamSpace.value?.name || '问卷列表'; + } +}) + +const activeValue = computed(() => { + if(workSpaceId.value !== '') { + return workSpaceId.value + } else if(groupId.value !== '') { + return groupId.value + } else if(menuType.value === MenuType.PersonalGroup) { + return MenuType.PersonalGroup + } else if(menuType.value === MenuType.SpaceGroup) { + return MenuType.SpaceGroup + } else { + return '' + } +}) + const loading = ref(false) const spaceListRef = ref(null) const spaceLoading = ref(false) +const groupLoading = ref(false) const fetchSpaceList = async (params?: any) => { spaceLoading.value = true @@ -94,24 +145,39 @@ const fetchSpaceList = async (params?: any) => { spaceLoading.value = false } -const handleSpaceSelect = (id: SpaceType | string) => { - if (id === spaceType.value || id === workSpaceId.value) { +const fetchGroupList = async (params?: any) => { + groupLoading.value = true + workSpaceStore.changeWorkSpace('') + workSpaceStore.getGroupList(params) + groupLoading.value = false +} + +const handleSpaceSelect = (id: MenuType | string) => { + if (groupId.value === id || workSpaceId.value === id) { return void 0 } - + let parentMenu = undefined switch (id) { - case SpaceType.Personal: - workSpaceStore.changeSpaceType(SpaceType.Personal) + case MenuType.PersonalGroup: + workSpaceStore.changeMenuType(MenuType.PersonalGroup) workSpaceStore.changeWorkSpace('') + fetchGroupList() break - case SpaceType.Group: - workSpaceStore.changeSpaceType(SpaceType.Group) + case MenuType.SpaceGroup: + workSpaceStore.changeMenuType(MenuType.SpaceGroup) workSpaceStore.changeWorkSpace('') fetchSpaceList() break default: - workSpaceStore.changeSpaceType(SpaceType.Teamwork) - workSpaceStore.changeWorkSpace(id) + parentMenu = spaceMenus.value.find((parent: any) => parent.children.find((children: any) => children.id.toString() === id)) + if(parentMenu != undefined) { + workSpaceStore.changeMenuType(parentMenu.id) + if(parentMenu.id === MenuType.PersonalGroup) { + workSpaceStore.changeGroup(id) + } else if (parentMenu.id === MenuType.SpaceGroup) { + workSpaceStore.changeWorkSpace(id) + } + } break } fetchSurveyList() @@ -133,6 +199,7 @@ const fetchSurveyList = async (params?: any) => { } onMounted(() => { + fetchGroupList() fetchSpaceList() fetchSurveyList() }) @@ -168,7 +235,7 @@ const onCloseModifyInTeamWork = (data: IWorkspace) => { } } -const onCloseModify = (type: string) => { +const onCloseSpaceModify = (type: string) => { showSpaceModify.value = false if (type === 'update' && spaceListRef.value) { fetchSpaceList() @@ -179,6 +246,20 @@ const onSpaceCreate = () => { modifyType.value = 'add' showSpaceModify.value = true } + +// 分组 + +const showGroupModify = ref(false) + +const onCloseGroupModify = () => { + showGroupModify.value = false + fetchGroupList() +} + +const onGroupCreate = () => { + showGroupModify.value = true +} + const onCreate = () => { router.push('/create') } diff --git a/web/src/management/stores/surveyList.ts b/web/src/management/stores/surveyList.ts index 7d403af6..fcd885ef 100644 --- a/web/src/management/stores/surveyList.ts +++ b/web/src/management/stores/surveyList.ts @@ -6,7 +6,7 @@ import 'element-plus/theme-chalk/src/message.scss' import { CODE_MAP } from '@/management/api/base' import { getSurveyList as getSurveyListReq } from '@/management/api/survey' - +import { GroupState } from '@/management/utils/workSpace' import { useWorkSpaceStore } from './workSpace' import { @@ -150,7 +150,8 @@ export const useSurveyListStore = defineStore('surveyList', () => { pageSize: payload?.pageSize || 10, // 默认一页10条 filter: filterString, order: orderString, - workspaceId: workSpaceStore.workSpaceId + workspaceId: workSpaceStore.workSpaceId, + groupId: workSpaceStore.groupId === GroupState.All ? '' : workSpaceStore.groupId } const res: any = await getSurveyListReq(params) diff --git a/web/src/management/stores/workSpace.ts b/web/src/management/stores/workSpace.ts index 9469bc02..1d3c95c3 100644 --- a/web/src/management/stores/workSpace.ts +++ b/web/src/management/stores/workSpace.ts @@ -10,11 +10,16 @@ import { updateSpace as updateSpaceReq, deleteSpace as deleteSpaceReq, getSpaceList as getSpaceListReq, - getSpaceDetail as getSpaceDetailReq + getSpaceDetail as getSpaceDetailReq, + createGroup, + getGroupList as getGroupListReq, + updateGroup as updateGroupReq, + deleteGroup as deleteGroupReq } from '@/management/api/space' -import { SpaceType } from '@/management/utils/workSpace' -import { type SpaceDetail, type SpaceItem, type IWorkspace } from '@/management/utils/workSpace' +import { GroupState, MenuType } from '@/management/utils/workSpace' +import { type SpaceDetail, type SpaceItem, type IWorkspace, type IGroup, type GroupItem, } from '@/management/utils/workSpace' + import { useSurveyListStore } from './surveyList' @@ -24,16 +29,18 @@ export const useWorkSpaceStore = defineStore('workSpace', () => { { icon: 'icon-wodekongjian', name: '我的空间', - id: SpaceType.Personal + id: MenuType.PersonalGroup, + children: [] }, { icon: 'icon-tuanduikongjian', name: '团队空间', - id: SpaceType.Group, + id: MenuType.SpaceGroup, children: [] } ]) - const spaceType = ref(SpaceType.Personal) + const menuType = ref(MenuType.PersonalGroup) + const groupId = ref('') const workSpaceId = ref('') const spaceDetail = ref(null) const workSpaceList = ref([]) @@ -50,7 +57,8 @@ export const useWorkSpaceStore = defineStore('workSpace', () => { const workSpace = list.map((item: SpaceDetail) => { return { id: item._id, - name: item.name + name: item.name, + total: item.surveyTotal } }) workSpaceList.value = list @@ -78,12 +86,19 @@ export const useWorkSpaceStore = defineStore('workSpace', () => { } } - function changeSpaceType(id: SpaceType) { - spaceType.value = id + function changeMenuType(id: MenuType) { + menuType.value = id } function changeWorkSpace(id: string) { workSpaceId.value = id + groupId.value = '' + surveyListStore.resetSearch() + } + + function changeGroup(id: string) { + groupId.value = id + workSpaceId.value = '' surveyListStore.resetSearch() } @@ -126,21 +141,130 @@ export const useWorkSpaceStore = defineStore('workSpace', () => { function setSpaceDetail(data: null | SpaceDetail) { spaceDetail.value = data } + + // 分组 + const groupList = ref([]) + const groupAllList = ref([]) + const groupListTotal = ref(0) + const groupDetail = ref(null) + async function addGroup(params: IGroup) { + const { name } = params + const res: any = await createGroup({ name }) + + if (res.code === CODE_MAP.SUCCESS) { + ElMessage.success('添加成功') + } else { + ElMessage.error('createGroup code err' + res.errmsg) + } + } + + async function updateGroup(params: Required) { + const { _id, name } = params + const res: any = await updateGroupReq({ _id, name }) + + if (res?.code === CODE_MAP.SUCCESS) { + ElMessage.success('更新成功') + } else { + ElMessage.error(res?.errmsg) + } + } + + async function getGroupList(params = { curPage: 1 }) { + try { + const res: any = await getGroupListReq(params) + if (res.code === CODE_MAP.SUCCESS) { + const { list, allList, total, notTotal } = res.data + let allTotal = notTotal + const group = list.map((item: GroupItem) => { + allTotal += item.surveyTotal + return { + id: item._id, + name: item.name, + total: item.surveyTotal, + } + }) + group.unshift({ + id: GroupState.All, + name: '全部' , + total: allTotal + }, { + id: GroupState.Not, + name: '未分组' , + total: notTotal + }) + allList.unshift({ + _id: '', + name: '未分组' + }) + groupList.value = list + groupListTotal.value = total + spaceMenus.value[0].children = group + groupAllList.value = allList + } else { + ElMessage.error('getGroupList' + res.errmsg) + } + } catch (err) { + ElMessage.error('getGroupList' + err) + } + } + + function getGroupDetail(id: string) { + try { + const data = groupList.value.find((item: GroupItem) => item._id === id) + if(data != undefined) { + groupDetail.value = data + } else { + ElMessage.error('groupDetail 未找到分组') + } + } catch (err) { + ElMessage.error('groupDetail' + err) + } + } + + function setGroupDetail(data: null | GroupItem) { + groupDetail.value = data + } + + async function deleteGroup(id: string) { + try { + const res: any = await deleteGroupReq(id) + + if (res.code === CODE_MAP.SUCCESS) { + ElMessage.success('删除成功') + } else { + ElMessage.error(res.errmsg) + } + } catch (err: any) { + ElMessage.error(err) + } + } return { + menuType, spaceMenus, - spaceType, + groupId, workSpaceId, spaceDetail, workSpaceList, workSpaceListTotal, getSpaceList, getSpaceDetail, - changeSpaceType, + changeMenuType, changeWorkSpace, + changeGroup, addSpace, deleteSpace, updateSpace, - setSpaceDetail + setSpaceDetail, + groupList, + groupAllList, + groupListTotal, + groupDetail, + addGroup, + updateGroup, + getGroupList, + getGroupDetail, + setGroupDetail, + deleteGroup } }) diff --git a/web/src/management/utils/workSpace.ts b/web/src/management/utils/workSpace.ts index 654cfd2a..ac0cc95f 100644 --- a/web/src/management/utils/workSpace.ts +++ b/web/src/management/utils/workSpace.ts @@ -7,9 +7,15 @@ export interface MenuItem { id: string name: string icon?: string + total?: Number children?: MenuItem[] } +export type IGroup = { + _id?: string + name: string +} + export type IWorkspace = { _id?: string name: string @@ -29,6 +35,7 @@ export interface SpaceDetail { name: string currentUserId?: string description: string + surveyTotal: number members: IMember[] } @@ -49,16 +56,30 @@ export interface ICollaborator { permissions: Array } -export enum SpaceType { - Personal = 'personal', - Group = 'group', - Teamwork = 'teamwork' +export type GroupItem = { + _id: string, + name: string, + createdAt: string + updatedAt?: string + ownerId: string + surveyTotal: number } + +export enum MenuType { + PersonalGroup = 'personalGroup', + SpaceGroup = 'spaceGroup', +} + export enum UserRole { Admin = 'admin', Member = 'user' } +export enum GroupState { + All = 'all', + Not = 'nogrouped' +} + // 定义角色标签映射对象 export const roleLabels: Record = { [UserRole.Admin]: '管理员',