[Feature]:我的空间新增分组管理 (#439)

* fix: 问卷列表更多按钮图标优化

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* feat: 我的空间新增分组管理

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改单元测试

* fix: 修改我的空间验收问题

* fix; 修改我的空间验收问题

* fix: 修改二次验收相关问题

---------

Co-authored-by: dayou <853094838@qq.com>
This commit is contained in:
Liang-Yaxin 2024-10-31 21:39:08 +08:00 committed by GitHub
parent 20f01768cf
commit 12f19559a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1355 additions and 78 deletions

View File

@ -29,6 +29,7 @@ import { SurveyHistory } from './models/surveyHistory.entity';
import { ResponseSchema } from './models/responseSchema.entity'; import { ResponseSchema } from './models/responseSchema.entity';
import { Counter } from './models/counter.entity'; import { Counter } from './models/counter.entity';
import { SurveyResponse } from './models/surveyResponse.entity'; import { SurveyResponse } from './models/surveyResponse.entity';
import { SurveyGroup } from './models/surveyGroup.entity';
import { ClientEncrypt } from './models/clientEncrypt.entity'; import { ClientEncrypt } from './models/clientEncrypt.entity';
import { Word } from './models/word.entity'; import { Word } from './models/word.entity';
import { MessagePushingTask } from './models/messagePushingTask.entity'; import { MessagePushingTask } from './models/messagePushingTask.entity';
@ -78,6 +79,7 @@ import { Logger } from './logger';
SurveyConf, SurveyConf,
SurveyHistory, SurveyHistory,
SurveyResponse, SurveyResponse,
SurveyGroup,
Counter, Counter,
ResponseSchema, ResponseSchema,
ClientEncrypt, ClientEncrypt,

View File

@ -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;
}

View File

@ -37,6 +37,9 @@ export class SurveyMeta extends BaseEntity {
@Column() @Column()
workspaceId: string; workspaceId: string;
@Column()
groupId: string;
@Column() @Column()
curStatus: { curStatus: {
status: RECORD_STATUS; status: RECORD_STATUS;

View File

@ -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>(SurveyGroupController);
service = module.get<SurveyGroupService>(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);
});
});
});

View File

@ -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>(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);
});
});
});

View File

@ -77,6 +77,7 @@ describe('SurveyMetaController', () => {
survey: { survey: {
title: reqBody.title, title: reqBody.title,
remark: reqBody.remark, remark: reqBody.remark,
groupId: null,
}, },
}); });

View File

@ -86,6 +86,7 @@ describe('SurveyMetaService', () => {
createMethod: params.createMethod, createMethod: params.createMethod,
createFrom: params.createFrom, createFrom: params.createFrom,
workspaceId: params.workspaceId, workspaceId: params.workspaceId,
groupId: null,
}); });
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey); expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
expect(result).toEqual(newSurvey); expect(result).toEqual(newSurvey);

View File

@ -78,7 +78,7 @@ export class SurveyController {
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
} }
const { title, remark, createMethod, createFrom } = value; const { title, remark, createMethod, createFrom, groupId } = value;
let surveyType = '', let surveyType = '',
workspaceId = null; workspaceId = null;
@ -100,6 +100,7 @@ export class SurveyController {
createMethod, createMethod,
createFrom, createFrom,
workspaceId, workspaceId,
groupId,
}); });
await this.surveyConfService.createSurveyConf({ await this.surveyConfService.createSurveyConf({
surveyId: surveyMeta._id.toString(), surveyId: surveyMeta._id.toString(),

View File

@ -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,
};
}
}

View File

@ -48,6 +48,7 @@ export class SurveyMetaController {
title: Joi.string().required(), title: Joi.string().required(),
remark: Joi.string().allow(null, '').default(''), remark: Joi.string().allow(null, '').default(''),
surveyId: Joi.string().required(), surveyId: Joi.string().required(),
groupId: Joi.string().allow(null, ''),
}).validate(reqBody, { allowUnknown: true }); }).validate(reqBody, { allowUnknown: true });
if (error) { if (error) {
@ -57,6 +58,8 @@ export class SurveyMetaController {
const survey = req.surveyMeta; const survey = req.surveyMeta;
survey.title = value.title; survey.title = value.title;
survey.remark = value.remark; survey.remark = value.remark;
survey.groupId =
value.groupId && value.groupId !== '' ? value.groupId : null;
await this.surveyMetaService.editSurveyMeta({ await this.surveyMetaService.editSurveyMeta({
survey, survey,
@ -86,7 +89,7 @@ export class SurveyMetaController {
this.logger.error(error.message); this.logger.error(error.message);
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
} }
const { curPage, pageSize, workspaceId } = value; const { curPage, pageSize, workspaceId, groupId } = value;
let filter = {}, let filter = {},
order = {}; order = {};
if (value.filter) { if (value.filter) {
@ -120,6 +123,7 @@ export class SurveyMetaController {
filter, filter,
order, order,
workspaceId, workspaceId,
groupId,
surveyIdList, surveyIdList,
}); });
return { return {

View File

@ -20,6 +20,9 @@ export class CreateSurveyDto {
@ApiProperty({ description: '问卷创建在哪个空间下', required: false }) @ApiProperty({ description: '问卷创建在哪个空间下', required: false })
workspaceId?: string; workspaceId?: string;
@ApiProperty({ description: '问卷创建在哪个分组下', required: false })
groupId?: string;
static validate(data) { static validate(data) {
return Joi.object({ return Joi.object({
title: Joi.string().required(), title: Joi.string().required(),
@ -36,6 +39,7 @@ export class CreateSurveyDto {
otherwise: Joi.allow(null), otherwise: Joi.allow(null),
}), }),
workspaceId: Joi.string().allow(null, ''), workspaceId: Joi.string().allow(null, ''),
groupId: Joi.string().allow(null, ''),
}).validate(data); }).validate(data);
} }
} }

View File

@ -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);
}
}

