From 1f1dd86f8916955bbf92d03c1068c29960d5e239 Mon Sep 17 00:00:00 2001 From: luch <32321690+luch1994@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:24:49 +0800 Subject: [PATCH 01/82] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=88=86?= =?UTF-8?q?=E9=A2=98=E7=BB=9F=E8=AE=A1=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20(#275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/guards/__test/survey.guard.spec.ts | 63 ++- .../modules/auth/__test/user.service.spec.ts | 130 ++++- .../__test/collaborator.controller.spec.ts | 298 ++++++++++- .../__test/dataStatistic.controller.spec.ts | 505 +++++++++++++++++- .../__test/dataStatistic.service.spec.ts | 158 +++++- .../controllers/collaborator.controller.ts | 1 + .../controllers/dataStatistic.controller.ts | 48 ++ .../survey/dto/aggregationStatis.dto.ts | 13 + .../survey/dto/batchSaveCollaborator.dto.ts | 7 +- .../survey/services/dataStatistic.service.ts | 60 ++- server/src/modules/survey/utils/index.ts | 174 ++++++ 11 files changed, 1442 insertions(+), 15 deletions(-) create mode 100644 server/src/modules/survey/dto/aggregationStatis.dto.ts diff --git a/server/src/guards/__test/survey.guard.spec.ts b/server/src/guards/__test/survey.guard.spec.ts index 4c30c453..ccc1e7fd 100644 --- a/server/src/guards/__test/survey.guard.spec.ts +++ b/server/src/guards/__test/survey.guard.spec.ts @@ -11,6 +11,7 @@ import { NoPermissionException } from 'src/exceptions/noPermissionException'; import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { WorkspaceMember } from 'src/models/workspaceMember.entity'; import { Collaborator } from 'src/models/collaborator.entity'; +import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; describe('SurveyGuard', () => { let guard: SurveyGuard; @@ -81,7 +82,19 @@ describe('SurveyGuard', () => { ); }); - it('should allow access if user is the owner of the survey', async () => { + it('should allow access if user is the owner of the survey by ownerId', async () => { + const context = createMockExecutionContext(); + const surveyMeta = { ownerId: 'testUserId', workspaceId: null }; + jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId'); + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + + it('should allow access if user is the owner of the survey by username', async () => { const context = createMockExecutionContext(); const surveyMeta = { owner: 'testUser', workspaceId: null }; jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId'); @@ -108,7 +121,35 @@ describe('SurveyGuard', () => { expect(result).toBe(true); }); - it('should throw NoPermissionException if user has no permissions', async () => { + it('should throw NoPermissionException if user is not a workspace member', async () => { + const context = createMockExecutionContext(); + const surveyMeta = { owner: 'anotherUser', workspaceId: 'workspaceId' }; + jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId'); + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + jest.spyOn(workspaceMemberService, 'findOne').mockResolvedValue(null); + + await expect(guard.canActivate(context)).rejects.toThrow( + NoPermissionException, + ); + }); + + it('should throw NoPermissionException if no permissions are provided', async () => { + const context = createMockExecutionContext(); + const surveyMeta = { owner: 'anotherUser', workspaceId: null }; + jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId'); + jest.spyOn(reflector, 'get').mockReturnValueOnce(null); + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + + await expect(guard.canActivate(context)).rejects.toThrow( + NoPermissionException, + ); + }); + + it('should throw NoPermissionException if user has no matching permissions', async () => { const context = createMockExecutionContext(); const surveyMeta = { owner: 'anotherUser', workspaceId: null }; jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId'); @@ -125,6 +166,24 @@ describe('SurveyGuard', () => { ); }); + it('should allow access if user has the required permissions', async () => { + const context = createMockExecutionContext(); + const surveyMeta = { owner: 'anotherUser', workspaceId: null }; + jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId'); + jest + .spyOn(reflector, 'get') + .mockReturnValueOnce([SURVEY_PERMISSION.SURVEY_CONF_MANAGE]); + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + jest.spyOn(collaboratorService, 'getCollaborator').mockResolvedValue({ + permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE], + } as Collaborator); + + const result = await guard.canActivate(context); + expect(result).toBe(true); + }); + function createMockExecutionContext(): ExecutionContext { return { switchToHttp: jest.fn().mockReturnValue({ diff --git a/server/src/modules/auth/__test/user.service.spec.ts b/server/src/modules/auth/__test/user.service.spec.ts index a3bcc058..e0cdfc19 100644 --- a/server/src/modules/auth/__test/user.service.spec.ts +++ b/server/src/modules/auth/__test/user.service.spec.ts @@ -6,6 +6,7 @@ import { User } from 'src/models/user.entity'; import { HttpException } from 'src/exceptions/httpException'; import { hash256 } from 'src/utils/hash256'; import { RECORD_STATUS } from 'src/enums'; +import { ObjectId } from 'mongodb'; describe('UserService', () => { let service: UserService; @@ -21,6 +22,7 @@ describe('UserService', () => { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), + find: jest.fn(), }, }, ], @@ -32,6 +34,10 @@ describe('UserService', () => { ); }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); + it('should create a user', async () => { const userInfo = { username: 'testUser', @@ -102,7 +108,7 @@ describe('UserService', () => { expect(user).toEqual({ ...userInfo, password: hashedPassword }); }); - it('should return undefined when user is not found by credentials', async () => { + it('should return null when user is not found by credentials', async () => { const userInfo = { username: 'nonExistingUser', password: 'nonExistingPassword', @@ -129,7 +135,8 @@ describe('UserService', () => { const userInfo = { username: username, password: 'existingPassword', - } as User; + curStatus: { status: 'ACTIVE' }, + } as unknown as User; jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo); @@ -137,10 +144,129 @@ describe('UserService', () => { expect(userRepository.findOne).toHaveBeenCalledWith({ where: { + username: username, 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, + }, + }); + expect(user).toEqual(userInfo); + }); + + it('should return null when user is not found by username', async () => { + const username = 'nonExistingUser'; + + const findOneSpy = jest + .spyOn(userRepository, 'findOne') + .mockResolvedValue(null); + + const user = await service.getUserByUsername(username); + + expect(findOneSpy).toHaveBeenCalledWith({ + where: { username: username, + 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, + }, + }); + expect(user).toBe(null); + }); + + it('should return a user by id', async () => { + const id = '60c72b2f9b1e8a5f4b123456'; + const userInfo = { + _id: new ObjectId(id), + username: 'testUser', + curStatus: { status: 'ACTIVE' }, + } as unknown as User; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo); + + const user = await service.getUserById(id); + + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { + _id: new ObjectId(id), + 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, }, }); expect(user).toEqual(userInfo); }); + + it('should return null when user is not found by id', async () => { + const id = '60c72b2f9b1e8a5f4b123456'; + + const findOneSpy = jest + .spyOn(userRepository, 'findOne') + .mockResolvedValue(null); + + const user = await service.getUserById(id); + + expect(findOneSpy).toHaveBeenCalledWith({ + where: { + _id: new ObjectId(id), + 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, + }, + }); + expect(user).toBe(null); + }); + + it('should return a list of users by username', async () => { + const username = 'test'; + const userList = [ + { _id: new ObjectId(), username: 'testUser1', createDate: new Date() }, + { _id: new ObjectId(), username: 'testUser2', createDate: new Date() }, + ]; + + jest + .spyOn(userRepository, 'find') + .mockResolvedValue(userList as unknown as User[]); + + const result = await service.getUserListByUsername({ + username, + skip: 0, + take: 10, + }); + + expect(userRepository.find).toHaveBeenCalledWith({ + where: { + username: new RegExp(username), + 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, + }, + skip: 0, + take: 10, + select: ['_id', 'username', 'createDate'], + }); + expect(result).toEqual(userList); + }); + + it('should return a list of users by ids', async () => { + const idList = ['60c72b2f9b1e8a5f4b123456', '60c72b2f9b1e8a5f4b123457']; + const userList = [ + { + _id: new ObjectId(idList[0]), + username: 'testUser1', + createDate: new Date(), + }, + { + _id: new ObjectId(idList[1]), + username: 'testUser2', + createDate: new Date(), + }, + ]; + + jest + .spyOn(userRepository, 'find') + .mockResolvedValue(userList as unknown as User[]); + + const result = await service.getUserListByIds({ idList }); + + expect(userRepository.find).toHaveBeenCalledWith({ + where: { + _id: { + $in: idList.map((id) => new ObjectId(id)), + }, + 'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, + }, + select: ['_id', 'username', 'createDate'], + }); + expect(result).toEqual(userList); + }); }); diff --git a/server/src/modules/survey/__test/collaborator.controller.spec.ts b/server/src/modules/survey/__test/collaborator.controller.spec.ts index 79819401..0119aaa3 100644 --- a/server/src/modules/survey/__test/collaborator.controller.spec.ts +++ b/server/src/modules/survey/__test/collaborator.controller.spec.ts @@ -10,7 +10,13 @@ import { UserService } from 'src/modules/auth/services/user.service'; import { ObjectId } from 'mongodb'; import { SurveyMetaService } from '../services/surveyMeta.service'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; -import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; +import { + SURVEY_PERMISSION, + SURVEY_PERMISSION_DESCRIPTION, +} from 'src/enums/surveyPermission'; +import { BatchSaveCollaboratorDto } from '../dto/batchSaveCollaborator.dto'; +import { User } from 'src/models/user.entity'; +import { SurveyMeta } from 'src/models/surveyMeta.entity'; jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/survey.guard'); @@ -21,6 +27,8 @@ describe('CollaboratorController', () => { let collaboratorService: CollaboratorService; let logger: Logger; let userService: UserService; + let surveyMetaService: SurveyMetaService; + let workspaceMemberServie: WorkspaceMemberService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -34,12 +42,18 @@ describe('CollaboratorController', () => { changeUserPermission: jest.fn(), deleteCollaborator: jest.fn(), getCollaborator: jest.fn(), + batchDeleteBySurveyId: jest.fn(), + batchCreate: jest.fn(), + batchDelete: jest.fn(), + updateById: jest.fn(), + batchSaveCollaborator: jest.fn(), }, }, { provide: Logger, useValue: { error: jest.fn(), + info: jest.fn(), }, }, { @@ -72,6 +86,10 @@ describe('CollaboratorController', () => { collaboratorService = module.get(CollaboratorService); logger = module.get(Logger); userService = module.get(UserService); + surveyMetaService = module.get(SurveyMetaService); + workspaceMemberServie = module.get( + WorkspaceMemberService, + ); }); it('should be defined', () => { @@ -115,6 +133,59 @@ describe('CollaboratorController', () => { HttpException, ); }); + + it('should throw an exception if user does not exist', async () => { + const reqBody: CreateCollaboratorDto = { + surveyId: 'surveyId', + userId: new ObjectId().toString(), + permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE], + }; + const req = { + user: { _id: 'userId' }, + surveyMeta: { ownerId: new ObjectId().toString() }, + }; + + jest.spyOn(userService, 'getUserById').mockResolvedValue(null); + + await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow( + HttpException, + ); + }); + + it('should throw an exception if user is the survey owner', async () => { + const userId = new ObjectId().toString(); + const reqBody: CreateCollaboratorDto = { + surveyId: 'surveyId', + userId: userId, + permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE], + }; + const req = { user: { _id: 'userId' }, surveyMeta: { ownerId: userId } }; + + await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow( + HttpException, + ); + }); + + it('should throw an exception if user is already a collaborator', async () => { + const userId = new ObjectId().toString(); + const reqBody: CreateCollaboratorDto = { + surveyId: 'surveyId', + userId: userId, + permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE], + }; + const req = { + user: { _id: 'userId' }, + surveyMeta: { ownerId: new ObjectId().toString() }, + }; + + jest + .spyOn(collaboratorService, 'getCollaborator') + .mockResolvedValue({} as unknown as Collaborator); + + await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow( + HttpException, + ); + }); }); describe('getSurveyCollaboratorList', () => { @@ -217,4 +288,229 @@ describe('CollaboratorController', () => { expect(logger.error).toHaveBeenCalledTimes(1); }); }); + + // 新增的测试方法 + describe('getPermissionList', () => { + it('should return the permission list', async () => { + const result = Object.values(SURVEY_PERMISSION_DESCRIPTION); + + const response = await controller.getPermissionList(); + + expect(response).toEqual({ + code: 200, + data: result, + }); + }); + }); + + describe('batchSaveCollaborator', () => { + it('should batch save collaborators successfully', async () => { + const userId0 = new ObjectId().toString(); + const userId1 = new ObjectId().toString(); + const existsCollaboratorId = new ObjectId().toString(); + const surveyId = new ObjectId().toString(); + const reqBody: BatchSaveCollaboratorDto = { + surveyId: surveyId, + collaborators: [ + { + userId: userId0, + permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE], + }, + { + _id: existsCollaboratorId, + userId: userId1, + permissions: [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE], + }, + ], + }; + const req = { + user: { _id: 'requestUserId' }, + surveyMeta: { ownerId: 'ownerId' }, + }; + + const userList = [ + { _id: new ObjectId(userId0) }, + { _id: new ObjectId(userId1) }, + ]; + + jest + .spyOn(userService, 'getUserListByIds') + .mockResolvedValue(userList as unknown as User[]); + jest + .spyOn(collaboratorService, 'batchDelete') + .mockResolvedValue({ deletedCount: 1, acknowledged: true }); + jest + .spyOn(collaboratorService, 'batchCreate') + .mockResolvedValue([{}] as any); + jest.spyOn(collaboratorService, 'updateById').mockResolvedValue({}); + + const response = await controller.batchSaveCollaborator(reqBody, req); + + expect(response).toEqual({ + code: 200, + }); + expect(userService.getUserListByIds).toHaveBeenCalled(); + expect(collaboratorService.batchDelete).toHaveBeenCalled(); + expect(collaboratorService.batchCreate).toHaveBeenCalled(); + expect(collaboratorService.updateById).toHaveBeenCalled(); + }); + + it('should throw an exception if validation fails', async () => { + const reqBody: BatchSaveCollaboratorDto = { + surveyId: '', + collaborators: [ + { + userId: '', + permissions: [SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE], + }, + ], + }; + const req = { user: { _id: 'userId' } }; + + await expect( + controller.batchSaveCollaborator(reqBody, req), + ).rejects.toThrow(HttpException); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + }); + + describe('getUserSurveyPermissions', () => { + it('should return owner permissions if user is the owner', async () => { + const req = { + user: { _id: new ObjectId(), username: 'owner' }, + }; + const query = { surveyId: 'surveyId' }; + const surveyMeta = { + ownerId: req.user._id.toString(), + owner: req.user.username, + workspaceId: 'workspaceId', + }; + + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + + const response = await controller.getUserSurveyPermissions(req, query); + + expect(response).toEqual({ + code: 200, + data: { + isOwner: true, + permissions: [ + SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE, + SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE, + SURVEY_PERMISSION.SURVEY_CONF_MANAGE, + ], + }, + }); + }); + + it('should return default permissions if user is a workspace member', async () => { + const req = { + user: { _id: new ObjectId(), username: 'user' }, + }; + const query = { surveyId: 'surveyId' }; + const surveyMeta = { + ownerId: 'ownerId', + owner: 'owner', + workspaceId: 'workspaceId', + }; + + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue({} as any); + + const response = await controller.getUserSurveyPermissions(req, query); + + expect(response).toEqual({ + code: 200, + data: { + isOwner: false, + permissions: [ + SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE, + SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE, + SURVEY_PERMISSION.SURVEY_CONF_MANAGE, + ], + }, + }); + }); + + it('should return collaborator permissions if user is a collaborator', async () => { + const req = { + user: { _id: new ObjectId(), username: 'user' }, + }; + const query = { surveyId: 'surveyId' }; + const surveyMeta = { + ownerId: 'ownerId', + owner: 'owner', + workspaceId: 'workspaceId', + }; + const collaborator = { + permissions: ['read', 'write'], + }; + + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null); + jest + .spyOn(collaboratorService, 'getCollaborator') + .mockResolvedValue(collaborator as Collaborator); + + const response = await controller.getUserSurveyPermissions(req, query); + + expect(response).toEqual({ + code: 200, + data: { + isOwner: false, + permissions: collaborator.permissions, + }, + }); + }); + + it('should return empty permissions if user has no permissions', async () => { + const req = { + user: { _id: new ObjectId(), username: 'user' }, + }; + const query = { surveyId: 'surveyId' }; + const surveyMeta = { + ownerId: 'ownerId', + owner: 'owner', + workspaceId: 'workspaceId', + }; + + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValue(surveyMeta as SurveyMeta); + jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null); + jest + .spyOn(collaboratorService, 'getCollaborator') + .mockResolvedValue(null); + + const response = await controller.getUserSurveyPermissions(req, query); + + expect(response).toEqual({ + code: 200, + data: { + isOwner: false, + permissions: [], + }, + }); + }); + + it('should throw an exception if survey does not exist', async () => { + const req = { user: { _id: 'userId' } }; + const query = { surveyId: 'nonexistentSurveyId' }; + + jest + .spyOn(surveyMetaService, 'getSurveyById') + .mockResolvedValueOnce(null); + + await expect( + controller.getUserSurveyPermissions(req, query), + ).rejects.toThrow(HttpException); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts index a3222029..a2ca640f 100644 --- a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts +++ b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts @@ -14,6 +14,7 @@ import { Logger } from 'src/logger'; import { UserService } from 'src/modules/auth/services/user.service'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; import { AuthService } from 'src/modules/auth/services/auth.service'; +import { HttpException } from 'src/exceptions/httpException'; jest.mock('../services/dataStatistic.service'); jest.mock('../services/surveyMeta.service'); @@ -21,11 +22,13 @@ jest.mock('../../surveyResponse/services/responseScheme.service'); jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/survey.guard'); -jest.mock('src/guards/workspace.guard'); describe('DataStatisticController', () => { let controller: DataStatisticController; let dataStatisticService: DataStatisticService; + let responseSchemaService: ResponseSchemaService; + let pluginManager: XiaojuSurveyPluginManager; + let logger: Logger; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -64,9 +67,14 @@ describe('DataStatisticController', () => { controller = module.get(DataStatisticController); dataStatisticService = module.get(DataStatisticService); - const pluginManager = module.get( + responseSchemaService = module.get( + ResponseSchemaService, + ); + pluginManager = module.get( XiaojuSurveyPluginManager, ); + logger = module.get(Logger); + pluginManager.registerPlugin( new ResponseSecurityPlugin('dataAesEncryptSecretKey'), ); @@ -82,6 +90,9 @@ describe('DataStatisticController', () => { const mockRequest = { query: { surveyId, + isDesensitive: false, + page: 1, + pageSize: 10, }, user: { username: 'testUser', @@ -105,13 +116,13 @@ describe('DataStatisticController', () => { }; jest - .spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId') + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') .mockResolvedValueOnce({} as any); jest .spyOn(dataStatisticService, 'getDataTable') .mockResolvedValueOnce(mockDataTable); - const result = await controller.data(mockRequest.query, {}); + const result = await controller.data(mockRequest.query, mockRequest); expect(result).toEqual({ code: 200, @@ -125,6 +136,8 @@ describe('DataStatisticController', () => { query: { surveyId, isDesensitive: true, + page: 1, + pageSize: 10, }, user: { username: 'testUser', @@ -146,19 +159,499 @@ describe('DataStatisticController', () => { { difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' }, ], }; + jest - .spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId') + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') .mockResolvedValueOnce({} as any); jest .spyOn(dataStatisticService, 'getDataTable') .mockResolvedValueOnce(mockDataTable); - const result = await controller.data(mockRequest.query, {}); + const result = await controller.data(mockRequest.query, mockRequest); expect(result).toEqual({ code: 200, data: mockDataTable, }); }); + + it('should throw an exception if validation fails', async () => { + const mockRequest = { + query: { + surveyId: '', + }, + user: { + username: 'testUser', + }, + }; + + await expect( + controller.data(mockRequest.query, mockRequest), + ).rejects.toThrow(HttpException); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + }); + + describe('aggregationStatis', () => { + it('should return aggregation statistics', async () => { + const mockRequest = { + query: { + surveyId: new ObjectId().toString(), + }, + }; + + const mockResponseSchema = { + _id: new ObjectId('6659c3283b1cb279bc2e2b0c'), + curStatus: { + status: 'published', + date: 1717159136024, + }, + statusList: [ + { + status: 'published', + date: 1717158851823, + }, + ], + createDate: 1717158851823, + updateDate: 1717159136025, + title: '问卷调研', + surveyPath: 'ZdGNzTTR', + code: { + bannerConf: { + titleConfig: { + mainTitle: + '

欢迎填写问卷

为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,期待您的参与!

', + subTitle: '', + applyTitle: '', + }, + bannerConfig: { + bgImage: '/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp', + bgImageAllowJump: false, + bgImageJumpLink: '', + videoLink: '', + postImg: '', + }, + }, + baseConf: { + begTime: '2024-05-31 20:31:36', + endTime: '2034-05-31 20:31:36', + language: 'chinese', + showVoteProcess: 'allow', + tLimit: 0, + answerBegTime: '00:00:00', + answerEndTime: '23:59:59', + answerLimitTime: 0, + }, + bottomConf: { + logoImage: '/imgs/Logo.webp', + logoImageWidth: '60%', + }, + skinConf: { + backgroundConf: { + color: '#fff', + }, + themeConf: { + color: '#ffa600', + }, + contentConf: { + opacity: 100, + }, + skinColor: '#4a4c5b', + inputBgColor: '#ffffff', + }, + submitConf: { + submitTitle: '提交', + msgContent: { + msg_200: '提交成功', + msg_9001: '您来晚了,感谢支持问卷~', + msg_9002: '请勿多次提交!', + msg_9003: '您来晚了,已经满额!', + msg_9004: '提交失败!', + }, + confirmAgain: { + is_again: true, + again_text: '确认要提交吗?', + }, + link: '', + }, + logicConf: { + showLogicConf: [], + }, + dataConf: { + dataList: [ + { + isRequired: true, + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio', + placeholderDesc: '', + field: 'data515', + title: '标题2', + placeholder: '', + randomSort: false, + checked: false, + minNum: '', + maxNum: '', + options: [ + { + text: '选项1', + imageUrl: '', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115019', + }, + { + text: '选项2', + imageUrl: '', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115020', + }, + ], + importKey: 'single', + importData: '', + cOption: '', + cOptions: [], + star: 5, + exclude: false, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data893', + showIndex: true, + showType: true, + showSpliter: true, + type: 'checkbox', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题2', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '466671', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '095415', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data820', + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio-nps', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题3', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '268884', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '371166', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data549', + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio-star', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题4', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '274183', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '842967', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + ], + }, + }, + pageId: '6659c3283b1cb279bc2e2b0c', + }; + + const mockAggregationResult = [ + { + field: 'data515', + data: { + aggregation: [ + { + id: '115019', + count: 1, + }, + { + id: '115020', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data893', + data: { + aggregation: [ + { + id: '466671', + count: 2, + }, + { + id: '095415', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data820', + data: { + aggregation: [ + { + id: '8', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + { + field: 'data549', + data: { + aggregation: [ + { + id: '5', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + ]; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') + .mockResolvedValueOnce(mockResponseSchema as any); + jest + .spyOn(dataStatisticService, 'aggregationStatis') + .mockResolvedValueOnce(mockAggregationResult); + + const result = await controller.aggregationStatis(mockRequest.query); + + expect(result).toEqual({ + code: 200, + data: expect.any(Array), + }); + }); + + it('should throw an exception if validation fails', async () => { + const mockRequest = { + query: { + surveyId: '', + }, + }; + + await expect( + controller.aggregationStatis(mockRequest.query), + ).rejects.toThrow(HttpException); + }); + + it('should return empty data if response schema does not exist', async () => { + const mockRequest = { + query: { + surveyId: new ObjectId().toString(), + }, + }; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') + .mockResolvedValueOnce(null); + + const result = await controller.aggregationStatis(mockRequest.query); + + expect(result).toEqual({ + code: 200, + data: [], + }); + }); }); }); diff --git a/server/src/modules/survey/__test/dataStatistic.service.spec.ts b/server/src/modules/survey/__test/dataStatistic.service.spec.ts index 830c7b59..1130364f 100644 --- a/server/src/modules/survey/__test/dataStatistic.service.spec.ts +++ b/server/src/modules/survey/__test/dataStatistic.service.spec.ts @@ -197,7 +197,6 @@ describe('DataStatisticService', () => { data413_3: expect.any(String), data413: expect.any(Number), data863: expect.any(String), - data413_custom: expect.any(String), difTime: expect.any(String), createDate: expect.any(String), }), @@ -310,4 +309,161 @@ describe('DataStatisticService', () => { ); }); }); + + describe('aggregationStatis', () => { + it('should return correct aggregation data', async () => { + const surveyId = '65afc62904d5db18534c0f78'; + const mockAggregationResult = { + data515: [ + { + count: 1, + data: { + data515: '115019', + }, + }, + { + count: 1, + data: { + data515: '115020', + }, + }, + ], + data893: [ + { + count: 1, + data: { + data893: ['466671'], + }, + }, + { + count: 1, + data: { + data893: ['466671', '095415'], + }, + }, + ], + data820: [ + { + count: 1, + data: { + data820: 8, + }, + }, + ], + data549: [ + { + count: 1, + data: { + data549: 5, + }, + }, + ], + }; + + const fieldList = Object.keys(mockAggregationResult); + + jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({ + next: jest.fn().mockResolvedValue(mockAggregationResult), + } as any); + + const result = await service.aggregationStatis({ + surveyId, + fieldList, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + field: 'data515', + data: { + aggregation: [ + { + id: '115019', + count: 1, + }, + { + id: '115020', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data893', + data: { + aggregation: [ + { + id: '466671', + count: 2, + }, + { + id: '095415', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data820', + data: { + aggregation: [ + { + id: '8', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + { + field: 'data549', + data: { + aggregation: [ + { + id: '5', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + ]), + ); + }); + + it('should return empty aggregation data when no responses', async () => { + const surveyId = '65afc62904d5db18534c0f78'; + const fieldList = ['data458', 'data515']; + + jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({ + next: jest.fn().mockResolvedValue({}), + } as any); + + const result = await service.aggregationStatis({ + surveyId, + fieldList, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + field: 'data458', + data: { + aggregation: [], + submitionCount: 0, + }, + }, + { + field: 'data515', + data: { + aggregation: [], + submitionCount: 0, + }, + }, + ]), + ); + }); + }); }); diff --git a/server/src/modules/survey/controllers/collaborator.controller.ts b/server/src/modules/survey/controllers/collaborator.controller.ts index 35fc1d11..08c97068 100644 --- a/server/src/modules/survey/controllers/collaborator.controller.ts +++ b/server/src/modules/survey/controllers/collaborator.controller.ts @@ -319,6 +319,7 @@ export class CollaboratorController { const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId }); if (!surveyMeta) { + this.logger.error(`问卷不存在: ${surveyId}`, { req }); throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND); } diff --git a/server/src/modules/survey/controllers/dataStatistic.controller.ts b/server/src/modules/survey/controllers/dataStatistic.controller.ts index a0b1754c..452b4fc6 100644 --- a/server/src/modules/survey/controllers/dataStatistic.controller.ts +++ b/server/src/modules/survey/controllers/dataStatistic.controller.ts @@ -20,6 +20,8 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; import { Logger } from 'src/logger'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +import { AggregationStatisDto } from '../dto/aggregationStatis.dto'; +import { handleAggretionData } from '../utils'; @ApiTags('survey') @ApiBearerAuth() @@ -80,4 +82,50 @@ export class DataStatisticController { }, }; } + + @Get('/aggregationStatis') + @HttpCode(200) + @UseGuards(Authentication) + async aggregationStatis(@Query() queryInfo: AggregationStatisDto) { + // 聚合统计 + const { value, error } = AggregationStatisDto.validate(queryInfo); + if (error) { + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const responseSchema = + await this.responseSchemaService.getResponseSchemaByPageId( + value.surveyId, + ); + if (!responseSchema) { + return { + code: 200, + data: [], + }; + } + const allowQuestionType = [ + 'radio', + 'checkbox', + 'binary-choice', + 'radio-star', + 'radio-nps', + 'vote', + ]; + const fieldList = responseSchema.code.dataConf.dataList + .filter((item) => allowQuestionType.includes(item.type)) + .map((item) => item.field); + const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => { + pre[cur.field] = cur; + return pre; + }, {}); + const res = await this.dataStatisticService.aggregationStatis({ + surveyId: value.surveyId, + fieldList, + }); + return { + code: 200, + data: res.map((item) => { + return handleAggretionData({ item, dataMap }); + }), + }; + } } diff --git a/server/src/modules/survey/dto/aggregationStatis.dto.ts b/server/src/modules/survey/dto/aggregationStatis.dto.ts new file mode 100644 index 00000000..a8f747a7 --- /dev/null +++ b/server/src/modules/survey/dto/aggregationStatis.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class AggregationStatisDto { + @ApiProperty({ description: '问卷id', required: true }) + surveyId: string; + + static validate(data) { + return Joi.object({ + surveyId: Joi.string().required(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts b/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts index 1ae04787..119fe584 100644 --- a/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts +++ b/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts @@ -3,7 +3,10 @@ import Joi from 'joi'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; export class CollaboratorDto { - @ApiProperty({ description: '用户id', required: false }) + @ApiProperty({ description: '协作id', required: false }) + _id?: string; + + @ApiProperty({ description: '用户id', required: true }) userId: string; @ApiProperty({ @@ -16,7 +19,7 @@ export class CollaboratorDto { SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE, ], }) - permissions: Array; + permissions: Array; } export class BatchSaveCollaboratorDto { diff --git a/server/src/modules/survey/services/dataStatistic.service.ts b/server/src/modules/survey/services/dataStatistic.service.ts index cd958a7f..ce31fed6 100644 --- a/server/src/modules/survey/services/dataStatistic.service.ts +++ b/server/src/modules/survey/services/dataStatistic.service.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { keyBy } from 'lodash'; import { DataItem } from 'src/interfaces/survey'; import { ResponseSchema } from 'src/models/responseSchema.entity'; -import { getListHeadByDataList } from '../utils'; +import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils'; @Injectable() export class DataStatisticService { private radioType = ['radio-star', 'radio-nps']; @@ -101,4 +101,62 @@ export class DataStatisticService { listBody, }; } + + async aggregationStatis({ surveyId, fieldList }) { + const $facet = fieldList.reduce((pre, cur) => { + const $match = { $match: { [`data.${cur}`]: { $nin: [[], '', null] } } }; + const $group = { $group: { _id: `$data.${cur}`, count: { $sum: 1 } } }; + const $project = { + $project: { + _id: 0, + count: 1, + secretKeys: 1, + sensitiveKeys: 1, + [`data.${cur}`]: '$_id', + }, + }; + pre[cur] = [$match, $group, $project]; + return pre; + }, {}); + const aggregation = this.surveyResponseRepository.aggregate( + [ + { + $match: { + pageId: surveyId, + 'curStatus.status': { + $ne: 'removed', + }, + }, + }, + { $facet }, + ], + { maxTimeMS: 30000, allowDiskUse: true }, + ); + const res = await aggregation.next(); + const submitionCountMap: Record = {}; + for (const field in res) { + let count = 0; + if (Array.isArray(res[field])) { + for (const optionItem of res[field]) { + count += optionItem.count; + } + } + submitionCountMap[field] = count; + } + const transformedData = transformAndMergeArrayFields(res); + return fieldList.map((field) => { + return { + field, + data: { + aggregation: (transformedData?.[field] || []).map((optionItem) => { + return { + id: optionItem.data[field], + count: optionItem.count, + }; + }), + submitionCount: submitionCountMap?.[field] || 0, + }, + }; + }); + } } diff --git a/server/src/modules/survey/utils/index.ts b/server/src/modules/survey/utils/index.ts index fd988f55..ced78f14 100644 --- a/server/src/modules/survey/utils/index.ts +++ b/server/src/modules/survey/utils/index.ts @@ -66,3 +66,177 @@ export function getListHeadByDataList(dataList) { }); return listHead; } + +export function transformAndMergeArrayFields(data) { + const transformedData = {}; + + for (const key in data) { + const valueMap: Record = {}; + + for (const entry of data[key]) { + const nestedDataKey = Object.keys(entry.data)[0]; + const nestedDataValue = entry.data[nestedDataKey]; + + if (Array.isArray(nestedDataValue)) { + for (const value of nestedDataValue) { + if (!valueMap[value]) { + valueMap[value] = 0; + } + valueMap[value] += entry.count; + } + } else { + if (!valueMap[nestedDataValue]) { + valueMap[nestedDataValue] = 0; + } + valueMap[nestedDataValue] += entry.count; + } + } + + transformedData[key] = Object.keys(valueMap).map((value) => ({ + count: valueMap[value], + data: { + [key]: value, + }, + })); + } + + return transformedData; +} + +export function handleAggretionData({ dataMap, item }) { + const type = dataMap[item.field].type; + const aggregationMap = item.data.aggregation.reduce((pre, cur) => { + pre[cur.id] = cur; + return pre; + }, {}); + if (['radio', 'checkbox', 'vote', 'binary-choice'].includes(type)) { + return { + ...item, + title: dataMap[item.field].title, + type: dataMap[item.field].type, + data: { + ...item.data, + aggregation: dataMap[item.field].options.map((optionItem) => { + return { + id: optionItem.hash, + text: optionItem.text, + count: aggregationMap[optionItem.hash]?.count || 0, + }; + }), + }, + }; + } else if (['radio-star', 'radio-nps'].includes(type)) { + const summary: Record = {}; + const average = getAverage({ aggregation: item.data.aggregation }); + const median = getMedian({ aggregation: item.data.aggregation }); + const variance = getVariance({ + aggregation: item.data.aggregation, + average, + }); + summary['average'] = average; + summary['median'] = median; + summary['variance'] = variance; + if (type === 'radio-nps') { + summary['nps'] = getNps({ aggregation: item.data.aggregation }); + } + const range = type === 'radio-nps' ? [0, 10] : [1, 5]; + const arr = []; + for (let i = range[0]; i <= range[1]; i++) { + arr.push(i); + } + return { + ...item, + title: dataMap[item.field].title, + type: dataMap[item.field].type, + data: { + aggregation: arr.map((item) => { + const num = item.toString(); + return { + text: num, + id: num, + count: aggregationMap?.[num]?.count || 0, + }; + }), + submitionCount: item.data.submitionCount, + summary, + }, + }; + } else { + return { + ...item, + title: dataMap[item.field].title, + type: dataMap[item.field].type, + }; + } +} + +function getAverage({ aggregation }) { + const { sum, count } = aggregation.reduce( + (pre, cur) => { + const num = parseInt(cur.id); + pre.sum += num * cur.count; + pre.count += cur.count; + return pre; + }, + { sum: 0, count: 0 }, + ); + if (count === 0) { + return 0; + } + return (sum / count).toFixed(2); +} + +function getMedian({ aggregation }) { + const sortedArr = aggregation.sort((a, b) => { + return parseInt(a.id) - parseInt(b.id); + }); + const resArr = []; + for (const item of sortedArr) { + const tmp = new Array(item.count).fill(parseInt(item.id)); + resArr.push(...tmp); + } + if (resArr.length === 0) { + return 0; + } + if (resArr.length % 2 === 1) { + const midIndex = Math.floor(resArr.length / 2); + return resArr[midIndex].toFixed(2); + } + const rightIndex = resArr.length / 2; + const leftIndex = rightIndex - 1; + return ((resArr[leftIndex] + resArr[rightIndex]) / 2).toFixed(2); +} + +function getVariance({ aggregation, average }) { + const { sum, count } = aggregation.reduce( + (pre, cur) => { + const sub = Number(cur.id) - average; + pre.sum += sub * sub; + pre.count += cur.count; + return pre; + }, + { sum: 0, count: 0 }, + ); + if (count === 0 || count === 1) { + return '0.00'; + } + return (sum / (count - 1)).toFixed(2); +} + +function getNps({ aggregation }) { + // 净推荐值(NPS)=(推荐者数/总样本数)×100%-(贬损者数/总样本数)×100% + // 0~10分举例子:推荐者(9-10分);被动者(7-8分);贬损者(0-6分) + let recommand = 0, + derogatory = 0, + total = 0; + for (const item of aggregation) { + const num = parseInt(item.id); + if (num >= 9) { + recommand += item.count; + } else if (num <= 6) { + derogatory += item.count; + } + total += item.count; + } + return ((recommand / total - derogatory / total) * 100).toFixed(2) + '%'; +} From cce508d17af790e99b7f2f563ad8324ea62bb5e5 Mon Sep 17 00:00:00 2001 From: hiStephen <30630927+1004801012@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:33:20 +0800 Subject: [PATCH 02/82] =?UTF-8?q?feat:=20=E5=88=86=E9=A2=98=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=89=8D=E7=AB=AF=E5=BC=80=E5=8F=91=20(#276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/components.d.ts | 2 + web/package.json | 1 + web/src/management/api/analysis.js | 8 + web/src/management/config/analysisConfig.js | 77 ++++++ web/src/management/config/chartConfig/bar.js | 57 +++++ .../management/config/chartConfig/gauge.js | 146 +++++++++++ .../management/config/chartConfig/index.js | 9 + web/src/management/config/chartConfig/pie.js | 57 +++++ .../config/index.js => config/listConfig.js} | 0 .../management/config/questionMenuConfig.js | 2 +- web/src/management/hooks/useCharts.js | 25 ++ web/src/management/hooks/useResizeObserver.js | 20 ++ .../hooks/useStatisticsItemChart.js | 77 ++++++ .../pages/analysis/AnalysisPage.vue | 212 +++++---------- .../pages/analysis/components/DataTable.vue | 45 ++-- .../analysis/components/StatisticsItem.vue | 242 ++++++++++++++++++ .../pages/analysis/pages/DataTablePage.vue | 142 ++++++++++ .../analysis/pages/SeparateStatisticsPage.vue | 49 ++++ .../pages/list/components/BaseList.vue | 2 +- .../pages/list/components/SpaceList.vue | 2 +- .../pages/list/components/StateModule.vue | 2 +- .../pages/list/components/TagModule.vue | 2 +- web/src/management/router/index.ts | 26 +- web/src/management/styles/icon.scss | 33 ++- 24 files changed, 1067 insertions(+), 171 deletions(-) create mode 100644 web/src/management/config/analysisConfig.js create mode 100644 web/src/management/config/chartConfig/bar.js create mode 100644 web/src/management/config/chartConfig/gauge.js create mode 100644 web/src/management/config/chartConfig/index.js create mode 100644 web/src/management/config/chartConfig/pie.js rename web/src/management/{pages/list/config/index.js => config/listConfig.js} (100%) create mode 100644 web/src/management/hooks/useCharts.js create mode 100644 web/src/management/hooks/useResizeObserver.js create mode 100644 web/src/management/hooks/useStatisticsItemChart.js create mode 100644 web/src/management/pages/analysis/components/StatisticsItem.vue create mode 100644 web/src/management/pages/analysis/pages/DataTablePage.vue create mode 100644 web/src/management/pages/analysis/pages/SeparateStatisticsPage.vue diff --git a/web/components.d.ts b/web/components.d.ts index c897a9e0..13eb4c58 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -17,6 +17,7 @@ declare module 'vue' { ElDialog: typeof import('element-plus/es')['ElDialog'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMenu: typeof import('element-plus/es')['ElMenu'] @@ -29,6 +30,7 @@ declare module 'vue' { ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] + ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelectV2: typeof import('element-plus/es')['ElSelectV2'] ElSlider: typeof import('element-plus/es')['ElSlider'] diff --git a/web/package.json b/web/package.json index 3cc9e456..114191e1 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "axios": "^1.4.0", "clipboard": "^2.0.11", "crypto-js": "^4.2.0", + "echarts": "^5.5.0", "element-plus": "^2.7.0", "lodash-es": "^4.17.21", "moment": "^2.29.4", diff --git a/web/src/management/api/analysis.js b/web/src/management/api/analysis.js index 8e55a48a..5f211572 100644 --- a/web/src/management/api/analysis.js +++ b/web/src/management/api/analysis.js @@ -8,3 +8,11 @@ export const getRecycleList = (data) => { } }) } + +export const getStatisticList = (data) => { + return axios.get('/survey/dataStatistic/aggregationStatis', { + params: { + ...data + } + }) +} diff --git a/web/src/management/config/analysisConfig.js b/web/src/management/config/analysisConfig.js new file mode 100644 index 00000000..c0957837 --- /dev/null +++ b/web/src/management/config/analysisConfig.js @@ -0,0 +1,77 @@ +import { menuItems } from './questionMenuConfig' + +export const noDataConfig = { + title: '暂无数据', + desc: '您的问卷当前还没有数据,快去回收问卷吧!', + img: '/imgs/icons/analysis-empty.webp' +} + +export const separateItemListHead = [ + { + title: '选项', + field: 'text' + }, + { + title: '数量', + field: 'count' + }, + { + title: '占比', + field: 'percent' + } +] + +// 图表名称需要和./chartConfig.js中保持一致 +export const questionChartsConfig = { + [menuItems['checkbox']['type']]: ['bar'], + [menuItems['radio-nps']['type']]: ['gauge', 'pie', 'bar'], + default: ['pie', 'bar'] +} + +export const analysisTypeMap = { + dataTable: 'dataTable', + separateStatistics: 'separateStatistics' +} + +export const analysisType = [ + { + value: analysisTypeMap.dataTable, + label: '数据列表', + icon: 'icon-shujuliebiao' + }, + { + value: analysisTypeMap.separateStatistics, + label: '分题统计', + icon: 'icon-fentitongji' + } +] + +export const summaryType = { + between: 'between' +} + +export const summaryItemConfig = { + 'radio-nps': [ + { + text: '推荐者', + field: 'id', + type: summaryType.between, + max: 10, + min: 9 + }, + { + text: '中立者', + field: 'id', + type: summaryType.between, + max: 8, + min: 7 + }, + { + text: '贬损者', + field: 'id', + type: summaryType.between, + max: 6, + min: 0 + } + ] +} diff --git a/web/src/management/config/chartConfig/bar.js b/web/src/management/config/chartConfig/bar.js new file mode 100644 index 00000000..32b6bb22 --- /dev/null +++ b/web/src/management/config/chartConfig/bar.js @@ -0,0 +1,57 @@ +/** + * @Description: 柱状图配置 + * @CreateDate: 2024-04-30 + */ +export default (data) => { + const xAxisData = data.map((item) => item.name) + return { + color: ['#55A8FD'], + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: '{a}
{b}: {c}' + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: [ + { + type: 'category', + data: xAxisData, + axisTick: { + alignWithLabel: true + }, + axisLabel: { + interval: 0, + formatter(value) { + return value + } + } + } + ], + yAxis: [ + { + type: 'value', + splitLine: { + lineStyle: { + type: 'dashed' + } + } + } + ], + series: [ + { + showAllSymbol: true, + name: '提交人数', + type: 'bar', + barMaxWidth: 50, + data + } + ] + } +} diff --git a/web/src/management/config/chartConfig/gauge.js b/web/src/management/config/chartConfig/gauge.js new file mode 100644 index 00000000..c3c2678f --- /dev/null +++ b/web/src/management/config/chartConfig/gauge.js @@ -0,0 +1,146 @@ +/** + * @Description: gauge(仪表盘) + * @CreateDate: 2024-04-30 + */ +export default (data) => { + return { + series: [ + { + type: 'gauge', + startAngle: 180, + endAngle: 0, + min: -100, + max: 100, + radius: '130%', + center: ['50%', '80%'], + splitNumber: 4, + z: 2, + axisLabel: { + show: false, + distance: 0, + color: '#AAB1C0', + fontSize: 12, + fontFamily: 'DaQi-Font' + }, + splitLine: { + show: false + }, + axisTick: { + show: false + }, + axisLine: { + lineStyle: { + width: 40, + color: [[1, '#e3efff']] + } + } + }, + { + type: 'gauge', + startAngle: 174, + endAngle: 5, + min: -100, + max: 100, + radius: '130%', + splitNumber: 4, + center: ['50%', '80%'], + z: 3, + axisLabel: { + distance: -5, + color: '#666', + rotate: 'tangential', + fontSize: 12, + fontFamily: 'DaQi-Font' + }, + splitLine: { + show: false + }, + axisTick: { + show: false + }, + axisLine: { + lineStyle: { + width: 40, + color: [[1, '#e3efff']] + } + } + }, + { + type: 'gauge', + startAngle: 178, + endAngle: 0, + min: -100, + max: 100, + radius: '109%', + z: 4, + center: ['50%', '80%'], + splitNumber: 4, + itemStyle: { + color: '#58D9F9' + }, + progress: { + show: true, + roundCap: true, + width: 15, + itemStyle: { + color: '#55A8FD', + shadowBlur: 10, + shadowColor: '#55A8FD' + } + }, + pointer: { + icon: 'triangle', + length: '10%', + width: 8, + offsetCenter: [0, '-80%'], + itemStyle: { + color: '#55A8FD' + } + }, + axisLine: { + lineStyle: { + width: 15, + color: [[1, '#d3e5fe']] + } + }, + axisTick: { + show: false + }, + splitLine: { + show: false + }, + axisLabel: { + show: false + }, + title: { + offsetCenter: [0, '-15%'], + fontSize: 18, + color: '#666' + }, + detail: { + fontSize: 46, + lineHeight: 40, + height: 40, + offsetCenter: [0, '-45%'], + valueAnimation: true, + color: '#55A8FD', + formatter: function (value) { + if (value) { + return value + '%' + } else if (value === 0) { + return value + } else { + return '--' + } + } + }, + data: [ + { + value: data, + name: 'NPS' + } + ] + } + ] + } +} diff --git a/web/src/management/config/chartConfig/index.js b/web/src/management/config/chartConfig/index.js new file mode 100644 index 00000000..6529b379 --- /dev/null +++ b/web/src/management/config/chartConfig/index.js @@ -0,0 +1,9 @@ +import pie from './pie' +import bar from './bar' +import gauge from './gauge' + +export const getOption = { + pie, + bar, + gauge +} diff --git a/web/src/management/config/chartConfig/pie.js b/web/src/management/config/chartConfig/pie.js new file mode 100644 index 00000000..fe5aa5b2 --- /dev/null +++ b/web/src/management/config/chartConfig/pie.js @@ -0,0 +1,57 @@ +const color = [ + '#55A8FD', + '#36CBCB', + '#FAD337', + '#A6D6FF', + '#A177DC', + '#F46C73', + '#FFBA62', + '#ACE474', + '#BEECD6', + '#AFD2FF' +] +/* + * @Description: 饼图配置 + * @CreateDate: 2024-04-30 + */ +export default (data) => { + return { + color, + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c} ({d}%)' + }, + legend: { + orient: 'vertical', + right: 12, + top: 12, + tooltip: { + show: true + }, + formatter(name) { + return name.length > 17 ? name.substr(0, 17) + '...' : name + } + }, + series: [ + { + name: '提交人数', + type: 'pie', + radius: ['50%', '80%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: true, + formatter({ data }) { + const name = data?.name || '' + return name.length > 17 ? name.substr(0, 17) + '...' : name + } + }, + data + } + ] + } +} diff --git a/web/src/management/pages/list/config/index.js b/web/src/management/config/listConfig.js similarity index 100% rename from web/src/management/pages/list/config/index.js rename to web/src/management/config/listConfig.js diff --git a/web/src/management/config/questionMenuConfig.js b/web/src/management/config/questionMenuConfig.js index 2eaa27f0..66784bc5 100644 --- a/web/src/management/config/questionMenuConfig.js +++ b/web/src/management/config/questionMenuConfig.js @@ -1,4 +1,4 @@ -const menuItems = { +export const menuItems = { text: { type: 'text', snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp', diff --git a/web/src/management/hooks/useCharts.js b/web/src/management/hooks/useCharts.js new file mode 100644 index 00000000..e15583f7 --- /dev/null +++ b/web/src/management/hooks/useCharts.js @@ -0,0 +1,25 @@ +import * as echarts from 'echarts' +import { getOption } from '@/management/config/chartConfig' + +/** + * 绘制图表 + * @param {Object} el + * @param {String} type + * @param {Array} data + */ +export default (el, type, data) => { + const chart = echarts.init(el) + const option = getOption[type](data) + + chart.setOption(option, true) + + const resize = () => { + chart.resize() + } + + const changeType = (type, data) => { + chart.setOption(getOption[type](data), true) + } + + return { chart, resize, changeType } +} diff --git a/web/src/management/hooks/useResizeObserver.js b/web/src/management/hooks/useResizeObserver.js new file mode 100644 index 00000000..486962f2 --- /dev/null +++ b/web/src/management/hooks/useResizeObserver.js @@ -0,0 +1,20 @@ +// 引入防抖函数 +import _debounce from 'lodash/debounce' +/** + * @description: 监听元素尺寸变化 + * @param {*} el 元素dom + * @param {*} cb resize变化时执行的方法 + * @param {*} wait 防抖间隔 + * @return {*} + */ +export default (el, cb, wait = 200) => { + const resizeObserver = new ResizeObserver(_debounce(cb, wait)) + + resizeObserver.observe(el) + + const destroy = () => { + resizeObserver.disconnect(el) + } + + return { destroy, resizeObserver } +} diff --git a/web/src/management/hooks/useStatisticsItemChart.js b/web/src/management/hooks/useStatisticsItemChart.js new file mode 100644 index 00000000..cc7b18cf --- /dev/null +++ b/web/src/management/hooks/useStatisticsItemChart.js @@ -0,0 +1,77 @@ +import { ref, watchEffect } from 'vue' +import { cleanRichText } from '@/common/xss' +import { questionChartsConfig } from '../config/analysisConfig' + +// 饼图数据处理 +const pie = (data) => { + const aggregation = data?.aggregation + return ( + aggregation?.map?.((item) => { + const { id, count, text } = item + return { + id, + value: count, + name: cleanRichText(text) + } + }) || [] + ) +} +// 柱状图数据处理 +const bar = (data) => { + const aggregation = data?.aggregation + return ( + aggregation?.map?.((item) => { + const { id, count, text } = item + return { + id, + value: count, + name: cleanRichText(text) + } + }) || [] + ) +} +// 仪表盘数据处理 +const gauge = (data) => { + return parseFloat(data?.summary?.nps) || 0 +} + +const dataFormateConfig = { + pie, + bar, + gauge +} + +/** + * @description: 分题统计图表hook + * @param {*} chartType + * @param {*} data + * @return {*} chartRef 图表实例 chartTypeList 图表类型列表 chartType 图表类型 chartData 图表数据 + */ +export default ({ questionType, data }) => { + const chartRef = ref(null) + const chartTypeList = ref([]) + const chartType = ref('') + const chartData = ref({}) + + watchEffect(() => { + if (questionType.value) { + // 根据题型获取图表类型列表 + chartTypeList.value = questionChartsConfig[questionType.value] || questionChartsConfig.default + if (!chartType.value) { + // 默认选中第一项 + chartType.value = chartTypeList.value?.[0] + } + if (chartType.value) { + // 根据图表类型获取图表数据 + chartData.value = dataFormateConfig[chartType.value](data) + } + } + }) + + return { + chartRef, + chartTypeList, + chartType, + chartData + } +} diff --git a/web/src/management/pages/analysis/AnalysisPage.vue b/web/src/management/pages/analysis/AnalysisPage.vue index 0577b8c3..46eb24c2 100644 --- a/web/src/management/pages/analysis/AnalysisPage.vue +++ b/web/src/management/pages/analysis/AnalysisPage.vue @@ -1,139 +1,28 @@ - diff --git a/web/src/management/pages/analysis/components/DataTable.vue b/web/src/management/pages/analysis/components/DataTable.vue index 4ad07baf..5c0e047d 100644 --- a/web/src/management/pages/analysis/components/DataTable.vue +++ b/web/src/management/pages/analysis/components/DataTable.vue @@ -17,22 +17,23 @@ minWidth="200" > diff --git a/web/src/management/pages/login/LoginPage.vue b/web/src/management/pages/login/LoginPage.vue index d502cf84..6564c9bf 100644 --- a/web/src/management/pages/login/LoginPage.vue +++ b/web/src/management/pages/login/LoginPage.vue @@ -57,7 +57,6 @@ diff --git a/web/src/management/pages/list/index.vue b/web/src/management/pages/list/index.vue index 4b2cc0ee..33b4a069 100644 --- a/web/src/management/pages/list/index.vue +++ b/web/src/management/pages/list/index.vue @@ -60,60 +60,52 @@ diff --git a/web/src/management/pages/list/index.vue b/web/src/management/pages/list/index.vue index 33b4a069..87c1d658 100644 --- a/web/src/management/pages/list/index.vue +++ b/web/src/management/pages/list/index.vue @@ -68,15 +68,15 @@ import SliderBar from './components/SliderBar.vue' import SpaceModify from './components/SpaceModify.vue' import { SpaceType } from '@/management/utils/types/workSpace' import { useUserStore } from '@/management/stores/user' -import { useTeamSpaceStore } from '@/management/stores/teamSpace' +import { useWorkSpaceStore } from '@/management/stores/workSpace' import { useSurveyListStore } from '@/management/stores/surveyList' const userStore = useUserStore() -const teamSpaceStore = useTeamSpaceStore() +const workSpaceStore = useWorkSpaceStore() const surveyListStore = useSurveyListStore() const { surveyList, surveyTotal } = storeToRefs(surveyListStore) -const { spaceMenus, workSpaceId, spaceType } = storeToRefs(teamSpaceStore) +const { spaceMenus, workSpaceId, spaceType } = storeToRefs(workSpaceStore) const router = useRouter() const userInfo = computed(() => { return userStore.userInfo @@ -87,25 +87,25 @@ const activeIndex = ref('1') const handleSpaceSelect = (id: any) => { if (id === SpaceType.Personal) { // 点击个人空间菜单 - if (teamSpaceStore.spaceType === SpaceType.Personal) { + if (workSpaceStore.spaceType === SpaceType.Personal) { return } - teamSpaceStore.changeSpaceType(SpaceType.Personal) - teamSpaceStore.changeWorkSpace('') + workSpaceStore.changeSpaceType(SpaceType.Personal) + workSpaceStore.changeWorkSpace('') } else if (id === SpaceType.Group) { // 点击团队空间组菜单 - if (teamSpaceStore.spaceType === SpaceType.Group) { + if (workSpaceStore.spaceType === SpaceType.Group) { return } - teamSpaceStore.changeSpaceType(SpaceType.Group) - teamSpaceStore.changeWorkSpace('') + workSpaceStore.changeSpaceType(SpaceType.Group) + workSpaceStore.changeWorkSpace('') } else if (!Object.values(SpaceType).includes(id)) { // 点击具体团队空间 - if (teamSpaceStore.workSpaceId === id) { + if (workSpaceStore.workSpaceId === id) { return } - teamSpaceStore.changeSpaceType(SpaceType.Teamwork) - teamSpaceStore.changeWorkSpace(id) + workSpaceStore.changeSpaceType(SpaceType.Teamwork) + workSpaceStore.changeWorkSpace(id) } fetchSurveyList() @@ -115,7 +115,7 @@ onMounted(() => { fetchSurveyList() }) const fetchSpaceList = () => { - teamSpaceStore.getSpaceList() + workSpaceStore.getSpaceList() } const fetchSurveyList = async (params?: any) => { if (!params) { diff --git a/web/src/management/store/list/index.js b/web/src/management/store/list/index.js index e347bebd..7561bcbe 100644 --- a/web/src/management/store/list/index.js +++ b/web/src/management/store/list/index.js @@ -37,7 +37,7 @@ export default { spaceType: SpaceType.Personal, workSpaceId: '', spaceDetail: null, - teamSpaceList: [], + workSpaceList: [], // 列表管理 surveyList: [], surveyTotal: 0, @@ -52,7 +52,7 @@ export default { } }, getters: { - listFliter(state) { + listFilter(state) { return [ { comparator: '', @@ -96,25 +96,25 @@ export default { } }, mutations: { - updateSpaceMenus(state, teamSpace) { + updateSpaceMenus(state, workSpace) { // 更新空间列表下的团队空间 - set(state, 'spaceMenus[1].children', teamSpace) + set(state, 'spaceMenus[1].children', workSpace) }, changeSpaceType(state, spaceType) { state.spaceType = spaceType }, changeWorkSpace(state, workSpaceId) { // 切换空间清除筛选条件 - this.commit('list/reserSelectValueMap') - this.commit('list/reserButtonValueMap') + this.commit('list/resetSelectValueMap') + this.commit('list/resetButtonValueMap') this.commit('list/setSearchVal', '') state.workSpaceId = workSpaceId }, setSpaceDetail(state, data) { state.spaceDetail = data }, - setTeamSpaceList(state, data) { - state.teamSpaceList = data + setWorkSpaceList(state, data) { + state.workSpaceList = data }, setSurveyList(state, list) { state.surveyList = list @@ -125,7 +125,7 @@ export default { setSearchVal(state, data) { state.searchVal = data }, - reserSelectValueMap(state) { + resetSelectValueMap(state) { state.selectValueMap = { surveyType: '', 'curStatus.status': '' @@ -134,7 +134,7 @@ export default { changeSelectValueMap(state, { key, value }) { state.selectValueMap[key] = value }, - reserButtonValueMap(state) { + resetButtonValueMap(state) { state.buttonValueMap = { 'curStatus.date': '', createDate: -1 @@ -151,14 +151,14 @@ export default { if (res.code === CODE_MAP.SUCCESS) { const { list } = res.data - const teamSpace = list.map((item) => { + const workSpace = list.map((item) => { return { id: item._id, name: item.name } }) - commit('setTeamSpaceList', list) - commit('updateSpaceMenus', teamSpace) + commit('setWorkSpaceList', list) + commit('updateSpaceMenus', workSpace) } else { ElMessage.error('getSpaceList' + res.errmsg) } @@ -221,7 +221,7 @@ export default { }, async getSurveyList({ state, getters, commit }, payload) { const filterString = JSON.stringify( - getters.listFliter.filter((item) => { + getters.listFilter.filter((item) => { return item.condition[0].value }) ) diff --git a/web/src/management/stores/surveyList.ts b/web/src/management/stores/surveyList.ts index 35d5d19e..017834b3 100644 --- a/web/src/management/stores/surveyList.ts +++ b/web/src/management/stores/surveyList.ts @@ -1,73 +1,12 @@ -import { CODE_MAP } from '@/management/api/base' import { ElMessage } from 'element-plus' import 'element-plus/theme-chalk/src/message.scss' -import { getSurveyList as getSurveyListReq } from '@/management/api/survey' import { defineStore } from 'pinia' -import { useTeamSpaceStore } from './teamSpace' -import { ref, computed } from 'vue' - -export const useSurveyListStore = defineStore('surveyList', () => { - const surveyList = ref([]) - const surveyTotal = ref(0) - - const { - searchVal, - selectValueMap, - buttonValueMap, - listFilter, - listOrder, - resetSearch, - resetSelectValueMap, - resetButtonValueMap, - changeSelectValueMap, - changeButtonValueMap - } = useSearchSurvey() - - const teamSpaceStore = useTeamSpaceStore() - async function getSurveyList(payload: { curPage?: number; pageSize?: number }) { - const filterString = JSON.stringify( - listFilter.value.filter((item) => { - return item.condition[0].value - }) - ) - const orderString = JSON.stringify(listOrder.value) - try { - const params = { - curPage: payload?.curPage || 1, - pageSize: payload?.pageSize || 10, // 默认一页10条 - filter: filterString, - order: orderString, - workspaceId: teamSpaceStore.workSpaceId - } - const res: any = await getSurveyListReq(params) - if (res.code === CODE_MAP.SUCCESS) { - surveyList.value = res.data.data - surveyTotal.value = res.data.count - } else { - ElMessage.error(res.errmsg) - } - } catch (error) { - ElMessage.error('getSurveyList status' + error) - } - } +import { CODE_MAP } from '@/management/api/base' +import { getSurveyList as getSurveyListReq } from '@/management/api/survey' - return { - surveyList, - surveyTotal, - searchVal, - selectValueMap, - buttonValueMap, - listFliter: listFilter, - listOrder, - resetSearch, - getSurveyList, - resetSelectValueMap, - resetButtonValueMap, - changeSelectValueMap, - changeButtonValueMap - } -}) +import { useWorkSpaceStore } from './workSpace' +import { ref, computed } from 'vue' function useSearchSurvey() { const searchVal = ref('') @@ -117,7 +56,7 @@ function useSearchSurvey() { const listOrder = computed(() => { return Object.entries(buttonValueMap.value) .filter(([, effectValue]) => effectValue) - .reduce((prev: { field: string, value: string | number }[], item) => { + .reduce((prev: { field: string; value: string | number }[], item) => { const [effectKey, effectValue] = item prev.push({ field: effectKey, value: effectValue }) return prev @@ -165,3 +104,66 @@ function useSearchSurvey() { changeButtonValueMap } } + +export const useSurveyListStore = defineStore('surveyList', () => { + const surveyList = ref([]) + const surveyTotal = ref(0) + + const { + searchVal, + selectValueMap, + buttonValueMap, + listFilter, + listOrder, + resetSearch, + resetSelectValueMap, + resetButtonValueMap, + changeSelectValueMap, + changeButtonValueMap + } = useSearchSurvey() + + const workSpaceStore = useWorkSpaceStore() + async function getSurveyList(payload: { curPage?: number; pageSize?: number }) { + const filterString = JSON.stringify( + listFilter.value.filter((item) => { + return item.condition[0].value + }) + ) + const orderString = JSON.stringify(listOrder.value) + try { + const params = { + curPage: payload?.curPage || 1, + pageSize: payload?.pageSize || 10, // 默认一页10条 + filter: filterString, + order: orderString, + workspaceId: workSpaceStore.workSpaceId + } + + const res: any = await getSurveyListReq(params) + if (res.code === CODE_MAP.SUCCESS) { + surveyList.value = res.data.data + surveyTotal.value = res.data.count + } else { + ElMessage.error(res.errmsg) + } + } catch (error) { + ElMessage.error('getSurveyList status' + error) + } + } + + return { + surveyList, + surveyTotal, + searchVal, + selectValueMap, + buttonValueMap, + listFilter: listFilter, + listOrder, + resetSearch, + getSurveyList, + resetSelectValueMap, + resetButtonValueMap, + changeSelectValueMap, + changeButtonValueMap + } +}) diff --git a/web/src/management/stores/teamSpace.ts b/web/src/management/stores/workSpace.ts similarity index 88% rename from web/src/management/stores/teamSpace.ts rename to web/src/management/stores/workSpace.ts index d0902c7a..80e09a34 100644 --- a/web/src/management/stores/teamSpace.ts +++ b/web/src/management/stores/workSpace.ts @@ -1,3 +1,9 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { ElMessage } from 'element-plus' +import 'element-plus/theme-chalk/src/message.scss' + +import { CODE_MAP } from '@/management/api/base' import { createSpace, updateSpace as updateSpaceReq, @@ -5,16 +11,16 @@ import { getSpaceList as getSpaceListReq, getSpaceDetail as getSpaceDetailReq } from '@/management/api/space' -import { CODE_MAP } from '@/management/api/base' import { SpaceType } from '@/management/utils/types/workSpace' -import { defineStore } from 'pinia' -import { ref } from 'vue' -import { ElMessage } from 'element-plus' -import { useSurveyListStore } from './surveyList' -import { type SpaceDetail, type SpaceItem, type IWorkspace } from '@/management/utils/types/workSpace' +import { + type SpaceDetail, + type SpaceItem, + type IWorkspace +} from '@/management/utils/types/workSpace' +import { useSurveyListStore } from './surveyList' -export const useTeamSpaceStore = defineStore('teamSpace', () => { +export const useWorkSpaceStore = defineStore('workSpace', () => { // list空间 const spaceMenus = ref([ { @@ -32,7 +38,7 @@ export const useTeamSpaceStore = defineStore('teamSpace', () => { const spaceType = ref(SpaceType.Personal) const workSpaceId = ref('') const spaceDetail = ref(null) - const teamSpaceList = ref([]) + const workSpaceList = ref([]) const surveyListStore = useSurveyListStore() @@ -42,14 +48,14 @@ export const useTeamSpaceStore = defineStore('teamSpace', () => { if (res.code === CODE_MAP.SUCCESS) { const { list } = res.data - const teamSpace = list.map((item: SpaceDetail) => { + const workSpace = list.map((item: SpaceDetail) => { return { id: item._id, name: item.name } }) - teamSpaceList.value = list - spaceMenus.value[1].children = teamSpace + workSpaceList.value = list + spaceMenus.value[1].children = workSpace } else { ElMessage.error('getSpaceList' + res.errmsg) } @@ -126,7 +132,7 @@ export const useTeamSpaceStore = defineStore('teamSpace', () => { spaceType, workSpaceId, spaceDetail, - teamSpaceList, + workSpaceList, getSpaceList, getSpaceDetail, changeSpaceType, From 2f0736fd9575178d5fc9cfa127e229ef2c3c3dcd Mon Sep 17 00:00:00 2001 From: Ken <66313154+HeHasGun@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:12:13 +0800 Subject: [PATCH 17/82] =?UTF-8?q?feat:=20=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E4=BC=98=E5=8C=96=20(#326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 移动端预览优化 * feat: C端底部logo优化 --- web/src/render/index.html | 9 ++++++++- web/src/render/pages/RenderPage.vue | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/render/index.html b/web/src/render/index.html index 7a55f7f4..cd6aaa9b 100644 --- a/web/src/render/index.html +++ b/web/src/render/index.html @@ -38,7 +38,14 @@ window.addEventListener('resize', resetRemUnit) })() - + diff --git a/web/src/render/pages/RenderPage.vue b/web/src/render/pages/RenderPage.vue index a43e0715..30cf4797 100644 --- a/web/src/render/pages/RenderPage.vue +++ b/web/src/render/pages/RenderPage.vue @@ -13,8 +13,8 @@ :renderData="renderData" @submit="handleSubmit" > - + From f45cf7982ff871c0887b8d578403d45fdcf70ab1 Mon Sep 17 00:00:00 2001 From: Jiangchunfu Date: Tue, 9 Jul 2024 14:17:44 +0800 Subject: [PATCH 18/82] =?UTF-8?q?=E5=B0=8F=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20(#329)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(换肤设置优化): 边距的颜色优化成背景色一致 * feat: 问卷设置优化 --- .../management/pages/edit/components/QuestionWrapper.vue | 2 +- .../materials/questions/QuestionRuleContainer/style.scss | 2 +- web/src/materials/setters/widgets/QuestionTime.vue | 2 ++ web/src/materials/setters/widgets/QuestionTimeHour.vue | 9 ++++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web/src/management/pages/edit/components/QuestionWrapper.vue b/web/src/management/pages/edit/components/QuestionWrapper.vue index fa25395e..262153e1 100644 --- a/web/src/management/pages/edit/components/QuestionWrapper.vue +++ b/web/src/management/pages/edit/components/QuestionWrapper.vue @@ -163,7 +163,7 @@ const onMove = () => {} padding: 0.36rem 0 0.36rem; border: 1px solid transparent; &.spliter { - border-bottom: 0.12rem solid $spliter-color; + border-bottom: 0.12rem solid var(--primary-background-color); } &.mouse-hover { diff --git a/web/src/materials/questions/QuestionRuleContainer/style.scss b/web/src/materials/questions/QuestionRuleContainer/style.scss index fc676f84..10fca671 100644 --- a/web/src/materials/questions/QuestionRuleContainer/style.scss +++ b/web/src/materials/questions/QuestionRuleContainer/style.scss @@ -94,7 +94,7 @@ } &.spliter { - border-bottom: 0.12rem solid $spliter-color; + border-bottom: 0.12rem solid var(--primary-background-color); } .sort-tip { diff --git a/web/src/materials/setters/widgets/QuestionTime.vue b/web/src/materials/setters/widgets/QuestionTime.vue index 6fa5a6e0..2a20ea8a 100644 --- a/web/src/materials/setters/widgets/QuestionTime.vue +++ b/web/src/materials/setters/widgets/QuestionTime.vue @@ -73,7 +73,9 @@ watch( From 122f584cad1bc999a7c5fd2abe7305387f0ea78e Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Tue, 9 Jul 2024 15:23:02 +0800 Subject: [PATCH 19/82] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E9=A1=B5logo=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edit/modules/settingModule/skin/PreviewPanel.vue | 10 +++++----- .../materials/communals/widgets/LogoIcon/index.scss | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue b/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue index 1f8b6718..64af2b95 100644 --- a/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue @@ -13,12 +13,12 @@ :readonly="false" :is-selected="currentEditOne === 'submit'" /> - + diff --git a/web/src/materials/communals/widgets/LogoIcon/index.scss b/web/src/materials/communals/widgets/LogoIcon/index.scss index a14fa920..d3a63060 100644 --- a/web/src/materials/communals/widgets/LogoIcon/index.scss +++ b/web/src/materials/communals/widgets/LogoIcon/index.scss @@ -1,6 +1,9 @@ .logo-icon-warp { display: flex; justify-content: center; + background: #fff; + margin: 0 0.3rem; + padding: 0.1rem 0 0.5rem; .logo-wrapper { text-align: center; font-size: 0; @@ -9,8 +12,6 @@ .question-logo { max-width: 300px; text-align: center; - padding: 0 0 0.6rem; - margin-top: -0.2rem; cursor: pointer; } .logo-placeholder-wrapper { From 1a15faad4289e832627fb41cfc2afb02c3f31a3d Mon Sep 17 00:00:00 2001 From: Jiangchunfu Date: Wed, 10 Jul 2024 14:05:41 +0800 Subject: [PATCH 20/82] =?UTF-8?q?feat:=20edit=20vuex=E8=BF=81=E7=A7=BBpini?= =?UTF-8?q?a=20(#325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: dev reload optimized * feat: edit store change pinia * feat: store中状态修改增加方法 * feat: js改为ts --------- Co-authored-by: jiangchunfu --- web/src/management/hooks/useQuestionInfo.js | 12 +- web/src/management/hooks/useResizeObserver.js | 2 +- .../analysis/components/StatisticsItem.vue | 2 +- .../pages/edit/components/MaterialGroup.vue | 8 +- .../pages/edit/components/ModuleNavbar.vue | 7 +- web/src/management/pages/edit/index.vue | 11 +- .../modules/contentModule/HistoryPanel.vue | 15 +- .../modules/contentModule/PublishPanel.vue | 11 +- .../edit/modules/contentModule/SavePanel.vue | 13 +- .../modules/questionModule/PreviewPanel.vue | 38 +- .../modules/questionModule/SetterPanel.vue | 18 +- .../components/QuestionCatalog.vue | 24 +- .../questionModule/components/TypeList.vue | 20 +- .../modules/settingModule/SettingPanel.vue | 11 +- .../settingModule/result/CatalogPanel.vue | 12 +- .../settingModule/result/PreviewPanel.vue | 15 +- .../settingModule/result/SetterPanel.vue | 18 +- .../settingModule/skin/CatalogPanel.vue | 5 +- .../settingModule/skin/PreviewPanel.vue | 18 +- .../settingModule/skin/SetterPanel.vue | 10 +- .../pages/edit/pages/edit/LogicEditPage.vue | 10 +- .../management/pages/publish/PublishPage.vue | 15 +- web/src/management/stores/edit.ts | 343 ++++++++++++++++++ web/vite.config.ts | 8 +- 24 files changed, 494 insertions(+), 152 deletions(-) create mode 100644 web/src/management/stores/edit.ts diff --git a/web/src/management/hooks/useQuestionInfo.js b/web/src/management/hooks/useQuestionInfo.js index f8f66e3f..19049008 100644 --- a/web/src/management/hooks/useQuestionInfo.js +++ b/web/src/management/hooks/useQuestionInfo.js @@ -1,17 +1,19 @@ import { computed } from 'vue' -import store from '@/management/store' +import { storeToRefs } from 'pinia' +import { useEditStore } from '@/management/stores/edit' import { cleanRichText } from '@/common/xss' export const useQuestionInfo = (field) => { + const editStore = useEditStore() + const { questionDataList } = storeToRefs(editStore) + const getQuestionTitle = computed(() => { - const questionDataList = store.state.edit.schema.questionDataList return () => { - return questionDataList.find((item) => item.field === field)?.title + return questionDataList.value.find((item) => item.field === field)?.title } }) const getOptionTitle = computed(() => { - const questionDataList = store.state.edit.schema.questionDataList return (value) => { - const options = questionDataList.find((item) => item.field === field)?.options || [] + const options = questionDataList.value.find((item) => item.field === field)?.options || [] if (value instanceof Array) { return options .filter((item) => value.includes(item.hash)) diff --git a/web/src/management/hooks/useResizeObserver.js b/web/src/management/hooks/useResizeObserver.js index 486962f2..cbf400ea 100644 --- a/web/src/management/hooks/useResizeObserver.js +++ b/web/src/management/hooks/useResizeObserver.js @@ -1,5 +1,5 @@ // 引入防抖函数 -import _debounce from 'lodash/debounce' +import { debounce as _debounce } from 'lodash-es' /** * @description: 监听元素尺寸变化 * @param {*} el 元素dom diff --git a/web/src/management/pages/analysis/components/StatisticsItem.vue b/web/src/management/pages/analysis/components/StatisticsItem.vue index 7cd95b1e..9d43ce2d 100644 --- a/web/src/management/pages/analysis/components/StatisticsItem.vue +++ b/web/src/management/pages/analysis/components/StatisticsItem.vue @@ -34,7 +34,7 @@ diff --git a/web/src/management/pages/edit/components/ModuleNavbar.vue b/web/src/management/pages/edit/components/ModuleNavbar.vue index 0965cddf..59f8b303 100644 --- a/web/src/management/pages/edit/components/ModuleNavbar.vue +++ b/web/src/management/pages/edit/components/ModuleNavbar.vue @@ -8,6 +8,14 @@
+ + + @@ -27,11 +35,18 @@ import HistoryPanel from '../modules/contentModule/HistoryPanel.vue' import PreviewPanel from '../modules/contentModule/PreviewPanel.vue' import SavePanel from '../modules/contentModule/SavePanel.vue' import PublishPanel from '../modules/contentModule/PublishPanel.vue' +import CooperationPanel from './CooperationPanel.vue'; const store = useStore() const title = computed(() => _get(store.state, 'edit.schema.metaData.title')) diff --git a/web/src/management/pages/edit/components/ModuleNavbar.vue b/web/src/management/pages/edit/components/ModuleNavbar.vue index 59f8b303..9949b348 100644 --- a/web/src/management/pages/edit/components/ModuleNavbar.vue +++ b/web/src/management/pages/edit/components/ModuleNavbar.vue @@ -8,14 +8,7 @@
- - - + @@ -35,18 +28,12 @@ import HistoryPanel from '../modules/contentModule/HistoryPanel.vue' import PreviewPanel from '../modules/contentModule/PreviewPanel.vue' import SavePanel from '../modules/contentModule/SavePanel.vue' import PublishPanel from '../modules/contentModule/PublishPanel.vue' -import CooperationPanel from './CooperationPanel.vue'; +import CooperationPanel from '../modules/contentModule/CooperationPanel.vue' const store = useStore() const title = computed(() => _get(store.state, 'edit.schema.metaData.title')) diff --git a/web/src/management/pages/edit/modules/contentModule/PreviewPanel.vue b/web/src/management/pages/edit/modules/contentModule/PreviewPanel.vue index 7c51c200..18fcf08e 100644 --- a/web/src/management/pages/edit/modules/contentModule/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/contentModule/PreviewPanel.vue @@ -87,12 +87,6 @@ const closedDialog = () => { margin-left: 75px; } - .view-icon { - font-size: 20px; - height: 29px; - line-height: 29px; - } - .preview-tab { display: flex; align-items: center; diff --git a/web/src/management/pages/list/components/BaseList.vue b/web/src/management/pages/list/components/BaseList.vue index fa1ff189..59ad68aa 100644 --- a/web/src/management/pages/list/components/BaseList.vue +++ b/web/src/management/pages/list/components/BaseList.vue @@ -120,6 +120,7 @@ import 'moment/locale/zh-cn' moment.locale('zh-cn') import EmptyIndex from '@/management/components/EmptyIndex.vue' +import CooperModify from '@/management/components/CooperModify/ModifyDialog.vue' import { CODE_MAP } from '@/management/api/base' import { QOP_MAP } from '@/management/utils/constant.ts' import { deleteSurvey } from '@/management/api/survey' @@ -130,7 +131,6 @@ import ToolBar from './ToolBar.vue' import TextSearch from './TextSearch.vue' import TextSelect from './TextSelect.vue' import TextButton from './TextButton.vue' -import CooperModify from './CooperModify.vue' import { SurveyPermissions } from '@/management/utils/types/workSpace' import { diff --git a/web/src/management/pages/list/components/SpaceModify.vue b/web/src/management/pages/list/components/SpaceModify.vue index 668fd038..e9675467 100644 --- a/web/src/management/pages/list/components/SpaceModify.vue +++ b/web/src/management/pages/list/components/SpaceModify.vue @@ -48,10 +48,12 @@ import { useStore } from 'vuex' 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 MemberSelect from './MemberSelect.vue' import { type IMember, type IWorkspace, UserRole } from '@/management/utils/types/workSpace' +import MemberSelect from '@/management/components/CooperModify/MemberSelect.vue' + const store = useStore() const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur']) const props = defineProps({ diff --git a/web/src/management/styles/edit-btn.scss b/web/src/management/styles/edit-btn.scss index 84cf2d68..0be725fa 100644 --- a/web/src/management/styles/edit-btn.scss +++ b/web/src/management/styles/edit-btn.scss @@ -15,4 +15,10 @@ .btn-txt { font-size: 12px; } + + .view-icon { + font-size: 20px; + height: 29px; + line-height: 29px; + } } From 36dd5a4f2d2609c97eb37cc7f36faf520ddcc68a Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Fri, 12 Jul 2024 16:49:40 +0800 Subject: [PATCH 28/82] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0csdn=E5=92=8Cx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_EN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9071bc9..d0fcc8f8 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,6 @@ npm run serve ## 文章分享 -1、[掘金](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish) +1、[掘金](https://juejin.cn/user/3705833332160473/posts)、2、[CSDN](https://blog.csdn.net/XIAOJUSURVEY)、3、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish) [欢迎投稿](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B) diff --git a/README_EN.md b/README_EN.md index 644fdf4e..3fbc59b8 100644 --- a/README_EN.md +++ b/README_EN.md @@ -228,6 +228,6 @@ Follow major changes: [MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/is ## Article Sharing -1、[JueJin](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish) +1、[x.com](https://x.com/t_sudoooooo) [Welcome to contribute.](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B) From bc39e9933debb4c0d1dc96944b52ff7a46c54259 Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 15 Jul 2024 11:21:35 +0800 Subject: [PATCH 29/82] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8Delement-plus?= =?UTF-8?q?=E6=8F=90=E7=A4=BAsass=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 55593dec..cc5d22b5 100644 --- a/web/package.json +++ b/web/package.json @@ -51,7 +51,7 @@ "husky": "^9.0.11", "npm-run-all2": "^6.1.1", "prettier": "^3.0.3", - "sass": "^1.72.0", + "sass": "1.77.6", "typescript": "~5.3.0", "unplugin-auto-import": "^0.17.5", "unplugin-icons": "^0.18.5", From f08c8bcd2a24167474a4f074d4ece436cc378fa5 Mon Sep 17 00:00:00 2001 From: dayou <853094838@qq.com> Date: Mon, 15 Jul 2024 11:23:09 +0800 Subject: [PATCH 30/82] =?UTF-8?q?feature:=20=E6=90=AD=E5=BB=BA=E7=AB=AF?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E6=A8=A1=E5=9D=97=E4=B8=8B=E7=9A=84=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E8=BF=81=E7=A7=BBpinia-edit=E6=A8=A1=E5=9D=97=20(#336?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 搭建端的pinia迁移完成 * fix:删除store --- web/src/management/components/LeftMenu.vue | 6 +- web/src/management/main.js | 2 - .../settingModule/skin/CatalogPanel.vue | 3 +- .../pages/edit/pages/skin/ContentPage.vue | 6 +- web/src/management/router/index.ts | 17 +- web/src/management/store/actions.js | 21 -- web/src/management/store/edit/actions.js | 72 ----- web/src/management/store/edit/getters.js | 71 ----- web/src/management/store/edit/index.js | 12 - web/src/management/store/edit/mutations.js | 68 ----- web/src/management/store/edit/state.js | 60 ---- web/src/management/store/index.js | 19 -- web/src/management/store/list/index.js | 258 ------------------ web/src/management/store/mutations.js | 8 - web/src/management/store/state.js | 5 - web/src/management/store/user/index.js | 64 ----- web/src/management/stores/edit.ts | 28 ++ 17 files changed, 43 insertions(+), 677 deletions(-) delete mode 100644 web/src/management/store/actions.js delete mode 100644 web/src/management/store/edit/actions.js delete mode 100644 web/src/management/store/edit/getters.js delete mode 100644 web/src/management/store/edit/index.js delete mode 100644 web/src/management/store/edit/mutations.js delete mode 100644 web/src/management/store/edit/state.js delete mode 100644 web/src/management/store/index.js delete mode 100644 web/src/management/store/list/index.js delete mode 100644 web/src/management/store/mutations.js delete mode 100644 web/src/management/store/state.js delete mode 100644 web/src/management/store/user/index.js diff --git a/web/src/management/components/LeftMenu.vue b/web/src/management/components/LeftMenu.vue index f60668a4..deace7d4 100644 --- a/web/src/management/components/LeftMenu.vue +++ b/web/src/management/components/LeftMenu.vue @@ -27,12 +27,12 @@ diff --git a/web/src/management/pages/list/components/SpaceList.vue b/web/src/management/pages/list/components/SpaceList.vue index a9237e96..4f104203 100644 --- a/web/src/management/pages/list/components/SpaceList.vue +++ b/web/src/management/pages/list/components/SpaceList.vue @@ -38,25 +38,13 @@ class-name="table-options" > @@ -77,6 +65,7 @@ import 'element-plus/theme-chalk/src/message-box.scss' import { get, map } from 'lodash-es' import { spaceListConfig } from '@/management/config/listConfig' import SpaceModify from './SpaceModify.vue' +import ToolBar from './ToolBar.vue' import { UserRole } from '@/management/utils/types/workSpace' const showSpaceModify = ref(false) @@ -98,6 +87,15 @@ const isAdmin = (id: string) => { ) } +const getTools = (data: any) => { + const flag = isAdmin(data._id) + const tools = [{ key: 'modify', label: flag ? '管理' : '查看' }] + if (flag) { + tools.push({ key: 'delete', label: '删除' }) + } + return tools +} + const handleModify = async (id: string) => { await store.dispatch('list/getSpaceDetail', id) modifyType.value = 'edit' @@ -120,6 +118,15 @@ const handleDelete = (id: string) => { }) .catch(() => {}) } + +const handleClick = (key: string, data: any) => { + if (key === 'modify') { + handleModify(data._id) + } else if (key === 'delete') { + handleDelete(data._id) + } +} + const onCloseModify = () => { showSpaceModify.value = false store.dispatch('list/getSpaceList') @@ -133,6 +140,7 @@ const onCloseModify = () => { .list-wrap { padding: 20px; background: #fff; + .list-table { :deep(.el-table__header) { .tableview-header .el-table__cell { @@ -143,12 +151,15 @@ const onCloseModify = () => { } } } + :deep(.tableview-row) { .tableview-cell { padding: 5px 0; + &.link { cursor: pointer; } + .cell .cell-span { font-size: 14px; } @@ -156,9 +167,7 @@ const onCloseModify = () => { } .tool-root { display: flex; - &:first-child { - margin-left: -10px; - } + .tool-root-btn-text { font-weight: normal !important; } diff --git a/web/src/management/pages/list/index.vue b/web/src/management/pages/list/index.vue index 89ab66ee..e27e229d 100644 --- a/web/src/management/pages/list/index.vue +++ b/web/src/management/pages/list/index.vue @@ -22,7 +22,7 @@
{ .create-btn { background: #4a4c5b; } - .space-btn { - background: $primary-color; - } + .btn { width: 132px; height: 32px; From 5c3915a74d9082aac5ea5d435f97c1207f8f5b69 Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 15 Jul 2024 12:11:57 +0800 Subject: [PATCH 32/82] =?UTF-8?q?fix:=20=E7=A9=BA=E9=97=B4=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/management/pages/list/components/SpaceList.vue | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/web/src/management/pages/list/components/SpaceList.vue b/web/src/management/pages/list/components/SpaceList.vue index 4f104203..0c78e6c1 100644 --- a/web/src/management/pages/list/components/SpaceList.vue +++ b/web/src/management/pages/list/components/SpaceList.vue @@ -154,7 +154,7 @@ const onCloseModify = () => { :deep(.tableview-row) { .tableview-cell { - padding: 5px 0; + height: 42px; &.link { cursor: pointer; @@ -165,13 +165,6 @@ const onCloseModify = () => { } } } - .tool-root { - display: flex; - - .tool-root-btn-text { - font-weight: normal !important; - } - } } } From 8740685a4d3a7706cc5853603dcc2f2ee0d03bd7 Mon Sep 17 00:00:00 2001 From: Jiangchunfu Date: Mon, 15 Jul 2024 17:50:00 +0800 Subject: [PATCH 33/82] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD18=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20(#342)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 题目标题编辑自动focus --- web/src/common/Editor/RichEditor.vue | 3 +- .../EditOptions/Options/OptionEdit.vue | 33 +++++++++++++++++++ .../widgets/TitleModules/EditTitle/index.jsx | 5 +++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/web/src/common/Editor/RichEditor.vue b/web/src/common/Editor/RichEditor.vue index 4d616459..d15b810a 100644 --- a/web/src/common/Editor/RichEditor.vue +++ b/web/src/common/Editor/RichEditor.vue @@ -28,7 +28,7 @@ import './styles/reset-wangeditor.scss' import { Editor, Toolbar } from '@wangeditor/editor-for-vue' import { ref, shallowRef, onBeforeMount, watch } from 'vue' -const emit = defineEmits(['input', 'onFocus', 'change', 'blur']) +const emit = defineEmits(['input', 'onFocus', 'change', 'blur', 'created']) const model = defineModel() const props = defineProps(['staticToolBar']) @@ -60,6 +60,7 @@ const onCreated = (editor) => { if (model.value) { setHtml(model.value) } + emit('created', editor) } const onChange = (editor) => { const editorHtml = editor.getHtml() diff --git a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue index 8025a528..d8eb8aff 100644 --- a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue +++ b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue @@ -14,6 +14,7 @@
@@ -41,6 +42,7 @@ import draggable from 'vuedraggable' import { cloneDeep as _cloneDeep } from 'lodash-es' import RichEditor from '@/common/Editor/RichEditor.vue' +import { mapGetters, mapState } from 'vuex' export default { name: 'OptionEdit', @@ -58,6 +60,14 @@ export default { draggable, RichEditor }, + data() { + return { + // 编辑器创建完成数量 + createdEditorCount: 0, + // 编辑器实例列表 + editorList: [] + } + }, mounted() { // 选项hash兜底 const hashMap = {} @@ -101,6 +111,29 @@ export default { optionSortChange() { const optionList = _cloneDeep(this.optionList) this.$emit('optionChange', optionList) + }, + // 监听 editor 创建完成 + handleCreated(editor) { + this.createdEditorCount++ + this.editorList.push(editor) + } + }, + computed: { + ...mapGetters({ + moduleConfig: 'edit/moduleConfig' + }), + // 当前题目所有编辑器是否创建完成 + createdAllCurrentEditQuestionEditor() { + return this.createdEditorCount === this.moduleConfig?.options?.length + } + }, + watch: { + // 监听当前编辑选项所有编辑器创建完成 + // 如果所有选项创建完成,则聚焦第一个选项 + createdAllCurrentEditQuestionEditor(bool) { + if (bool) { + this.editorList[0]?.focus() + } } } } diff --git a/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx b/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx index 29abab89..5f2ca12d 100644 --- a/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx +++ b/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx @@ -70,6 +70,11 @@ export default defineComponent({ class="rich-editor" modelValue={filterXSS(this.title)} onChange={this.handleChange} + onCreated={ + (editor) => { + editor?.focus() + } + } /> ) } From 3227a799f9386a87d96ca2bc946efc7b32fb1c06 Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 15 Jul 2024 18:00:07 +0800 Subject: [PATCH 34/82] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E9=BB=98=E8=AE=A4=E9=80=89=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditOptions/Options/OptionEdit.vue | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue index d8eb8aff..8025a528 100644 --- a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue +++ b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue @@ -14,7 +14,6 @@
@@ -42,7 +41,6 @@ import draggable from 'vuedraggable' import { cloneDeep as _cloneDeep } from 'lodash-es' import RichEditor from '@/common/Editor/RichEditor.vue' -import { mapGetters, mapState } from 'vuex' export default { name: 'OptionEdit', @@ -60,14 +58,6 @@ export default { draggable, RichEditor }, - data() { - return { - // 编辑器创建完成数量 - createdEditorCount: 0, - // 编辑器实例列表 - editorList: [] - } - }, mounted() { // 选项hash兜底 const hashMap = {} @@ -111,29 +101,6 @@ export default { optionSortChange() { const optionList = _cloneDeep(this.optionList) this.$emit('optionChange', optionList) - }, - // 监听 editor 创建完成 - handleCreated(editor) { - this.createdEditorCount++ - this.editorList.push(editor) - } - }, - computed: { - ...mapGetters({ - moduleConfig: 'edit/moduleConfig' - }), - // 当前题目所有编辑器是否创建完成 - createdAllCurrentEditQuestionEditor() { - return this.createdEditorCount === this.moduleConfig?.options?.length - } - }, - watch: { - // 监听当前编辑选项所有编辑器创建完成 - // 如果所有选项创建完成,则聚焦第一个选项 - createdAllCurrentEditQuestionEditor(bool) { - if (bool) { - this.editorList[0]?.focus() - } } } } From 5bc5eb871921bc5a87c823cfbc4d1bde352f11a1 Mon Sep 17 00:00:00 2001 From: Ken <66313154+HeHasGun@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:30:50 +0800 Subject: [PATCH 35/82] =?UTF-8?q?feat:=20=E7=A9=BA=E9=97=B4=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=88=86=E9=A1=B5=E5=92=8C=E6=90=9C=E7=B4=A2=E5=8F=8A?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=97=E8=A1=A8=E4=BC=98=E5=8C=96=20(#344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、fix: 修复因滚动条宽度影响皮肤标签的问题 2、feat: 空间列表分页和搜索及数据列表样式优化 3、feat: 对部分代码进行了优化 --- .../_test/workspace.controller.spec.ts | 17 +++- .../controllers/workspace.controller.ts | 30 +++++- .../workspace/dto/getWorkspaceList.dto.ts | 21 ++++ .../workspace/services/workspace.service.ts | 58 +++++++++-- web/src/management/api/space.ts | 8 +- web/src/management/config/listConfig.js | 12 ++- .../pages/analysis/components/DataTable.vue | 10 +- .../pages/edit/components/ModuleNavbar.vue | 15 ++- .../settingModule/skin/CatalogPanel.vue | 4 + .../pages/list/components/SpaceList.vue | 96 ++++++++++++++++--- .../pages/list/components/TextSearch.vue | 2 +- web/src/management/pages/list/index.vue | 77 +++++++++------ web/src/management/store/list/index.js | 13 ++- 13 files changed, 294 insertions(+), 69 deletions(-) create mode 100644 server/src/modules/workspace/dto/getWorkspaceList.dto.ts diff --git a/server/src/modules/workspace/_test/workspace.controller.spec.ts b/server/src/modules/workspace/_test/workspace.controller.spec.ts index 3a5b2733..23c393d9 100644 --- a/server/src/modules/workspace/_test/workspace.controller.spec.ts +++ b/server/src/modules/workspace/_test/workspace.controller.spec.ts @@ -32,6 +32,7 @@ describe('WorkspaceController', () => { useValue: { create: jest.fn(), findAllById: jest.fn(), + findAllByIdWithPagination: jest.fn(), update: jest.fn(), delete: jest.fn(), }, @@ -145,20 +146,28 @@ describe('WorkspaceController', () => { jest .spyOn(workspaceMemberService, 'findAllByUserId') .mockResolvedValue(memberList as unknown as Array); + jest - .spyOn(workspaceService, 'findAllById') - .mockResolvedValue(workspaces as Array); + .spyOn(workspaceService, 'findAllByIdWithPagination') + .mockResolvedValue({ + list: workspaces as Array, + count: workspaces.length, + }); + jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]); - const result = await controller.findAll(req); + const result = await controller.findAll(req, {curPage:1,pageSize:10}); expect(result.code).toEqual(200); expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({ userId: req.user._id.toString(), }); - expect(workspaceService.findAllById).toHaveBeenCalledWith({ + expect(workspaceService.findAllByIdWithPagination).toHaveBeenCalledWith({ workspaceIdList: memberList.map((item) => item.workspaceId), + page: 1, + limit: 10, + name: undefined }); }); }); diff --git a/server/src/modules/workspace/controllers/workspace.controller.ts b/server/src/modules/workspace/controllers/workspace.controller.ts index 81196830..87669803 100644 --- a/server/src/modules/workspace/controllers/workspace.controller.ts +++ b/server/src/modules/workspace/controllers/workspace.controller.ts @@ -9,6 +9,7 @@ import { Request, SetMetadata, HttpCode, + Query, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import moment from 'moment'; @@ -31,6 +32,7 @@ import { splitMembers } from '../utils/splitMember'; import { UserService } from 'src/modules/auth/services/user.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { Logger } from 'src/logger'; +import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto'; @ApiTags('workspace') @ApiBearerAuth() @@ -128,8 +130,21 @@ export class WorkspaceController { @Get() @HttpCode(200) - async findAll(@Request() req) { + async findAll(@Request() req, @Query() queryInfo: GetWorkspaceListDto) { + const { value, error } = GetWorkspaceListDto.validate(queryInfo); + if (error) { + this.logger.error( + `GetWorkspaceListDto validate failed: ${error.message}`, + { req }, + ); + throw new HttpException( + `参数错误: 请联系管理员`, + EXCEPTION_CODE.PARAMETER_ERROR, + ); + } const userId = req.user._id.toString(); + const curPage = Number(value.curPage); + const pageSize = Number(value.pageSize); // 查询当前用户参与的空间 const workspaceInfoList = await this.workspaceMemberService.findAllByUserId( { userId }, @@ -139,9 +154,16 @@ export class WorkspaceController { pre[cur.workspaceId] = cur; return pre; }, {}); + // 查询当前用户的空间列表 - const list = await this.workspaceService.findAllById({ workspaceIdList }); - const ownerIdList = list.map((item) => item.ownerId); + const { list, count } = + await this.workspaceService.findAllByIdWithPagination({ + workspaceIdList, + page: curPage, + limit: pageSize, + name: queryInfo.name, + }); + const ownerIdList = list.map((item: { ownerId: any }) => item.ownerId); const userList = await this.userService.getUserListByIds({ idList: ownerIdList, }); @@ -150,6 +172,7 @@ export class WorkspaceController { pre[id] = cur; return pre; }, {}); + const surveyTotalList = await Promise.all( workspaceIdList.map((item) => { return this.surveyMetaService.countSurveyMetaByWorkspaceId({ @@ -193,6 +216,7 @@ export class WorkspaceController { memberTotal: memberTotalMap[workspaceId] || 0, }; }), + count, }, }; } diff --git a/server/src/modules/workspace/dto/getWorkspaceList.dto.ts b/server/src/modules/workspace/dto/getWorkspaceList.dto.ts new file mode 100644 index 00000000..5990ff82 --- /dev/null +++ b/server/src/modules/workspace/dto/getWorkspaceList.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class GetWorkspaceListDto { + @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/workspace/services/workspace.service.ts b/server/src/modules/workspace/services/workspace.service.ts index aaf37f69..4607cb1f 100644 --- a/server/src/modules/workspace/services/workspace.service.ts +++ b/server/src/modules/workspace/services/workspace.service.ts @@ -8,6 +8,17 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { ObjectId } from 'mongodb'; import { RECORD_STATUS } from 'src/enums'; +interface FindAllByIdWithPaginationParams { + workspaceIdList: string[]; + page: number; + limit: number; + name?: string; +} +interface FindAllByIdWithPaginationResult { + list: Workspace[]; + count: number; +} + @Injectable() export class WorkspaceService { constructor( @@ -41,15 +52,17 @@ export class WorkspaceService { }: { workspaceIdList: string[]; }): Promise { - return this.workspaceRepository.find({ - where: { - _id: { - $in: workspaceIdList.map((item) => new ObjectId(item)), - }, - 'curStatus.status': { - $ne: RECORD_STATUS.REMOVED, - }, + const query = { + _id: { + $in: workspaceIdList.map((item) => new ObjectId(item)), }, + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }; + + return this.workspaceRepository.find({ + where: query, order: { _id: -1, }, @@ -64,6 +77,35 @@ export class WorkspaceService { }); } + async findAllByIdWithPagination({ + workspaceIdList, + page, + limit, + name, + }: FindAllByIdWithPaginationParams): Promise { + const skip = (page - 1) * limit; + if (!Array.isArray(workspaceIdList) || workspaceIdList.length === 0) { + return { list: [], count: 0 }; + } + const query = { + _id: { + $in: workspaceIdList.map((m) => new ObjectId(m)), + }, + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }; + if (name) { + query['name'] = { $regex: name, $options: 'i' }; + } + const [data, count] = await this.workspaceRepository.findAndCount({ + where: query, + skip, + take: limit, + }); + return { list: data, count }; + } + update(id: string, workspace: Partial) { return this.workspaceRepository.update(id, workspace); } diff --git a/web/src/management/api/space.ts b/web/src/management/api/space.ts index b0aadda9..83f76ff8 100644 --- a/web/src/management/api/space.ts +++ b/web/src/management/api/space.ts @@ -8,8 +8,10 @@ export const updateSpace = ({ workspaceId, name, description, members }: any) => return axios.post(`/workspace/${workspaceId}`, { name, description, members }) } -export const getSpaceList = () => { - return axios.get('/workspace') +export const getSpaceList = (params: any) => { + return axios.get('/workspace', { + params + }) } export const getSpaceDetail = (workspaceId: string) => { @@ -71,4 +73,4 @@ export const getCollaboratorPermissions = (surveyId: string) => { surveyId } }) -} \ No newline at end of file +} diff --git a/web/src/management/config/listConfig.js b/web/src/management/config/listConfig.js index 5576d50a..8a558651 100644 --- a/web/src/management/config/listConfig.js +++ b/web/src/management/config/listConfig.js @@ -9,7 +9,7 @@ export const spaceListConfig = { name: { title: '空间名称', key: 'name', - width: 300 + width: 200 }, surveyTotal: { title: '问卷数', @@ -82,6 +82,16 @@ export const noListDataConfig = { img: '/imgs/icons/list-empty.webp' } +export const noSpaceDataConfig = { + title: '您还没有创建团队空间', + desc: '赶快点击右上角立即创建团队空间吧!', + img: '/imgs/icons/list-empty.webp' +} +export const noSpaceSearchDataConfig = { + title: '没有满足该查询条件的团队空间哦', + desc: '可以更换条件查询试试', + img: '/imgs/icons/list-empty.webp' +} export const noSearchDataConfig = { title: '没有满足该查询条件的问卷哦', desc: '可以更换条件查询试试', diff --git a/web/src/management/pages/analysis/components/DataTable.vue b/web/src/management/pages/analysis/components/DataTable.vue index 5c0e047d..d5df1d29 100644 --- a/web/src/management/pages/analysis/components/DataTable.vue +++ b/web/src/management/pages/analysis/components/DataTable.vue @@ -42,7 +42,7 @@ { } const onPopoverRefOver = (scope, type) => { let popoverContent - if (type == 'head') { + if (type === 'head') { popoverVirtualRef.value = popoverRefMap.value[scope.column.id] popoverContent = scope.column.label.replace(/ /g, '') } - if (type == 'content') { + if (type === 'content') { popoverVirtualRef.value = popoverRefMap.value[scope.$index + scope.column.property] popoverContent = getContent(scope.row[scope.column.property]) } @@ -99,7 +99,6 @@ const onPopoverRefOver = (scope, type) => { .data-table-wrapper { position: relative; width: 100%; - padding-bottom: 20px; min-height: v-bind('tableMinHeight'); background: #fff; padding: 10px 20px; @@ -132,4 +131,7 @@ const onPopoverRefOver = (scope, type) => { /* 显示省略号 */ } } +:deep(.el-table td.el-table__cell div) { + font-size: 13px; +} diff --git a/web/src/management/pages/edit/components/ModuleNavbar.vue b/web/src/management/pages/edit/components/ModuleNavbar.vue index 9949b348..d1484a5f 100644 --- a/web/src/management/pages/edit/components/ModuleNavbar.vue +++ b/web/src/management/pages/edit/components/ModuleNavbar.vue @@ -8,7 +8,14 @@
- + + + @@ -34,6 +41,12 @@ const store = useStore() const title = computed(() => _get(store.state, 'edit.schema.metaData.title')) + diff --git a/web/src/management/pages/analysis/components/ImagePreview.vue b/web/src/management/pages/analysis/components/ImagePreview.vue new file mode 100644 index 00000000..5b160210 --- /dev/null +++ b/web/src/management/pages/analysis/components/ImagePreview.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/web/src/management/pages/edit/components/MaterialGroup.vue b/web/src/management/pages/edit/components/MaterialGroup.vue index f59932ab..fe6f2886 100644 --- a/web/src/management/pages/edit/components/MaterialGroup.vue +++ b/web/src/management/pages/edit/components/MaterialGroup.vue @@ -3,6 +3,7 @@ v-model="renderData" handle=".question-wrapper.isSelected" filter=".question-wrapper.isSelected .question.isSelected" + :preventOnFilter="false" :group="DND_GROUP" :onEnd="checkEnd" :move="checkMove" diff --git a/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue b/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue index 01065654..ae98af75 100644 --- a/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue +++ b/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue @@ -57,7 +57,7 @@ import { computed, inject, ref, type ComputedRef } from 'vue' import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild' import { CHOICES } from '@/common/typeEnum' -import { cleanRichText } from '@/common/xss' +import { cleanRichTextWithMediaTag } from '@/common/xss' const renderData = inject>>('renderData') || ref([]) const props = defineProps({ index: { @@ -88,7 +88,7 @@ const fieldList = computed(() => { .filter((question: any) => CHOICES.includes(question.type)) .map((item: any) => { return { - label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`, + label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichTextWithMediaTag(item.title)}`, value: item.field } }) @@ -102,7 +102,7 @@ const getRelyOptions = computed(() => { return ( currentQuestion?.options.map((item: any) => { return { - label: cleanRichText(item.text), + label: cleanRichTextWithMediaTag(item.text), value: item.hash } }) || [] diff --git a/web/src/management/styles/common.scss b/web/src/management/styles/common.scss new file mode 100644 index 00000000..1ad8d701 --- /dev/null +++ b/web/src/management/styles/common.scss @@ -0,0 +1,8 @@ +// 富文本标题、选项中的预览弹窗的图片宽度 +.el-popover { + p { + img { + max-width: 100%; + } + } + } \ No newline at end of file diff --git a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue index 8025a528..0fa44cf9 100644 --- a/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue +++ b/web/src/materials/questions/widgets/EditOptions/Options/OptionEdit.vue @@ -12,6 +12,7 @@
diff --git a/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx b/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx index 598c9b00..dd4ecf5a 100644 --- a/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx +++ b/web/src/materials/questions/widgets/TitleModules/EditTitle/index.jsx @@ -69,6 +69,7 @@ export default defineComponent({ { editor?.focus() diff --git a/web/src/render/App.vue b/web/src/render/App.vue index c4f914ea..acafc152 100644 --- a/web/src/render/App.vue +++ b/web/src/render/App.vue @@ -1,7 +1,5 @@ @@ -140,12 +137,14 @@ const onPreviewImage = (e) => { overflow: hidden; /* 超出部分隐藏 */ text-overflow: ellipsis; /* 显示省略号 */ :deep(img) { - height: 23px; - width: auto; + height: 23px !important; + width: auto !important; + object-fit: cover; + margin-left: 5px; } :deep(p) { - display: flex; - align-items: center; + display: flex; + align-items: center; } } } @@ -153,8 +152,3 @@ const onPreviewImage = (e) => { font-size: 13px; } - diff --git a/web/src/management/pages/analysis/components/ImagePreview.vue b/web/src/management/pages/analysis/components/ImagePreview.vue index 5b160210..3e0d99ce 100644 --- a/web/src/management/pages/analysis/components/ImagePreview.vue +++ b/web/src/management/pages/analysis/components/ImagePreview.vue @@ -1,72 +1,70 @@ \ No newline at end of file + diff --git a/web/src/management/pages/edit/pages/skin/index.vue b/web/src/management/pages/edit/pages/skin/index.vue index 24085c1f..8fa30e22 100644 --- a/web/src/management/pages/edit/pages/skin/index.vue +++ b/web/src/management/pages/edit/pages/skin/index.vue @@ -58,7 +58,7 @@ watch( position: absolute; top: 10px; cursor: pointer; - z-index: 9999; + z-index: 999; :deep(.el-radio-button__original-radio + .el-radio-button__inner) { font-size: 12px; height: 28px; diff --git a/web/src/management/styles/common.scss b/web/src/management/styles/common.scss index 1ad8d701..ca8e0e70 100644 --- a/web/src/management/styles/common.scss +++ b/web/src/management/styles/common.scss @@ -1,8 +1,8 @@ // 富文本标题、选项中的预览弹窗的图片宽度 .el-popover { - p { - img { - max-width: 100%; - } + p { + img { + max-width: 100%; } - } \ No newline at end of file + } +} From 6b7b3a12d84815dd0a2b718a277924e7b12c47d3 Mon Sep 17 00:00:00 2001 From: ysansan Date: Fri, 19 Jul 2024 18:55:22 +0800 Subject: [PATCH 45/82] =?UTF-8?q?feat:=20pinia=E8=BF=81=E7=A7=BB--?= =?UTF-8?q?=E9=97=AE=E5=8D=B7=E7=9B=B8=E5=85=B3=E9=81=97=E6=BC=8F=E8=A1=A5?= =?UTF-8?q?=E5=85=85=20(#355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ysansan --- web/src/render/App.vue | 10 +- web/src/render/components/MainRenderer.vue | 11 +- web/src/render/components/QuestionWrapper.vue | 8 +- web/src/render/hooks/useProgress.js | 5 +- web/src/render/hooks/useShowInput.js | 8 +- web/src/render/hooks/useShowOthers.js | 7 +- web/src/render/main.js | 2 - web/src/render/pages/ErrorPage.vue | 11 +- web/src/render/pages/IndexPage.vue | 4 +- web/src/render/pages/RenderPage.vue | 11 +- web/src/render/pages/SuccessPage.vue | 8 +- web/src/render/store/actions.js | 202 ------------------ web/src/render/store/getters.js | 25 --- web/src/render/store/index.js | 13 -- web/src/render/store/mutations.js | 50 ----- web/src/render/store/state.js | 16 -- web/src/render/stores/survey.js | 114 ++++++++++ 17 files changed, 155 insertions(+), 350 deletions(-) delete mode 100644 web/src/render/store/actions.js delete mode 100644 web/src/render/store/getters.js delete mode 100644 web/src/render/store/index.js delete mode 100644 web/src/render/store/mutations.js delete mode 100644 web/src/render/store/state.js diff --git a/web/src/render/App.vue b/web/src/render/App.vue index c4f914ea..242905c1 100644 --- a/web/src/render/App.vue +++ b/web/src/render/App.vue @@ -4,12 +4,12 @@
diff --git a/web/src/render/components/QuestionWrapper.vue b/web/src/render/components/QuestionWrapper.vue index df8e0113..e18e9280 100644 --- a/web/src/render/components/QuestionWrapper.vue +++ b/web/src/render/components/QuestionWrapper.vue @@ -13,10 +13,10 @@ import QuestionRuleContainer from '../../materials/questions/QuestionRuleContain import { useVoteMap } from '@/render/hooks/useVoteMap' import { useShowOthers } from '@/render/hooks/useShowOthers' import { useShowInput } from '@/render/hooks/useShowInput' -import store from '@/render/store' import { cloneDeep } from 'lodash-es' import { ruleEngine } from '@/render/hooks/useRuleEngine.js' import { useQuestionStore } from '../stores/question' +import { useSurveyStore } from '../stores/survey' import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts' @@ -34,9 +34,10 @@ const props = defineProps({ }) const emit = defineEmits(['change']) const questionStore = useQuestionStore() +const surveyStore = useSurveyStore() const formValues = computed(() => { - return store.state.formValues + return surveyStore.formValues }) const questionConfig = computed(() => { let moduleConfig = props.moduleConfig @@ -98,7 +99,8 @@ watch( key: field, value: value } - store.commit('changeFormData', data) + // store.commit('changeFormData', data) + surveyStore.changeData(data) } } ) diff --git a/web/src/render/hooks/useProgress.js b/web/src/render/hooks/useProgress.js index 731b0a48..b76981cb 100644 --- a/web/src/render/hooks/useProgress.js +++ b/web/src/render/hooks/useProgress.js @@ -1,6 +1,7 @@ -import store from '../store/index' +import { useSurveyStore } from '../stores/survey' import { computed } from 'vue' export const useProgressBar = () => { + const surveyStore = useSurveyStore() const isVariableEmpty = (variable) => { if (variable === undefined || variable === null) { return true @@ -22,7 +23,7 @@ export const useProgressBar = () => { fillCount: 0, topicCount: 0 } - const formValues = store.state.formValues + const formValues = surveyStore.formValues for (let key in formValues) { if (key.split('_').length > 1) continue diff --git a/web/src/render/hooks/useShowInput.js b/web/src/render/hooks/useShowInput.js index 66ee9018..b4982c4f 100644 --- a/web/src/render/hooks/useShowInput.js +++ b/web/src/render/hooks/useShowInput.js @@ -1,9 +1,10 @@ -import store from '../store/index' import { useQuestionStore } from '../stores/question' +import { useSurveyStore } from '../stores/survey' export const useShowInput = (questionKey) => { const questionStore = useQuestionStore() - const formValues = store.state.formValues + const surveyStore = useSurveyStore() + const formValues = surveyStore.formValues const questionVal = formValues[questionKey] let rangeConfig = questionStore.questionData[questionKey].rangeConfig let othersValue = {} @@ -21,7 +22,8 @@ export const useShowInput = (questionKey) => { key: rangeKey, value: '' } - store.commit('changeFormData', data) + + surveyStore.changeData(data) } } } diff --git a/web/src/render/hooks/useShowOthers.js b/web/src/render/hooks/useShowOthers.js index c405eae4..df5a8b9e 100644 --- a/web/src/render/hooks/useShowOthers.js +++ b/web/src/render/hooks/useShowOthers.js @@ -1,9 +1,10 @@ -import store from '../store/index' import { useQuestionStore } from '../stores/question' +import { useSurveyStore } from '../stores/survey' export const useShowOthers = (questionKey) => { const questionStore = useQuestionStore() - const formValues = store.state.formValues + const surveyStore = useSurveyStore() + const formValues = surveyStore.formValues const questionVal = formValues[questionKey] let othersValue = {} let options = questionStore.questionData[questionKey].options.map((optionItem) => { @@ -16,7 +17,7 @@ export const useShowOthers = (questionKey) => { key: opKey, value: '' } - store.commit('changeFormData', data) + surveyStore.changeData(data) } return { ...optionItem, diff --git a/web/src/render/main.js b/web/src/render/main.js index 6d8f5c74..ca68f229 100644 --- a/web/src/render/main.js +++ b/web/src/render/main.js @@ -2,7 +2,6 @@ import { createApp } from 'vue' import App from './App.vue' import EventBus from './utils/eventbus' import router from './router' -import store from './store' import { createPinia } from 'pinia' const app = createApp(App) @@ -12,7 +11,6 @@ const $bus = new EventBus() app.provide('$bus', $bus) // 挂载到this上 app.config.globalProperties.$bus = $bus -app.use(store) app.use(pinia) app.use(router) diff --git a/web/src/render/pages/ErrorPage.vue b/web/src/render/pages/ErrorPage.vue index 47dfac44..28ac2d80 100755 --- a/web/src/render/pages/ErrorPage.vue +++ b/web/src/render/pages/ErrorPage.vue @@ -11,20 +11,18 @@ \ No newline at end of file diff --git a/web/src/materials/setters/widgets/InputWordLimit.vue b/web/src/materials/setters/widgets/InputWordLimit.vue new file mode 100644 index 00000000..fd08461e --- /dev/null +++ b/web/src/materials/setters/widgets/InputWordLimit.vue @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/web/src/materials/setters/widgets/SwitchInput.vue b/web/src/materials/setters/widgets/SwitchInput.vue new file mode 100644 index 00000000..fe954eeb --- /dev/null +++ b/web/src/materials/setters/widgets/SwitchInput.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/web/src/materials/setters/widgets/teamMemberList.vue b/web/src/materials/setters/widgets/teamMemberList.vue new file mode 100644 index 00000000..552bd4d9 --- /dev/null +++ b/web/src/materials/setters/widgets/teamMemberList.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/web/src/materials/setters/widgets/whiteList.vue b/web/src/materials/setters/widgets/whiteList.vue new file mode 100644 index 00000000..46cfcbca --- /dev/null +++ b/web/src/materials/setters/widgets/whiteList.vue @@ -0,0 +1,145 @@ + + + \ No newline at end of file diff --git a/web/src/render/adapter/rules.js b/web/src/render/adapter/rules.js index 15e848bc..8c0ec353 100644 --- a/web/src/render/adapter/rules.js +++ b/web/src/render/adapter/rules.js @@ -6,18 +6,7 @@ import { set as _set } from 'lodash-es' import { INPUT, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts' - -const regexpMap = { - nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/, - m: /^[1]([3-9])[0-9]{9}$/, - idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/, - strictIdcard: - /(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/, - n: /^[0-9]+([.]{1}[0-9]+){0,1}$/, - e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/, - licensePlate: - /^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/ -} +import { regexpMap } from '@/common/regexpMap.ts' const msgMap = { '*': '必填', diff --git a/web/src/render/api/survey.js b/web/src/render/api/survey.js index 5b6824f3..988da894 100644 --- a/web/src/render/api/survey.js +++ b/web/src/render/api/survey.js @@ -32,3 +32,10 @@ export const queryVote = ({ surveyPath, fieldList }) => { export const getEncryptInfo = () => { return axios.get('/clientEncrypt/getEncryptInfo') } + +export const validate = ({ surveyPath,password, whitelist }) => { + return axios.post(`/responseSchema/${surveyPath}/validate`, { + password, + whitelist + }) +} \ No newline at end of file diff --git a/web/src/render/components/VerifyWhiteDialog.vue b/web/src/render/components/VerifyWhiteDialog.vue new file mode 100644 index 00000000..42770742 --- /dev/null +++ b/web/src/render/components/VerifyWhiteDialog.vue @@ -0,0 +1,138 @@ + + + diff --git a/web/src/render/pages/RenderPage.vue b/web/src/render/pages/RenderPage.vue index 30cf4797..8ac7e4ae 100644 --- a/web/src/render/pages/RenderPage.vue +++ b/web/src/render/pages/RenderPage.vue @@ -63,6 +63,7 @@ const renderData = computed(() => store.getters.renderData) const submitConf = computed(() => store.state?.submitConf || {}) const logoConf = computed(() => store.state?.bottomConf || {}) const surveyPath = computed(() => store.state?.surveyPath || '') +const whiteData = computed(() => store.state?.whiteData || {}) const validate = (cbk: (v: boolean) => void) => { const index = 0 @@ -78,7 +79,8 @@ const normalizationRequestBody = () => { surveyPath: surveyPath.value, data: JSON.stringify(formValues), difTime: Date.now() - enterTime, - clientTime: Date.now() + clientTime: Date.now(), + ...whiteData.value } if (encryptInfo?.encryptType) { diff --git a/web/src/render/store/mutations.js b/web/src/render/store/mutations.js index 4ada3d86..39585610 100644 --- a/web/src/render/store/mutations.js +++ b/web/src/render/store/mutations.js @@ -49,5 +49,8 @@ export default { }, setRuleEgine(state, ruleEngine) { state.ruleEngine = ruleEngine + }, + setWhiteData(state, data) { + state.whiteData = data } } diff --git a/web/src/render/store/state.js b/web/src/render/store/state.js index 2a63c735..11ba0ef0 100644 --- a/web/src/render/store/state.js +++ b/web/src/render/store/state.js @@ -12,5 +12,8 @@ export default { questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]] voteMap: {}, encryptInfo: null, - ruleEngine: null + ruleEngine: null, + whiteData: { + + } } From ba418c5cd7c373febe7f7c6b50e326d6af10dbc8 Mon Sep 17 00:00:00 2001 From: Stahsf <30379566+50431040@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:11:19 +0800 Subject: [PATCH 47/82] =?UTF-8?q?feat:=20=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD-=E6=9C=8D=E5=8A=A1=E7=AB=AF=20(#357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/enums/exceptionCode.ts | 1 + server/src/interfaces/survey.ts | 29 +++ .../survey/controllers/survey.controller.ts | 11 ++ .../__test/responseSchema.controller.spec.ts | 181 +++++++++++++++++- .../__test/surveyResponse.controller.spec.ts | 42 ++++ .../controllers/responseSchema.controller.ts | 93 ++++++++- .../controllers/surveyResponse.controller.ts | 63 +++++- .../surveyResponse/surveyResponse.module.ts | 4 + .../_test/workspace.controller.spec.ts | 29 +++ .../workspace/_test/workspace.service.spec.ts | 21 ++ .../_test/workspaceMember.service.spec.ts | 17 ++ .../controllers/workspace.controller.ts | 40 ++++ .../workspace/services/workspace.service.ts | 23 +++ .../services/workspaceMember.service.ts | 15 ++ 14 files changed, 564 insertions(+), 5 deletions(-) diff --git a/server/src/enums/exceptionCode.ts b/server/src/enums/exceptionCode.ts index 30d943ba..85cffaa7 100644 --- a/server/src/enums/exceptionCode.ts +++ b/server/src/enums/exceptionCode.ts @@ -12,6 +12,7 @@ export enum EXCEPTION_CODE { SURVEY_NOT_FOUND = 3004, // 问卷不存在 SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容 CAPTCHA_INCORRECT = 4001, // 验证码不正确 + WHITELIST_ERROR = 4002, // 白名单校验错误 RESPONSE_SIGN_ERROR = 9001, // 签名不正确 RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交 diff --git a/server/src/interfaces/survey.ts b/server/src/interfaces/survey.ts index 30403597..1e68afcc 100644 --- a/server/src/interfaces/survey.ts +++ b/server/src/interfaces/survey.ts @@ -94,6 +94,23 @@ export interface SubmitConf { msgContent: MsgContent; } +// 白名单类型 +export enum WhitelistType { + ALL = 'ALL', + // 空间成员 + MEMBER = 'MEMBER', + // 自定义 + CUSTOM = 'CUSTOM', +} + +// 白名单用户类型 +export enum MemberType { + // 手机号 + MOBILE = 'MOBILE', + // 邮箱 + EMAIL = 'EMAIL', +} + export interface BaseConf { begTime: string; endTime: string; @@ -101,6 +118,18 @@ export interface BaseConf { answerEndTime: string; tLimit: number; language: string; + // 访问密码开关 + passwordSwitch?: boolean; + // 密码 + password?: string | null; + // 白名单类型 + whitelistType?: WhitelistType; + // 白名单用户类型 + memberType?: MemberType; + // 白名单列表 + whitelist?: string[]; + // 提示语 + whitelistTip?: string; } export interface SkinConf { diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index df1a3d62..6976b5c6 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -31,6 +31,7 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; import { WorkspaceGuard } from 'src/guards/workspace.guard'; import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace'; +import { MemberType, WhitelistType } from 'src/interfaces/survey'; @ApiTags('survey') @Controller('/api/survey') @@ -214,6 +215,16 @@ export class SurveyController { surveyMeta.isCollaborated = false; } + // 白名单相关字段的默认值 + const baseConf = surveyConf.code?.baseConf; + if (baseConf) { + baseConf.passwordSwitch = baseConf.passwordSwitch ?? false; + baseConf.password = baseConf.password ?? ''; + baseConf.whitelistType = baseConf.whitelistType ?? WhitelistType.ALL; + baseConf.whitelist = baseConf.whitelist ?? []; + baseConf.memberType = baseConf.memberType ?? MemberType.MOBILE; + } + return { code: 200, data: { diff --git a/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts b/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts index bb1395e9..497580df 100644 --- a/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts @@ -6,6 +6,11 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; import { ResponseSchema } from 'src/models/responseSchema.entity'; +import { Logger } from 'src/logger'; +import { UserService } from 'src/modules/auth/services/user.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; +import { AuthService } from 'src/modules/auth/services/auth.service'; +import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; jest.mock('../services/responseScheme.service'); @@ -16,7 +21,40 @@ describe('ResponseSchemaController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ResponseSchemaController], - providers: [ResponseSchemaService], + providers: [ + ResponseSchemaService, + AuthService, + { + provide: Logger, + useValue: { + info: jest.fn(), + }, + }, + { + provide: UserService, + useValue: { + getUserByUsername: jest.fn(), + }, + }, + { + provide: WorkspaceMemberService, + useValue: { + findAllByUserId: jest.fn(), + }, + }, + { + provide: AuthService, + useValue: { + create: jest.fn(), + }, + }, + { + provide: Logger, + useValue: { + error: jest.fn(), + }, + }, + ], }).compile(); controller = module.get(ResponseSchemaController); @@ -66,5 +104,146 @@ describe('ResponseSchemaController', () => { new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED), ); }); + + it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue(null); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + }), + ).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交')); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123457', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + + it('whitelistValidate should be successfully', async () => { + const surveyPath = 'test'; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + }), + ).resolves.toEqual({ code: 200, data: null }); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'CUSTOM', + memberType: 'MOBILE', + whitelist: ['13500000000'], + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: '13500000001', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'MEMBER', + whitelist: ['Jack'], + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: 'James', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + }); + + it('whitelistValidate should return verifyId successfully', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'CUSTOM', + memberType: 'MOBILE', + whitelist: ['13500000000'], + }, + }, + } as ResponseSchema); + + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: '13500000000', + }), + ).resolves.toEqual({ code: 200, data: null }); }); }); diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts index 5cb5a950..7ba95267 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts @@ -21,6 +21,10 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi import { RECORD_STATUS } from 'src/enums'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { Logger } from 'src/logger'; +import { ResponseSchema } from 'src/models/responseSchema.entity'; +import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +import { UserService } from 'src/modules/auth/services/user.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; const mockDecryptErrorBody = { surveyPath: 'EBzdmnSp', @@ -124,6 +128,18 @@ describe('SurveyResponseController', () => { info: jest.fn(), }, }, + { + provide: UserService, + useValue: { + getUserByUsername: jest.fn(), + }, + }, + { + provide: WorkspaceMemberService, + useValue: { + findAllByUserId: jest.fn(), + }, + }, ], }).compile(); @@ -306,5 +322,31 @@ describe('SurveyResponseController', () => { HttpException, ); }); + + it('should throw HttpException if password does not match', async () => { + const reqBody = { + ...mockSubmitData, + password: '123457', + sign: '4ff02062141d92d80629eae4797ba68056f29a9709cdf59bf206776fc0971c1a.1710400229589', + }; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValueOnce({ + curStatus: { + status: RECORD_STATUS.PUBLISHED, + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + + await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); }); }); diff --git a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts index 30fd2055..8df092bf 100644 --- a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts +++ b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts @@ -1,14 +1,33 @@ -import { Controller, Get, HttpCode, Query } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Query, +} from '@nestjs/common'; import { ResponseSchemaService } from '../services/responseScheme.service'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; import { ApiTags } from '@nestjs/swagger'; +import Joi from 'joi'; +import { Logger } from 'src/logger'; +import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; +import { WhitelistType } from 'src/interfaces/survey'; +import { UserService } from 'src/modules/auth/services/user.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; @ApiTags('surveyResponse') @Controller('/api/responseSchema') export class ResponseSchemaController { - constructor(private readonly responseSchemaService: ResponseSchemaService) {} + constructor( + private readonly responseSchemaService: ResponseSchemaService, + private readonly logger: Logger, + private readonly userService: UserService, + private readonly workspaceMemberService: WorkspaceMemberService, + ) {} @Get('/getSchema') @HttpCode(200) @@ -34,9 +53,79 @@ export class ResponseSchemaController { EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED, ); } + + // 去掉C端的敏感字段 + if (responseSchema.code?.baseConf) { + responseSchema.code.baseConf.password = null; + responseSchema.code.baseConf.whitelist = []; + } return { code: 200, data: responseSchema, }; } + + // 白名单验证 + @Post('/:surveyPath/validate') + @HttpCode(200) + async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) { + const { value, error } = Joi.object({ + password: Joi.string().allow(null, ''), + whitelist: Joi.string().allow(null, ''), + }).validate(body, { allowUnknown: true }); + + if (error) { + this.logger.error(`whitelistValidate error: ${error.message}`, {}); + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + + // 问卷信息 + const schema = + await this.responseSchemaService.getResponseSchemaByPath(surveyPath); + if (!schema || schema.curStatus.status === 'removed') { + throw new SurveyNotFoundException('该问卷不存在,无法提交'); + } + + const { password, whitelist: whitelistValue } = value; + const { + passwordSwitch, + password: settingPassword, + whitelistType, + whitelist, + } = schema.code.baseConf; + + // 密码校验 + if (passwordSwitch) { + if (settingPassword !== password) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + // 名单校验(手机号/邮箱) + if (whitelistType === WhitelistType.CUSTOM) { + if (!whitelist.includes(whitelistValue)) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + // 团队成员昵称校验 + if (whitelistType === WhitelistType.MEMBER) { + const user = await this.userService.getUserByUsername(whitelistValue); + if (!user) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + + const workspaceMember = await this.workspaceMemberService.findAllByUserId( + { userId: user._id.toString() }, + ); + if (!workspaceMember.length) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + return { + code: 200, + data: null, + }; + } } diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 9f3bbb61..bdf493f3 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -17,6 +17,9 @@ import * as Joi from 'joi'; import * as forge from 'node-forge'; import { ApiTags } from '@nestjs/swagger'; import { Logger } from 'src/logger'; +import { WhitelistType } from 'src/interfaces/survey'; +import { UserService } from 'src/modules/auth/services/user.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; @ApiTags('surveyResponse') @Controller('/api/surveyResponse') @@ -28,6 +31,8 @@ export class SurveyResponseController { private readonly clientEncryptService: ClientEncryptService, private readonly messagePushingTaskService: MessagePushingTaskService, private readonly logger: Logger, + private readonly userService: UserService, + private readonly workspaceMemberService: WorkspaceMemberService, ) {} @Post('/createResponse') @@ -43,6 +48,8 @@ export class SurveyResponseController { sessionId: Joi.string(), clientTime: Joi.number().required(), difTime: Joi.number(), + password: Joi.string().allow(null, ''), + whitelist: Joi.string().allow(null, ''), }).validate(reqBody, { allowUnknown: true }); if (error) { @@ -52,8 +59,16 @@ export class SurveyResponseController { throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } - const { surveyPath, encryptType, data, sessionId, clientTime, difTime } = - value; + const { + surveyPath, + encryptType, + data, + sessionId, + clientTime, + difTime, + password, + whitelist: whitelistValue, + } = value; // 查询schema const responseSchema = @@ -62,6 +77,50 @@ export class SurveyResponseController { throw new SurveyNotFoundException('该问卷不存在,无法提交'); } + // 白名单的verifyId校验 + const baseConf = responseSchema.code.baseConf; + + // 密码校验 + if (baseConf?.passwordSwitch && baseConf.password) { + if (baseConf.password !== password) { + throw new HttpException( + '白名单验证失败', + EXCEPTION_CODE.WHITELIST_ERROR, + ); + } + } + + // 名单校验(手机号/邮箱) + if (baseConf?.whitelistType === WhitelistType.CUSTOM) { + if (!baseConf.whitelist.includes(whitelistValue)) { + throw new HttpException( + '白名单验证失败', + EXCEPTION_CODE.WHITELIST_ERROR, + ); + } + } + + // 团队成员昵称校验 + if (baseConf?.whitelistType === WhitelistType.MEMBER) { + const user = await this.userService.getUserByUsername(whitelistValue); + if (!user) { + throw new HttpException( + '白名单验证失败', + EXCEPTION_CODE.WHITELIST_ERROR, + ); + } + + const workspaceMember = await this.workspaceMemberService.findAllByUserId( + { userId: user._id.toString() }, + ); + if (!workspaceMember.length) { + throw new HttpException( + '白名单验证失败', + EXCEPTION_CODE.WHITELIST_ERROR, + ); + } + } + const now = Date.now(); // 提交时间限制 const begTime = responseSchema.code?.baseConf?.begTime || 0; diff --git a/server/src/modules/surveyResponse/surveyResponse.module.ts b/server/src/modules/surveyResponse/surveyResponse.module.ts index 6f9b6fc9..faf1a86d 100644 --- a/server/src/modules/surveyResponse/surveyResponse.module.ts +++ b/server/src/modules/surveyResponse/surveyResponse.module.ts @@ -20,6 +20,8 @@ import { CounterController } from './controllers/counter.controller'; import { ResponseSchemaController } from './controllers/responseSchema.controller'; import { SurveyResponseController } from './controllers/surveyResponse.controller'; import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller'; +import { AuthModule } from '../auth/auth.module'; +import { WorkspaceModule } from '../workspace/workspace.module'; @Module({ imports: [ @@ -31,6 +33,8 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr ]), ConfigModule, MessageModule, + AuthModule, + WorkspaceModule, ], controllers: [ ClientEncryptController, diff --git a/server/src/modules/workspace/_test/workspace.controller.spec.ts b/server/src/modules/workspace/_test/workspace.controller.spec.ts index e34409fb..73d84ab3 100644 --- a/server/src/modules/workspace/_test/workspace.controller.spec.ts +++ b/server/src/modules/workspace/_test/workspace.controller.spec.ts @@ -35,6 +35,7 @@ describe('WorkspaceController', () => { findAllByIdWithPagination: jest.fn(), update: jest.fn(), delete: jest.fn(), + findAllByUserId: jest.fn(), }, }, { @@ -46,6 +47,7 @@ describe('WorkspaceController', () => { batchUpdate: jest.fn(), batchDelete: jest.fn(), countByWorkspaceId: jest.fn(), + batchSearchByWorkspace: jest.fn(), }, }, { @@ -237,4 +239,31 @@ describe('WorkspaceController', () => { expect(workspaceService.delete).toHaveBeenCalledWith(id); }); }); + + describe('getWorkspaceAndMember', () => { + it('should return a list of workspaces and members for the user', async () => { + const req = { user: { _id: new ObjectId() } }; + + const workspaceId = new ObjectId(); + const memberList = [{ workspaceId, userId: new ObjectId() }]; + const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }]; + + jest + .spyOn(workspaceService, 'findAllByUserId') + .mockResolvedValue(workspaces as Array); + jest + .spyOn(workspaceMemberService, 'batchSearchByWorkspace') + .mockResolvedValue(memberList as unknown as Array); + + const result = await controller.getWorkspaceAndMember(req); + + expect(result.code).toEqual(200); + expect(workspaceService.findAllByUserId).toHaveBeenCalledWith( + req.user._id.toString(), + ); + expect( + workspaceMemberService.batchSearchByWorkspace, + ).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString())); + }); + }); }); diff --git a/server/src/modules/workspace/_test/workspace.service.spec.ts b/server/src/modules/workspace/_test/workspace.service.spec.ts index 1eb12233..a243dec6 100644 --- a/server/src/modules/workspace/_test/workspace.service.spec.ts +++ b/server/src/modules/workspace/_test/workspace.service.spec.ts @@ -123,4 +123,25 @@ describe('WorkspaceService', () => { expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1); }); }); + + describe('findAllByUserId', () => { + it('should return all workspaces under a user', async () => { + const workspaceIdList = [ + new ObjectId().toString(), + new ObjectId().toString(), + ]; + const workspaces = [ + { _id: workspaceIdList[0], name: 'Workspace 1' }, + { _id: workspaceIdList[1], name: 'Workspace 2' }, + ]; + + jest + .spyOn(workspaceRepository, 'find') + .mockResolvedValue(workspaces as any); + + const result = await service.findAllByUserId(''); + expect(result).toEqual(workspaces); + expect(workspaceRepository.find).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/modules/workspace/_test/workspaceMember.service.spec.ts b/server/src/modules/workspace/_test/workspaceMember.service.spec.ts index d115c3fe..8770bf20 100644 --- a/server/src/modules/workspace/_test/workspaceMember.service.spec.ts +++ b/server/src/modules/workspace/_test/workspaceMember.service.spec.ts @@ -193,4 +193,21 @@ describe('WorkspaceMemberService', () => { }); }); }); + + describe('batchSearchByWorkspace', () => { + it('should return all workspace members by workspace id list', async () => { + const workspaceList = ['workspaceId1', 'workspaceId2']; + const members = [ + { userId: 'userId1', workspaceId: workspaceList[0] }, + { userId: 'userId2', workspaceId: workspaceList[1] }, + ]; + + jest.spyOn(repository, 'find').mockResolvedValue(members as any); + + const result = await service.batchSearchByWorkspace(workspaceList); + + expect(result).toEqual(members); + expect(repository.find).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/modules/workspace/controllers/workspace.controller.ts b/server/src/modules/workspace/controllers/workspace.controller.ts index 87669803..b1985490 100644 --- a/server/src/modules/workspace/controllers/workspace.controller.ts +++ b/server/src/modules/workspace/controllers/workspace.controller.ts @@ -33,6 +33,8 @@ import { UserService } from 'src/modules/auth/services/user.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { Logger } from 'src/logger'; import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto'; +import { WorkspaceMember } from 'src/models/workspaceMember.entity'; +import { Workspace } from 'src/models/workspace.entity'; @ApiTags('workspace') @ApiBearerAuth() @@ -350,4 +352,42 @@ export class WorkspaceController { code: 200, }; } + + @Get('/member/list') + @HttpCode(200) + async getWorkspaceAndMember(@Request() req) { + const userId = req.user._id.toString(); + + // 所在所有空间 + const workspaceList = await this.workspaceService.findAllByUserId(userId); + if (!workspaceList.length) { + return { + code: 200, + data: [], + }; + } + + // 所有空间下的所有成员 + const workspaceMemberList = + await this.workspaceMemberService.batchSearchByWorkspace( + workspaceList.map((item) => item._id.toString()), + ); + + const temp: Record = {}; + const list = workspaceList.map( + (item: Workspace & { members: WorkspaceMember[] }) => { + temp[item._id.toString()] = item.members = []; + return item; + }, + ); + + workspaceMemberList.forEach((member) => { + temp[member.workspaceId.toString()].push(member); + }); + + return { + code: 200, + data: list, + }; + } } diff --git a/server/src/modules/workspace/services/workspace.service.ts b/server/src/modules/workspace/services/workspace.service.ts index 7ceeec6d..fb097a7a 100644 --- a/server/src/modules/workspace/services/workspace.service.ts +++ b/server/src/modules/workspace/services/workspace.service.ts @@ -149,4 +149,27 @@ export class WorkspaceService { surveyRes, }; } + + // 用户下的所有空间 + async findAllByUserId(userId: string) { + return await this.workspaceRepository.find({ + where: { + ownerId: userId, + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }, + order: { + _id: -1, + }, + select: [ + '_id', + 'curStatus', + 'name', + 'description', + 'ownerId', + 'createDate', + ], + }); + } } diff --git a/server/src/modules/workspace/services/workspaceMember.service.ts b/server/src/modules/workspace/services/workspaceMember.service.ts index f4535f75..baf9f8b2 100644 --- a/server/src/modules/workspace/services/workspaceMember.service.ts +++ b/server/src/modules/workspace/services/workspaceMember.service.ts @@ -140,4 +140,19 @@ export class WorkspaceMemberService { }, }); } + + // 根据空间id批量查询成员 + async batchSearchByWorkspace(workspaceList: string[]) { + return await this.workspaceMemberRepository.find({ + where: { + workspaceId: { + $in: workspaceList, + }, + }, + order: { + _id: -1, + }, + select: ['_id', 'userId', 'username', 'role', 'workspaceId'], + }); + } } From 9afd1d1c7c4d9c71df062b63359b7f98a614ea63 Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 22 Jul 2024 17:18:37 +0800 Subject: [PATCH 48/82] =?UTF-8?q?fix:=20render=E9=94=99=E8=AF=AF=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=B8=8D=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/questionModule/PreviewPanel.vue | 48 ++++++++++--------- web/src/render/pages/ErrorPage.vue | 26 +++++----- web/src/render/stores/errorInfo.js | 6 +-- web/src/render/stores/survey.js | 6 ++- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue index d9640982..a8ca36a2 100644 --- a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue @@ -30,7 +30,7 @@ diff --git a/web/src/management/pages/edit/modules/settingModule/components/SuccessContent.vue b/web/src/management/pages/edit/modules/settingModule/components/SuccessContent.vue deleted file mode 100644 index 284b5e74..00000000 --- a/web/src/management/pages/edit/modules/settingModule/components/SuccessContent.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/web/src/management/pages/edit/modules/settingModule/config/statusConfig.js b/web/src/management/pages/edit/modules/settingModule/config/statusConfig.js deleted file mode 100644 index 520732a0..00000000 --- a/web/src/management/pages/edit/modules/settingModule/config/statusConfig.js +++ /dev/null @@ -1,26 +0,0 @@ -export default { - Success: [ - { - label: '提示文案', - type: 'RichText', - key: 'msgContent.msg_200', - placeholder: '提交成功', - value: '提交成功', - labelStyle: { - 'font-weight': 'bold' - } - } - ], - OverTime: [ - { - label: '提示文案', - type: 'RichText', - key: 'msgContent.msg_9001', - placeholder: '问卷已过期', - value: '问卷已过期', - labelStyle: { - 'font-weight': 'bold' - } - } - ] -} diff --git a/web/src/management/pages/edit/modules/settingModule/result/CatalogPanel.vue b/web/src/management/pages/edit/modules/settingModule/result/CatalogPanel.vue deleted file mode 100644 index 4873709b..00000000 --- a/web/src/management/pages/edit/modules/settingModule/result/CatalogPanel.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - diff --git a/web/src/management/pages/edit/modules/settingModule/result/PreviewPanel.vue b/web/src/management/pages/edit/modules/settingModule/result/PreviewPanel.vue deleted file mode 100644 index e1d35a93..00000000 --- a/web/src/management/pages/edit/modules/settingModule/result/PreviewPanel.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/web/src/management/pages/edit/modules/settingModule/result/SetterPanel.vue b/web/src/management/pages/edit/modules/settingModule/result/SetterPanel.vue deleted file mode 100644 index 28c74093..00000000 --- a/web/src/management/pages/edit/modules/settingModule/result/SetterPanel.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue b/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue deleted file mode 100644 index 2a6be08d..00000000 --- a/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - diff --git a/web/src/management/pages/edit/pages/skin/ContentPage.vue b/web/src/management/pages/edit/pages/skin/ContentPage.vue index 5960d677..f298ea12 100644 --- a/web/src/management/pages/edit/pages/skin/ContentPage.vue +++ b/web/src/management/pages/edit/pages/skin/ContentPage.vue @@ -16,9 +16,9 @@ import { onMounted } from 'vue' import { useStore } from 'vuex' import CommonTemplate from '../../components/CommonTemplate.vue' -import CatalogPanel from '../../modules/settingModule/skin/CatalogPanel.vue' -import PreviewPanel from '../../modules/settingModule/skin/PreviewPanel.vue' -import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue' +import CatalogPanel from '../../modules/skinModule/CatalogPanel.vue' +import PreviewPanel from '../../modules/skinModule/PreviewPanel.vue' +import SetterPanel from '../../modules/skinModule/SetterPanel.vue' const store = useStore() diff --git a/web/src/management/pages/edit/pages/skin/ResultPage.vue b/web/src/management/pages/edit/pages/skin/ResultPage.vue index fbaa46b0..be199083 100644 --- a/web/src/management/pages/edit/pages/skin/ResultPage.vue +++ b/web/src/management/pages/edit/pages/skin/ResultPage.vue @@ -13,7 +13,7 @@ diff --git a/web/src/materials/setters/widgets/AnswerRadio.vue b/web/src/materials/setters/widgets/AnswerRadio.vue deleted file mode 100644 index 8bc8cc33..00000000 --- a/web/src/materials/setters/widgets/AnswerRadio.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - \ No newline at end of file From b494bd617478a392347ea236e1f292cfb7426608 Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 22 Jul 2024 20:27:12 +0800 Subject: [PATCH 50/82] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E9=A1=B5=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/edit/components/ModuleNavbar.vue | 2 + .../modules/contentModule/PublishPanel.vue | 6 - .../edit/modules/contentModule/SavePanel.vue | 2 - .../modules/resultModule/CatalogPanel.vue | 98 +++++++++++ .../modules/resultModule/PreviewPanel.vue | 62 +++++++ .../edit/modules/resultModule/SetterPanel.vue | 120 ++++++++++++++ .../resultModule/components/OverTime.vue | 34 ++++ .../components/SuccessContent.vue | 56 +++++++ .../pages/edit/modules/resultModule/enum.js | 4 + .../components/TeamMemberList.vue | 101 ++++++++++++ .../settingModule/components/WhiteList.vue | 155 ++++++++++++++++++ .../skin => skinModule}/CatalogPanel.vue | 7 +- .../skin => skinModule}/PreviewPanel.vue | 10 +- .../edit/modules/skinModule/SetterPanel.vue | 85 ++++++++++ .../pages/edit/setterConfig/bannerConfig.js | 35 ++++ .../config => setterConfig}/baseConfig.js | 5 +- .../config => setterConfig}/baseFormConfig.js | 37 +++-- .../pages/edit/setterConfig/logoConfig.js | 16 ++ .../pages/edit/setterConfig/skinConfig.js | 48 ++++++ .../pages/edit/setterConfig/statusConfig.js | 26 +++ .../pages/edit/setterConfig/submitConfig.js | 62 +++++++ web/src/management/store/edit/getters.js | 2 +- 22 files changed, 940 insertions(+), 33 deletions(-) create mode 100644 web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue create mode 100644 web/src/management/pages/edit/modules/resultModule/PreviewPanel.vue create mode 100644 web/src/management/pages/edit/modules/resultModule/SetterPanel.vue create mode 100755 web/src/management/pages/edit/modules/resultModule/components/OverTime.vue create mode 100644 web/src/management/pages/edit/modules/resultModule/components/SuccessContent.vue create mode 100644 web/src/management/pages/edit/modules/resultModule/enum.js create mode 100644 web/src/management/pages/edit/modules/settingModule/components/TeamMemberList.vue create mode 100644 web/src/management/pages/edit/modules/settingModule/components/WhiteList.vue rename web/src/management/pages/edit/modules/{settingModule/skin => skinModule}/CatalogPanel.vue (96%) rename web/src/management/pages/edit/modules/{settingModule/skin => skinModule}/PreviewPanel.vue (96%) create mode 100644 web/src/management/pages/edit/modules/skinModule/SetterPanel.vue create mode 100644 web/src/management/pages/edit/setterConfig/bannerConfig.js rename web/src/management/pages/edit/{modules/settingModule/config => setterConfig}/baseConfig.js (97%) rename web/src/management/pages/edit/{modules/settingModule/config => setterConfig}/baseFormConfig.js (69%) create mode 100644 web/src/management/pages/edit/setterConfig/logoConfig.js create mode 100644 web/src/management/pages/edit/setterConfig/skinConfig.js create mode 100644 web/src/management/pages/edit/setterConfig/statusConfig.js create mode 100644 web/src/management/pages/edit/setterConfig/submitConfig.js diff --git a/web/src/management/pages/edit/components/ModuleNavbar.vue b/web/src/management/pages/edit/components/ModuleNavbar.vue index 14cdaf12..719af815 100644 --- a/web/src/management/pages/edit/components/ModuleNavbar.vue +++ b/web/src/management/pages/edit/components/ModuleNavbar.vue @@ -72,6 +72,8 @@ const updateLogicConf = () => { return res } + + return res } // 校验 - 白名单 diff --git a/web/src/management/pages/edit/modules/contentModule/PublishPanel.vue b/web/src/management/pages/edit/modules/contentModule/PublishPanel.vue index 118345a8..e8e68409 100644 --- a/web/src/management/pages/edit/modules/contentModule/PublishPanel.vue +++ b/web/src/management/pages/edit/modules/contentModule/PublishPanel.vue @@ -66,12 +66,6 @@ const handlePublish = async () => { return } - if(updateWhiteConf()){ - isPublishing.value = false - ElMessage.error('请检查问卷设置是否有误') - return - } - try { const saveRes: any = await saveSurvey(saveData) if (saveRes.code !== 200) { diff --git a/web/src/management/pages/edit/modules/contentModule/SavePanel.vue b/web/src/management/pages/edit/modules/contentModule/SavePanel.vue index 9332d06f..ae09f7c0 100644 --- a/web/src/management/pages/edit/modules/contentModule/SavePanel.vue +++ b/web/src/management/pages/edit/modules/contentModule/SavePanel.vue @@ -32,7 +32,6 @@ interface Props { const route = useRoute() const props = defineProps() - const isSaving = ref(false) const isShowAutoSave = ref(false) const autoSaveStatus = ref<'succeed' | 'saving' | 'failed'>('succeed') @@ -127,7 +126,6 @@ const handleSave = async () => { const { checked, msg } = validate() if (!checked) { isSaving.value = false - ElMessage.error('请检查问卷设置是否有误') ElMessage.error(msg) return } diff --git a/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue b/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue new file mode 100644 index 00000000..2659584c --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue @@ -0,0 +1,98 @@ + + + diff --git a/web/src/management/pages/edit/modules/resultModule/PreviewPanel.vue b/web/src/management/pages/edit/modules/resultModule/PreviewPanel.vue new file mode 100644 index 00000000..4c7ded3e --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/PreviewPanel.vue @@ -0,0 +1,62 @@ + + + diff --git a/web/src/management/pages/edit/modules/resultModule/SetterPanel.vue b/web/src/management/pages/edit/modules/resultModule/SetterPanel.vue new file mode 100644 index 00000000..6b651797 --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/SetterPanel.vue @@ -0,0 +1,120 @@ + + + diff --git a/web/src/management/pages/edit/modules/resultModule/components/OverTime.vue b/web/src/management/pages/edit/modules/resultModule/components/OverTime.vue new file mode 100755 index 00000000..95a92c25 --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/components/OverTime.vue @@ -0,0 +1,34 @@ + + + diff --git a/web/src/management/pages/edit/modules/resultModule/components/SuccessContent.vue b/web/src/management/pages/edit/modules/resultModule/components/SuccessContent.vue new file mode 100644 index 00000000..284b5e74 --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/components/SuccessContent.vue @@ -0,0 +1,56 @@ + + + diff --git a/web/src/management/pages/edit/modules/resultModule/enum.js b/web/src/management/pages/edit/modules/resultModule/enum.js new file mode 100644 index 00000000..31f86c81 --- /dev/null +++ b/web/src/management/pages/edit/modules/resultModule/enum.js @@ -0,0 +1,4 @@ +export const EDIT_STATUS_MAP = { + SUCCESS: 'Success', + OVERTIME: 'OverTime' +} diff --git a/web/src/management/pages/edit/modules/settingModule/components/TeamMemberList.vue b/web/src/management/pages/edit/modules/settingModule/components/TeamMemberList.vue new file mode 100644 index 00000000..609009eb --- /dev/null +++ b/web/src/management/pages/edit/modules/settingModule/components/TeamMemberList.vue @@ -0,0 +1,101 @@ + + + diff --git a/web/src/management/pages/edit/modules/settingModule/components/WhiteList.vue b/web/src/management/pages/edit/modules/settingModule/components/WhiteList.vue new file mode 100644 index 00000000..6db2d252 --- /dev/null +++ b/web/src/management/pages/edit/modules/settingModule/components/WhiteList.vue @@ -0,0 +1,155 @@ + + + diff --git a/web/src/management/pages/edit/modules/settingModule/skin/CatalogPanel.vue b/web/src/management/pages/edit/modules/skinModule/CatalogPanel.vue similarity index 96% rename from web/src/management/pages/edit/modules/settingModule/skin/CatalogPanel.vue rename to web/src/management/pages/edit/modules/skinModule/CatalogPanel.vue index f39d169d..7f663cd6 100644 --- a/web/src/management/pages/edit/modules/settingModule/skin/CatalogPanel.vue +++ b/web/src/management/pages/edit/modules/skinModule/CatalogPanel.vue @@ -92,10 +92,6 @@ const changePreset = (banner: any) => { border: none; overflow-y: auto; background-color: #fff; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } .title { height: 40px; line-height: 40px; @@ -112,9 +108,8 @@ const changePreset = (banner: any) => { display: flex; flex-wrap: wrap; .tag { + margin: 5px 8px; cursor: pointer; - width: 51px; - margin: 5px 2px; &.current { color: $primary-color; background-color: $primary-bg-color; diff --git a/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue b/web/src/management/pages/edit/modules/skinModule/PreviewPanel.vue similarity index 96% rename from web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue rename to web/src/management/pages/edit/modules/skinModule/PreviewPanel.vue index 64af2b95..1f8b6718 100644 --- a/web/src/management/pages/edit/modules/settingModule/skin/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/skinModule/PreviewPanel.vue @@ -13,12 +13,12 @@ :readonly="false" :is-selected="currentEditOne === 'submit'" /> +
- diff --git a/web/src/management/pages/edit/modules/skinModule/SetterPanel.vue b/web/src/management/pages/edit/modules/skinModule/SetterPanel.vue new file mode 100644 index 00000000..3918286b --- /dev/null +++ b/web/src/management/pages/edit/modules/skinModule/SetterPanel.vue @@ -0,0 +1,85 @@ + + + diff --git a/web/src/management/pages/edit/setterConfig/bannerConfig.js b/web/src/management/pages/edit/setterConfig/bannerConfig.js new file mode 100644 index 00000000..9263920e --- /dev/null +++ b/web/src/management/pages/edit/setterConfig/bannerConfig.js @@ -0,0 +1,35 @@ +export default [ + { + label: '顶部图片地址', + type: 'InputSetter', + key: 'bgImage', + labelStyle: { width: '120px' } + }, + { + label: '顶部视频地址', + type: 'InputSetter', + key: 'videoLink', + labelStyle: { width: '120px' } + }, + { + label: '视频海报地址', + type: 'InputSetter', + key: 'postImg', + labelStyle: { width: '120px' } + }, + { + label: '图片支持点击', + type: 'CustomedSwitch', + labelStyle: { width: '120px' }, + key: 'bgImageAllowJump' + }, + { + label: '跳转链接', + type: 'InputSetter', + labelStyle: { width: '120px' }, + key: 'bgImageJumpLink', + relyFunc: (data) => { + return !!data?.bgImageAllowJump + } + } +] diff --git a/web/src/management/pages/edit/modules/settingModule/config/baseConfig.js b/web/src/management/pages/edit/setterConfig/baseConfig.js similarity index 97% rename from web/src/management/pages/edit/modules/settingModule/config/baseConfig.js rename to web/src/management/pages/edit/setterConfig/baseConfig.js index 146a5a1b..5146ca37 100644 --- a/web/src/management/pages/edit/modules/settingModule/config/baseConfig.js +++ b/web/src/management/pages/edit/setterConfig/baseConfig.js @@ -8,10 +8,11 @@ export default [ title: '提交限制', key: 'limitConfig', formList: ['limit_tLimit'] - }, - { + }, { title: '作答限制', key: 'respondConfig', formList: ['interview_pwd','answer_type','white_placeholder','white_list','team_list'] } ] + + diff --git a/web/src/management/pages/edit/modules/settingModule/config/baseFormConfig.js b/web/src/management/pages/edit/setterConfig/baseFormConfig.js similarity index 69% rename from web/src/management/pages/edit/modules/settingModule/config/baseFormConfig.js rename to web/src/management/pages/edit/setterConfig/baseFormConfig.js index 58f28292..726d0c48 100644 --- a/web/src/management/pages/edit/modules/settingModule/config/baseFormConfig.js +++ b/web/src/management/pages/edit/setterConfig/baseFormConfig.js @@ -27,36 +27,51 @@ export default { label: '访问密码', type: 'SwitchInput', placeholder: '请输入6位字符串类型访问密码 ', - maxLength: 6, + maxLength: 6 }, answer_type: { key: 'baseConf.whitelistType', label: '答题名单', - type: 'AnswerRadio', + type: 'RadioGroup', + options: [ + { + label: '所有人', + value: 'ALL' + }, + { + label: '空间成员', + value: 'MEMBER' + }, + { + label: '白名单', + value: 'CUSTOM' + } + ] }, - white_placeholder:{ + white_placeholder: { key: 'baseConf.whitelistTip', label: '名单登录提示语', - placeholder:'请输入名单提示语', + placeholder: '请输入名单提示语', type: 'InputWordLimit', maxLength: 40, relyFunc: (data) => { - return ['CUSTOM','MEMBER'].includes(data.whitelistType) + return ['CUSTOM', 'MEMBER'].includes(data.whitelistType) } }, - white_list:{ - keys: ['baseConf.whitelist','baseConf.memberType'], + white_list: { + keys: ['baseConf.whitelist', 'baseConf.memberType'], label: '白名单列表', - type: 'whiteList', + type: 'WhiteList', + custom: true, // 自定义导入高级组件 relyFunc: (data) => { - return data.whitelistType == 'CUSTOM' } }, - team_list:{ + team_list: { key: 'baseConf.whitelist', label: '团队空间成员选择', - type: 'teamMemberList', + type: 'TeamMemberList', + custom: true, // 自定义导入高级组件 relyFunc: (data) => { return data.whitelistType == 'MEMBER' } diff --git a/web/src/management/pages/edit/setterConfig/logoConfig.js b/web/src/management/pages/edit/setterConfig/logoConfig.js new file mode 100644 index 00000000..9d2bff7f --- /dev/null +++ b/web/src/management/pages/edit/setterConfig/logoConfig.js @@ -0,0 +1,16 @@ +export default [ + { + label: '自定义Logo', + type: 'InputSetter', + key: 'logoImage', + tip: '默认尺寸200px*50px', + labelStyle: { width: '120px' } + }, + { + label: 'Logo大小', + type: 'InputPercent', + key: 'logoImageWidth', + tip: '填写宽度百分比,例如30%', + labelStyle: { width: '120px' } + } +] diff --git a/web/src/management/pages/edit/setterConfig/skinConfig.js b/web/src/management/pages/edit/setterConfig/skinConfig.js new file mode 100644 index 00000000..9177466d --- /dev/null +++ b/web/src/management/pages/edit/setterConfig/skinConfig.js @@ -0,0 +1,48 @@ +import bannerConfig from './bannerConfig' +import logoConfig from './logoConfig' + +export default [ + { + name: '头图', + key: 'bannerConf.bannerConfig', + formConfigList: bannerConfig + }, + { + name: '背景', + key: 'skinConf.backgroundConf', + formConfigList: [ + { + label: '背景颜色', + type: 'ColorPicker', + key: 'color' + } + ] + }, + { + name: '主题色', + key: 'skinConf.themeConf', + formConfigList: [ + { + label: '全局应用', + type: 'ColorPicker', + key: 'color' + } + ] + }, + { + key: 'skinConf.contentConf', + name: '内容区域', + formConfigList: [ + { + label: '内容透明度', + type: 'SliderSetter', + key: 'opacity' + } + ] + }, + { + name: '品牌logo', + key: 'bottomConf', + formConfigList: logoConfig + } +] diff --git a/web/src/management/pages/edit/setterConfig/statusConfig.js b/web/src/management/pages/edit/setterConfig/statusConfig.js new file mode 100644 index 00000000..520732a0 --- /dev/null +++ b/web/src/management/pages/edit/setterConfig/statusConfig.js @@ -0,0 +1,26 @@ +export default { + Success: [ + { + label: '提示文案', + type: 'RichText', + key: 'msgContent.msg_200', + placeholder: '提交成功', + value: '提交成功', + labelStyle: { + 'font-weight': 'bold' + } + } + ], + OverTime: [ + { + label: '提示文案', + type: 'RichText', + key: 'msgContent.msg_9001', + placeholder: '问卷已过期', + value: '问卷已过期', + labelStyle: { + 'font-weight': 'bold' + } + } + ] +} diff --git a/web/src/management/pages/edit/setterConfig/submitConfig.js b/web/src/management/pages/edit/setterConfig/submitConfig.js new file mode 100644 index 00000000..ae4efa97 --- /dev/null +++ b/web/src/management/pages/edit/setterConfig/submitConfig.js @@ -0,0 +1,62 @@ +export default [ + { + title: '提交按钮文案', + type: 'InputSetter', + key: 'submitTitle', + placeholder: '提交', + value: '' + }, + { + title: '提交确认弹窗', + type: 'Customed', + key: 'confirmAgain', + content: [ + { + label: '是否配置该项', + labelStyle: { width: '120px' }, + type: 'CustomedSwitch', + key: 'confirmAgain.is_again', + value: true + }, + { + label: '二次确认文案', + labelStyle: { width: '120px' }, + type: 'InputSetter', + key: 'confirmAgain.again_text', + placeholder: '确认要提交吗?', + value: '确认要提交吗?' + } + ] + }, + { + title: '提交文案配置', + type: 'Customed', + key: 'msgContent', + content: [ + { + label: '已提交', + labelStyle: { width: '120px' }, + type: 'InputSetter', + key: 'msgContent.msg_9002', + placeholder: '请勿多次提交!', + value: '请勿多次提交!' + }, + { + label: '提交结束', + labelStyle: { width: '120px' }, + type: 'InputSetter', + key: 'msgContent.msg_9003', + placeholder: '您来晚了,已经满额!', + value: '您来晚了,已经满额!' + }, + { + label: '其他提交失败', + labelStyle: { width: '120px' }, + type: 'InputSetter', + key: 'msgContent.msg_9004', + placeholder: '提交失败!', + value: '提交失败!' + } + ] + } +] diff --git a/web/src/management/store/edit/getters.js b/web/src/management/store/edit/getters.js index 3350922d..2129819c 100644 --- a/web/src/management/store/edit/getters.js +++ b/web/src/management/store/edit/getters.js @@ -1,4 +1,4 @@ -import submitFormConfig from '@/management/config/setterConfig/submitConfig' +import submitFormConfig from '@/management/pages/edit/setterConfig/submitConfig' import questionLoader from '@/materials/questions/questionLoader' const innerMetaConfig = { From fb57eaaba71f297eb1375da13af30b73489b33ad Mon Sep 17 00:00:00 2001 From: sudoooooo Date: Mon, 22 Jul 2024 20:37:11 +0800 Subject: [PATCH 51/82] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/common/Editor/utils.js | 2 - .../modules/resultModule/CatalogPanel.vue | 2 +- .../modules/resultModule/PreviewPanel.vue | 2 +- .../resultModule/{ => components}/enum.js | 0 .../components/TeamMemberList.vue | 2 +- .../pages/edit/modules/settingModule/enum.js | 4 - .../setters/widgets/teamMemberList.vue | 96 ------------ .../materials/setters/widgets/whiteList.vue | 145 ------------------ 8 files changed, 3 insertions(+), 250 deletions(-) rename web/src/management/pages/edit/modules/resultModule/{ => components}/enum.js (100%) delete mode 100644 web/src/management/pages/edit/modules/settingModule/enum.js delete mode 100644 web/src/materials/setters/widgets/teamMemberList.vue delete mode 100644 web/src/materials/setters/widgets/whiteList.vue diff --git a/web/src/common/Editor/utils.js b/web/src/common/Editor/utils.js index 5adfe0b6..3300c0c6 100644 --- a/web/src/common/Editor/utils.js +++ b/web/src/common/Editor/utils.js @@ -24,7 +24,5 @@ export const replacePxWithRem = (html) => { }) }) - console.log('生成的结果', res) - return res } diff --git a/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue b/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue index 2659584c..c71055d4 100644 --- a/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue +++ b/web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue @@ -18,7 +18,7 @@ - \ No newline at end of file diff --git a/web/src/materials/setters/widgets/whiteList.vue b/web/src/materials/setters/widgets/whiteList.vue deleted file mode 100644 index 46cfcbca..00000000 --- a/web/src/materials/setters/widgets/whiteList.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - \ No newline at end of file From 7e5a8ae5c1cc65f2d299e1079dc01d0b201208be Mon Sep 17 00:00:00 2001 From: dayou <853094838@qq.com> Date: Tue, 23 Jul 2024 15:39:43 +0800 Subject: [PATCH 52/82] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=99=A8=E4=B8=8D=E6=9B=B4=E6=96=B0=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/materials/setters/widgets/CheckBox.vue | 2 +- web/src/materials/setters/widgets/InputNumber.vue | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/web/src/materials/setters/widgets/CheckBox.vue b/web/src/materials/setters/widgets/CheckBox.vue index 6df14725..8d2d7bef 100644 --- a/web/src/materials/setters/widgets/CheckBox.vue +++ b/web/src/materials/setters/widgets/CheckBox.vue @@ -68,7 +68,7 @@ watch( () => props.formConfig.value, (newVal: boolean) => { if (newVal !== modelValue.value) { - modelValue.value == !!newVal + modelValue.value = !!newVal } }, { diff --git a/web/src/materials/setters/widgets/InputNumber.vue b/web/src/materials/setters/widgets/InputNumber.vue index 12d535ae..b34e02f8 100644 --- a/web/src/materials/setters/widgets/InputNumber.vue +++ b/web/src/materials/setters/widgets/InputNumber.vue @@ -8,7 +8,7 @@ /> diff --git a/web/src/management/pages/edit/components/QuestionWrapper.vue b/web/src/management/pages/edit/components/QuestionWrapper.vue index dea0b0ff..6b03f5ad 100644 --- a/web/src/management/pages/edit/components/QuestionWrapper.vue +++ b/web/src/management/pages/edit/components/QuestionWrapper.vue @@ -51,6 +51,10 @@ const props = defineProps({ type: Boolean, default: false }, + isFirst: { + type: Boolean, + default: false + }, isLast: { type: Boolean, default: false @@ -81,7 +85,7 @@ const showHover = computed(() => { return isHover.value || props.isSelected }) const showUp = computed(() => { - return props.qIndex !== 0 + return !props.isFirst }) const showDown = computed(() => { return !props.isLast diff --git a/web/src/management/pages/edit/modules/contentModule/buildData.js b/web/src/management/pages/edit/modules/contentModule/buildData.js index e9cd602f..c9229cc2 100644 --- a/web/src/management/pages/edit/modules/contentModule/buildData.js +++ b/web/src/management/pages/edit/modules/contentModule/buildData.js @@ -10,6 +10,7 @@ export default function (schema) { 'skinConf', 'submitConf', 'questionDataList', + "pagingConf", 'logicConf' ]) configData.dataConf = { diff --git a/web/src/management/pages/edit/modules/pagingModule/PaginationPanel.vue b/web/src/management/pages/edit/modules/pagingModule/PaginationPanel.vue new file mode 100644 index 00000000..0cfbd201 --- /dev/null +++ b/web/src/management/pages/edit/modules/pagingModule/PaginationPanel.vue @@ -0,0 +1,282 @@ + + + + \ No newline at end of file diff --git a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue index a8ca36a2..94acb146 100644 --- a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue @@ -1,8 +1,12 @@