fix: peking分支同步develop并解决冲突
This commit is contained in:
parent
3003c2cbfa
commit
212a3329ad
18
README.md
18
README.md
@ -53,11 +53,11 @@ _**(个人和企业用户均可快速构建特定领域的调研类解决方案
|
|||||||
|
|
||||||
# 技术
|
# 技术
|
||||||
|
|
||||||
Web 端:Vue3 + ElementPlus;C 端多端渲染(在建,[申请加入共建](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE#%E6%88%90%E4%B8%BA%E5%AE%98%E6%96%B9%E4%B8%93%E9%A1%B9%E5%BB%BA%E8%AE%BE%E8%80%85))
|
Web 端:Vue3 + ElementPlus;C 端多端渲染(规划中)
|
||||||
|
|
||||||
Server 端:Nestjs + MongoDB;Java(在建,[申请加入共建](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE#%E6%88%90%E4%B8%BA%E5%AE%98%E6%96%B9%E4%B8%93%E9%A1%B9%E5%BB%BA%E8%AE%BE%E8%80%85))
|
Server 端:Nestjs + MongoDB;Java(在建,[欢迎加入共建](https://github.com/didi/xiaoju-survey/issues/306))
|
||||||
|
|
||||||
智能化基座:(在建)
|
智能化基座:(规划中)
|
||||||
|
|
||||||
# 项目优势
|
# 项目优势
|
||||||
|
|
||||||
@ -194,18 +194,18 @@ npm run serve
|
|||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
## 微信交流群(推荐)
|
||||||
|
|
||||||
|
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。任何问题和合作可以联系小助手:
|
||||||
|
|
||||||
|
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||||
|
|
||||||
## QQ 交流群
|
## QQ 交流群
|
||||||
|
|
||||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入:
|
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入:
|
||||||
|
|
||||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
||||||
|
|
||||||
## 微信交流群
|
|
||||||
|
|
||||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。任何问题和合作可以联系小助手:
|
|
||||||
|
|
||||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
|
||||||
|
|
||||||
## Star
|
## Star
|
||||||
|
|
||||||
开源不易,如果该项目对你有帮助,请 star 一下 ❤️❤️❤️,你的支持是我们最大的动力。
|
开源不易,如果该项目对你有帮助,请 star 一下 ❤️❤️❤️,你的支持是我们最大的动力。
|
||||||
|
20
README_EN.md
20
README_EN.md
@ -47,17 +47,17 @@
|
|||||||
|
|
||||||
> For more comprehensive features, please refer to the official Feature documentation.
|
> For more comprehensive features, please refer to the official Feature documentation.
|
||||||
|
|
||||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
|
||||||
|
|
||||||
_**(Both individual and enterprise users can quickly build survey solutions specific to their fields.)**_
|
_**(Both individual and enterprise users can quickly build survey solutions specific to their fields.)**_
|
||||||
|
|
||||||
# Technology
|
# Technology
|
||||||
|
|
||||||
Web: Vue3 + ElementPlus; Multi-end rendering for C-end (under construction, application for co-construction welcome).
|
Web: Vue3 + ElementPlus; Multi-end rendering for C-end (planning).
|
||||||
|
|
||||||
Server: Nestjs + MongoDB; Java (under construction).
|
Server: Nestjs + MongoDB; Java ([under construction](https://github.com/didi/xiaoju-survey/issues/306)).
|
||||||
|
|
||||||
Intelligent Foundation: (under construction).
|
Intelligent Foundation: (planning).
|
||||||
|
|
||||||
# Project Advantages
|
# Project Advantages
|
||||||
|
|
||||||
@ -194,18 +194,18 @@ Create and publish a questionnaire.
|
|||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
## QQ Group
|
|
||||||
|
|
||||||
The official group will release the latest project news, construction plans, and community activities. Welcome to join:
|
|
||||||
|
|
||||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
|
||||||
|
|
||||||
## WeChat Group
|
## WeChat Group
|
||||||
|
|
||||||
The official group will release the latest project news, construction plans, and community activities. Any questions and cooperation can contact the assistant:
|
The official group will release the latest project news, construction plans, and community activities. Any questions and cooperation can contact the assistant:
|
||||||
|
|
||||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||||
|
|
||||||
|
## QQ Group
|
||||||
|
|
||||||
|
The official group will release the latest project news, construction plans, and community activities. Welcome to join:
|
||||||
|
|
||||||
|
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
||||||
|
|
||||||
## Star
|
## Star
|
||||||
|
|
||||||
Open source is not easy. If this project helps you, please star it ❤️❤️❤️. Your support is our greatest motivation.
|
Open source is not easy. If this project helps you, please star it ❤️❤️❤️. Your support is our greatest motivation.
|
||||||
|
@ -11,6 +11,7 @@ import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
|||||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||||
import { Collaborator } from 'src/models/collaborator.entity';
|
import { Collaborator } from 'src/models/collaborator.entity';
|
||||||
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
|
||||||
describe('SurveyGuard', () => {
|
describe('SurveyGuard', () => {
|
||||||
let guard: 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 context = createMockExecutionContext();
|
||||||
const surveyMeta = { owner: 'testUser', workspaceId: null };
|
const surveyMeta = { owner: 'testUser', workspaceId: null };
|
||||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||||
@ -108,7 +121,35 @@ describe('SurveyGuard', () => {
|
|||||||
expect(result).toBe(true);
|
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 context = createMockExecutionContext();
|
||||||
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
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 {
|
function createMockExecutionContext(): ExecutionContext {
|
||||||
return {
|
return {
|
||||||
switchToHttp: jest.fn().mockReturnValue({
|
switchToHttp: jest.fn().mockReturnValue({
|
||||||
|
@ -6,6 +6,7 @@ import { User } from 'src/models/user.entity';
|
|||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { hash256 } from 'src/utils/hash256';
|
import { hash256 } from 'src/utils/hash256';
|
||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
@ -21,6 +22,7 @@ describe('UserService', () => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
findOne: 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 () => {
|
it('should create a user', async () => {
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -102,7 +108,7 @@ describe('UserService', () => {
|
|||||||
expect(user).toEqual({ ...userInfo, password: hashedPassword });
|
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 = {
|
const userInfo = {
|
||||||
username: 'nonExistingUser',
|
username: 'nonExistingUser',
|
||||||
password: 'nonExistingPassword',
|
password: 'nonExistingPassword',
|
||||||
@ -129,7 +135,8 @@ describe('UserService', () => {
|
|||||||
const userInfo = {
|
const userInfo = {
|
||||||
username: username,
|
username: username,
|
||||||
password: 'existingPassword',
|
password: 'existingPassword',
|
||||||
} as User;
|
curStatus: { status: 'ACTIVE' },
|
||||||
|
} as unknown as User;
|
||||||
|
|
||||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||||
|
|
||||||
@ -137,10 +144,129 @@ describe('UserService', () => {
|
|||||||
|
|
||||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
|
||||||
username: username,
|
username: username,
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(user).toEqual(userInfo);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,13 @@ import { UserService } from 'src/modules/auth/services/user.service';
|
|||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.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/authentication.guard');
|
||||||
jest.mock('src/guards/survey.guard');
|
jest.mock('src/guards/survey.guard');
|
||||||
@ -21,6 +27,8 @@ describe('CollaboratorController', () => {
|
|||||||
let collaboratorService: CollaboratorService;
|
let collaboratorService: CollaboratorService;
|
||||||
let logger: Logger;
|
let logger: Logger;
|
||||||
let userService: UserService;
|
let userService: UserService;
|
||||||
|
let surveyMetaService: SurveyMetaService;
|
||||||
|
let workspaceMemberServie: WorkspaceMemberService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -34,12 +42,18 @@ describe('CollaboratorController', () => {
|
|||||||
changeUserPermission: jest.fn(),
|
changeUserPermission: jest.fn(),
|
||||||
deleteCollaborator: jest.fn(),
|
deleteCollaborator: jest.fn(),
|
||||||
getCollaborator: jest.fn(),
|
getCollaborator: jest.fn(),
|
||||||
|
batchDeleteBySurveyId: jest.fn(),
|
||||||
|
batchCreate: jest.fn(),
|
||||||
|
batchDelete: jest.fn(),
|
||||||
|
updateById: jest.fn(),
|
||||||
|
batchSaveCollaborator: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: Logger,
|
||||||
useValue: {
|
useValue: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -72,6 +86,10 @@ describe('CollaboratorController', () => {
|
|||||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||||
logger = module.get<Logger>(Logger);
|
logger = module.get<Logger>(Logger);
|
||||||
userService = module.get<UserService>(UserService);
|
userService = module.get<UserService>(UserService);
|
||||||
|
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||||
|
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||||
|
WorkspaceMemberService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -115,6 +133,59 @@ describe('CollaboratorController', () => {
|
|||||||
HttpException,
|
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', () => {
|
describe('getSurveyCollaboratorList', () => {
|
||||||
@ -217,4 +288,229 @@ describe('CollaboratorController', () => {
|
|||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ import { Logger } from 'src/logger';
|
|||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
|
||||||
jest.mock('../services/dataStatistic.service');
|
jest.mock('../services/dataStatistic.service');
|
||||||
jest.mock('../services/surveyMeta.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/authentication.guard');
|
||||||
jest.mock('src/guards/survey.guard');
|
jest.mock('src/guards/survey.guard');
|
||||||
jest.mock('src/guards/workspace.guard');
|
|
||||||
|
|
||||||
describe('DataStatisticController', () => {
|
describe('DataStatisticController', () => {
|
||||||
let controller: DataStatisticController;
|
let controller: DataStatisticController;
|
||||||
let dataStatisticService: DataStatisticService;
|
let dataStatisticService: DataStatisticService;
|
||||||
|
let responseSchemaService: ResponseSchemaService;
|
||||||
|
let pluginManager: XiaojuSurveyPluginManager;
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -64,9 +67,14 @@ describe('DataStatisticController', () => {
|
|||||||
controller = module.get<DataStatisticController>(DataStatisticController);
|
controller = module.get<DataStatisticController>(DataStatisticController);
|
||||||
dataStatisticService =
|
dataStatisticService =
|
||||||
module.get<DataStatisticService>(DataStatisticService);
|
module.get<DataStatisticService>(DataStatisticService);
|
||||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
responseSchemaService = module.get<ResponseSchemaService>(
|
||||||
|
ResponseSchemaService,
|
||||||
|
);
|
||||||
|
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||||
XiaojuSurveyPluginManager,
|
XiaojuSurveyPluginManager,
|
||||||
);
|
);
|
||||||
|
logger = module.get<Logger>(Logger);
|
||||||
|
|
||||||
pluginManager.registerPlugin(
|
pluginManager.registerPlugin(
|
||||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||||
);
|
);
|
||||||
@ -82,6 +90,9 @@ describe('DataStatisticController', () => {
|
|||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
query: {
|
query: {
|
||||||
surveyId,
|
surveyId,
|
||||||
|
isDesensitive: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -105,13 +116,13 @@ describe('DataStatisticController', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||||
.mockResolvedValueOnce({} as any);
|
.mockResolvedValueOnce({} as any);
|
||||||
jest
|
jest
|
||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, {});
|
const result = await controller.data(mockRequest.query, mockRequest);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -125,6 +136,8 @@ describe('DataStatisticController', () => {
|
|||||||
query: {
|
query: {
|
||||||
surveyId,
|
surveyId,
|
||||||
isDesensitive: true,
|
isDesensitive: true,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -146,19 +159,499 @@ describe('DataStatisticController', () => {
|
|||||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||||
.mockResolvedValueOnce({} as any);
|
.mockResolvedValueOnce({} as any);
|
||||||
jest
|
jest
|
||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, {});
|
const result = await controller.data(mockRequest.query, mockRequest);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: mockDataTable,
|
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:
|
||||||
|
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||||
|
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: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -197,7 +197,6 @@ describe('DataStatisticService', () => {
|
|||||||
data413_3: expect.any(String),
|
data413_3: expect.any(String),
|
||||||
data413: expect.any(Number),
|
data413: expect.any(Number),
|
||||||
data863: expect.any(String),
|
data863: expect.any(String),
|
||||||
data413_custom: expect.any(String),
|
|
||||||
difTime: expect.any(String),
|
difTime: expect.any(String),
|
||||||
createDate: 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -319,6 +319,7 @@ export class CollaboratorController {
|
|||||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||||
|
|
||||||
if (!surveyMeta) {
|
if (!surveyMeta) {
|
||||||
|
this.logger.error(`问卷不存在: ${surveyId}`, { req });
|
||||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
|||||||
import { Logger } from 'src/logger';
|
import { Logger } from 'src/logger';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
||||||
|
import { handleAggretionData } from '../utils';
|
||||||
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@ -83,4 +85,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 });
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
server/src/modules/survey/dto/aggregationStatis.dto.ts
Normal file
13
server/src/modules/survey/dto/aggregationStatis.dto.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,10 @@ import Joi from 'joi';
|
|||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
|
||||||
export class CollaboratorDto {
|
export class CollaboratorDto {
|
||||||
@ApiProperty({ description: '用户id', required: false })
|
@ApiProperty({ description: '协作id', required: false })
|
||||||
|
_id?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户id', required: true })
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@ -16,7 +19,7 @@ export class CollaboratorDto {
|
|||||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
permissions: Array<number>;
|
permissions: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BatchSaveCollaboratorDto {
|
export class BatchSaveCollaboratorDto {
|
||||||
|
@ -7,8 +7,7 @@ import moment from 'moment';
|
|||||||
import { keyBy } from 'lodash';
|
import { keyBy } from 'lodash';
|
||||||
import { DataItem } from 'src/interfaces/survey';
|
import { DataItem } from 'src/interfaces/survey';
|
||||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
import { getListHeadByDataList } from '../utils';
|
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataStatisticService {
|
export class DataStatisticService {
|
||||||
private radioType = ['radio-star', 'radio-nps'];
|
private radioType = ['radio-star', 'radio-nps'];
|
||||||
@ -102,4 +101,62 @@ export class DataStatisticService {
|
|||||||
listBody,
|
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<string, number> = {};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,16 @@
|
|||||||
"field": "data458",
|
"field": "data458",
|
||||||
"title": "标题1",
|
"title": "标题1",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"randomSort": false,
|
"numberRange": {
|
||||||
"checked": false,
|
"max": {
|
||||||
"minNum": "",
|
"placeholder": "1000",
|
||||||
"maxNum": "",
|
"value": 1000
|
||||||
"star": 5,
|
},
|
||||||
"placeholderDesc": "",
|
"min": {
|
||||||
"urlKey": "",
|
"placeholder": "0",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
"textRange": {
|
"textRange": {
|
||||||
"min": {
|
"min": {
|
||||||
"placeholder": "0",
|
"placeholder": "0",
|
||||||
@ -35,14 +38,8 @@
|
|||||||
"showType": true,
|
"showType": true,
|
||||||
"showSpliter": true,
|
"showSpliter": true,
|
||||||
"type": "radio",
|
"type": "radio",
|
||||||
"placeholderDesc": "",
|
|
||||||
"field": "data515",
|
"field": "data515",
|
||||||
"title": "标题2",
|
"title": "标题2",
|
||||||
"placeholder": "",
|
|
||||||
"randomSort": false,
|
|
||||||
"checked": false,
|
|
||||||
"minNum": "",
|
|
||||||
"maxNum": "",
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"text": "选项1",
|
"text": "选项1",
|
||||||
@ -65,22 +62,6 @@
|
|||||||
"quota": "0"
|
"quota": "0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"importKey": "single",
|
|
||||||
"importData": "",
|
|
||||||
"cOption": "",
|
|
||||||
"cOptions": [],
|
|
||||||
"star": 5,
|
|
||||||
"exclude": false,
|
|
||||||
"textRange": {
|
|
||||||
"min": {
|
|
||||||
"placeholder": "0",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"max": {
|
|
||||||
"placeholder": "500",
|
|
||||||
"value": 500
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deleteRecover": false,
|
"deleteRecover": false,
|
||||||
"noDisplay": false
|
"noDisplay": false
|
||||||
}
|
}
|
||||||
|
@ -6,30 +6,21 @@
|
|||||||
"showIndex": true,
|
"showIndex": true,
|
||||||
"showType": true,
|
"showType": true,
|
||||||
"showSpliter": true,
|
"showSpliter": true,
|
||||||
"placeholderDesc": "",
|
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"isRequired": true,
|
"isRequired": true,
|
||||||
"randomSort": false,
|
|
||||||
"innerRandom": false,
|
|
||||||
"hideSubTitleIndex": false,
|
|
||||||
"checked": false,
|
|
||||||
"minNum": "",
|
|
||||||
"maxNum": "",
|
|
||||||
"relyType": "and",
|
|
||||||
"extraOptions": [],
|
|
||||||
"importKey": "single",
|
|
||||||
"importData": "",
|
|
||||||
"addressType": 3,
|
|
||||||
"isAuto": false,
|
|
||||||
"urlKey": "",
|
|
||||||
"hasRely": true,
|
|
||||||
"relyList": [],
|
|
||||||
"optionOrigin": "",
|
|
||||||
"answerTip": "",
|
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"valid": "",
|
"valid": "",
|
||||||
"title": "标题1",
|
"title": "标题1",
|
||||||
"answer": "",
|
"numberRange": {
|
||||||
|
"max": {
|
||||||
|
"placeholder": "1000",
|
||||||
|
"value": 1000
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"placeholder": "0",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
"textRange": {
|
"textRange": {
|
||||||
"min": {
|
"min": {
|
||||||
"placeholder": "0",
|
"placeholder": "0",
|
||||||
@ -46,45 +37,12 @@
|
|||||||
"showIndex": true,
|
"showIndex": true,
|
||||||
"showType": true,
|
"showType": true,
|
||||||
"showSpliter": true,
|
"showSpliter": true,
|
||||||
"placeholderDesc": "",
|
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"isRequired": true,
|
"isRequired": true,
|
||||||
"randomSort": false,
|
"min": "",
|
||||||
"innerRandom": false,
|
"max": "",
|
||||||
"hideSubTitleIndex": false,
|
|
||||||
"checked": false,
|
|
||||||
"minNum": "",
|
|
||||||
"maxNum": "",
|
|
||||||
"relyType": "and",
|
|
||||||
"extraOptions": [],
|
|
||||||
"importKey": "single",
|
|
||||||
"importData": "",
|
|
||||||
"cOption": "",
|
|
||||||
"cOptions": [],
|
|
||||||
"star": 5,
|
|
||||||
"urlKey": "",
|
|
||||||
"defaultProps": {
|
|
||||||
"children": "children",
|
|
||||||
"label": "name",
|
|
||||||
"id": "id"
|
|
||||||
},
|
|
||||||
"hasRely": true,
|
|
||||||
"relyList": [],
|
|
||||||
"optionOrigin": "",
|
|
||||||
"answerTip": "",
|
|
||||||
"type": "radio-star",
|
"type": "radio-star",
|
||||||
"title": "标题2",
|
"title": "标题2"
|
||||||
"answer": "",
|
|
||||||
"textRange": {
|
|
||||||
"min": {
|
|
||||||
"placeholder": "0",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"max": {
|
|
||||||
"placeholder": "500",
|
|
||||||
"value": 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,16 @@
|
|||||||
"field": "data458",
|
"field": "data458",
|
||||||
"title": "姓名",
|
"title": "姓名",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"randomSort": false,
|
"numberRange": {
|
||||||
"checked": false,
|
"max": {
|
||||||
"minNum": "",
|
"placeholder": "1000",
|
||||||
"maxNum": "",
|
"value": 1000
|
||||||
"star": 5,
|
},
|
||||||
"exclude": false,
|
"min": {
|
||||||
"placeholderDesc": "",
|
"placeholder": "0",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
"textRange": {
|
"textRange": {
|
||||||
"min": {
|
"min": {
|
||||||
"placeholder": "0",
|
"placeholder": "0",
|
||||||
@ -40,10 +43,6 @@
|
|||||||
"title": "选择您感兴趣的课程进行报名",
|
"title": "选择您感兴趣的课程进行报名",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"valid": "",
|
"valid": "",
|
||||||
"randomSort": false,
|
|
||||||
"checked": false,
|
|
||||||
"minNum": "",
|
|
||||||
"maxNum": "",
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"text": "课程1",
|
"text": "课程1",
|
||||||
@ -81,27 +80,7 @@
|
|||||||
"othersKey": "",
|
"othersKey": "",
|
||||||
"placeholderDesc": ""
|
"placeholderDesc": ""
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"star": 5,
|
|
||||||
"exclude": false,
|
|
||||||
"urlKey": "",
|
|
||||||
"defaultProps": {
|
|
||||||
"children": "children",
|
|
||||||
"label": "name",
|
|
||||||
"id": "id"
|
|
||||||
},
|
|
||||||
"startDate": "",
|
|
||||||
"endDate": "",
|
|
||||||
"textRange": {
|
|
||||||
"min": {
|
|
||||||
"placeholder": "0",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"max": {
|
|
||||||
"placeholder": "500",
|
|
||||||
"value": 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,16 @@
|
|||||||
"field": "data631",
|
"field": "data631",
|
||||||
"title": "标题1",
|
"title": "标题1",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"sLimit": 1,
|
"numberRange": {
|
||||||
"randomSort": false,
|
"max": {
|
||||||
"checked": false,
|
"placeholder": "1000",
|
||||||
"minNum": "",
|
"value": 1000
|
||||||
"maxNum": "",
|
},
|
||||||
"star": 5,
|
"min": {
|
||||||
"exclude": false,
|
"placeholder": "0",
|
||||||
"placeholderDesc": "",
|
"value": 0
|
||||||
"urlKey": "",
|
}
|
||||||
|
},
|
||||||
"textRange": {
|
"textRange": {
|
||||||
"min": {
|
"min": {
|
||||||
"placeholder": "0",
|
"placeholder": "0",
|
||||||
@ -38,12 +39,8 @@
|
|||||||
"showSpliter": true,
|
"showSpliter": true,
|
||||||
"type": "vote",
|
"type": "vote",
|
||||||
"innerType": "radio",
|
"innerType": "radio",
|
||||||
"placeholderDesc": "",
|
|
||||||
"field": "data606",
|
"field": "data606",
|
||||||
"title": "标题2",
|
"title": "标题2",
|
||||||
"placeholder": "",
|
|
||||||
"randomSort": false,
|
|
||||||
"checked": false,
|
|
||||||
"minNum": "",
|
"minNum": "",
|
||||||
"maxNum": "",
|
"maxNum": "",
|
||||||
"options": [
|
"options": [
|
||||||
@ -65,18 +62,7 @@
|
|||||||
"placeholderDesc": "",
|
"placeholderDesc": "",
|
||||||
"hash": "115020"
|
"hash": "115020"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"star": 5,
|
|
||||||
"textRange": {
|
|
||||||
"min": {
|
|
||||||
"placeholder": "0",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"max": {
|
|
||||||
"placeholder": "500",
|
|
||||||
"value": 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export function getListHeadByDataList(dataList) {
|
|||||||
let othersCode;
|
let othersCode;
|
||||||
const radioType = ['radio-star', 'radio-nps'];
|
const radioType = ['radio-star', 'radio-nps'];
|
||||||
if (radioType.includes(question.type)) {
|
if (radioType.includes(question.type)) {
|
||||||
const rangeConfigKeys = Object.keys(question.rangeConfig);
|
const rangeConfigKeys = question.rangeConfig ? Object.keys(question.rangeConfig) : [];
|
||||||
if (rangeConfigKeys.length > 0) {
|
if (rangeConfigKeys.length > 0) {
|
||||||
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
||||||
}
|
}
|
||||||
@ -66,3 +66,177 @@ export function getListHeadByDataList(dataList) {
|
|||||||
});
|
});
|
||||||
return listHead;
|
return listHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformAndMergeArrayFields(data) {
|
||||||
|
const transformedData = {};
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
const valueMap: Record<string, number> = {};
|
||||||
|
|
||||||
|
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<string, any> = {};
|
||||||
|
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) + '%';
|
||||||
|
}
|
||||||
|
17
web/components.d.ts
vendored
17
web/components.d.ts
vendored
@ -14,13 +14,11 @@ declare module 'vue' {
|
|||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
@ -32,12 +30,7 @@ declare module 'vue' {
|
|||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
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']
|
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||||
@ -48,8 +41,6 @@ declare module 'vue' {
|
|||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
||||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
IEpCheck: typeof import('~icons/ep/check')['default']
|
||||||
@ -62,8 +53,6 @@ declare module 'vue' {
|
|||||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
IEpMinus: typeof import('~icons/ep/minus')['default']
|
||||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
||||||
IEpMore: typeof import('~icons/ep/more')['default']
|
IEpMore: typeof import('~icons/ep/more')['default']
|
||||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
|
||||||
IEpMore: typeof import('~icons/ep/more')['default']
|
|
||||||
IEpPlus: typeof import('~icons/ep/plus')['default']
|
IEpPlus: typeof import('~icons/ep/plus')['default']
|
||||||
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
||||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
IEpRank: typeof import('~icons/ep/rank')['default']
|
||||||
@ -75,8 +64,6 @@ declare module 'vue' {
|
|||||||
IEpTop: typeof import('~icons/ep/top')['default']
|
IEpTop: typeof import('~icons/ep/top')['default']
|
||||||
IEpView: typeof import('~icons/ep/view')['default']
|
IEpView: typeof import('~icons/ep/view')['default']
|
||||||
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
||||||
IEpView: typeof import('~icons/ep/view')['default']
|
|
||||||
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,13 @@
|
|||||||
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.7.0",
|
"element-plus": "^2.7.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
@ -39,6 +39,7 @@
|
|||||||
"@iconify-json/ep": "^1.1.15",
|
"@iconify-json/ep": "^1.1.15",
|
||||||
"@rushstack/eslint-patch": "^1.10.2",
|
"@rushstack/eslint-patch": "^1.10.2",
|
||||||
"@tsconfig/node20": "^20.1.2",
|
"@tsconfig/node20": "^20.1.2",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
|
@ -8,6 +8,14 @@ export const getRecycleList = (data) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getStatisticList = (data) => {
|
||||||
|
return axios.get('/survey/dataStatistic/aggregationStatis', {
|
||||||
|
params: {
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
//问卷下载
|
//问卷下载
|
||||||
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
|
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
|
||||||
return axios.get('/survey/surveyDownload/download', {
|
return axios.get('/survey/surveyDownload/download', {
|
||||||
|
77
web/src/management/config/analysisConfig.js
Normal file
77
web/src/management/config/analysisConfig.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
57
web/src/management/config/chartConfig/bar.js
Normal file
57
web/src/management/config/chartConfig/bar.js
Normal file
@ -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} <br/>{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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
146
web/src/management/config/chartConfig/gauge.js
Normal file
146
web/src/management/config/chartConfig/gauge.js
Normal file
@ -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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
9
web/src/management/config/chartConfig/index.js
Normal file
9
web/src/management/config/chartConfig/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import pie from './pie'
|
||||||
|
import bar from './bar'
|
||||||
|
import gauge from './gauge'
|
||||||
|
|
||||||
|
export const getOption = {
|
||||||
|
pie,
|
||||||
|
bar,
|
||||||
|
gauge
|
||||||
|
}
|
57
web/src/management/config/chartConfig/pie.js
Normal file
57
web/src/management/config/chartConfig/pie.js
Normal file
@ -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} <br/>{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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
const menuItems = {
|
export const menuItems = {
|
||||||
text: {
|
text: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',
|
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',
|
||||||
|
25
web/src/management/hooks/useCharts.js
Normal file
25
web/src/management/hooks/useCharts.js
Normal file
@ -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 }
|
||||||
|
}
|
20
web/src/management/hooks/useResizeObserver.js
Normal file
20
web/src/management/hooks/useResizeObserver.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 引入防抖函数
|
||||||
|
import { debounce as _debounce } from 'lodash-es'
|
||||||
|
/**
|
||||||
|
* @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 }
|
||||||
|
}
|
77
web/src/management/hooks/useStatisticsItemChart.js
Normal file
77
web/src/management/hooks/useStatisticsItemChart.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,202 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="analysis-page">
|
<div class="analysis-page">
|
||||||
<leftMenu class="left"></leftMenu>
|
<leftMenu class="left"></leftMenu>
|
||||||
<div class="content-wrapper right">
|
<div class="right">
|
||||||
<template v-if="tableData.total">
|
<div class="analysis-tabs">
|
||||||
<h2 class="data-list">数据列表</h2>
|
<router-link
|
||||||
<div class="menus">
|
v-for="item in analysisType"
|
||||||
<el-switch
|
class="analysis-tabs__item"
|
||||||
:model-value="isShowOriginData"
|
:key="item.value"
|
||||||
active-text="是否展示原数据"
|
:to="{ name: item.value }"
|
||||||
@input="onIsShowOriginChange"
|
|
||||||
>
|
|
||||||
</el-switch>
|
|
||||||
<div style="display: flex; justify-content: flex-end">
|
|
||||||
<el-switch
|
|
||||||
:model-value="isDownloadDesensitive"
|
|
||||||
active-text="是否下载脱敏数据"
|
|
||||||
@input="onisDownloadDesensitive"
|
|
||||||
style="margin-right: 20px"
|
|
||||||
>
|
|
||||||
</el-switch>
|
|
||||||
<el-button type="primary" @click="onDownload">导出数据</el-button>
|
|
||||||
</div>
|
|
||||||
<!-- <el-button type="primary" @click="exportData">导出数据</el-button> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="tableData.total">
|
|
||||||
<DataTable :main-table-loading="mainTableLoading" :table-data="tableData" />
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="prev, pager, next"
|
|
||||||
popper-class="analysis-pagination"
|
|
||||||
:total="tableData.total"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
>
|
>
|
||||||
</el-pagination>
|
<i class="iconfont" :class="item.icon"></i>
|
||||||
</template>
|
<span>{{ item.label }}</span>
|
||||||
<div v-else>
|
</router-link>
|
||||||
<EmptyIndex :data="noDataConfig" />
|
</div>
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
|
||||||
|
|
||||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
|
||||||
import LeftMenu from '@/management/components/LeftMenu.vue'
|
import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||||
import { getRecycleList, downloadSurvey } from '@/management/api/analysis'
|
import { analysisType } from '@/management/config/analysisConfig'
|
||||||
|
|
||||||
import DataTable from './components/DataTable.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AnalysisPage',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
mainTableLoading: false,
|
|
||||||
tableData: {
|
|
||||||
total: 0,
|
|
||||||
listHead: [],
|
|
||||||
listBody: []
|
|
||||||
},
|
|
||||||
noDataConfig: {
|
|
||||||
title: '暂无数据',
|
|
||||||
desc: '您的问卷当前还没有数据,快去回收问卷吧!',
|
|
||||||
img: '/imgs/icons/analysis-empty.webp'
|
|
||||||
},
|
|
||||||
currentPage: 1,
|
|
||||||
isShowOriginData: false,
|
|
||||||
tmpIsShowOriginData: false,
|
|
||||||
isDownloadDesensitive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
created() {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async init() {
|
|
||||||
if (!this.$route.params.id) {
|
|
||||||
ElMessage.error('没有传入问卷参数~')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.mainTableLoading = true
|
|
||||||
try {
|
|
||||||
const res = await getRecycleList({
|
|
||||||
page: this.currentPage,
|
|
||||||
surveyId: this.$route.params.id,
|
|
||||||
isDesensitive: !this.tmpIsShowOriginData // 发起请求的时候,isShowOriginData还没改变,暂存了一个字段
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
const listHead = this.formatHead(res.data.listHead)
|
|
||||||
this.tableData = { ...res.data, listHead }
|
|
||||||
this.mainTableLoading = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('查询回收数据失败,请重试')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onDownload() {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm('是否确认下载?', '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log('取消下载')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.exportData()
|
|
||||||
this.gotoDownloadList()
|
|
||||||
},
|
|
||||||
async gotoDownloadList() {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm('计算中,是否前往下载中心?', '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log('取消跳转')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.push('/survey/download')
|
|
||||||
},
|
|
||||||
handleCurrentChange(current) {
|
|
||||||
if (this.mainTableLoading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.currentPage = current
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
formatHead(listHead = []) {
|
|
||||||
const head = []
|
|
||||||
|
|
||||||
listHead.forEach((headItem) => {
|
|
||||||
head.push({
|
|
||||||
field: headItem.field,
|
|
||||||
title: headItem.title
|
|
||||||
})
|
|
||||||
|
|
||||||
if (headItem.othersCode?.length) {
|
|
||||||
headItem.othersCode.forEach((item) => {
|
|
||||||
head.push({
|
|
||||||
field: item.code,
|
|
||||||
title: `${headItem.title}-${item.option}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return head
|
|
||||||
},
|
|
||||||
async onIsShowOriginChange(data) {
|
|
||||||
if (this.mainTableLoading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// console.log(data)
|
|
||||||
this.tmpIsShowOriginData = data
|
|
||||||
await this.init()
|
|
||||||
this.isShowOriginData = data
|
|
||||||
},
|
|
||||||
async onisDownloadDesensitive() {
|
|
||||||
if (this.isDownloadDesensitive) {
|
|
||||||
this.isDownloadDesensitive = false
|
|
||||||
} else {
|
|
||||||
this.isDownloadDesensitive = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async exportData() {
|
|
||||||
try {
|
|
||||||
const res = await downloadSurvey({
|
|
||||||
surveyId: String(this.$route.params.id),
|
|
||||||
isDesensitive: this.isDownloadDesensitive
|
|
||||||
})
|
|
||||||
console.log(this.$route.params.id)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('下载成功')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('下载失败')
|
|
||||||
ElMessage.error(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
DataTable,
|
|
||||||
EmptyIndex,
|
|
||||||
LeftMenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -215,31 +41,60 @@ export default {
|
|||||||
.right {
|
.right {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-left: 120px;
|
min-width: 1160px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menus {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
padding: 30px 40px 50px 40px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: #f6f7f9;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f6f7f9;
|
||||||
|
|
||||||
|
.analysis-tabs {
|
||||||
|
flex: none;
|
||||||
|
gap: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e7e9eb;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #92949d;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-link-active {
|
||||||
|
color: $font-color-title;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% + 5px);
|
||||||
|
height: 3px;
|
||||||
|
background-color: $primary-color;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list {
|
.content-wrapper {
|
||||||
margin-bottom: 20px;
|
flex: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 24px 24px 24px 104px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -17,22 +17,23 @@
|
|||||||
minWidth="200"
|
minWidth="200"
|
||||||
>
|
>
|
||||||
<template #header="scope">
|
<template #header="scope">
|
||||||
<div class="table-row-cell">
|
<div
|
||||||
<span
|
class="table-row-cell"
|
||||||
@mouseover="onPopoverRefOver(scope, 'head')"
|
@mouseover="onPopoverRefOver(scope, 'head')"
|
||||||
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
|
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
|
||||||
>
|
>
|
||||||
|
<span>
|
||||||
{{ scope.column.label.replace(/ /g, '') }}
|
{{ scope.column.label.replace(/ /g, '') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div>
|
<div
|
||||||
<span
|
class="table-row-cell"
|
||||||
class="table-row-cell"
|
@mouseover="onPopoverRefOver(scope, 'content')"
|
||||||
@mouseover="onPopoverRefOver(scope, 'content')"
|
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
|
||||||
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
|
>
|
||||||
>
|
<span>
|
||||||
{{ getContent(scope.row[scope.column.property]) }}
|
{{ getContent(scope.row[scope.column.property]) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -44,6 +45,7 @@
|
|||||||
popper-style="text-align: center;"
|
popper-style="text-align: center;"
|
||||||
:virtual-ref="popoverVirtualRef"
|
:virtual-ref="popoverVirtualRef"
|
||||||
placement="top"
|
placement="top"
|
||||||
|
width="400"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
virtual-triggering
|
virtual-triggering
|
||||||
:content="popoverContent"
|
:content="popoverContent"
|
||||||
@ -62,6 +64,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
mainTableLoading: {
|
mainTableLoading: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
|
},
|
||||||
|
tableMinHeight: {
|
||||||
|
type: String,
|
||||||
|
default: '620px'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const popoverRefMap = ref({})
|
const popoverRefMap = ref({})
|
||||||
@ -94,15 +100,18 @@ const onPopoverRefOver = (scope, type) => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
min-height: 620px;
|
min-height: v-bind('tableMinHeight');
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
|
||||||
.table-border {
|
.table-border {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__header) {
|
:deep(.el-table__header) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.thead-cell .el-table__cell {
|
.thead-cell .el-table__cell {
|
||||||
.cell {
|
.cell {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@ -111,10 +120,16 @@ const onPopoverRefOver = (scope, type) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row-cell {
|
.table-row-cell {
|
||||||
white-space: nowrap; /* 禁止自动换行 */
|
max-width: 100%;
|
||||||
overflow: hidden; /* 超出部分隐藏 */
|
display: inline-block;
|
||||||
text-overflow: ellipsis; /* 显示省略号 */
|
white-space: nowrap;
|
||||||
|
/* 禁止自动换行 */
|
||||||
|
overflow: hidden;
|
||||||
|
/* 超出部分隐藏 */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* 显示省略号 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
242
web/src/management/pages/analysis/components/StatisticsItem.vue
Normal file
242
web/src/management/pages/analysis/components/StatisticsItem.vue
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div class="separate-item">
|
||||||
|
<div class="separate-item-title">
|
||||||
|
<el-popover
|
||||||
|
placement="top"
|
||||||
|
width="400"
|
||||||
|
trigger="hover"
|
||||||
|
:disabled="!titlePoppverShow"
|
||||||
|
:content="cleanRichText(StatisticsData.title)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<p ref="titleRef" class="text" v-html="cleanRichText(StatisticsData.title)"></p>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
<p v-if="questionTypeDesc" class="type">{{ questionTypeDesc }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="separate-item-content">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div ref="chartRef" class="chart"></div>
|
||||||
|
<div v-if="chartTypeList.length > 1" class="chart-type-list">
|
||||||
|
<el-segmented v-model="chartType" :options="chartTypeList" size="small">
|
||||||
|
<template #default="{ item }">
|
||||||
|
<i class="iconfont" :class="`icon-${item}`"></i>
|
||||||
|
</template>
|
||||||
|
</el-segmented>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<data-table :table-data :table-min-height />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, toRefs, computed, watch, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { cloneDeep as _cloneDeep } from 'lodash-es'
|
||||||
|
import {
|
||||||
|
separateItemListHead,
|
||||||
|
summaryType,
|
||||||
|
summaryItemConfig
|
||||||
|
} from '@/management/config/analysisConfig'
|
||||||
|
import useCharts from '@/management/hooks/useCharts'
|
||||||
|
import useStatisticsItemChart from '@/management/hooks/useStatisticsItemChart'
|
||||||
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
import { menuItems } from '@/management/config/questionMenuConfig'
|
||||||
|
import DataTable from './DataTable.vue'
|
||||||
|
import useResizeObserver from '@/management/hooks/useResizeObserver'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
StatisticsData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const questionType = computed(() => {
|
||||||
|
return props?.StatisticsData?.type
|
||||||
|
})
|
||||||
|
|
||||||
|
const questionTypeDesc = computed(() => {
|
||||||
|
return menuItems?.[questionType.value]?.title || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const separateItemListBody = computed(() => {
|
||||||
|
try {
|
||||||
|
const aggregation = _cloneDeep(props?.StatisticsData?.data?.aggregation)
|
||||||
|
const submitionCount = props?.StatisticsData?.data?.submitionCount
|
||||||
|
const summaryList = summaryItemConfig[questionType.value]
|
||||||
|
// 增加聚合信息
|
||||||
|
if (summaryList?.length) {
|
||||||
|
summaryList.forEach((item, index) => {
|
||||||
|
const { type, text, field, max, min } = item
|
||||||
|
if (text && field && type === summaryType.between) {
|
||||||
|
aggregation.push({
|
||||||
|
id: `summary_${index}`,
|
||||||
|
text,
|
||||||
|
count: aggregation.reduce((n, item) => {
|
||||||
|
if (item[field] >= min && item[field] <= max) {
|
||||||
|
return n + item.count
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
aggregation?.map((item) => {
|
||||||
|
const { id, count, text } = item
|
||||||
|
const percent = submitionCount ? `${((count / submitionCount) * 100).toFixed(1)}%` : '0%'
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
count,
|
||||||
|
text,
|
||||||
|
percent
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const separateItemState = reactive({
|
||||||
|
tableData: {
|
||||||
|
total: 0,
|
||||||
|
listHead: separateItemListHead,
|
||||||
|
listBody: separateItemListBody
|
||||||
|
},
|
||||||
|
tableMinHeight: '0px'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { tableData, tableMinHeight } = toRefs(separateItemState)
|
||||||
|
|
||||||
|
const titlePoppverShow = ref(false)
|
||||||
|
const titleRef = ref(null)
|
||||||
|
|
||||||
|
const titleResize = () => {
|
||||||
|
if (titleRef.value?.scrollWidth > titleRef.value?.offsetWidth) {
|
||||||
|
titlePoppverShow.value = true
|
||||||
|
} else {
|
||||||
|
titlePoppverShow.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chartRef, chartTypeList, chartType, chartData } = useStatisticsItemChart({
|
||||||
|
questionType,
|
||||||
|
data: props?.StatisticsData?.data
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 需要获取图表dom,所以得在mounted中执行
|
||||||
|
const { changeType, resize: chartResize } = useCharts(
|
||||||
|
chartRef.value,
|
||||||
|
chartType.value,
|
||||||
|
chartData.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const { destroy } = useResizeObserver(chartRef.value, () => {
|
||||||
|
chartResize()
|
||||||
|
titleResize()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图型切换
|
||||||
|
watch(chartType, () => {
|
||||||
|
changeType(chartType.value, chartData.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 销毁resizeObserver
|
||||||
|
onUnmounted(destroy)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.separate-item {
|
||||||
|
padding: 32px 12px;
|
||||||
|
border-bottom: 1px solid #efefef;
|
||||||
|
|
||||||
|
&:nth-last-of-type(1) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 7px 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 50px;
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: auto;
|
||||||
|
width: 50%;
|
||||||
|
min-width: 300px;
|
||||||
|
height: 320px;
|
||||||
|
max-width: 1000px;
|
||||||
|
box-shadow: 0 2px 8px -2px rgba(136, 136, 157, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
.chart-type-list {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: auto;
|
||||||
|
width: 50%;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1660px) {
|
||||||
|
&-content {
|
||||||
|
gap: 80px;
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
207
web/src/management/pages/analysis/pages/DataTablePage.vue
Normal file
207
web/src/management/pages/analysis/pages/DataTablePage.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="data-table-page">
|
||||||
|
<template v-if="tableData.total">
|
||||||
|
<div class="menus">
|
||||||
|
<el-switch
|
||||||
|
:model-value="isShowOriginData"
|
||||||
|
active-text="是否展示原数据"
|
||||||
|
@input="onIsShowOriginChange"
|
||||||
|
>
|
||||||
|
</el-switch>
|
||||||
|
<div style="display: flex; justify-content: flex-end">
|
||||||
|
<el-switch
|
||||||
|
:model-value="isDownloadDesensitive"
|
||||||
|
active-text="是否下载脱敏数据"
|
||||||
|
@input="onisDownloadDesensitive"
|
||||||
|
style="margin-right: 20px"
|
||||||
|
>
|
||||||
|
</el-switch>
|
||||||
|
<el-button type="primary" @click="onDownload">导出数据</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="tableData.total">
|
||||||
|
<DataTable :main-table-loading :table-data />
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
popper-class="analysis-pagination"
|
||||||
|
:total="tableData.total"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</template>
|
||||||
|
<div v-else>
|
||||||
|
<EmptyIndex :data="noDataConfig" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, toRefs, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||||
|
import { getRecycleList, downloadSurvey } from '@/management/api/analysis'
|
||||||
|
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||||
|
import DataTable from '../components/DataTable.vue'
|
||||||
|
|
||||||
|
const dataTableState = reactive({
|
||||||
|
mainTableLoading: false,
|
||||||
|
tableData: {
|
||||||
|
total: 0,
|
||||||
|
listHead: [],
|
||||||
|
listBody: []
|
||||||
|
},
|
||||||
|
currentPage: 1,
|
||||||
|
isShowOriginData: false,
|
||||||
|
tmpIsShowOriginData: false,
|
||||||
|
isDownloadDesensitive: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mainTableLoading, tableData, isShowOriginData, isDownloadDesensitive } = toRefs(dataTableState)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatHead = (listHead) => {
|
||||||
|
const head = []
|
||||||
|
|
||||||
|
listHead.forEach((headItem) => {
|
||||||
|
head.push({
|
||||||
|
field: headItem.field,
|
||||||
|
title: headItem.title
|
||||||
|
})
|
||||||
|
|
||||||
|
if (headItem.othersCode?.length) {
|
||||||
|
headItem.othersCode.forEach((item) => {
|
||||||
|
head.push({
|
||||||
|
field: item.code,
|
||||||
|
title: `${headItem.title}-${item.option}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
|
const onIsShowOriginChange = async (data) => {
|
||||||
|
if (dataTableState.mainTableLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataTableState.tmpIsShowOriginData = data
|
||||||
|
await init()
|
||||||
|
dataTableState.isShowOriginData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = async (page) => {
|
||||||
|
if (dataTableState.mainTableLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataTableState.currentPage = page
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
if (!route.params.id) {
|
||||||
|
ElMessage.error('没有传入问卷参数~')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataTableState.mainTableLoading = true
|
||||||
|
try {
|
||||||
|
const res = await getRecycleList({
|
||||||
|
page: dataTableState.currentPage,
|
||||||
|
surveyId: route.params.id,
|
||||||
|
isDesensitive: !dataTableState.tmpIsShowOriginData // 发起请求的时候,isShowOriginData还没改变,暂存了一个字段
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
const listHead = formatHead(res.data.listHead)
|
||||||
|
dataTableState.tableData = { ...res.data, listHead }
|
||||||
|
dataTableState.mainTableLoading = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('查询回收数据失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
const onDownload = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('是否确认下载?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('取消下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exportData()
|
||||||
|
gotoDownloadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const gotoDownloadList = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('计算中,是否前往下载中心?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('取消跳转')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push('/survey/download')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onisDownloadDesensitive = async () => {
|
||||||
|
if (dataTableState.isDownloadDesensitive) {
|
||||||
|
dataTableState.isDownloadDesensitive = false
|
||||||
|
} else {
|
||||||
|
dataTableState.isDownloadDesensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const exportData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await downloadSurvey({
|
||||||
|
surveyId: String(route.params.id),
|
||||||
|
isDesensitive: dataTableState.isDownloadDesensitive
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('下载成功')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('下载失败')
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.data-table-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menus {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-pagination) {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="data.length" class="separate-statis-page">
|
||||||
|
<StatisticsItem v-for="StatisticsData in data" :key="StatisticsData.field" :StatisticsData />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<EmptyIndex :data="noDataConfig" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||||
|
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||||
|
import { getStatisticList } from '@/management/api/analysis'
|
||||||
|
import StatisticsItem from '../components/StatisticsItem.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getStatisticList({
|
||||||
|
surveyId: route.params.id
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
data.value = res?.data || []
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.errmsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '查询回收数据失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(initData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.separate-statis-page {
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -59,8 +59,8 @@ const emit = defineEmits<Emit>()
|
|||||||
|
|
||||||
// 静态配置设置动态值
|
// 静态配置设置动态值
|
||||||
const formatValue = ({ item, moduleConfig }: any) => {
|
const formatValue = ({ item, moduleConfig }: any) => {
|
||||||
if (_isFunction(item.valueAdapter)) {
|
if (_isFunction(item.valueGetter)) {
|
||||||
const value = item.valueAdapter({ moduleConfig })
|
const value = item.valueGetter({ moduleConfig })
|
||||||
|
|
||||||
return value
|
return value
|
||||||
} else {
|
} else {
|
||||||
@ -82,8 +82,8 @@ const init = ref<boolean>(true)
|
|||||||
const components = shallowRef<any>({})
|
const components = shallowRef<any>({})
|
||||||
|
|
||||||
const handleFormChange = (data: any, formConfig: any) => {
|
const handleFormChange = (data: any, formConfig: any) => {
|
||||||
if (_isFunction(formConfig?.setterAdapter)) {
|
if (_isFunction(formConfig?.valueSetter)) {
|
||||||
const resultData = formConfig.setterAdapter(data)
|
const resultData = formConfig.valueSetter(data)
|
||||||
|
|
||||||
if (Array.isArray(resultData)) {
|
if (Array.isArray(resultData)) {
|
||||||
resultData.forEach((item) => {
|
resultData.forEach((item) => {
|
||||||
|
@ -139,7 +139,7 @@ import {
|
|||||||
noSearchDataConfig,
|
noSearchDataConfig,
|
||||||
selectOptionsDict,
|
selectOptionsDict,
|
||||||
buttonOptionsDict
|
buttonOptionsDict
|
||||||
} from '../config'
|
} from '@/management/config/listConfig'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, withDefaults } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { type IMember, type ListItem } from '@/management/utils/types/workSpace'
|
import { type IMember, type ListItem } from '@/management/utils/types/workSpace'
|
||||||
import OperationSelect from './OperationSelect.vue'
|
import OperationSelect from './OperationSelect.vue'
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, withDefaults } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import MemberList from './MemberList.vue'
|
import MemberList from './MemberList.vue'
|
||||||
import { getUserList } from '@/management/api/space'
|
import { getUserList } from '@/management/api/space'
|
||||||
|
@ -75,7 +75,7 @@ import { useStore } from 'vuex'
|
|||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
import { get, map } from 'lodash-es'
|
import { get, map } from 'lodash-es'
|
||||||
import { spaceListConfig } from '../config'
|
import { spaceListConfig } from '@/management/config/listConfig'
|
||||||
import SpaceModify from './SpaceModify.vue'
|
import SpaceModify from './SpaceModify.vue'
|
||||||
import { UserRole } from '@/management/utils/types/workSpace'
|
import { UserRole } from '@/management/utils/types/workSpace'
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { statusMaps } from '../config'
|
import { statusMaps } from '@/management/config/listConfig'
|
||||||
export default {
|
export default {
|
||||||
name: 'StateModule',
|
name: 'StateModule',
|
||||||
props: {
|
props: {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { type as surveyType } from '../config'
|
import { type as surveyType } from '@/management/config/listConfig'
|
||||||
export default {
|
export default {
|
||||||
name: 'TagModule',
|
name: 'TagModule',
|
||||||
props: {
|
props: {
|
||||||
|
@ -2,6 +2,7 @@ import { createRouter, createWebHistory, type RouteLocationNormalized, type Navi
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { useStore, type Store } from 'vuex'
|
import { useStore, type Store } from 'vuex'
|
||||||
import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||||
|
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
@ -100,11 +101,34 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: '/survey/:id/analysis',
|
path: '/survey/:id/analysis',
|
||||||
name: 'analysisPage',
|
name: 'analysisPage',
|
||||||
|
redirect: {
|
||||||
|
name: analysisTypeMap.dataTable
|
||||||
|
},
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true,
|
needLogin: true,
|
||||||
permissions: [SurveyPermissions.DataManage]
|
permissions: [SurveyPermissions.DataManage]
|
||||||
},
|
},
|
||||||
component: () => import('../pages/analysis/AnalysisPage.vue')
|
component: () => import('../pages/analysis/AnalysisPage.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: analysisTypeMap.dataTable,
|
||||||
|
name: analysisTypeMap.dataTable,
|
||||||
|
meta: {
|
||||||
|
needLogin: true,
|
||||||
|
premissions: [SurveyPermissions.DataManage]
|
||||||
|
},
|
||||||
|
component: () => import('../pages/analysis/pages/DataTablePage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: analysisTypeMap.separateStatistics,
|
||||||
|
name: analysisTypeMap.separateStatistics,
|
||||||
|
meta: {
|
||||||
|
needLogin: true,
|
||||||
|
premissions: [SurveyPermissions.DataManage]
|
||||||
|
},
|
||||||
|
component: () => import('../pages/analysis/pages/SeparateStatisticsPage.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/survey/:id/publish',
|
path: '/survey/:id/publish',
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'iconfont'; /* Project id 4263849 */
|
font-family: 'iconfont';
|
||||||
|
/* Project id 4263849 */
|
||||||
src:
|
src:
|
||||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff2?t=1716556097756') format('woff2'),
|
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff2?t=1717580126029') format('woff2'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.woff?t=1716556097756') format('woff'),
|
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff?t=1717580126029') format('woff'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_xfsn9z31epc.ttf?t=1716556097756') format('truetype');
|
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.ttf?t=1717580126029') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -125,15 +126,39 @@
|
|||||||
.icon-erweima:before {
|
.icon-erweima:before {
|
||||||
content: '\e6c0';
|
content: '\e6c0';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-yangshishezhi:before {
|
.icon-yangshishezhi:before {
|
||||||
content: '\e6e6';
|
content: '\e6e6';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-NPSpingfen::before {
|
.icon-NPSpingfen::before {
|
||||||
content: '\e6e7';
|
content: '\e6e7';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-wodekongjian::before {
|
.icon-wodekongjian::before {
|
||||||
content: '\e6ee';
|
content: '\e6ee';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-tuanduikongjian::before {
|
.icon-tuanduikongjian::before {
|
||||||
content: '\e6ec';
|
content: '\e6ec';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-shujuliebiao:before {
|
||||||
|
content: '\e6f2';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fentitongji:before {
|
||||||
|
content: '\e6f3';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-bar:before {
|
||||||
|
content: '\e600';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pie:before {
|
||||||
|
content: '\e606';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gauge:before {
|
||||||
|
content: '\e6db';
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { defaultQuestionConfig } from '../config/questionConfig'
|
import { defaultQuestionConfig } from '../config/questionConfig'
|
||||||
import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es'
|
import { map as _map } from 'lodash-es'
|
||||||
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
import questionLoader from '@/materials/questions/questionLoader'
|
||||||
|
|
||||||
const generateQuestionField = () => {
|
const generateQuestionField = () => {
|
||||||
const num = Math.floor(Math.random() * 1000)
|
const num = Math.floor(Math.random() * 1000)
|
||||||
return `data${num}`
|
return `data${num}`
|
||||||
@ -23,15 +24,6 @@ const generateHash = (hashList) => {
|
|||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptions(type) {
|
|
||||||
const options = [].concat({ ..._cloneDeep(defaultQuestionConfig) }.options)
|
|
||||||
if (type === QUESTION_TYPE.BINARY_CHOICE) {
|
|
||||||
options[0].text = '对'
|
|
||||||
options[1].text = '错'
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getNewField = (fields) => {
|
export const getNewField = (fields) => {
|
||||||
let field = generateQuestionField()
|
let field = generateQuestionField()
|
||||||
let isFieldExists = fields.includes(field)
|
let isFieldExists = fields.includes(field)
|
||||||
@ -44,16 +36,30 @@ export const getNewField = (fields) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getQuestionByType = (type, fields) => {
|
export const getQuestionByType = (type, fields) => {
|
||||||
const newQuestion = _cloneDeep(defaultQuestionConfig)
|
const questionMeta = questionLoader.getMeta(type)
|
||||||
newQuestion.type = type
|
const { attrs } = questionMeta
|
||||||
newQuestion.field = getNewField(fields)
|
let newQuestion = defaultQuestionConfig
|
||||||
newQuestion.options = getOptions(type)
|
if( attrs ) {
|
||||||
const hashList = []
|
let questionSchema = {}
|
||||||
for (const option of newQuestion.options) {
|
attrs.forEach(element => {
|
||||||
const hash = generateHash(hashList)
|
questionSchema[element.name] = element.defaultValue
|
||||||
hashList.push(hash)
|
});
|
||||||
option.hash = hash
|
newQuestion = questionSchema
|
||||||
|
} else {
|
||||||
|
newQuestion = defaultQuestionConfig
|
||||||
|
newQuestion.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newQuestion.field = getNewField(fields) // 动态生成题目id
|
||||||
|
if('options ' in newQuestion) { // 动态更新选项的hash-id
|
||||||
|
const hashList = []
|
||||||
|
for (const option of newQuestion.options) {
|
||||||
|
const hash = generateHash(hashList)
|
||||||
|
hashList.push(hash)
|
||||||
|
option.hash = hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return newQuestion
|
return newQuestion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,13 +109,13 @@ export default defineComponent({
|
|||||||
<EditOptions
|
<EditOptions
|
||||||
moduleConfig={props.moduleConfig}
|
moduleConfig={props.moduleConfig}
|
||||||
editConfigure={questionMeta?.editConfigure}
|
editConfigure={questionMeta?.editConfigure}
|
||||||
|
onChange={this.onChange}
|
||||||
>
|
>
|
||||||
<dynamicComponent
|
<dynamicComponent
|
||||||
readonly
|
readonly
|
||||||
{...props}
|
{...props}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
change={this.onChange}
|
|
||||||
/>
|
/>
|
||||||
</EditOptions>
|
</EditOptions>
|
||||||
) : (
|
) : (
|
||||||
@ -124,7 +124,6 @@ export default defineComponent({
|
|||||||
{...props}
|
{...props}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
change={this.onChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@ export default {
|
|||||||
tip: '题目下方分割线,仅在移动端展示。'
|
tip: '题目下方分割线,仅在移动端展示。'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
valueAdapter({ moduleConfig }) {
|
valueGetter({ moduleConfig }) {
|
||||||
return _pick(
|
return _pick(
|
||||||
moduleConfig,
|
moduleConfig,
|
||||||
this.options.map((item) => item.key)
|
this.options.map((item) => item.key)
|
||||||
|
@ -4,6 +4,75 @@ const meta = {
|
|||||||
title: '判断题',
|
title: '判断题',
|
||||||
type: 'binary-choice',
|
type: 'binary-choice',
|
||||||
componentName: 'BinaryChoiceModule',
|
componentName: 'BinaryChoiceModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'binary-choice'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
|
propType: Array,
|
||||||
|
description: '这是用于描述选项',
|
||||||
|
defaultValue: [
|
||||||
|
{
|
||||||
|
"text": "对",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "错",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115020"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
formConfig: [basicConfig],
|
formConfig: [basicConfig],
|
||||||
editConfigure: {
|
editConfigure: {
|
||||||
optionEdit: {
|
optionEdit: {
|
||||||
|
@ -4,13 +4,93 @@ const meta = {
|
|||||||
title: '多选',
|
title: '多选',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
componentName: 'CheckBoxModule',
|
componentName: 'CheckBoxModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'checkbox'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
|
propType: Array,
|
||||||
|
description: '这是用于描述选项',
|
||||||
|
defaultValue: [
|
||||||
|
{
|
||||||
|
"text": "选项1",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "选项2",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115020"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minNum',
|
||||||
|
propType: Number,
|
||||||
|
description: '最少选择数',
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxNum',
|
||||||
|
propType: Number,
|
||||||
|
description: '最多选择数',
|
||||||
|
defaultValue: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
name: 'optionConfig',
|
name: 'optionConfig',
|
||||||
title: '选项配置',
|
title: '选项配置',
|
||||||
type: 'Customed',
|
type: 'Customed',
|
||||||
key: 'optionConfig',
|
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
label: '至少选择数',
|
label: '至少选择数',
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { get as _get } from 'lodash-es'
|
import { get as _get } from 'lodash-es'
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
min: {
|
min: {
|
||||||
@ -80,9 +79,6 @@ export default {
|
|||||||
this.initRange()
|
this.initRange()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
|
||||||
currentEditKey: 'edit/currentEditKey'
|
|
||||||
}),
|
|
||||||
innerVisible: {
|
innerVisible: {
|
||||||
get() {
|
get() {
|
||||||
return this.visible
|
return this.visible
|
||||||
@ -119,9 +115,8 @@ export default {
|
|||||||
explain: item.explain
|
explain: item.explain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const paramsKey = `rangeConfig`
|
|
||||||
const payload = {
|
const payload = {
|
||||||
key: `${this.currentEditKey}.${paramsKey}`,
|
key: `rangeConfig`,
|
||||||
value: res
|
value: res
|
||||||
}
|
}
|
||||||
this.$emit('confirm', payload)
|
this.$emit('confirm', payload)
|
||||||
|
@ -39,20 +39,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import { cloneDeep as _cloneDeep } from 'lodash-es'
|
import { cloneDeep as _cloneDeep } from 'lodash-es'
|
||||||
import RichEditor from '@/common/Editor/RichEditor.vue'
|
import RichEditor from '@/common/Editor/RichEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OptionEdit',
|
name: 'OptionEdit',
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
currentEditKey: 'edit/currentEditKey'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
optionList: {
|
optionList: {
|
||||||
type: Array
|
type: Array,
|
||||||
|
default: () => []
|
||||||
},
|
},
|
||||||
isShowOperation: {
|
isShowOperation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -92,7 +87,7 @@ export default {
|
|||||||
handleChange(index, value) {
|
handleChange(index, value) {
|
||||||
// 更新单个选项文案
|
// 更新单个选项文案
|
||||||
const optionKey = `options[${index}].text`
|
const optionKey = `options[${index}].text`
|
||||||
const key = `${this.currentEditKey}.${optionKey}`
|
const key = `${optionKey}`
|
||||||
this.$emit('change', { key, value })
|
this.$emit('change', { key, value })
|
||||||
},
|
},
|
||||||
onAddOption(index) {
|
onAddOption(index) {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { defineComponent, ref, computed, onMounted } from 'vue'
|
import { defineComponent, ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
import store from '@/management/store'
|
|
||||||
|
|
||||||
import OptionEdit from './Options/OptionEdit.vue'
|
import OptionEdit from './Options/OptionEdit.vue'
|
||||||
import OptionEditBar from './Options/OptionEditBar.vue'
|
import OptionEditBar from './Options/OptionEditBar.vue'
|
||||||
import UseOptionBase from './Options/UseOptionBase'
|
import UseOptionBase from './Options/UseOptionBase'
|
||||||
@ -10,7 +8,6 @@ export default defineComponent({
|
|||||||
name: 'EditOptions',
|
name: 'EditOptions',
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
currentEditKey: store.getters['edit/currentEditKey'],
|
|
||||||
moduleConfig: computed(() => this.moduleConfig)
|
moduleConfig: computed(() => this.moduleConfig)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -24,10 +21,7 @@ export default defineComponent({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props, { slots }) {
|
setup(props, { slots, emit }) {
|
||||||
const currentEditKey = computed(() => {
|
|
||||||
return store.getters['edit/currentEditKey']
|
|
||||||
})
|
|
||||||
const getOptions = computed(() => {
|
const getOptions = computed(() => {
|
||||||
return props.moduleConfig.options
|
return props.moduleConfig.options
|
||||||
})
|
})
|
||||||
@ -44,12 +38,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
const handleOptionChange = (value) => {
|
const handleOptionChange = (value) => {
|
||||||
const optionKey = `options`
|
const optionKey = `options`
|
||||||
const key = `${currentEditKey.value}.${optionKey}`
|
const key = `${optionKey}`
|
||||||
handleChange({ key, value })
|
handleChange({ key, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = ({ key, value }) => {
|
const handleChange = ({ key, value }) => {
|
||||||
store.dispatch('edit/changeSchema', { key, value })
|
emit('change', { key, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAdvancedConfig = ref(false)
|
const hasAdvancedConfig = ref(false)
|
||||||
|
@ -4,6 +4,93 @@ export const meta = {
|
|||||||
title: '单行输入框',
|
title: '单行输入框',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
componentName: 'InputModule',
|
componentName: 'InputModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'placeholder',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述引导提示文案',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'valid',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述内容限制格式',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'numberRange',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于数字限制',
|
||||||
|
defaultValue: {
|
||||||
|
max: {
|
||||||
|
placeholder: '1000',
|
||||||
|
value: 1000
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
placeholder: '0',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'textRange',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于字数限制',
|
||||||
|
defaultValue: {
|
||||||
|
max: {
|
||||||
|
placeholder: '500',
|
||||||
|
value: 500
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
placeholder: '0',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
@ -42,28 +129,14 @@ export const meta = {
|
|||||||
name: 'numberRange',
|
name: 'numberRange',
|
||||||
title: '数字限制',
|
title: '数字限制',
|
||||||
type: 'RangeSetter',
|
type: 'RangeSetter',
|
||||||
options: [],
|
|
||||||
key: 'numberRange',
|
key: 'numberRange',
|
||||||
value: [],
|
value: [],
|
||||||
cleanKeys: {
|
|
||||||
numberRange: {
|
|
||||||
min: {
|
|
||||||
placeholder: '0',
|
|
||||||
value: 0
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
placeholder: '1000',
|
|
||||||
value: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
relyFunc: (data) => data.valid && data.valid === 'n'
|
relyFunc: (data) => data.valid && data.valid === 'n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'textRange',
|
name: 'textRange',
|
||||||
title: '字数限制',
|
title: '字数限制',
|
||||||
type: 'RangeSetter',
|
type: 'RangeSetter',
|
||||||
options: [],
|
|
||||||
key: 'textRange',
|
key: 'textRange',
|
||||||
value: []
|
value: []
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,8 @@ export default defineComponent({
|
|||||||
max,
|
max,
|
||||||
readonly,
|
readonly,
|
||||||
rangeConfig,
|
rangeConfig,
|
||||||
onMoreDataChange
|
onMoreDataChange,
|
||||||
|
selectMoreView
|
||||||
} = this
|
} = this
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -139,21 +140,20 @@ export default defineComponent({
|
|||||||
iconClass="number"
|
iconClass="number"
|
||||||
onChange={confirmNps}
|
onChange={confirmNps}
|
||||||
class={!readonly ? 'radio-nps-hover' : ''}
|
class={!readonly ? 'radio-nps-hover' : ''}
|
||||||
>
|
/>
|
||||||
{isShowInput && (
|
{isShowInput && (
|
||||||
<selectMoreView
|
<selectMoreView
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
key={`${field}_${rating}`}
|
key={`${field}_${rating}`}
|
||||||
moduleConfig={{
|
moduleConfig={{
|
||||||
type: 'selectMoreModule',
|
type: 'selectMoreModule',
|
||||||
field: `${field}_${rating}`,
|
field: `${field}_${rating}`,
|
||||||
placeholder: rangeConfig[rating]?.text,
|
placeholder: rangeConfig[rating]?.text,
|
||||||
value: rangeConfig[rating]?.othersValue || ''
|
value: rangeConfig[rating]?.othersValue || ''
|
||||||
}}
|
}}
|
||||||
onChange={(e) => onMoreDataChange(e)}
|
onChange={(e) => onMoreDataChange(e)}
|
||||||
></selectMoreView>
|
></selectMoreView>
|
||||||
)}
|
)}
|
||||||
</BaseRate>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,84 @@ import { ElMessage } from 'element-plus'
|
|||||||
import basicConfig from '@materials/questions/common/config/basicConfig'
|
import basicConfig from '@materials/questions/common/config/basicConfig'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: '评分',
|
title: 'nps评分',
|
||||||
type: 'radio-nps',
|
type: 'radio-nps',
|
||||||
componentName: 'NpsModule',
|
componentName: 'NpsModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'radio-nps'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'min',
|
||||||
|
propType: Number,
|
||||||
|
description: '这是用于描述NPS量表最小值',
|
||||||
|
defaultValue: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'max',
|
||||||
|
propType: Number,
|
||||||
|
description: '这是用于描述NPS量表最大值',
|
||||||
|
defaultValue: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'minMsg',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述最小值文案',
|
||||||
|
defaultValue: '极不满意'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxMsg',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述最大值文案',
|
||||||
|
defaultValue: '十分满意'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rangeConfig',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于描述评分高级设置',
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
@ -17,11 +92,12 @@ const meta = {
|
|||||||
value: v,
|
value: v,
|
||||||
label: v
|
label: v
|
||||||
})),
|
})),
|
||||||
valueSetter: (val, moduleConfig) => {
|
validate: (val, moduleConfig) => {
|
||||||
if (moduleConfig['max'] && val >= moduleConfig['max']) {
|
if (moduleConfig['max'] && val >= moduleConfig['max']) {
|
||||||
ElMessage.info('最小值不可大于最大值')
|
ElMessage.info('最小值不可大于最大值')
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -34,11 +110,12 @@ const meta = {
|
|||||||
value: v,
|
value: v,
|
||||||
label: v
|
label: v
|
||||||
})),
|
})),
|
||||||
valueSetter: (val, moduleConfig) => {
|
validate: (val, moduleConfig) => {
|
||||||
if (moduleConfig['min'] && val <= moduleConfig['min']) {
|
if (moduleConfig['min'] && val <= moduleConfig['min']) {
|
||||||
ElMessage.info('最大值不可小于最小值')
|
ElMessage.info('最大值不可小于最小值')
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -18,10 +18,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 930px) {
|
.question-block {
|
||||||
:deep(.question-block) {
|
padding: 0!important;
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.radio-nps-hover {
|
.radio-nps-hover {
|
||||||
.rate-item {
|
.rate-item {
|
||||||
|
@ -4,7 +4,13 @@ const meta = {
|
|||||||
title: '单选',
|
title: '单选',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
componentName: 'RadioModule',
|
componentName: 'RadioModule',
|
||||||
props: [
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
propType: 'String',
|
propType: 'String',
|
||||||
@ -15,14 +21,57 @@ const meta = {
|
|||||||
name: 'type',
|
name: 'type',
|
||||||
propType: 'String',
|
propType: 'String',
|
||||||
description: '这是用于描述题目类型',
|
description: '这是用于描述题目类型',
|
||||||
defaultValue: '标题一'
|
defaultValue: 'radio'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'extraOptions',
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
propType: Array,
|
propType: Array,
|
||||||
description: '这是用于固定选项配置',
|
description: '这是用于描述选项',
|
||||||
defaultValue: []
|
defaultValue: [
|
||||||
}
|
{
|
||||||
|
"text": "选项1",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "选项2",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115020"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
|
@ -4,6 +4,74 @@ const meta = {
|
|||||||
title: '评分',
|
title: '评分',
|
||||||
type: 'radio-star',
|
type: 'radio-star',
|
||||||
componentName: 'StarModule',
|
componentName: 'StarModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'radio-star'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'starMin',
|
||||||
|
propType: Number,
|
||||||
|
description: '这是用于描述评分最小值',
|
||||||
|
defaultValue: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'starMax',
|
||||||
|
propType: Number,
|
||||||
|
description: '这是用于描述评分最大值',
|
||||||
|
defaultValue: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'starStyle',
|
||||||
|
propType: String,
|
||||||
|
description: '',
|
||||||
|
defaultValue: 'star',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rangeConfig',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于描述评分高级设置',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,92 @@ const meta = {
|
|||||||
title: '多行输入框',
|
title: '多行输入框',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
componentName: 'TextareaModule',
|
componentName: 'TextareaModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'textarea'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'placeholder',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述引导提示文案',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'valid',
|
||||||
|
propType: String,
|
||||||
|
description: '这是用于描述内容限制格式',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'numberRange',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于数字限制',
|
||||||
|
defaultValue: {
|
||||||
|
max: {
|
||||||
|
placeholder: '1000',
|
||||||
|
value: 1000
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
placeholder: '0',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textRange',
|
||||||
|
propType: Object,
|
||||||
|
description: '这是用于字数限制',
|
||||||
|
defaultValue: {
|
||||||
|
max: {
|
||||||
|
placeholder: '500',
|
||||||
|
value: 500
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
placeholder: '0',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
@ -42,28 +128,14 @@ const meta = {
|
|||||||
name: 'numberRange',
|
name: 'numberRange',
|
||||||
title: '数字限制',
|
title: '数字限制',
|
||||||
type: 'RangeSetter',
|
type: 'RangeSetter',
|
||||||
options: [],
|
|
||||||
key: 'numberRange',
|
key: 'numberRange',
|
||||||
value: [],
|
value: [],
|
||||||
cleanKeys: {
|
|
||||||
numberRange: {
|
|
||||||
min: {
|
|
||||||
placeholder: '0',
|
|
||||||
value: 0
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
placeholder: '1000',
|
|
||||||
value: 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
relyFunc: (data) => data.valid && data.valid === 'n'
|
relyFunc: (data) => data.valid && data.valid === 'n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'textRange',
|
name: 'textRange',
|
||||||
title: '字数限制',
|
title: '字数限制',
|
||||||
type: 'RangeSetter',
|
type: 'RangeSetter',
|
||||||
options: [],
|
|
||||||
key: 'textRange',
|
key: 'textRange',
|
||||||
value: []
|
value: []
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,87 @@ const meta = {
|
|||||||
title: '投票',
|
title: '投票',
|
||||||
type: 'vote',
|
type: 'vote',
|
||||||
componentName: 'VoteModule',
|
componentName: 'VoteModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目id',
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目标题',
|
||||||
|
defaultValue: '标题一'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'vote'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
|
propType: Array,
|
||||||
|
description: '这是用于描述选项',
|
||||||
|
defaultValue: [
|
||||||
|
{
|
||||||
|
"text": "选项1",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "选项2",
|
||||||
|
"imageUrl": "",
|
||||||
|
"others": false,
|
||||||
|
"mustOthers": false,
|
||||||
|
"othersKey": "",
|
||||||
|
"placeholderDesc": "",
|
||||||
|
"hash": "115020"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minNum',
|
||||||
|
propType: Number,
|
||||||
|
description: '最少选择数',
|
||||||
|
defaultValue: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxNum',
|
||||||
|
propType: Number,
|
||||||
|
description: '最多选择数',
|
||||||
|
defaultValue: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
formConfig: [
|
formConfig: [
|
||||||
basicConfig,
|
basicConfig,
|
||||||
{
|
{
|
||||||
@ -18,7 +99,7 @@ const meta = {
|
|||||||
key: 'innerType',
|
key: 'innerType',
|
||||||
value: false,
|
value: false,
|
||||||
// 输入转换
|
// 输入转换
|
||||||
valueAdapter({ moduleConfig }) {
|
valueGetter({ moduleConfig }) {
|
||||||
if (moduleConfig.innerType === 'checkbox') {
|
if (moduleConfig.innerType === 'checkbox') {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@ -26,7 +107,7 @@ const meta = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 输出转换
|
// 输出转换
|
||||||
setterAdapter({ value }) {
|
valueSetter({ value }) {
|
||||||
return {
|
return {
|
||||||
key: 'innerType',
|
key: 'innerType',
|
||||||
value: value ? 'checkbox' : 'radio'
|
value: value ? 'checkbox' : 'radio'
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
import { QUESTION_TYPE } from '@/common/typeEnum'
|
|
||||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -25,18 +24,11 @@ interface Emit {
|
|||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
const emit = defineEmits<Emit>()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const setterTypes = [QUESTION_TYPE.CHECKBOX, QUESTION_TYPE.VOTE]
|
const modelValue = ref(Number(props.formConfig.value) || 0)
|
||||||
const modelValue = ref(props.formConfig.value || 0)
|
|
||||||
const minModelValue = computed(() => {
|
const minModelValue = computed(() => {
|
||||||
const { min } = props.formConfig
|
const { min } = props.formConfig
|
||||||
const { type } = props.moduleConfig
|
if (min) {
|
||||||
|
if (typeof min === 'function') {
|
||||||
if (min !== undefined) {
|
|
||||||
if (typeof min === 'string') {
|
|
||||||
return setterTypes.includes(type)
|
|
||||||
? Number(props.moduleConfig[min])
|
|
||||||
: Number(Number(props.moduleConfig[min]) + 1)
|
|
||||||
} else if (typeof props.formConfig.min === 'function') {
|
|
||||||
return min(props.moduleConfig)
|
return min(props.moduleConfig)
|
||||||
} else {
|
} else {
|
||||||
return Number(min)
|
return Number(min)
|
||||||
@ -46,18 +38,14 @@ const minModelValue = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const maxModelValue = computed(() => {
|
const maxModelValue = computed(() => {
|
||||||
const { type } = props.moduleConfig
|
|
||||||
const { max, min } = props.formConfig
|
const { max, min } = props.formConfig
|
||||||
|
|
||||||
if (max) {
|
if (max) {
|
||||||
if (typeof max === 'string') {
|
if (typeof max === 'function') {
|
||||||
return setterTypes.includes(type)
|
|
||||||
? Number(props.moduleConfig[max])
|
|
||||||
: props.moduleConfig[max] - 1
|
|
||||||
} else if (typeof max === 'function') {
|
|
||||||
return max(props.moduleConfig)
|
return max(props.moduleConfig)
|
||||||
|
} else {
|
||||||
|
return Number(max)
|
||||||
}
|
}
|
||||||
return Number(max)
|
|
||||||
} else if (min !== undefined && Array.isArray(props.moduleConfig?.options)) {
|
} else if (min !== undefined && Array.isArray(props.moduleConfig?.options)) {
|
||||||
return props.moduleConfig.options.length
|
return props.moduleConfig.options.length
|
||||||
} else {
|
} else {
|
||||||
|
@ -33,25 +33,15 @@ const emit = defineEmits<Emit>()
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const minModelValue = computed(() => {
|
const minModelValue = computed(() => {
|
||||||
const key = props.formConfig.key
|
|
||||||
const minValue = props.formConfig?.value?.min?.value
|
const minValue = props.formConfig?.value?.min?.value
|
||||||
|
|
||||||
if (key === 'textRange') {
|
return parseInt(minValue)
|
||||||
return parseInt(minValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return minValue || 1
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxModelValue = computed(() => {
|
const maxModelValue = computed(() => {
|
||||||
const key = props.formConfig.key
|
|
||||||
const maxValue = props.formConfig?.value?.max?.value
|
const maxValue = props.formConfig?.value?.max?.value
|
||||||
|
|
||||||
if (key === 'textRange') {
|
return maxValue ? parseInt(maxValue) : 1
|
||||||
return parseInt(maxValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxValue || 1
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleRangeChange = (eventType: 'max' | 'min', value: number) => {
|
const handleRangeChange = (eventType: 'max' | 'min', value: number) => {
|
||||||
|
@ -61,10 +61,10 @@ const modelValue = ref(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleSelectChange = (value: string) => {
|
const handleSelectChange = (value: string) => {
|
||||||
const { key, valueSetter } = props.formConfig
|
const { key, validate } = props.formConfig
|
||||||
|
|
||||||
if (valueSetter && typeof valueSetter == 'function') {
|
if (validate && typeof validate == 'function') {
|
||||||
let verification: boolean = valueSetter(value, props.moduleConfig)
|
let verification: boolean = validate(value, props.moduleConfig)
|
||||||
|
|
||||||
if (!verification) {
|
if (!verification) {
|
||||||
return
|
return
|
||||||
@ -72,7 +72,6 @@ const handleSelectChange = (value: string) => {
|
|||||||
|
|
||||||
modelValue.value = props.moduleConfig[key]
|
modelValue.value = props.moduleConfig[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export default function ({ dataConf }) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||||
if (/checkbox/.test(type) || innerType === QUESTION_TYPE.CHECKBOX) {
|
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
|
||||||
value = value ? [value] : []
|
value = value ? [value] : []
|
||||||
}
|
}
|
||||||
formValues[key] = value
|
formValues[key] = value
|
||||||
|
@ -2,26 +2,7 @@
|
|||||||
* 处理单题的配置
|
* 处理单题的配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get as _get, map as _map } from 'lodash-es'
|
import { get as _get } from 'lodash-es'
|
||||||
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
|
||||||
// 处理选择题的options
|
|
||||||
function handleOptions(item) {
|
|
||||||
const { type } = item
|
|
||||||
const options = item.options || []
|
|
||||||
|
|
||||||
const arr = _map(options, (optionItem) => {
|
|
||||||
const cleanOption = {}
|
|
||||||
|
|
||||||
// 投票逻辑处理
|
|
||||||
if (type.indexOf(QUESTION_TYPE.VOTE) > -1) {
|
|
||||||
cleanOption.voteCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value: optionItem['hash'], ...optionItem, ...cleanOption }
|
|
||||||
})
|
|
||||||
|
|
||||||
return { options: arr }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (questionConfig) {
|
export default function (questionConfig) {
|
||||||
let dataList = _get(questionConfig, 'dataConf.dataList')
|
let dataList = _get(questionConfig, 'dataConf.dataList')
|
||||||
@ -31,8 +12,7 @@ export default function (questionConfig) {
|
|||||||
[item.field]: {
|
[item.field]: {
|
||||||
indexNumber: '',
|
indexNumber: '',
|
||||||
voteTotal: 0,
|
voteTotal: 0,
|
||||||
...item,
|
...item
|
||||||
...handleOptions(item)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return pre
|
return pre
|
||||||
|
@ -195,17 +195,19 @@ export function generateValidArr(
|
|||||||
|
|
||||||
// 生成选择类或者评分类的题目的更多输入框
|
// 生成选择类或者评分类的题目的更多输入框
|
||||||
const generateOthersKeyMap = (question) => {
|
const generateOthersKeyMap = (question) => {
|
||||||
const { type, field, options, rangeConfig } = question
|
const { type, field } = question
|
||||||
let othersKeyMap = undefined
|
let othersKeyMap = undefined
|
||||||
|
|
||||||
if (RATES.includes(type)) {
|
if (RATES.includes(type)) {
|
||||||
|
const { rangeConfig } = question
|
||||||
othersKeyMap = {}
|
othersKeyMap = {}
|
||||||
for (const key in rangeConfig) {
|
for (const key in rangeConfig) {
|
||||||
if (rangeConfig[key].isShowInput) {
|
if (rangeConfig[key].isShowInput) {
|
||||||
othersKeyMap[`${field}_${key}`] = key
|
othersKeyMap[`${field}_${key}`] = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type.includes(QUESTION_TYPE.RADIO) || type.includes(QUESTION_TYPE.CHECKBOX)) {
|
} else if (type?.includes(QUESTION_TYPE.RADIO) || type?.includes(QUESTION_TYPE.CHECKBOX)) {
|
||||||
|
const { options } = question
|
||||||
othersKeyMap = {}
|
othersKeyMap = {}
|
||||||
options
|
options
|
||||||
.filter((op) => op.others)
|
.filter((op) => op.others)
|
||||||
|
@ -38,7 +38,7 @@ const formValues = computed(() => {
|
|||||||
})
|
})
|
||||||
const questionConfig = computed(() => {
|
const questionConfig = computed(() => {
|
||||||
let moduleConfig = props.moduleConfig
|
let moduleConfig = props.moduleConfig
|
||||||
const { type, field, options, ...rest } = cloneDeep(moduleConfig)
|
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
|
||||||
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
||||||
let alloptions = options
|
let alloptions = options
|
||||||
if (type === QUESTION_TYPE.VOTE || NORMAL_CHOICES.includes(type)) {
|
if (type === QUESTION_TYPE.VOTE || NORMAL_CHOICES.includes(type)) {
|
||||||
@ -57,8 +57,8 @@ const questionConfig = computed(() => {
|
|||||||
moduleConfig.othersValue = unref(othersValue)
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
RATES.includes(type) &&
|
RATES.includes(type) && rest?.rangeConfig &&
|
||||||
Object.keys(rest.rangeConfig).filter((index) => rest.rangeConfig[index].isShowInput).length > 0
|
Object.keys(rest?.rangeConfig).filter((index) => rest?.rangeConfig[index].isShowInput).length > 0
|
||||||
) {
|
) {
|
||||||
let { rangeConfig, othersValue } = useShowInput(field)
|
let { rangeConfig, othersValue } = useShowInput(field)
|
||||||
moduleConfig.rangeConfig = unref(rangeConfig)
|
moduleConfig.rangeConfig = unref(rangeConfig)
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {
|
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {
|
||||||
width = width < PC_W ? width : PC_W
|
width = width < PC_W ? width : PC_W
|
||||||
docEl.className += ' ispc-html'
|
if (!docEl.className.includes('ispc-html')) {
|
||||||
|
docEl.className += ' ispc-html'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var f = Math.min(width / 7.5, 50)
|
var f = Math.min(width / 7.5, 50)
|
||||||
|
@ -62,6 +62,8 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
router.push({ name: 'renderPage' })
|
||||||
|
|
||||||
|
|
||||||
//回填,断点续填
|
//回填,断点续填
|
||||||
const localData = JSON.parse(localStorage.getItem(state.surveyPath + "_questionData"))
|
const localData = JSON.parse(localStorage.getItem(state.surveyPath + "_questionData"))
|
||||||
|
@ -2,7 +2,9 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
import { defineConfig, normalizePath } from 'vite'
|
import { defineConfig, normalizePath } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
import { createMpaPlugin, createPages } from 'vite-plugin-virtual-mpa'
|
import { createMpaPlugin, createPages } from 'vite-plugin-virtual-mpa'
|
||||||
|
|
||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite'
|
||||||
@ -116,6 +118,31 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {}
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
assetFileNames: '[ext]/[name]-[hash].[ext]',
|
||||||
|
chunkFileNames: 'js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'js/[name]-[hash].js',
|
||||||
|
manualChunks(id) {
|
||||||
|
// 建议根据项目生产实际情况进行优化,部分可走cdn或进行小资源包合并
|
||||||
|
if (id.includes('element-plus')) {
|
||||||
|
return 'element-plus'
|
||||||
|
}
|
||||||
|
if (id.includes('wangeditor')) {
|
||||||
|
return 'wangeditor'
|
||||||
|
}
|
||||||
|
if (id.includes('node-forg')) {
|
||||||
|
return 'node-forg'
|
||||||
|
}
|
||||||
|
if (id.includes('echarts')) {
|
||||||
|
return 'echarts'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return 'packages'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user