View File

@ -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<GetGroupListDto>): Joi.ValidationResult {
return Joi.object({
curPage: Joi.number().required(),
pageSize: Joi.number().allow(null).default(10),
name: Joi.string().allow(null, '').optional(),
}).validate(data);
}
}

View File

@ -17,6 +17,9 @@ export class GetSurveyListDto {
@ApiProperty({ description: '空间id', required: false }) @ApiProperty({ description: '空间id', required: false })
workspaceId?: string; workspaceId?: string;
@ApiProperty({ description: '分组id', required: false })
groupId?: string;
static validate(data) { static validate(data) {
return Joi.object({ return Joi.object({
curPage: Joi.number().required(), curPage: Joi.number().required(),
@ -24,6 +27,7 @@ export class GetSurveyListDto {
filter: Joi.string().allow(null), filter: Joi.string().allow(null),
order: Joi.string().allow(null), order: Joi.string().allow(null),
workspaceId: Joi.string().allow(null, ''), workspaceId: Joi.string().allow(null, ''),
groupId: Joi.string().allow(null, ''),
}).validate(data); }).validate(data);
} }
} }

View File

@ -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);
}
}

View File

@ -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<SurveyGroup>,
@InjectRepository(SurveyMeta)
private surveyMetaRepository: MongoRepository<SurveyMeta>,
) {}
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<SurveyGroup>) {
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);
}
}

View File

@ -47,6 +47,7 @@ export class SurveyMetaService {
createMethod: string; createMethod: string;
createFrom: string; createFrom: string;
workspaceId?: string; workspaceId?: string;
groupId?: string;
}) { }) {
const { const {
title, title,
@ -57,6 +58,7 @@ export class SurveyMetaService {
createFrom, createFrom,
userId, userId,
workspaceId, workspaceId,
groupId,
} = params; } = params;
const surveyPath = await this.getNewSurveyPath(); const surveyPath = await this.getNewSurveyPath();
const newSurvey = this.surveyRepository.create({ const newSurvey = this.surveyRepository.create({
@ -71,6 +73,7 @@ export class SurveyMetaService {
createMethod, createMethod,
createFrom, createFrom,
workspaceId, workspaceId,
groupId: groupId && groupId !== '' ? groupId : null,
}); });
return await this.surveyRepository.save(newSurvey); return await this.surveyRepository.save(newSurvey);
@ -143,10 +146,18 @@ export class SurveyMetaService {
filter: Record<string, any>; filter: Record<string, any>;
order: Record<string, any>; order: Record<string, any>;
workspaceId?: string; workspaceId?: string;
groupId?: string;
surveyIdList?: Array<string>; surveyIdList?: Array<string>;
}): Promise<{ data: any[]; count: number }> { }): Promise<{ data: any[]; count: number }> {
const { pageNum, pageSize, userId, username, workspaceId, surveyIdList } = const {
condition; pageNum,
pageSize,
userId,
username,
workspaceId,
groupId,
surveyIdList,
} = condition;
const skip = (pageNum - 1) * pageSize; const skip = (pageNum - 1) * pageSize;
try { try {
const query: Record<string, any> = Object.assign( const query: Record<string, any> = Object.assign(
@ -160,6 +171,15 @@ export class SurveyMetaService {
if (condition.filter['curStatus.status']) { if (condition.filter['curStatus.status']) {
query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT; query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT;
} }
if (groupId && groupId !== 'all') {
query.groupId =
groupId === 'nogrouped'
? {
$exists: true,
$eq: null,
}
: groupId;
}
if (workspaceId) { if (workspaceId) {
query.workspaceId = workspaceId; query.workspaceId = workspaceId;
} else { } else {
@ -228,4 +248,21 @@ export class SurveyMetaService {
}); });
return total; 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;
}
} }

View File

@ -17,11 +17,13 @@ import { SurveyUIController } from './controllers/surveyUI.controller';
import { CollaboratorController } from './controllers/collaborator.controller'; import { CollaboratorController } from './controllers/collaborator.controller';
import { DownloadTaskController } from './controllers/downloadTask.controller'; import { DownloadTaskController } from './controllers/downloadTask.controller';
import { SessionController } from './controllers/session.controller'; import { SessionController } from './controllers/session.controller';
import { SurveyGroupController } from './controllers/surveyGroup.controller';
import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyConf } from 'src/models/surveyConf.entity';
import { SurveyHistory } from 'src/models/surveyHistory.entity'; import { SurveyHistory } from 'src/models/surveyHistory.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { SurveyGroup } from 'src/models/surveyGroup.entity';
import { Word } from 'src/models/word.entity'; import { Word } from 'src/models/word.entity';
import { Collaborator } from 'src/models/collaborator.entity'; import { Collaborator } from 'src/models/collaborator.entity';
import { DownloadTask } from 'src/models/downloadTask.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 { FileService } from '../file/services/file.service';
import { DownloadTaskService } from './services/downloadTask.service'; import { DownloadTaskService } from './services/downloadTask.service';
import { SessionService } from './services/session.service'; import { SessionService } from './services/session.service';
import { SurveyGroupService } from './services/surveyGroup.service';
import { Session } from 'src/models/session.entity'; import { Session } from 'src/models/session.entity';
@Module({ @Module({
@ -52,6 +55,7 @@ import { Session } from 'src/models/session.entity';
Counter, Counter,
DownloadTask, DownloadTask,
Session, Session,
SurveyGroup,
]), ]),
ConfigModule, ConfigModule,
SurveyResponseModule, SurveyResponseModule,
@ -68,6 +72,7 @@ import { Session } from 'src/models/session.entity';
CollaboratorController, CollaboratorController,
DownloadTaskController, DownloadTaskController,
SessionController, SessionController,
SurveyGroupController,
], ],
providers: [ providers: [
DataStatisticService, DataStatisticService,
@ -82,6 +87,7 @@ import { Session } from 'src/models/session.entity';
DownloadTaskService, DownloadTaskService,
FileService, FileService,
SessionService, SessionService,
SurveyGroupService,
], ],
}) })
export class SurveyModule {} export class SurveyModule {}

