Compare commits
9 Commits
main
...
feature/wh
Author | SHA1 | Date | |
---|---|---|---|
|
7ab790285f | ||
|
9aec0fb1ea | ||
|
d31f6178c0 | ||
|
5a8e821159 | ||
|
99e21def1c | ||
|
01ce20570f | ||
|
f6569a3899 | ||
|
dab83e4d4c | ||
|
667d13962c |
@ -12,6 +12,7 @@ export enum EXCEPTION_CODE {
|
||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||
|
||||
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
||||
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
||||
|
@ -94,6 +94,23 @@ export interface SubmitConf {
|
||||
msgContent: MsgContent;
|
||||
}
|
||||
|
||||
// 白名单类型
|
||||
export enum WhitelistType {
|
||||
ALL = 'ALL',
|
||||
// 空间成员
|
||||
MEMBER = 'MEMBER',
|
||||
// 自定义
|
||||
CUSTOM = 'CUSTOM',
|
||||
}
|
||||
|
||||
// 白名单用户类型
|
||||
export enum MemberType {
|
||||
// 手机号
|
||||
MOBILE = 'MOBILE',
|
||||
// 邮箱
|
||||
EMAIL = 'EMAIL',
|
||||
}
|
||||
|
||||
export interface BaseConf {
|
||||
begTime: string;
|
||||
endTime: string;
|
||||
@ -101,6 +118,18 @@ export interface BaseConf {
|
||||
answerEndTime: string;
|
||||
tLimit: number;
|
||||
language: string;
|
||||
// 访问密码开关
|
||||
passwordSwitch?: boolean;
|
||||
// 密码
|
||||
password?: string | null;
|
||||
// 白名单类型
|
||||
whitelistType?: WhitelistType;
|
||||
// 白名单用户类型
|
||||
memberType?: MemberType;
|
||||
// 白名单列表
|
||||
whitelist?: string[];
|
||||
// 提示语
|
||||
whitelistTip?: string;
|
||||
}
|
||||
|
||||
export interface SkinConf {
|
||||
|
@ -31,6 +31,7 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/survey')
|
||||
@ -214,6 +215,16 @@ export class SurveyController {
|
||||
surveyMeta.isCollaborated = false;
|
||||
}
|
||||
|
||||
// 白名单相关字段的默认值
|
||||
const baseConf = surveyConf.code?.baseConf;
|
||||
if (baseConf) {
|
||||
baseConf.passwordSwitch = baseConf.passwordSwitch ?? false;
|
||||
baseConf.password = baseConf.password ?? '';
|
||||
baseConf.whitelistType = baseConf.whitelistType ?? WhitelistType.ALL;
|
||||
baseConf.whitelist = baseConf.whitelist ?? [];
|
||||
baseConf.memberType = baseConf.memberType ?? MemberType.MOBILE;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
|
@ -6,6 +6,11 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
|
||||
jest.mock('../services/responseScheme.service');
|
||||
|
||||
@ -16,7 +21,40 @@ describe('ResponseSchemaController', () => {
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ResponseSchemaController],
|
||||
providers: [ResponseSchemaService],
|
||||
providers: [
|
||||
ResponseSchemaService,
|
||||
AuthService,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ResponseSchemaController>(ResponseSchemaController);
|
||||
@ -66,5 +104,146 @@ describe('ResponseSchemaController', () => {
|
||||
new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue(null);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
}),
|
||||
).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交'));
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123457',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should be successfully', async () => {
|
||||
const surveyPath = 'test';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
}),
|
||||
).resolves.toEqual({ code: 200, data: null });
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'CUSTOM',
|
||||
memberType: 'MOBILE',
|
||||
whitelist: ['13500000000'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: '13500000001',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'MEMBER',
|
||||
whitelist: ['Jack'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: 'James',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('whitelistValidate should return verifyId successfully', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'CUSTOM',
|
||||
memberType: 'MOBILE',
|
||||
whitelist: ['13500000000'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: '13500000000',
|
||||
}),
|
||||
).resolves.toEqual({ code: 200, data: null });
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,10 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
const mockDecryptErrorBody = {
|
||||
surveyPath: 'EBzdmnSp',
|
||||
@ -124,6 +128,18 @@ describe('SurveyResponseController', () => {
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -306,5 +322,31 @@ describe('SurveyResponseController', () => {
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException if password does not match', async () => {
|
||||
const reqBody = {
|
||||
...mockSubmitData,
|
||||
password: '123457',
|
||||
sign: '4ff02062141d92d80629eae4797ba68056f29a9709cdf59bf206776fc0971c1a.1710400229589',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce({
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,33 @@
|
||||
import { Controller, Get, HttpCode, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
import { Logger } from 'src/logger';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
@ApiTags('surveyResponse')
|
||||
@Controller('/api/responseSchema')
|
||||
export class ResponseSchemaController {
|
||||
constructor(private readonly responseSchemaService: ResponseSchemaService) {}
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Get('/getSchema')
|
||||
@HttpCode(200)
|
||||
@ -34,9 +53,79 @@ export class ResponseSchemaController {
|
||||
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
|
||||
);
|
||||
}
|
||||
|
||||
// 去掉C端的敏感字段
|
||||
if (responseSchema.code?.baseConf) {
|
||||
responseSchema.code.baseConf.password = null;
|
||||
responseSchema.code.baseConf.whitelist = [];
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: responseSchema,
|
||||
};
|
||||
}
|
||||
|
||||
// 白名单验证
|
||||
@Post('/:surveyPath/validate')
|
||||
@HttpCode(200)
|
||||
async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) {
|
||||
const { value, error } = Joi.object({
|
||||
password: Joi.string().allow(null, ''),
|
||||
whitelist: Joi.string().allow(null, ''),
|
||||
}).validate(body, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`whitelistValidate error: ${error.message}`, {});
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
// 问卷信息
|
||||
const schema =
|
||||
await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
|
||||
if (!schema || schema.curStatus.status === 'removed') {
|
||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||
}
|
||||
|
||||
const { password, whitelist: whitelistValue } = value;
|
||||
const {
|
||||
passwordSwitch,
|
||||
password: settingPassword,
|
||||
whitelistType,
|
||||
whitelist,
|
||||
} = schema.code.baseConf;
|
||||
|
||||
// 密码校验
|
||||
if (passwordSwitch) {
|
||||
if (settingPassword !== password) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 名单校验(手机号/邮箱)
|
||||
if (whitelistType === WhitelistType.CUSTOM) {
|
||||
if (!whitelist.includes(whitelistValue)) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 团队成员昵称校验
|
||||
if (whitelistType === WhitelistType.MEMBER) {
|
||||
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||
if (!user) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
|
||||
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||
{ userId: user._id.toString() },
|
||||
);
|
||||
if (!workspaceMember.length) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ import * as Joi from 'joi';
|
||||
import * as forge from 'node-forge';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Logger } from 'src/logger';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
@ApiTags('surveyResponse')
|
||||
@Controller('/api/surveyResponse')
|
||||
@ -28,6 +31,8 @@ export class SurveyResponseController {
|
||||
private readonly clientEncryptService: ClientEncryptService,
|
||||
private readonly messagePushingTaskService: MessagePushingTaskService,
|
||||
private readonly logger: Logger,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Post('/createResponse')
|
||||
@ -43,6 +48,8 @@ export class SurveyResponseController {
|
||||
sessionId: Joi.string(),
|
||||
clientTime: Joi.number().required(),
|
||||
difTime: Joi.number(),
|
||||
password: Joi.string().allow(null, ''),
|
||||
whitelist: Joi.string().allow(null, ''),
|
||||
}).validate(reqBody, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
@ -52,8 +59,16 @@ export class SurveyResponseController {
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const { surveyPath, encryptType, data, sessionId, clientTime, difTime } =
|
||||
value;
|
||||
const {
|
||||
surveyPath,
|
||||
encryptType,
|
||||
data,
|
||||
sessionId,
|
||||
clientTime,
|
||||
difTime,
|
||||
password,
|
||||
whitelist: whitelistValue,
|
||||
} = value;
|
||||
|
||||
// 查询schema
|
||||
const responseSchema =
|
||||
@ -62,6 +77,50 @@ export class SurveyResponseController {
|
||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||
}
|
||||
|
||||
// 白名单的verifyId校验
|
||||
const baseConf = responseSchema.code.baseConf;
|
||||
|
||||
// 密码校验
|
||||
if (baseConf?.passwordSwitch && baseConf.password) {
|
||||
if (baseConf.password !== password) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 名单校验(手机号/邮箱)
|
||||
if (baseConf?.whitelistType === WhitelistType.CUSTOM) {
|
||||
if (!baseConf.whitelist.includes(whitelistValue)) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 团队成员昵称校验
|
||||
if (baseConf?.whitelistType === WhitelistType.MEMBER) {
|
||||
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||
{ userId: user._id.toString() },
|
||||
);
|
||||
if (!workspaceMember.length) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// 提交时间限制
|
||||
const begTime = responseSchema.code?.baseConf?.begTime || 0;
|
||||
|
@ -20,6 +20,8 @@ import { CounterController } from './controllers/counter.controller';
|
||||
import { ResponseSchemaController } from './controllers/responseSchema.controller';
|
||||
import { SurveyResponseController } from './controllers/surveyResponse.controller';
|
||||
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -31,6 +33,8 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
|
||||
]),
|
||||
ConfigModule,
|
||||
MessageModule,
|
||||
AuthModule,
|
||||
WorkspaceModule,
|
||||
],
|
||||
controllers: [
|
||||
ClientEncryptController,
|
||||
|
@ -31,6 +31,8 @@ import { splitMembers } from '../utils/splitMember';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { Workspace } from 'src/models/workspace.entity';
|
||||
|
||||
@ApiTags('workspace')
|
||||
@ApiBearerAuth()
|
||||
@ -326,4 +328,42 @@ export class WorkspaceController {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/member/list')
|
||||
@HttpCode(200)
|
||||
async getWorkspaceAndMember(@Request() req) {
|
||||
const userId = req.user._id.toString();
|
||||
|
||||
// 所在所有空间
|
||||
const workspaceList = await this.workspaceService.findAllByUserId(userId);
|
||||
if (!workspaceList.length) {
|
||||
return {
|
||||
code: 200,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 所有空间下的所有成员
|
||||
const workspaceMemberList =
|
||||
await this.workspaceMemberService.batchSearchByWorkspace(
|
||||
workspaceList.map((item) => item._id.toString()),
|
||||
);
|
||||
|
||||
const temp: Record<string, WorkspaceMember[]> = {};
|
||||
const list = workspaceList.map(
|
||||
(item: Workspace & { members: WorkspaceMember[] }) => {
|
||||
temp[item._id.toString()] = item.members = [];
|
||||
return item;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceMemberList.forEach((member) => {
|
||||
temp[member.workspaceId.toString()].push(member);
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: list,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -104,4 +104,27 @@ export class WorkspaceService {
|
||||
surveyRes,
|
||||
};
|
||||
}
|
||||
|
||||
// 用户下的所有空间
|
||||
async findAllByUserId(userId: string) {
|
||||
return await this.workspaceRepository.find({
|
||||
where: {
|
||||
ownerId: userId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
_id: -1,
|
||||
},
|
||||
select: [
|
||||
'_id',
|
||||
'curStatus',
|
||||
'name',
|
||||
'description',
|
||||
'ownerId',
|
||||
'createDate',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -140,4 +140,20 @@ export class WorkspaceMemberService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 根据空间id批量查询成员
|
||||
async batchSearchByWorkspace(workspaceList: string[]) {
|
||||
console.log(workspaceList);
|
||||
return await this.workspaceMemberRepository.find({
|
||||
where: {
|
||||
workspaceId: {
|
||||
$in: workspaceList,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
_id: -1,
|
||||
},
|
||||
select: ['_id', 'userId', 'username', 'role', 'workspaceId'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@ -42,6 +42,7 @@ declare module 'vue' {
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
||||
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
||||
|
11
web/src/common/regexpMap.ts
Normal file
11
web/src/common/regexpMap.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const regexpMap = {
|
||||
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
||||
m: /^[1]([3-9])[0-9]{9}$/,
|
||||
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
|
||||
strictIdcard:
|
||||
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
|
||||
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
|
||||
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
|
||||
licensePlate:
|
||||
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
|
||||
}
|
@ -16,6 +16,10 @@ export const getSpaceDetail = (workspaceId: string) => {
|
||||
return axios.get(`/workspace/${workspaceId}`)
|
||||
}
|
||||
|
||||
export const getMemberList = () => {
|
||||
return axios.get('/workspace/member/list')
|
||||
}
|
||||
|
||||
export const deleteSpace = (workspaceId: string) => {
|
||||
return axios.delete(`/workspace/${workspaceId}`)
|
||||
}
|
||||
|
@ -31,6 +31,17 @@ const updateLogicConf = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateWhiteConf = () => {
|
||||
const baseConf = store.state.edit.schema.baseConf || {};
|
||||
if (baseConf.passwordSwitch && !baseConf.password) {
|
||||
return true;
|
||||
}
|
||||
if (baseConf.whitelistType!='ALL' && !baseConf.whitelist?.length) {
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handlePublish = async () => {
|
||||
if (isPublishing.value) {
|
||||
return
|
||||
@ -46,6 +57,14 @@ const handlePublish = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if(updateWhiteConf()){
|
||||
isPublishing.value = false
|
||||
ElMessage.error('请检查问卷设置是否有误')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const saveData = buildData(store.state.edit.schema)
|
||||
if (!saveData.surveyId) {
|
||||
isPublishing.value = false
|
||||
|
@ -63,6 +63,17 @@ const updateLogicConf = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateWhiteConf = () => {
|
||||
const baseConf = store.state.edit.schema.baseConf || {};
|
||||
if (baseConf.passwordSwitch && !baseConf.password) {
|
||||
return true;
|
||||
}
|
||||
if (baseConf.whitelistType!='ALL' && !baseConf.whitelist?.length) {
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const timerHandle = ref<NodeJS.Timeout | number | null>(null)
|
||||
const triggerAutoSave = () => {
|
||||
if (autoSaveStatus.value === 'saving') {
|
||||
@ -114,6 +125,12 @@ const handleSave = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if(updateWhiteConf()){
|
||||
isSaving.value = false
|
||||
ElMessage.error('请检查问卷设置是否有误')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await saveData()
|
||||
if (res.code === 200) {
|
||||
|
@ -8,26 +8,12 @@
|
||||
{{ form.title }}
|
||||
</span>
|
||||
</div>
|
||||
<el-form
|
||||
class="question-config-form"
|
||||
label-position="left"
|
||||
label-width="200px"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form class="question-config-form" label-position="left" label-width="200px" @submit.prevent>
|
||||
<template v-for="(item, index) in form.formList">
|
||||
<FormItem
|
||||
v-if="item.type && !item.hidden && Boolean(registerTypes[item.type])"
|
||||
:key="index"
|
||||
:form-config="item"
|
||||
:style="item.style"
|
||||
>
|
||||
<Component
|
||||
v-if="Boolean(registerTypes[item.type])"
|
||||
:is="components[item.type]"
|
||||
:module-config="form.dataConfig"
|
||||
:form-config="item"
|
||||
@form-change="handleFormChange"
|
||||
/>
|
||||
<FormItem v-if="item.type && !item.hidden && Boolean(registerTypes[item.type])" :key="index"
|
||||
:form-config="item" :style="item.style">
|
||||
<Component v-if="Boolean(registerTypes[item.type])" :is="components[item.type]"
|
||||
:module-config="form.dataConfig" :form-config="item" @form-change="handleFormChange" />
|
||||
</FormItem>
|
||||
</template>
|
||||
</el-form>
|
||||
@ -39,7 +25,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, shallowRef } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get } from 'lodash-es'
|
||||
import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get,isFunction as _isFunction} from 'lodash-es'
|
||||
|
||||
import baseConfig from './config/baseConfig'
|
||||
import baseFormConfig from './config/baseFormConfig'
|
||||
@ -49,6 +35,9 @@ import setterLoader from '@/materials/setters/setterLoader'
|
||||
const formConfigList = ref<Array<any>>([])
|
||||
const components = shallowRef<any>({})
|
||||
const registerTypes = ref<any>({})
|
||||
const store = useStore()
|
||||
const schemaBaseConf = computed(() => store.state.edit?.schema?.baseConf || {})
|
||||
|
||||
const setterList = computed(() => {
|
||||
const list = _cloneDeep(formConfigList.value)
|
||||
|
||||
@ -71,6 +60,14 @@ const setterList = computed(() => {
|
||||
}
|
||||
formItem.value = formValue
|
||||
}
|
||||
// 动态显隐设置器
|
||||
form.formList = form.formList.filter((item:any) => {
|
||||
if (_isFunction(item.relyFunc)) {
|
||||
return item.relyFunc(schemaBaseConf.value)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
form.dataConfig = dataConfig
|
||||
|
||||
@ -78,7 +75,6 @@ const setterList = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
const handleFormChange = (data: any) => {
|
||||
store.dispatch('edit/changeSchema', {
|
||||
key: data.key,
|
||||
@ -87,7 +83,7 @@ const handleFormChange = (data: any) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
formConfigList.value = baseConfig.map((item) => ({
|
||||
formConfigList.value = baseConfig.map((item) => ({
|
||||
...item,
|
||||
formList: item.formList.map((key) => (baseFormConfig as any)[key]).filter((config) => !!config)
|
||||
}))
|
||||
|
@ -8,5 +8,11 @@ export default [
|
||||
title: '提交限制',
|
||||
key: 'limitConfig',
|
||||
formList: ['limit_tLimit']
|
||||
}, {
|
||||
title: '作答限制',
|
||||
key: 'respondConfig',
|
||||
formList: ['interview_pwd','answer_type','white_placeholder','white_list','team_list']
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
@ -21,5 +21,45 @@ export default {
|
||||
tip: '问卷仅在指定时间段内可填写',
|
||||
type: 'QuestionTimeHour',
|
||||
placement: 'top'
|
||||
},
|
||||
interview_pwd: {
|
||||
keys: ['baseConf.passwordSwitch', 'baseConf.password'],
|
||||
label: '访问密码',
|
||||
type: 'SwitchInput',
|
||||
placeholder: '请输入6位字符串类型访问密码 ',
|
||||
maxLength: 6,
|
||||
},
|
||||
answer_type: {
|
||||
key: 'baseConf.whitelistType',
|
||||
label: '答题名单',
|
||||
type: 'AnswerRadio',
|
||||
},
|
||||
white_placeholder:{
|
||||
key: 'baseConf.whitelistTip',
|
||||
label: '名单登录提示语',
|
||||
placeholder:'请输入名单提示语',
|
||||
type: 'InputWordLimit',
|
||||
maxLength: 40,
|
||||
relyFunc: (data) => {
|
||||
return ['CUSTOM','MEMBER'].includes(data.whitelistType)
|
||||
}
|
||||
},
|
||||
white_list:{
|
||||
keys: ['baseConf.whitelist','baseConf.memberType'],
|
||||
label: '白名单列表',
|
||||
type: 'whiteList',
|
||||
relyFunc: (data) => {
|
||||
return data.whitelistType == 'CUSTOM'
|
||||
}
|
||||
},
|
||||
team_list:{
|
||||
key: 'baseConf.whitelist',
|
||||
label: '团队空间成员选择',
|
||||
type: 'teamMemberList',
|
||||
relyFunc: (data) => {
|
||||
return data.whitelistType == 'MEMBER'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,13 @@ export default {
|
||||
tLimit: 0,
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
answerLimitTime: 0
|
||||
answerLimitTime: 0,
|
||||
passwordSwitch: false,
|
||||
password:'',
|
||||
whitelistType: 'ALL',
|
||||
whitelistTip:'',
|
||||
whitelist: [],
|
||||
memberType:'MOBILE',
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '',
|
||||
|
41
web/src/materials/setters/widgets/AnswerRadio.vue
Normal file
41
web/src/materials/setters/widgets/AnswerRadio.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="answer-radio-wrap">
|
||||
<el-radio-group v-model="whitelistType" @change="handleRadioGroupChange">
|
||||
<el-radio value="ALL">所有人</el-radio>
|
||||
<el-radio value="MEMBER">空间成员</el-radio>
|
||||
<el-radio value="CUSTOM">白名单</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
const props = defineProps({
|
||||
formConfig: Object,
|
||||
})
|
||||
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
|
||||
|
||||
const whitelistType = ref(props.formConfig?.value || 'ALL')
|
||||
|
||||
const handleRadioGroupChange = (value) => {
|
||||
const key = props.formConfig.key
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key:'baseConf.whitelist', value: [] })
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key: 'baseConf.memberType', value: 'MOBILE' })
|
||||
if (whitelistType.value == 'ALL') {
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key:'baseConf.whitelistTip', value:'' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.switch-input-wrap{
|
||||
width: 100%;
|
||||
.mt16{
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
33
web/src/materials/setters/widgets/InputWordLimit.vue
Normal file
33
web/src/materials/setters/widgets/InputWordLimit.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-input
|
||||
:maxlength="maxLength"
|
||||
v-model="modelValue"
|
||||
:placeholder="placeholder"
|
||||
show-word-limit
|
||||
type="text"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed,ref } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
const props = defineProps({
|
||||
formConfig: Object,
|
||||
})
|
||||
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
|
||||
|
||||
const modelValue = ref(props.formConfig.value || '')
|
||||
|
||||
const maxLength = computed(() => props.formConfig.maxLength || 10)
|
||||
|
||||
const placeholder = computed(() => props.formConfig.placeholder || '')
|
||||
|
||||
const handleInputChange = (value) => {
|
||||
const key = props.formConfig.key
|
||||
|
||||
modelValue.value = value
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
||||
}
|
||||
|
||||
</script>
|
52
web/src/materials/setters/widgets/SwitchInput.vue
Normal file
52
web/src/materials/setters/widgets/SwitchInput.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="switch-input-wrap">
|
||||
<el-switch v-model="passwordSwitch" @change="changeData(props.formConfig.keys[0],passwordSwitch)" />
|
||||
<InputWordLimit
|
||||
v-if="passwordSwitch"
|
||||
class="mt16"
|
||||
@form-change="handleFormChange"
|
||||
:formConfig="{
|
||||
...props.formConfig,
|
||||
key: props.formConfig.keys[1],
|
||||
value:props.formConfig?.value[1]
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
import InputWordLimit from './InputWordLimit.vue'
|
||||
|
||||
const store = useStore();
|
||||
const props = defineProps({
|
||||
formConfig: Object,
|
||||
})
|
||||
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
|
||||
const passwordSwitch = ref(props.formConfig?.value[0] || false);
|
||||
|
||||
|
||||
const changeData = (key, value) => {
|
||||
emit(FORM_CHANGE_EVENT_KEY, {
|
||||
key,
|
||||
value
|
||||
})
|
||||
|
||||
}
|
||||
const handleFormChange = (data) => {
|
||||
store.dispatch('edit/changeSchema', {
|
||||
key: data.key,
|
||||
value: data.value
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.switch-input-wrap{
|
||||
width: 100%;
|
||||
.mt16{
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
95
web/src/materials/setters/widgets/teamMemberList.vue
Normal file
95
web/src/materials/setters/widgets/teamMemberList.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="team-member-wrap">
|
||||
<div class="team-tree-wrap">
|
||||
<el-tree ref="treeRef" :default-expanded-keys="defaultCheckedKeys" :default-checked-keys="defaultCheckedKeys"
|
||||
:data="treeData" empty-text="暂无数据" @check="handleChange" style="height:201px" highlight-current show-checkbox
|
||||
node-key="id" :props="defaultProps" />
|
||||
</div>
|
||||
<div class="member-count">已选择 <span>{{ selectCount }}</span> 人</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, defineProps, defineEmits, onMounted } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
import {
|
||||
getMemberList
|
||||
} from '@/management/api/space'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
formConfig: Object,
|
||||
})
|
||||
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
|
||||
|
||||
const treeRef = ref(null)
|
||||
const treeData = ref([])
|
||||
const defaultCheckedKeys = ref([])
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
}
|
||||
|
||||
const handleChange = () => {
|
||||
const key = props.formConfig.key;
|
||||
const userKeys = treeRef.value?.getCheckedKeys(true);
|
||||
if (userKeys.length > 100) {
|
||||
ElMessage.error('最多添加100个')
|
||||
return;
|
||||
}
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key: key, value: userKeys });
|
||||
}
|
||||
|
||||
|
||||
const selectCount = computed(() => {
|
||||
return treeRef.value?.getCheckedKeys(true).length || 0
|
||||
})
|
||||
|
||||
|
||||
const getSpaceMenus = async () => {
|
||||
const res = await getMemberList();
|
||||
if (res.code != 200) {
|
||||
ElMessage.error('获取空间成员列表失败');
|
||||
}
|
||||
const data = res.data;
|
||||
data.map((v) => {
|
||||
const members = v.members || [];
|
||||
treeData.value.push({
|
||||
id: v.ownerId,
|
||||
label: v.name,
|
||||
children: members?.map(v => ({
|
||||
id: v.userId,
|
||||
label: v.role,
|
||||
}))
|
||||
})
|
||||
})
|
||||
defaultCheckedKeys.value = props.formConfig.value;
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getSpaceMenus();
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.team-member-wrap {
|
||||
width: 508px;
|
||||
|
||||
.team-tree-wrap {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(227, 228, 232, 1);
|
||||
border-radius: 2px;
|
||||
min-height: 204px;
|
||||
max-height: 204px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
145
web/src/materials/setters/widgets/whiteList.vue
Normal file
145
web/src/materials/setters/widgets/whiteList.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="white-list-wrap">
|
||||
<el-button class="create-btn" type="primary" @click="whiteVisible=true">
|
||||
添加
|
||||
</el-button>
|
||||
<el-button v-if="whitelist.length>0" class="create-btn" color="#4A4C5B" @click="delAllList">
|
||||
全部删除
|
||||
</el-button>
|
||||
<el-table class="table-wrap" empty-text="暂无数据" :data="whitelist" height="240" style="width: 426px">
|
||||
<el-table-column label="名单" width="350" >
|
||||
<template #default="scope">
|
||||
<div>{{ whitelist[scope.$index] }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="74" >
|
||||
<template #default="scope">
|
||||
<div @click="delRowItem(scope.$index)" class="flex cursor"><i-ep-delete :size="16" /></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="whiteVisible" title="添加白名单" width="600" @closed="handleClose">
|
||||
<div>
|
||||
<el-form-item label-position="top" label="类型选择" label-width="auto">
|
||||
<el-radio-group v-model="memberType" >
|
||||
<el-radio value="MOBILE">手机号</el-radio>
|
||||
<el-radio value="EMAIL">邮箱</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label-position="top" class="flex-column" label="名单录入" label-width="auto">
|
||||
<el-input v-model="whiteTextarea" placeholder="多个用逗号(半角)“,”隔开" rows="7" resize="none" type="textarea" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="whiteVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleChange">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { ref,nextTick } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { regexpMap } from '@/common/regexpMap.ts'
|
||||
|
||||
const props = defineProps({
|
||||
formConfig: Object,
|
||||
})
|
||||
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
|
||||
|
||||
const whitelist = ref(props.formConfig.value[0] || [])
|
||||
const memberType = ref(props.formConfig.value[1] || 'MOBILE')
|
||||
const whiteVisible = ref(false)
|
||||
const whiteTextarea = ref(whitelist.value.join(','))
|
||||
|
||||
const regularMap = {
|
||||
MOBILE:regexpMap.m,
|
||||
EMAIL:regexpMap.e
|
||||
}
|
||||
|
||||
|
||||
const checkValRule = (list) => {
|
||||
let status = false;
|
||||
if (list.length > 100) {
|
||||
ElMessage.error('最多添加100个')
|
||||
return true;
|
||||
};
|
||||
const pattern = regularMap[memberType.value];
|
||||
if(!pattern) return false;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (!pattern.test(list[i])) {
|
||||
status = true;
|
||||
ElMessage.error('数据格式错误,请检查后重新输入~')
|
||||
break;
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleChange = () => {
|
||||
const keys = props.formConfig.keys;
|
||||
const list = whiteTextarea.value ? whiteTextarea.value.split(',') : []
|
||||
if(checkValRule(list)) return
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key:keys[0], value: list });
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key: keys[1], value: memberType.value })
|
||||
whiteVisible.value = false
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
nextTick(() => {
|
||||
whitelist.value = props.formConfig.value[0] || []
|
||||
whiteTextarea.value = whitelist.value.join(',')
|
||||
memberType.value = props.formConfig.value[1] || 'MOBILE'
|
||||
})
|
||||
}
|
||||
|
||||
const delRowItem = (index) => {
|
||||
whitelist.value.splice(index, 1);
|
||||
whiteTextarea.value = whitelist.value.join(',')
|
||||
const keys = props.formConfig.keys;
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key:keys[0], value: whitelist.value });
|
||||
}
|
||||
|
||||
const delAllList = () => {
|
||||
whitelist.value = []
|
||||
whiteTextarea.value = ''
|
||||
handleChange();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.white-list-wrap {
|
||||
.flex-column{
|
||||
flex-direction: column;
|
||||
}
|
||||
:deep(th){
|
||||
padding:4px 0;
|
||||
background: #F6F7F9;
|
||||
}
|
||||
:deep(td){
|
||||
padding:6px 0;
|
||||
}
|
||||
.table-wrap{
|
||||
margin-top: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 2px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.cursor{
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex{
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -6,18 +6,8 @@ import {
|
||||
set as _set
|
||||
} from 'lodash-es'
|
||||
import { INPUT, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||
import { regexpMap } from '@/common/regexpMap.ts'
|
||||
|
||||
const regexpMap = {
|
||||
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
||||
m: /^[1]([3-9])[0-9]{9}$/,
|
||||
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
|
||||
strictIdcard:
|
||||
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
|
||||
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
|
||||
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
|
||||
licensePlate:
|
||||
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
|
||||
}
|
||||
|
||||
const msgMap = {
|
||||
'*': '必填',
|
||||
|
@ -32,3 +32,10 @@ export const queryVote = ({ surveyPath, fieldList }) => {
|
||||
export const getEncryptInfo = () => {
|
||||
return axios.get('/clientEncrypt/getEncryptInfo')
|
||||
}
|
||||
|
||||
export const validate = ({ surveyPath,password, whitelist }) => {
|
||||
return axios.post(`/responseSchema/${surveyPath}/validate`, {
|
||||
password,
|
||||
whitelist
|
||||
})
|
||||
}
|
138
web/src/render/components/VerifyWhiteDialog.vue
Normal file
138
web/src/render/components/VerifyWhiteDialog.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="whiteVisible"
|
||||
title="验证"
|
||||
:show-close="false"
|
||||
class="verify-white-wrap"
|
||||
width="315"
|
||||
:close-on-press-escape="false"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
|
||||
>
|
||||
<template #header>
|
||||
<div class="verify-white-head">
|
||||
<div class="verify-white-title">验证</div>
|
||||
<div v-if="whitelistTip" class="verify-white-tips">{{ whitelistTip }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="verify-white-body">
|
||||
<el-input v-if="isPwd" v-model="state.password" class="wd255 mb16" placeholder="请输入6位字符串类型访问密码" />
|
||||
<el-input v-if="isValue" v-model="state.value" class="wd255 mb16" :placeholder="placeholder" />
|
||||
<div class="submit-btn" @click="handleSubmit">验证并开始答题</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref,reactive,computed,watch} from 'vue'
|
||||
import { validate } from '../api/survey'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
const whiteVisible = ref(false)
|
||||
|
||||
|
||||
const store = useStore()
|
||||
const state = reactive({
|
||||
password: '',
|
||||
value: '',
|
||||
is_submit:false
|
||||
})
|
||||
|
||||
const baseConf = computed(() => store.state.baseConf || {})
|
||||
const isPwd = computed(() => baseConf.value.passwordSwitch)
|
||||
const whitelistType = computed(() => baseConf.value.whitelistType)
|
||||
const memberType = computed(() => baseConf.value.memberType)
|
||||
const whitelistTip = computed(() => baseConf.value.whitelistTip)
|
||||
const surveyPath = computed(() => store.state?.surveyPath || '')
|
||||
|
||||
const isValue = computed(() => {
|
||||
if(!whitelistType.value) return false
|
||||
return whitelistType.value!='ALL'
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (whitelistType.value == 'MEMBER') {
|
||||
return '请输入用户名'
|
||||
}
|
||||
if(memberType.value == 'MOBILE'){
|
||||
return '请输入手机号'
|
||||
}
|
||||
if(memberType.value == 'EMAIL'){
|
||||
return '请输入邮箱'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleSubmit = async() => {
|
||||
if (state.is_submit) return;
|
||||
const params = {
|
||||
surveyPath:surveyPath.value
|
||||
}
|
||||
if (isValue.value) {
|
||||
params.whitelist = state.value
|
||||
}
|
||||
if(isPwd.value){
|
||||
params.password = state.password
|
||||
}
|
||||
const res = await validate(params)
|
||||
if (res.code != 200) {
|
||||
ElMessage.error(res.errmsg || '验证失败')
|
||||
return
|
||||
}
|
||||
whiteVisible.value = false
|
||||
store.commit('setWhiteData',params)
|
||||
}
|
||||
|
||||
watch(()=>baseConf.value, () => {
|
||||
if (whiteVisible.value) return
|
||||
if(isValue.value || isPwd.value){
|
||||
whiteVisible.value = true;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.verify-white-wrap{
|
||||
.verify-white-body{
|
||||
padding:0 14px
|
||||
}
|
||||
.verify-white-head{
|
||||
padding:0 14px;
|
||||
margin-bottom: 8px;
|
||||
margin-top:2px;
|
||||
}
|
||||
.mb16{
|
||||
margin-bottom:16px;
|
||||
}
|
||||
.verify-white-tips{
|
||||
text-align: center;
|
||||
margin-top:8px;
|
||||
font-size: 14px;
|
||||
color: #92949D;
|
||||
}
|
||||
.verify-white-title{
|
||||
font-size: 16px;
|
||||
color: #292A36;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
.submit-btn{
|
||||
background: #FAA600;
|
||||
border-radius: 2px;
|
||||
width:255px;
|
||||
height:32px;
|
||||
color:#fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top:4px;
|
||||
margin-bottom:14px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -14,6 +14,7 @@
|
||||
@submit="handleSubmit"
|
||||
></SubmitButton>
|
||||
<LogoIcon :logo-conf="logoConf" :readonly="true" />
|
||||
<VerifyWhiteDialog />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,6 +27,7 @@ import { useRouter } from 'vue-router'
|
||||
import communalLoader from '@materials/communals/communalLoader.js'
|
||||
import MainRenderer from '../components/MainRenderer.vue'
|
||||
import AlertDialog from '../components/AlertDialog.vue'
|
||||
import VerifyWhiteDialog from '../components/VerifyWhiteDialog.vue'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
import ProgressBar from '../components/ProgressBar.vue'
|
||||
|
||||
@ -63,6 +65,7 @@ const renderData = computed(() => store.getters.renderData)
|
||||
const submitConf = computed(() => store.state?.submitConf || {})
|
||||
const logoConf = computed(() => store.state?.bottomConf || {})
|
||||
const surveyPath = computed(() => store.state?.surveyPath || '')
|
||||
const whiteData = computed(() => store.state?.whiteData || {})
|
||||
|
||||
const validate = (cbk: (v: boolean) => void) => {
|
||||
const index = 0
|
||||
@ -78,7 +81,8 @@ const normalizationRequestBody = () => {
|
||||
surveyPath: surveyPath.value,
|
||||
data: JSON.stringify(formValues),
|
||||
difTime: Date.now() - enterTime,
|
||||
clientTime: Date.now()
|
||||
clientTime: Date.now(),
|
||||
...whiteData.value
|
||||
}
|
||||
|
||||
if (encryptInfo?.encryptType) {
|
||||
|
@ -49,5 +49,8 @@ export default {
|
||||
},
|
||||
setRuleEgine(state, ruleEngine) {
|
||||
state.ruleEngine = ruleEngine
|
||||
},
|
||||
setWhiteData(state, data) {
|
||||
state.whiteData = data
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,8 @@ export default {
|
||||
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||
voteMap: {},
|
||||
encryptInfo: null,
|
||||
ruleEngine: null
|
||||
ruleEngine: null,
|
||||
whiteData: {
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user