View File

@ -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}`)
}

View File

@ -1,13 +1,14 @@
import axios from './base' import axios from './base'
export const getSurveyList = ({ curPage, filter, order, workspaceId }) => { export const getSurveyList = ({ curPage, filter, order, workspaceId, groupId }) => {
return axios.get('/survey/getList', { return axios.get('/survey/getList', {
params: { params: {
pageSize: 10, pageSize: 10,
curPage, curPage,
filter, filter,
order, order,
workspaceId workspaceId,
groupId
} }
}) })
} }

View File

@ -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 = { export const fieldConfig = {
type: { type: {
title: '类型', title: '类型',
@ -92,6 +111,16 @@ export const noSpaceSearchDataConfig = {
desc: '可以更换条件查询试试', desc: '可以更换条件查询试试',
img: '/imgs/icons/list-empty.webp' 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 = { export const noSearchDataConfig = {
title: '没有满足该查询条件的问卷', title: '没有满足该查询条件的问卷',
desc: '可以更换条件查询试试', desc: '可以更换条件查询试试',

View File

@ -26,6 +26,19 @@
/> />
<p class="form-item-tip">备注仅自己可见</p> <p class="form-item-tip">备注仅自己可见</p>
</el-form-item> </el-form-item>
<el-form-item prop="groupId" label="问卷分组" v-if="menuType === MenuType.PersonalGroup">
<el-select
v-model="form.groupId"
placeholder="未分组"
>
<el-option
v-for="item in groupAllList"
:key="item?._id"
:label="item?.name"
:value="item?._id"
/>
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button class="create-btn" type="primary" @click="submit" :loading="!canSubmit"> <el-button class="create-btn" type="primary" @click="submit" :loading="!canSubmit">
开始创建 开始创建
@ -37,11 +50,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, toRefs } from 'vue' import { ref, reactive, computed, toRefs } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
import { createSurvey } from '@/management/api/survey' import { createSurvey } from '@/management/api/survey'
import { SURVEY_TYPE_LIST } from '../types' import { SURVEY_TYPE_LIST } from '../types'
import { MenuType, GroupState } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from '@/management/stores/workSpace' import { useWorkSpaceStore } from '@/management/stores/workSpace'
interface Props { interface Props {
@ -53,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const { groupAllList, menuType, groupId, workSpaceId } = storeToRefs(workSpaceStore)
const ruleForm = ref<any>(null) const ruleForm = ref<any>(null)
const state = reactive({ const state = reactive({
@ -62,7 +79,8 @@ const state = reactive({
canSubmit: true, canSubmit: true,
form: { form: {
title: '问卷调研', title: '问卷调研',
remark: '问卷调研' remark: '问卷调研',
groupId: groupId.value == GroupState.All || groupId.value == GroupState.Not ? '' : groupId.value
} }
}) })
const { rules, canSubmit, form } = toRefs(state) const { rules, canSubmit, form } = toRefs(state)
@ -92,8 +110,8 @@ const submit = () => {
surveyType: selectType, surveyType: selectType,
...state.form ...state.form
} }
if (workSpaceStore.workSpaceId) { if (workSpaceId.value) {
payload.workspaceId = workSpaceStore.workSpaceId payload.workspaceId = workSpaceId.value
} }
const res: any = await createSurvey(payload) const res: any = await createSurvey(payload)
if (res?.code === 200 && res?.data?.id) { if (res?.code === 200 && res?.data?.id) {

View File

@ -98,6 +98,8 @@
:type="modifyType" :type="modifyType"
:visible="showModify" :visible="showModify"
:question-info="questionInfo" :question-info="questionInfo"
:group-all-list="groupAllList"
:menu-type="menuType"
@on-close-codify="onCloseModify" @on-close-codify="onCloseModify"
/> />
<CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" /> <CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" />
@ -142,7 +144,7 @@ import {
const surveyListStore = useSurveyListStore() const surveyListStore = useSurveyListStore()
const workSpaceStore = useWorkSpaceStore() const workSpaceStore = useWorkSpaceStore()
const { workSpaceId } = storeToRefs(workSpaceStore) const { workSpaceId, groupAllList, menuType } = storeToRefs(workSpaceStore)
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
loading: { loading: {
@ -363,7 +365,6 @@ const onDelete = async (row) => {
type: 'warning' type: 'warning'
}) })
} catch (error) { } catch (error) {
console.log('取消删除')
return return
} }
@ -371,6 +372,8 @@ const onDelete = async (row) => {
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
onRefresh() onRefresh()
workSpaceStore.getGroupList()
workSpaceStore.getSpaceList()
} else { } else {
ElMessage.error(res.errmsg || '删除失败') ElMessage.error(res.errmsg || '删除失败')
} }
@ -409,6 +412,8 @@ const onCloseModify = (type) => {
questionInfo.value = {} questionInfo.value = {}
if (type === 'update') { if (type === 'update') {
onRefresh() onRefresh()
workSpaceStore.getGroupList()
workSpaceStore.getSpaceList()
} }
} }
const onRowClick = (row) => { const onRowClick = (row) => {

View File

@ -0,0 +1,245 @@
<template>
<div class="search">
<TextSearch placeholder="请输入分组名称" :value="searchVal" @search="onSearchText" />
</div>
<div class="list-wrap" v-if="props.total">
<el-table
v-if="props.total"
ref="multipleListTable"
class="list-table"
:data="data"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
v-loading="loading"
:height="550"
style="width: 100%"
>
<el-table-column column-key="space" width="20" />
<el-table-column
v-for="field in fieldList"
:key="(field as any)?.key"
:label="(field as any).title"
:column-key="(field as any).key"
:width="(field as any).width"
:min-width="(field as any).width || (field as any).minWidth"
class-name="link"
>
<template #default="scope">
<template v-if="(field as any).comp">
<component :is="(field as any).comp" type="table" :value="scope.row" />
</template>
<template v-else>
<span class="cell-span">{{ scope.row[(field as any).key] }}</span>
</template>
</template>
</el-table-column>
<el-table-column
label="操作"
:width="200"
label-class-name="operation"
class-name="table-options"
>
<template #default="scope">
<div class="space-tool-bar">
<ToolBar
:data="scope.row"
:tool-width="50"
:tools="tools"
@click="handleClick"
/>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<EmptyIndex :data="!searchVal ? noGroupDataConfig : noGroupSearchDataConfig" />
</div>
<div class="list-pagination">
<el-pagination
v-if="props.total"
v-model:current-page="curPage"
background
@current-change="handleCurrentChange"
layout="prev, pager, next"
:total="props.total"
>
</el-pagination>
</div>
<GroupModify
v-if="showGroupModify"
type="edit"
:visible="showGroupModify"
@on-close-codify="onCloseModify"
/>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
import { get, map } from 'lodash-es'
import {
noGroupDataConfig,
noGroupSearchDataConfig,
groupListConfig
} from '@/management/config/listConfig'
import { MenuType } from '@/management/utils/workSpace'
import GroupModify from './GroupModify.vue'
import TextSearch from '@/management/pages/list/components/TextSearch.vue'
import EmptyIndex from '@/management/components/EmptyIndex.vue'
import ToolBar from './ToolBar.vue'
import { useWorkSpaceStore } from '@/management/stores/workSpace'
import { useSurveyListStore } from '@/management/stores/surveyList'
const workSpaceStore = useWorkSpaceStore()
const surveyListStore = useSurveyListStore()
const showGroupModify = ref(false)
const props = defineProps({
loading: {
type: Boolean,
default: false
},
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
})
const emit = defineEmits(['refresh'])
const fields = ['name', 'surveyTotal', 'createdAt']
const fieldList = computed(() => {
return map(fields, (f) => {
return get(groupListConfig, f, null)
})
})
const tools = ref([{
key: 'open',
label: '进入'
}, {
key: 'modify',
label: '管理'
}, {
key: 'delete',
label: '删除'
}])
const data = computed(() => {
return props.data
})
let searchVal = ref('')
let curPage = ref(1)
const emitRefresh = (page: number, name: string) => {
curPage.value = page
emit('refresh', {
curPage: page,
name
})
}
const handleCurrentChange = async (val: number) => {
emitRefresh(val, searchVal.value)
}
const onSearchText = async (value: string) => {
searchVal.value = value
emitRefresh(1, value)
}
const handleModify = (id: string) => {
workSpaceStore.getGroupDetail(id)
showGroupModify.value = true
}
const handleDelete = (id: string) => {
ElMessageBox.confirm(
'删除分组后,属于该分组的问卷将会自动更换到“未分组”下,是否确认本次删除?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
await workSpaceStore.deleteGroup(id)
await workSpaceStore.getGroupList()
})
.catch(() => {})
}
const handleClick = (key: string, data: any) => {
if (key === 'modify') {
handleModify(data._id)
} else if (key === 'delete') {
handleDelete(data._id)
} else if(key === 'open') {
workSpaceStore.changeMenuType(MenuType.PersonalGroup)
workSpaceStore.changeGroup(data._id)
surveyListStore.getSurveyList({
pageSize: 10,
curPage: 1
})
}
}
const onCloseModify = () => {
showGroupModify.value = false
workSpaceStore.getGroupList()
}
defineExpose({ onCloseModify })
</script>
<style lang="scss" scoped>
.search {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
}
.list-wrap {
padding: 20px;
background: #fff;
.search {
display: flex;
}
.list-table {
:deep(.el-table__header) {
.tableview-header .el-table__cell {
.cell {
height: 24px;
color: #4a4c5b;
font-size: 14px;
}
}
}
:deep(.tableview-row) {
.tableview-cell {
height: 42px;
&.link {
cursor: pointer;
}
.cell .cell-span {
font-size: 14px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<el-dialog
class="base-dialog-root"
:model-value="visible"
width="40%"
:title="formTitle"
@close="onClose"
>
<el-form
class="base-form-root"
ref="ruleForm"
:model="formModel"
:rules="rules"
label-position="top"
size="large"
@submit.prevent
>
<el-form-item label="分组名称" prop="name">
<el-input v-model="formModel.name" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="onClose">取消</el-button>
<el-button type="primary" class="save-btn" @click="onConfirm">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, onMounted } from 'vue'
import { pick as _pick } from 'lodash-es'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { QOP_MAP } from '@/management/utils/constant'
import { type IGroup } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from '@/management/stores/workSpace'
const workSpaceStore = useWorkSpaceStore()
const emit = defineEmits(['on-close-codify'])
const props = defineProps({
type: String,
width: String,
visible: Boolean
})
const ruleForm = shallowRef<any>(null)
const formTitle = computed(() => {
return props.type === QOP_MAP.ADD ? '创建分组' : '管理分组'
})
const formModel = ref<Required<IGroup>>({
_id: '',
name: ''
})
const rules = {
name: [{ required: true, message: '请输入分组名称', trigger: 'blur' }]
}
const groupDetail = computed(() => {
return workSpaceStore.groupDetail
})
onMounted(() => {
if (props.type === QOP_MAP.EDIT) {
formModel.value = _pick(groupDetail.value as any, ['_id', 'name'])
}
})
const onClose = () => {
formModel.value = {
_id: '',
name: ''
}
//
workSpaceStore.setGroupDetail(null)
emit('on-close-codify')
}
const onConfirm = async () => {
ruleForm.value?.validate(async (valid: boolean) => {
if (valid) {
if (props.type === QOP_MAP.ADD) {
try {
await handleAdd()
emit('on-close-codify')
} catch (err) {
ElMessage.error('createGroup status err' + err)
}
} else {
try {
await handleUpdate()
emit('on-close-codify')
} catch (err) {
ElMessage.error('createGroup status err' + err)
}
}
} else {
return false
}
})
}
const handleUpdate = async () => {
await workSpaceStore.updateGroup(formModel.value)
}
const handleAdd = async () => {
await workSpaceStore.addGroup({ name: formModel.value.name })
}
</script>
<style lang="scss" rel="lang/scss" scoped>
.base-form-root {
padding: 20px;
}
</style>

View File

@ -21,6 +21,19 @@
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="current.remark" /> <el-input v-model="current.remark" />
</el-form-item> </el-form-item>
<el-form-item prop="groupId" label="问卷分组" v-if="menuType === MenuType.PersonalGroup">
<el-select
v-model="current.groupId"
placeholder="未分组"
>
<el-option
v-for="item in groupAllList"
:key="item._id"
:label="item.name"
:value="item._id"
/>
</el-select>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -35,7 +48,6 @@
<script> <script>
import { pick as _pick } from 'lodash-es' import { pick as _pick } from 'lodash-es'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
@ -43,17 +55,22 @@ import { CODE_MAP } from '@/management/api/base'
import { updateSurvey, createSurvey } from '@/management/api/survey' import { updateSurvey, createSurvey } from '@/management/api/survey'
import { QOP_MAP } from '@/management/utils/constant' import { QOP_MAP } from '@/management/utils/constant'
import { MenuType } from '@/management/utils/workSpace'
export default { export default {
name: 'ModifyDialog', name: 'ModifyDialog',
props: { props: {
type: String, type: String,
questionInfo: Object, questionInfo: Object,
width: String, width: String,
visible: Boolean visible: Boolean,
groupAllList: Array,
menuType: String,
}, },
data() { data() {
return { return {
QOP_MAP, QOP_MAP,
MenuType,
loadingInstance: null, loadingInstance: null,
rules: { rules: {
title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }] title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }]
@ -72,7 +89,8 @@ export default {
methods: { methods: {
getCurrent(val) { getCurrent(val) {
return { return {
..._pick(val, ['title', 'remark']) ..._pick(val, ['title', 'remark']),
groupId: val.groupId === null ? '' : val.groupId
} }
}, },
onClose() { onClose() {

View File

@ -1,14 +1,16 @@
<template> <template>
<el-menu <el-menu
:default-active="SpaceType.Personal" :default-active="active"
class="el-menu-vertical" class="el-menu-vertical"
ref="menuRef" ref="menuRef"
@select="handleSelect" @select="handleMenu"
@open="handleMenu"
@close="handleMenu"
> >
<template v-for="(menu, index) in menus" :key="menu.id"> <template v-for="(menu, index) in props.menus" :key="menu.id">
<el-menu-item <el-menu-item
:class="[index === 0 ? 'bottom' : '', index > 2 ? 'sub-item' : 'main-item']" :class="[index === 0 ? 'bottom' : '', index > 2 ? 'sub-item' : 'main-item', active == menu.id ? 'check-item' : '' ]"
:index="menu.id" :index="menu.id.toString()"
v-if="!menu.children?.length" v-if="!menu.children?.length"
> >
<template #title> <template #title>
@ -18,46 +20,65 @@
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
<el-menu-item-group v-else> <el-sub-menu v-else :index="menu.id.toString()" :class="[ active == menu.id ? 'check-item' : '' ]">
<template #title> <template #title>
<el-menu-item :index="menu.id" class="sub-title main-item"> <div class="title-content sub-title main-item">
<div class="title-content">
<i :class="['iconfont', menu.icon]"></i> <i :class="['iconfont', menu.icon]"></i>
<span>{{ menu.name }}</span> <span>{{ menu.name }}</span>
</div> </div>
</el-menu-item>
</template> </template>
<el-menu-item v-for="item in menu.children" :key="item.id" :index="item.id"> <el-menu-item v-for="item in menu.children" :key="item.id" :index="item.id.toString()" :class="[ active == item.id ? 'check-item' : '' ]">
<p> <div class="title-box">
{{ item.name }} <p class="title-text">{{ item.name }}</p>
</p> <p class="title-total">{{ item.total }}</p>
</div>
</el-menu-item> </el-menu-item>
</el-menu-item-group> </el-sub-menu>
</template> </template>
</el-menu> </el-menu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { type MenuItem } from '@/management/utils/workSpace' import { type MenuItem } from '@/management/utils/workSpace'
import { SpaceType } from '@/management/utils/workSpace' import { MenuType } from '@/management/utils/workSpace'
const menuRef = ref() const menuRef = ref()
const props = withDefaults(
withDefaults(
defineProps<{ defineProps<{
menus: Array<MenuItem> menus: Array<MenuItem>,
activeValue: string
}>(), }>(),
{ {
menus: () => [] menus: () => [],
activeValue: MenuType.PersonalGroup
} }
) )
const active = computed({
get: () => {
return props.activeValue
},
set: () => {}
})
const emit = defineEmits(['select']) const emit = defineEmits(['select'])
const handleSelect = (id: string) => { const handleMenu = (id: string) => {
active.value = id
emit('select', id) emit('select', id)
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-sub-menu {
:deep(.el-sub-menu__icon-arrow) {
transform: rotate(-90deg) !important;
}
&.is-opened {
> :deep(.el-sub-menu__title .el-sub-menu__icon-arrow) {
transform: rotate(0deg) !important;
}
}
}
.el-menu-vertical { .el-menu-vertical {
border: none; border: none;
width: 200px; width: 200px;
@ -94,10 +115,6 @@ const handleSelect = (id: string) => {
&.sub-item { &.sub-item {
margin: 0; margin: 0;
} }
&.is-active {
// background-color: #F2F4F7;
background: #fef6e6 100% !important;
}
&:hover { &:hover {
background-color: #f2f4f7; background-color: #f2f4f7;
} }
@ -106,6 +123,27 @@ const handleSelect = (id: string) => {
align-items: center; align-items: center;
font-weight: 400; font-weight: 400;
} }
.title-box {
width: 100%;
display: flex;
justify-content: space-between;
}
.title-text {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-total {
font-size: 14px;
color: #92949D;
text-align: right;
font-weight: 400;
}
} }
:deep(.el-menu-item-group) { :deep(.el-menu-item-group) {
> ul { > ul {
@ -128,4 +166,7 @@ const handleSelect = (id: string) => {
margin-right: 10px; margin-right: 10px;
color: #faa600 !important; color: #faa600 !important;
} }
.check-item {
background: #fef6e6 100% !important
}
</style> </style>

View File

@ -2,23 +2,32 @@
<div class="question-list-root"> <div class="question-list-root">
<TopNav></TopNav> <TopNav></TopNav>
<div class="content-wrap"> <div class="content-wrap">
<SliderBar :menus="spaceMenus" @select="handleSpaceSelect" /> <SliderBar :menus="spaceMenus" :activeValue="activeValue" @select="handleSpaceSelect" />
<div class="list-content"> <div class="list-content">
<div class="top"> <div class="top">
<h2> <h2>
{{ spaceType === SpaceType.Group ? '团队空间' : currentTeamSpace?.name || '问卷列表' }} {{ tableTitle }}
</h2> </h2>
<div class="operation"> <div class="operation">
<el-button <el-button
class="btn create-btn" class="btn create-btn"
type="default" type="default"
@click="onSpaceCreate" @click="onSpaceCreate"
v-if="spaceType == SpaceType.Group" v-if="menuType === MenuType.SpaceGroup && !workSpaceId"
> >
<i class="iconfont icon-chuangjian"></i> <i class="iconfont icon-chuangjian"></i>
<span>创建团队空间</span> <span>创建团队空间</span>
</el-button> </el-button>
<el-button type="default" class="btn" @click="onSetGroup" v-if="workSpaceId"> <el-button
class="btn create-btn"
type="default"
@click="onGroupCreate"
v-if="menuType === MenuType.PersonalGroup && !groupId"
>
<i class="iconfont icon-chuangjian"></i>
<span>创建分组</span>
</el-button>
<el-button type="default" class="btn" @click="onSetGroup" v-if="workSpaceId && menuType === MenuType.SpaceGroup">
<i class="iconfont icon-shujuliebiao"></i> <i class="iconfont icon-shujuliebiao"></i>
<span>团队管理</span> <span>团队管理</span>
</el-button> </el-button>
@ -26,7 +35,7 @@
class="btn create-btn" class="btn create-btn"
type="default" type="default"
@click="onCreate" @click="onCreate"
v-if="spaceType !== SpaceType.Group" v-if="workSpaceId || groupId"
> >
<i class="iconfont icon-chuangjian"></i> <i class="iconfont icon-chuangjian"></i>
<span>创建问卷</span> <span>创建问卷</span>
@ -38,7 +47,7 @@
:data="surveyList" :data="surveyList"
:total="surveyTotal" :total="surveyTotal"
@refresh="fetchSurveyList" @refresh="fetchSurveyList"
v-if="spaceType !== SpaceType.Group" v-if="workSpaceId || groupId"
></BaseList> ></BaseList>
<SpaceList <SpaceList
ref="spaceListRef" ref="spaceListRef"
@ -46,17 +55,31 @@
:loading="spaceLoading" :loading="spaceLoading"
:data="workSpaceList" :data="workSpaceList"
:total="workSpaceListTotal" :total="workSpaceListTotal"
v-if="spaceType === SpaceType.Group" v-if="menuType === MenuType.SpaceGroup && !workSpaceId"
></SpaceList> ></SpaceList>
<GroupList
ref="groupListRef"
@refresh="fetchGroupList"
:loading="groupLoading"
:data="groupList"
:total="groupListTotal"
v-if="menuType === MenuType.PersonalGroup && !groupId"
></GroupList>
</div> </div>
</div> </div>
<SpaceModify <SpaceModify
v-if="showSpaceModify" v-if="showSpaceModify"
:type="modifyType" :type="modifyType"
:visible="showSpaceModify" :visible="showSpaceModify"
@on-close-codify="onCloseModify" @on-close-codify="onCloseSpaceModify"
@update-data="onCloseModifyInTeamWork" @update-data="onCloseModifyInTeamWork"
/> />
<GroupModify
v-if="showGroupModify"
type="add"
:visible="showGroupModify"
@on-close-codify="onCloseGroupModify"
/>
</div> </div>
</template> </template>
@ -66,10 +89,13 @@ import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import BaseList from './components/BaseList.vue' import BaseList from './components/BaseList.vue'
import SpaceList from './components/SpaceList.vue' import SpaceList from './components/SpaceList.vue'
import GroupList from './components/GroupList.vue'
import SliderBar from './components/SliderBar.vue' import SliderBar from './components/SliderBar.vue'
import SpaceModify from './components/SpaceModify.vue' import SpaceModify from './components/SpaceModify.vue'
import GroupModify from './components/GroupModify.vue'
import TopNav from '@/management/components/TopNav.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 { useWorkSpaceStore } from '@/management/stores/workSpace'
import { useSurveyListStore } from '@/management/stores/surveyList' import { useSurveyListStore } from '@/management/stores/surveyList'
import { type IWorkspace } from '@/management/utils/workSpace' import { type IWorkspace } from '@/management/utils/workSpace'
@ -78,14 +104,39 @@ const workSpaceStore = useWorkSpaceStore()
const surveyListStore = useSurveyListStore() const surveyListStore = useSurveyListStore()
const { surveyList, surveyTotal } = storeToRefs(surveyListStore) const { surveyList, surveyTotal } = storeToRefs(surveyListStore)
const { spaceMenus, workSpaceId, spaceType, workSpaceList, workSpaceListTotal } = const { spaceMenus, workSpaceId, groupId, menuType, workSpaceList, workSpaceListTotal, groupList, groupListTotal } =
storeToRefs(workSpaceStore) storeToRefs(workSpaceStore)
const router = useRouter() 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 loading = ref(false)
const spaceListRef = ref<any>(null) const spaceListRef = ref<any>(null)
const spaceLoading = ref(false) const spaceLoading = ref(false)
const groupLoading = ref(false)
const fetchSpaceList = async (params?: any) => { const fetchSpaceList = async (params?: any) => {
spaceLoading.value = true spaceLoading.value = true
@ -94,24 +145,39 @@ const fetchSpaceList = async (params?: any) => {
spaceLoading.value = false spaceLoading.value = false
} }
const handleSpaceSelect = (id: SpaceType | string) => { const fetchGroupList = async (params?: any) => {
if (id === spaceType.value || id === workSpaceId.value) { 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 return void 0
} }
let parentMenu = undefined
switch (id) { switch (id) {
case SpaceType.Personal: case MenuType.PersonalGroup:
workSpaceStore.changeSpaceType(SpaceType.Personal) workSpaceStore.changeMenuType(MenuType.PersonalGroup)
workSpaceStore.changeWorkSpace('') workSpaceStore.changeWorkSpace('')
fetchGroupList()
break break
case SpaceType.Group: case MenuType.SpaceGroup:
workSpaceStore.changeSpaceType(SpaceType.Group) workSpaceStore.changeMenuType(MenuType.SpaceGroup)
workSpaceStore.changeWorkSpace('') workSpaceStore.changeWorkSpace('')
fetchSpaceList() fetchSpaceList()
break break
default: default:
workSpaceStore.changeSpaceType(SpaceType.Teamwork) parentMenu = spaceMenus.value.find((parent: any) => parent.children.find((children: any) => children.id.toString() === id))
workSpaceStore.changeWorkSpace(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 break
} }
fetchSurveyList() fetchSurveyList()
@ -133,6 +199,7 @@ const fetchSurveyList = async (params?: any) => {
} }
onMounted(() => { onMounted(() => {
fetchGroupList()
fetchSpaceList() fetchSpaceList()
fetchSurveyList() fetchSurveyList()
}) })
@ -168,7 +235,7 @@ const onCloseModifyInTeamWork = (data: IWorkspace) => {
} }
} }
const onCloseModify = (type: string) => { const onCloseSpaceModify = (type: string) => {
showSpaceModify.value = false showSpaceModify.value = false
if (type === 'update' && spaceListRef.value) { if (type === 'update' && spaceListRef.value) {
fetchSpaceList() fetchSpaceList()
@ -179,6 +246,20 @@ const onSpaceCreate = () => {
modifyType.value = 'add' modifyType.value = 'add'
showSpaceModify.value = true showSpaceModify.value = true
} }
//
const showGroupModify = ref<boolean>(false)
const onCloseGroupModify = () => {
showGroupModify.value = false
fetchGroupList()
}
const onGroupCreate = () => {
showGroupModify.value = true
}
const onCreate = () => { const onCreate = () => {
router.push('/create') router.push('/create')
} }

View File

@ -6,7 +6,7 @@ import 'element-plus/theme-chalk/src/message.scss'
import { CODE_MAP } from '@/management/api/base' import { CODE_MAP } from '@/management/api/base'
import { getSurveyList as getSurveyListReq } from '@/management/api/survey' import { getSurveyList as getSurveyListReq } from '@/management/api/survey'
import { GroupState } from '@/management/utils/workSpace'
import { useWorkSpaceStore } from './workSpace' import { useWorkSpaceStore } from './workSpace'
import { import {
@ -150,7 +150,8 @@ export const useSurveyListStore = defineStore('surveyList', () => {
pageSize: payload?.pageSize || 10, // 默认一页10条 pageSize: payload?.pageSize || 10, // 默认一页10条
filter: filterString, filter: filterString,
order: orderString, order: orderString,
workspaceId: workSpaceStore.workSpaceId workspaceId: workSpaceStore.workSpaceId,
groupId: workSpaceStore.groupId === GroupState.All ? '' : workSpaceStore.groupId
} }
const res: any = await getSurveyListReq(params) const res: any = await getSurveyListReq(params)

View File

@ -10,11 +10,16 @@ import {
updateSpace as updateSpaceReq, updateSpace as updateSpaceReq,
deleteSpace as deleteSpaceReq, deleteSpace as deleteSpaceReq,
getSpaceList as getSpaceListReq, getSpaceList as getSpaceListReq,
getSpaceDetail as getSpaceDetailReq getSpaceDetail as getSpaceDetailReq,
createGroup,
getGroupList as getGroupListReq,
updateGroup as updateGroupReq,
deleteGroup as deleteGroupReq
} from '@/management/api/space' } from '@/management/api/space'
import { SpaceType } from '@/management/utils/workSpace' import { GroupState, MenuType } from '@/management/utils/workSpace'
import { type SpaceDetail, type SpaceItem, type IWorkspace } from '@/management/utils/workSpace' import { type SpaceDetail, type SpaceItem, type IWorkspace, type IGroup, type GroupItem, } from '@/management/utils/workSpace'
import { useSurveyListStore } from './surveyList' import { useSurveyListStore } from './surveyList'
@ -24,16 +29,18 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
{ {
icon: 'icon-wodekongjian', icon: 'icon-wodekongjian',
name: '我的空间', name: '我的空间',
id: SpaceType.Personal id: MenuType.PersonalGroup,
children: []
}, },
{ {
icon: 'icon-tuanduikongjian', icon: 'icon-tuanduikongjian',
name: '团队空间', name: '团队空间',
id: SpaceType.Group, id: MenuType.SpaceGroup,
children: [] children: []
} }
]) ])
const spaceType = ref(SpaceType.Personal) const menuType = ref(MenuType.PersonalGroup)
const groupId = ref('')
const workSpaceId = ref('') const workSpaceId = ref('')
const spaceDetail = ref<SpaceDetail | null>(null) const spaceDetail = ref<SpaceDetail | null>(null)
const workSpaceList = ref<SpaceItem[]>([]) const workSpaceList = ref<SpaceItem[]>([])
@ -50,7 +57,8 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
const workSpace = list.map((item: SpaceDetail) => { const workSpace = list.map((item: SpaceDetail) => {
return { return {
id: item._id, id: item._id,
name: item.name name: item.name,
total: item.surveyTotal
} }
}) })
workSpaceList.value = list workSpaceList.value = list
@ -78,12 +86,19 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
} }
} }
function changeSpaceType(id: SpaceType) { function changeMenuType(id: MenuType) {
spaceType.value = id menuType.value = id
} }
function changeWorkSpace(id: string) { function changeWorkSpace(id: string) {
workSpaceId.value = id workSpaceId.value = id
groupId.value = ''
surveyListStore.resetSearch()
}
function changeGroup(id: string) {
groupId.value = id
workSpaceId.value = ''
surveyListStore.resetSearch() surveyListStore.resetSearch()
} }
@ -126,21 +141,130 @@ export const useWorkSpaceStore = defineStore('workSpace', () => {
function setSpaceDetail(data: null | SpaceDetail) { function setSpaceDetail(data: null | SpaceDetail) {
spaceDetail.value = data spaceDetail.value = data
} }
// 分组
const groupList = ref<GroupItem[]>([])
const groupAllList = ref<IGroup[]>([])
const groupListTotal = ref(0)
const groupDetail = ref<GroupItem | null>(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<IGroup>) {
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 { return {
menuType,
spaceMenus, spaceMenus,
spaceType, groupId,
workSpaceId, workSpaceId,
spaceDetail, spaceDetail,
workSpaceList, workSpaceList,
workSpaceListTotal, workSpaceListTotal,
getSpaceList, getSpaceList,
getSpaceDetail, getSpaceDetail,
changeSpaceType, changeMenuType,
changeWorkSpace, changeWorkSpace,
changeGroup,
addSpace, addSpace,
deleteSpace, deleteSpace,
updateSpace, updateSpace,
setSpaceDetail setSpaceDetail,
groupList,
groupAllList,
groupListTotal,
groupDetail,
addGroup,
updateGroup,
getGroupList,
getGroupDetail,
setGroupDetail,
deleteGroup
} }
}) })

View File

@ -7,9 +7,15 @@ export interface MenuItem {
id: string id: string
name: string name: string
icon?: string icon?: string
total?: Number
children?: MenuItem[] children?: MenuItem[]
} }
export type IGroup = {
_id?: string
name: string
}
export type IWorkspace = { export type IWorkspace = {
_id?: string _id?: string
name: string name: string
@ -29,6 +35,7 @@ export interface SpaceDetail {
name: string name: string
currentUserId?: string currentUserId?: string
description: string description: string
surveyTotal: number
members: IMember[] members: IMember[]
} }
@ -49,16 +56,30 @@ export interface ICollaborator {
permissions: Array<number> permissions: Array<number>
} }
export enum SpaceType { export type GroupItem = {
Personal = 'personal', _id: string,
Group = 'group', name: string,
Teamwork = 'teamwork' createdAt: string
updatedAt?: string
ownerId: string
surveyTotal: number
} }
export enum MenuType {
PersonalGroup = 'personalGroup',
SpaceGroup = 'spaceGroup',
}
export enum UserRole { export enum UserRole {
Admin = 'admin', Admin = 'admin',
Member = 'user' Member = 'user'
} }
export enum GroupState {
All = 'all',
Not = 'nogrouped'
}
// 定义角色标签映射对象 // 定义角色标签映射对象
export const roleLabels: Record<UserRole, string> = { export const roleLabels: Record<UserRole, string> = {
[UserRole.Admin]: '管理员', [UserRole.Admin]: '管理员',