feat: 修改验收问题 (#421)
This commit is contained in:
parent
6cbfe20be1
commit
b484b786ea
@ -1,5 +1,5 @@
|
|||||||
XIAOJU_SURVEY_MONGO_DB_NAME= xiaojuSurvey
|
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||||
XIAOJU_SURVEY_MONGO_URL= # mongodb://localhost:27017 # 建议设置强密码
|
XIAOJU_SURVEY_MONGO_URL= # mongodb://127.0.0.1:27017 # 建议设置强密码
|
||||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
||||||
|
|
||||||
XIAOJU_SURVEY_REDIS_HOST=
|
XIAOJU_SURVEY_REDIS_HOST=
|
||||||
|
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@ -39,3 +39,5 @@ yarn.lock
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
tmp
|
tmp
|
||||||
|
exportfile
|
||||||
|
userUpload
|
@ -12,4 +12,7 @@ export class Session extends BaseEntity {
|
|||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
surveyId: string;
|
surveyId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: string;
|
||||||
}
|
}
|
||||||
|
@ -79,15 +79,16 @@ export class DownloadTaskController {
|
|||||||
async downloadList(
|
async downloadList(
|
||||||
@Query()
|
@Query()
|
||||||
queryInfo: GetDownloadTaskListDto,
|
queryInfo: GetDownloadTaskListDto,
|
||||||
|
@Request() req,
|
||||||
) {
|
) {
|
||||||
const { value, error } = GetDownloadTaskListDto.validate(queryInfo);
|
const { value, error } = GetDownloadTaskListDto.validate(queryInfo);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message);
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const { ownerId, pageIndex, pageSize } = value;
|
const { pageIndex, pageSize } = value;
|
||||||
const { total, list } = await this.downloadTaskService.getDownloadTaskList({
|
const { total, list } = await this.downloadTaskService.getDownloadTaskList({
|
||||||
ownerId,
|
ownerId: req.user._id.toString(),
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
|
@ -39,6 +39,8 @@ export class SessionController {
|
|||||||
reqBody: {
|
reqBody: {
|
||||||
surveyId: string;
|
surveyId: string;
|
||||||
},
|
},
|
||||||
|
@Request()
|
||||||
|
req,
|
||||||
) {
|
) {
|
||||||
const { value, error } = Joi.object({
|
const { value, error } = Joi.object({
|
||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
@ -50,7 +52,10 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const surveyId = value.surveyId;
|
const surveyId = value.surveyId;
|
||||||
const session = await this.sessionService.create({ surveyId });
|
const session = await this.sessionService.create({
|
||||||
|
surveyId,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
|
@ -34,6 +34,7 @@ import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
|||||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||||
import { SessionService } from '../services/session.service';
|
import { SessionService } from '../services/session.service';
|
||||||
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@Controller('/api/survey')
|
@Controller('/api/survey')
|
||||||
@ -47,6 +48,7 @@ export class SurveyController {
|
|||||||
private readonly logger: XiaojuSurveyLogger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
private readonly counterService: CounterService,
|
private readonly counterService: CounterService,
|
||||||
private readonly sessionService: SessionService,
|
private readonly sessionService: SessionService,
|
||||||
|
private readonly userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/getBannerData')
|
@Get('/getBannerData')
|
||||||
@ -146,12 +148,22 @@ export class SurveyController {
|
|||||||
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
|
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
|
||||||
const curSession = await this.sessionService.findOne(sessionId);
|
const curSession = await this.sessionService.findOne(sessionId);
|
||||||
if (curSession.createDate <= latestEditingOne.updateDate) {
|
if (curSession.createDate <= latestEditingOne.updateDate) {
|
||||||
// 在当前用户打开之后,有人保存过了
|
// 在当前用户打开之后,被其他页面保存过了
|
||||||
throw new HttpException(
|
const isSameOperator =
|
||||||
'当前问卷已在其它页面开启编辑',
|
latestEditingOne.userId === req.user._id.toString();
|
||||||
EXCEPTION_CODE.SURVEY_SAVE_CONFLICT,
|
let preOperator;
|
||||||
|
if (!isSameOperator) {
|
||||||
|
preOperator = await this.userService.getUserById(
|
||||||
|
latestEditingOne.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
code: EXCEPTION_CODE.SURVEY_SAVE_CONFLICT,
|
||||||
|
errmsg: isSameOperator
|
||||||
|
? '当前问卷已在其它页面开启编辑,刷新以获取最新内容'
|
||||||
|
: `当前问卷已由 ${preOperator.username} 编辑,刷新以获取最新内容`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await this.sessionService.updateSessionToEditing({ sessionId, surveyId });
|
await this.sessionService.updateSessionToEditing({ sessionId, surveyId });
|
||||||
|
|
||||||
@ -331,11 +343,6 @@ export class SurveyController {
|
|||||||
pageId: surveyId,
|
pageId: surveyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.counterService.createCounters({
|
|
||||||
surveyPath: surveyMeta.surveyPath,
|
|
||||||
dataList: surveyConf.code.dataConf.dataList,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.surveyHistoryService.addHistory({
|
await this.surveyHistoryService.addHistory({
|
||||||
surveyId,
|
surveyId,
|
||||||
schema: surveyConf.code,
|
schema: surveyConf.code,
|
||||||
|
@ -13,6 +13,7 @@ import { load } from 'cheerio';
|
|||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { FileService } from 'src/modules/file/services/file.service';
|
import { FileService } from 'src/modules/file/services/file.service';
|
||||||
import { XiaojuSurveyLogger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DownloadTaskService {
|
export class DownloadTaskService {
|
||||||
@ -41,6 +42,7 @@ export class DownloadTaskService {
|
|||||||
operatorId: string;
|
operatorId: string;
|
||||||
params: any;
|
params: any;
|
||||||
}) {
|
}) {
|
||||||
|
const filename = `${responseSchema.title}-${params.isDesensitive ? '脱敏' : '原'}回收数据-${moment().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||||
const downloadTask = this.downloadTaskRepository.create({
|
const downloadTask = this.downloadTaskRepository.create({
|
||||||
surveyId,
|
surveyId,
|
||||||
surveyPath: responseSchema.surveyPath,
|
surveyPath: responseSchema.surveyPath,
|
||||||
@ -50,6 +52,7 @@ export class DownloadTaskService {
|
|||||||
...params,
|
...params,
|
||||||
title: responseSchema.title,
|
title: responseSchema.title,
|
||||||
},
|
},
|
||||||
|
filename,
|
||||||
});
|
});
|
||||||
await this.downloadTaskRepository.save(downloadTask);
|
await this.downloadTaskRepository.save(downloadTask);
|
||||||
return downloadTask._id.toString();
|
return downloadTask._id.toString();
|
||||||
@ -65,7 +68,7 @@ export class DownloadTaskService {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}) {
|
}) {
|
||||||
const where = {
|
const where = {
|
||||||
onwer: ownerId,
|
ownerId,
|
||||||
'curStatus.status': {
|
'curStatus.status': {
|
||||||
$ne: RECORD_STATUS.REMOVED,
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
},
|
},
|
||||||
@ -209,16 +212,12 @@ export class DownloadTaskService {
|
|||||||
{ name: 'sheet1', data: xlsxData, options: {} },
|
{ name: 'sheet1', data: xlsxData, options: {} },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isDesensitive = taskInfo.params?.isDesensitive;
|
|
||||||
|
|
||||||
const originalname = `${taskInfo.params.title}-${isDesensitive ? '脱敏' : '原'}回收数据.xlsx`;
|
|
||||||
|
|
||||||
const file: Express.Multer.File = {
|
const file: Express.Multer.File = {
|
||||||
fieldname: 'file',
|
fieldname: 'file',
|
||||||
originalname: originalname,
|
originalname: taskInfo.filename,
|
||||||
encoding: '7bit',
|
encoding: '7bit',
|
||||||
mimetype: 'application/octet-stream',
|
mimetype: 'application/octet-stream',
|
||||||
filename: originalname,
|
filename: taskInfo.filename,
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
buffer: buffer,
|
buffer: buffer,
|
||||||
stream: null,
|
stream: null,
|
||||||
@ -246,7 +245,6 @@ export class DownloadTaskService {
|
|||||||
$set: {
|
$set: {
|
||||||
curStatus,
|
curStatus,
|
||||||
url,
|
url,
|
||||||
filename: originalname,
|
|
||||||
fileKey: key,
|
fileKey: key,
|
||||||
fileSize: buffer.length,
|
fileSize: buffer.length,
|
||||||
},
|
},
|
||||||
|
@ -12,9 +12,10 @@ export class SessionService {
|
|||||||
private readonly sessionRepository: MongoRepository<Session>,
|
private readonly sessionRepository: MongoRepository<Session>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create({ surveyId }) {
|
create({ surveyId, userId }) {
|
||||||
const session = this.sessionRepository.create({
|
const session = this.sessionRepository.create({
|
||||||
surveyId,
|
surveyId,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
return this.sessionRepository.save(session);
|
return this.sessionRepository.save(session);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
|||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
import { checkSign } from 'src/utils/checkSign';
|
import { checkSign } from 'src/utils/checkSign';
|
||||||
import { cleanRichTextWithMediaTag } from 'src/utils/xss'
|
|
||||||
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
import { getPushingData } from 'src/utils/messagePushing';
|
import { getPushingData } from 'src/utils/messagePushing';
|
||||||
@ -23,6 +22,14 @@ import { XiaojuSurveyLogger } from 'src/logger';
|
|||||||
import { WhitelistType } from 'src/interfaces/survey';
|
import { WhitelistType } from 'src/interfaces/survey';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
import { QUESTION_TYPE } from 'src/enums/question';
|
||||||
|
|
||||||
|
const optionQuestionType: Array<string> = [
|
||||||
|
QUESTION_TYPE.RADIO,
|
||||||
|
QUESTION_TYPE.CHECKBOX,
|
||||||
|
QUESTION_TYPE.BINARY_CHOICE,
|
||||||
|
QUESTION_TYPE.VOTE,
|
||||||
|
];
|
||||||
|
|
||||||
@ApiTags('surveyResponse')
|
@ApiTags('surveyResponse')
|
||||||
@Controller('/api/surveyResponse')
|
@Controller('/api/surveyResponse')
|
||||||
@ -207,6 +214,7 @@ export class SurveyResponseController {
|
|||||||
const optionTextAndId = dataList
|
const optionTextAndId = dataList
|
||||||
.filter((questionItem) => {
|
.filter((questionItem) => {
|
||||||
return (
|
return (
|
||||||
|
optionQuestionType.includes(questionItem.type) &&
|
||||||
Array.isArray(questionItem.options) &&
|
Array.isArray(questionItem.options) &&
|
||||||
questionItem.options.length > 0 &&
|
questionItem.options.length > 0 &&
|
||||||
decryptedData[questionItem.field]
|
decryptedData[questionItem.field]
|
||||||
@ -222,20 +230,23 @@ export class SurveyResponseController {
|
|||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
// 使用redis作为锁,校验选项配额
|
||||||
const surveyId = responseSchema.pageId;
|
const surveyId = responseSchema.pageId;
|
||||||
const lockKey = `locks:optionSelectedCount:${surveyId}`;
|
const lockKey = `locks:optionSelectedCount:${surveyId}`;
|
||||||
const lock = await this.redisService.lockResource(lockKey, 1000);
|
const lock = await this.redisService.lockResource(lockKey, 1000);
|
||||||
this.logger.info(`lockKey: ${lockKey}`);
|
this.logger.info(`lockKey: ${lockKey}`);
|
||||||
try {
|
try {
|
||||||
|
const successParams = [];
|
||||||
for (const field in decryptedData) {
|
for (const field in decryptedData) {
|
||||||
const value = decryptedData[field];
|
const value = decryptedData[field];
|
||||||
const values = Array.isArray(value) ? value : [value];
|
const values = Array.isArray(value) ? value : [value];
|
||||||
if (field in optionTextAndId) {
|
if (field in optionTextAndId) {
|
||||||
const optionCountData = await this.counterService.get({
|
const optionCountData =
|
||||||
|
(await this.counterService.get({
|
||||||
key: field,
|
key: field,
|
||||||
surveyPath,
|
surveyPath,
|
||||||
type: 'option',
|
type: 'option',
|
||||||
});
|
})) || {};
|
||||||
|
|
||||||
//遍历选项hash值
|
//遍历选项hash值
|
||||||
for (const val of values) {
|
for (const val of values) {
|
||||||
@ -243,38 +254,41 @@ export class SurveyResponseController {
|
|||||||
(opt) => opt['hash'] === val,
|
(opt) => opt['hash'] === val,
|
||||||
);
|
);
|
||||||
const quota = parseInt(option['quota']);
|
const quota = parseInt(option['quota']);
|
||||||
if (quota !== 0 && quota <= optionCountData[val]) {
|
if (
|
||||||
const item = dataList.find((item) => item['field'] === field);
|
quota &&
|
||||||
throw new HttpException(
|
optionCountData?.[val] &&
|
||||||
`【${cleanRichTextWithMediaTag(item['title'])}】中的【${cleanRichTextWithMediaTag(option['text'])}】所选人数已达到上限,请重新选择`,
|
quota <= optionCountData[val]
|
||||||
EXCEPTION_CODE.RESPONSE_OVER_LIMIT,
|
) {
|
||||||
);
|
return {
|
||||||
|
code: EXCEPTION_CODE.RESPONSE_OVER_LIMIT,
|
||||||
|
data: {
|
||||||
|
field,
|
||||||
|
optionHash: option.hash,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
if (!optionCountData[val]) {
|
||||||
|
optionCountData[val] = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const field in decryptedData) {
|
|
||||||
const value = decryptedData[field];
|
|
||||||
const values = Array.isArray(value) ? value : [value];
|
|
||||||
if (field in optionTextAndId) {
|
|
||||||
const optionCountData = await this.counterService.get({
|
|
||||||
key: field,
|
|
||||||
surveyPath,
|
|
||||||
type: 'option',
|
|
||||||
});
|
|
||||||
for (const val of values) {
|
|
||||||
optionCountData[val]++;
|
optionCountData[val]++;
|
||||||
this.counterService.set({
|
}
|
||||||
|
if (!optionCountData['total']) {
|
||||||
|
optionCountData['total'] = 1;
|
||||||
|
} else {
|
||||||
|
optionCountData['total']++;
|
||||||
|
}
|
||||||
|
successParams.push({
|
||||||
key: field,
|
key: field,
|
||||||
surveyPath,
|
surveyPath,
|
||||||
type: 'option',
|
type: 'option',
|
||||||
data: optionCountData,
|
data: optionCountData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
optionCountData['total']++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 校验通过后统一更新
|
||||||
|
await Promise.all(
|
||||||
|
successParams.map((item) => this.counterService.set(item)),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error.message);
|
this.logger.error(error.message);
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -65,25 +65,4 @@ export class CounterService {
|
|||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCounters({ surveyPath, dataList }) {
|
|
||||||
const optionList = dataList.filter((questionItem) => {
|
|
||||||
return (
|
|
||||||
Array.isArray(questionItem.options) && questionItem.options.length > 0
|
|
||||||
);
|
|
||||||
});
|
|
||||||
optionList.forEach((option) => {
|
|
||||||
const data = {};
|
|
||||||
option.options.forEach((option) => {
|
|
||||||
data[option.hash] = 0;
|
|
||||||
});
|
|
||||||
data['total'] = 0;
|
|
||||||
this.set({
|
|
||||||
surveyPath,
|
|
||||||
key: option.field,
|
|
||||||
type: 'option',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,53 @@
|
|||||||
import xss from 'xss'
|
import xss from 'xss';
|
||||||
|
|
||||||
const myxss = new (xss as any).FilterXSS({
|
const myxss = new (xss as any).FilterXSS({
|
||||||
onIgnoreTagAttr(tag, name, value) {
|
onIgnoreTagAttr(tag, name, value) {
|
||||||
if (name === 'style' || name === 'class') {
|
if (name === 'style' || name === 'class') {
|
||||||
return `${name}="${value}"`
|
return `${name}="${value}"`;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
onIgnoreTag(tag, html) {
|
onIgnoreTag(tag, html) {
|
||||||
// <xxx>过滤为空,否则不过滤为空
|
// <xxx>过滤为空,否则不过滤为空
|
||||||
var re1 = new RegExp('<.+?>', 'g')
|
const re1 = new RegExp('<.+?>', 'g');
|
||||||
if (re1.test(html)) {
|
if (re1.test(html)) {
|
||||||
return ''
|
return '';
|
||||||
} else {
|
} else {
|
||||||
return html
|
return html;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const cleanRichTextWithMediaTag = (text) => {
|
export const cleanRichTextWithMediaTag = (text) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return text === 0 ? 0 : ''
|
return text === 0 ? 0 : '';
|
||||||
}
|
}
|
||||||
const html = transformHtmlTag(text)
|
const html = transformHtmlTag(text)
|
||||||
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
|
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
|
||||||
.replace(/<video.*\/video>/g, '[视频]')
|
.replace(/<video.*\/video>/g, '[视频]');
|
||||||
const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '')
|
const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '');
|
||||||
|
|
||||||
return content
|
return content;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function escapeHtml(html) {
|
export function escapeHtml(html) {
|
||||||
return html.replace(/</g, '<').replace(/>/g, '>')
|
return html.replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
export const transformHtmlTag = (html) => {
|
export const transformHtmlTag = (html) => {
|
||||||
if (!html) return ''
|
if (!html) return '';
|
||||||
if (typeof html !== 'string') return html + ''
|
if (typeof html !== 'string') return html + '';
|
||||||
return html
|
return html
|
||||||
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&')
|
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, "'")
|
.replace(/'/g, "'")
|
||||||
.replace(/\\\n/g, '\\n')
|
.replace(/\\\n/g, '\\n');
|
||||||
//.replace(/ /g, "")
|
//.replace(/ /g, "")
|
||||||
}
|
};
|
||||||
|
|
||||||
const filterXSSClone = myxss.process.bind(myxss)
|
const filterXSSClone = myxss.process.bind(myxss);
|
||||||
|
|
||||||
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html))
|
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html));
|
||||||
|
|
||||||
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html))
|
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html));
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import ImagePreview from './ImagePreview.vue'
|
import ImagePreview from './ImagePreview.vue'
|
||||||
|
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tableData: {
|
tableData: {
|
||||||
@ -78,8 +79,16 @@ const popoverVirtualRef = ref()
|
|||||||
const popoverContent = ref('')
|
const popoverContent = ref('')
|
||||||
|
|
||||||
const getContent = (content) => {
|
const getContent = (content) => {
|
||||||
// const content = cleanRichText(value)
|
if (Array.isArray(content)) {
|
||||||
return content === 0 ? 0 : content || '未知'
|
return content.map(item => getContent(item)).join(',');
|
||||||
|
}
|
||||||
|
if (content === null || content === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (typeof content !== 'string') {
|
||||||
|
content = content + ''
|
||||||
|
}
|
||||||
|
return cleanRichTextWithMediaTag(content) || '未知'
|
||||||
}
|
}
|
||||||
const setPopoverContent = (content) => {
|
const setPopoverContent = (content) => {
|
||||||
popoverContent.value = content
|
popoverContent.value = content
|
||||||
|
@ -32,14 +32,22 @@
|
|||||||
v-model="downloadDialogVisible"
|
v-model="downloadDialogVisible"
|
||||||
title="导出确认"
|
title="导出确认"
|
||||||
width="500"
|
width="500"
|
||||||
|
style="padding: 40px;"
|
||||||
>
|
>
|
||||||
<el-form :model="downloadForm">
|
<el-form :model="downloadForm" label-width="100px" label-position="left" >
|
||||||
<el-form-item label="导出内容">
|
<el-form-item label="导出内容">
|
||||||
<el-radio-group v-model="downloadForm.isDesensitive">
|
<el-radio-group v-model="downloadForm.isDesensitive">
|
||||||
<el-radio :value="true">脱敏数据</el-radio>
|
<el-radio :value="true">脱敏数据</el-radio>
|
||||||
<el-radio value="Venue">原回收数据</el-radio>
|
<el-radio :value="false">原回收数据</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<div class="download-tips">
|
||||||
|
<div>注:</div>
|
||||||
|
<div>
|
||||||
|
<p>推荐优先下载脱敏数据,如手机号:1***3。</p>
|
||||||
|
<p>原回收数据可能存在敏感信息,请谨慎下载。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
@ -163,9 +171,9 @@ const confirmDownload = async () => {
|
|||||||
const createRes = await createDownloadSurveyResponseTask({ surveyId: route.params.id, isDesensitive: downloadForm.isDesensitive })
|
const createRes = await createDownloadSurveyResponseTask({ surveyId: route.params.id, isDesensitive: downloadForm.isDesensitive })
|
||||||
dataTableState.downloadDialogVisible = false
|
dataTableState.downloadDialogVisible = false
|
||||||
if (createRes.code === 200) {
|
if (createRes.code === 200) {
|
||||||
|
ElMessage.success(`下载文件计算中,可前往“下载中心”查看`)
|
||||||
try {
|
try {
|
||||||
const taskInfo = await checkIsTaskFinished(createRes.data.taskId)
|
const taskInfo = await checkIsTaskFinished(createRes.data.taskId)
|
||||||
console.log(taskInfo)
|
|
||||||
if (taskInfo.url) {
|
if (taskInfo.url) {
|
||||||
window.open(taskInfo.url)
|
window.open(taskInfo.url)
|
||||||
ElMessage.success("导出成功")
|
ElMessage.success("导出成功")
|
||||||
@ -217,6 +225,11 @@ const checkIsTaskFinished = (taskId) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-tips {
|
||||||
|
display: flex;
|
||||||
|
color: #ec4e29;
|
||||||
|
}
|
||||||
|
|
||||||
.menus {
|
.menus {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ const handleLogout = () => {
|
|||||||
const activeIndex = ref('2')
|
const activeIndex = ref('2')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
.question-list-root {
|
.question-list-root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f6f7f9;
|
background-color: #f6f7f9;
|
||||||
@ -94,7 +94,7 @@ const activeIndex = ref('2')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.table-container {
|
.table-container {
|
||||||
padding: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%; /* 确保容器宽度为100% */
|
width: 100%; /* 确保容器宽度为100% */
|
||||||
|
@ -19,14 +19,14 @@
|
|||||||
:prop="field.key"
|
:prop="field.key"
|
||||||
:label="field.title"
|
:label="field.title"
|
||||||
:width="field.width"
|
:width="field.width"
|
||||||
class-name="link"
|
:class-name="[field.key]"
|
||||||
:formatter="field.formatter"
|
:formatter="field.formatter"
|
||||||
>
|
>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200">
|
<el-table-column label="操作" width="200">
|
||||||
<template v-slot="{ row }">
|
<template v-slot="{ row }">
|
||||||
<el-button size="small" @click="handleDownload(row)"> 下载 </el-button>
|
<span v-if="row.curStatus?.status === 'finished'" class="text-btn download-btn" @click="handleDownload(row)"> 下载 </span>
|
||||||
<el-button type="primary" size="small" @click="openDeleteDialog(row)"> 删除 </el-button>
|
<span class="text-btn delete-btn" @click="openDeleteDialog(row)"> 删除 </span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -35,31 +35,21 @@
|
|||||||
background
|
background
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
:total="total"
|
:total="total"
|
||||||
:size="pageSize"
|
small
|
||||||
|
:page-size="pageSize"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
</el-pagination>
|
</el-pagination>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="centerDialogVisible" title="" width="500" align-center>
|
|
||||||
<span>确认删除下载记录吗?</span>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="centerDialogVisible = false"> 取消 </el-button>
|
|
||||||
<el-button type="primary" @click="confirmDelete"> 确认 </el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { get, map } from 'lodash-es'
|
import { get, map } from 'lodash-es'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { deleteDownloadTask, getDownloadTaskList } from '@/management/api/downloadTask'
|
import { deleteDownloadTask, getDownloadTaskList } from '@/management/api/downloadTask'
|
||||||
import { CODE_MAP } from '@/management/api/base'
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
|
||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
// 引入中文
|
// 引入中文
|
||||||
@ -68,9 +58,9 @@ import 'moment/locale/zh-cn'
|
|||||||
moment.locale('zh-cn')
|
moment.locale('zh-cn')
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const pageSize = ref(15)
|
const pageSize = ref(10)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const dataList = reactive([])
|
const dataList: Array<any> = reactive([])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList({ pageIndex: 1 })
|
getList({ pageIndex: 1 })
|
||||||
@ -87,7 +77,8 @@ const getList = async ({ pageIndex }: { pageIndex: number }) => {
|
|||||||
const res: Record<string, any> = await getDownloadTaskList(params)
|
const res: Record<string, any> = await getDownloadTaskList(params)
|
||||||
if (res.code === CODE_MAP.SUCCESS) {
|
if (res.code === CODE_MAP.SUCCESS) {
|
||||||
total.value = res.data.total
|
total.value = res.data.total
|
||||||
dataList.values = res.data.list
|
const list = res.data.list as any
|
||||||
|
dataList.splice(0, dataList.length, ...list);
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -99,7 +90,6 @@ const statusTextMap: Record<string, string> = {
|
|||||||
removed: '已删除',
|
removed: '已删除',
|
||||||
};
|
};
|
||||||
|
|
||||||
const centerDialogVisible = ref(false)
|
|
||||||
let currentDelRow: Record<string, any> = {}
|
let currentDelRow: Record<string, any> = {}
|
||||||
// 下载文件
|
// 下载文件
|
||||||
const handleDownload = async (row: any) => {
|
const handleDownload = async (row: any) => {
|
||||||
@ -112,20 +102,33 @@ const handleDownload = async (row: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 删除文件
|
// 删除文件
|
||||||
const openDeleteDialog = (row: any) => {
|
const openDeleteDialog = async (row: any) => {
|
||||||
centerDialogVisible.value = true
|
try {
|
||||||
|
await ElMessageBox.confirm('是否确认删除?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
currentDelRow = row
|
currentDelRow = row
|
||||||
|
confirmDelete()
|
||||||
|
} catch (error) {
|
||||||
|
console.log('取消删除')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认删除文件
|
// 确认删除文件
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDownloadTask(currentDelRow.taskId)
|
const res: Record<string, any> = await deleteDownloadTask(currentDelRow.taskId)
|
||||||
|
if (res.code !== CODE_MAP.SUCCESS) {
|
||||||
|
ElMessage.error(res.errmsg)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
await getList({ pageIndex: 1 })
|
await getList({ pageIndex: 1 })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("删除失败,请刷新重试")
|
ElMessage.error("删除失败,请刷新重试")
|
||||||
}
|
}
|
||||||
centerDialogVisible.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = ['filename', 'fileSize', 'createDate', 'curStatus']
|
const fields = ['filename', 'fileSize', 'createDate', 'curStatus']
|
||||||
@ -177,13 +180,30 @@ const handleCurrentChange = (val: number) => {
|
|||||||
background-color: #f6f7f9;
|
background-color: #f6f7f9;
|
||||||
|
|
||||||
.list-wrapper {
|
.list-wrapper {
|
||||||
|
width: 90%;
|
||||||
|
min-width: 1080px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
.list-table {
|
.list-table {
|
||||||
.cell {
|
.cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.text-btn {
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.download-btn {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.small-text {
|
.small-text {
|
||||||
color: red;
|
color: red;
|
||||||
|
@ -7,11 +7,12 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
import { publishSurvey, saveSurvey, seizeSession } from '@/management/api/survey'
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
updateLogicConf: any
|
updateLogicConf: any
|
||||||
@ -27,6 +28,16 @@ const { schema, sessionId } = storeToRefs(editStore)
|
|||||||
const saveData = computed(() => {
|
const saveData = computed(() => {
|
||||||
return buildData(schema.value, sessionId.value)
|
return buildData(schema.value, sessionId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const seize = async () => {
|
||||||
|
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
|
||||||
|
if (seizeRes.code === 200) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取权限失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
@ -61,8 +72,33 @@ const onSave = async () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: Record<string, any> = await saveSurvey(saveData.value)
|
try {
|
||||||
|
const res: any = await saveSurvey(saveData.value)
|
||||||
|
if(!res) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
return res
|
return res
|
||||||
|
} else if (res.code === 3006) {
|
||||||
|
ElMessageBox.alert(res.errmsg, '提示', {
|
||||||
|
confirmButtonText: '刷新同步',
|
||||||
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
seize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.errmsg)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存问卷失败')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
if (isPublishing.value) {
|
if (isPublishing.value) {
|
||||||
@ -81,11 +117,7 @@ const handlePublish = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const saveRes: any = await onSave()
|
const saveRes: any = await onSave()
|
||||||
if (!saveRes) {
|
if (!saveRes || saveRes.code !== CODE_MAP.SUCCESS) {
|
||||||
return
|
|
||||||
}
|
|
||||||
if(saveRes && saveRes?.code !== 200) {
|
|
||||||
ElMessage.error(`保存失败 ${saveRes.errmsg}`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId })
|
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId })
|
||||||
|
@ -86,7 +86,7 @@ const onSave = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seize = async () => {
|
const seize = async () => {
|
||||||
const seizeRes: Record<string, any> = await seizeSession({ sessionId:sessionId.value })
|
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
|
||||||
if (seizeRes.code === 200) {
|
if (seizeRes.code === 200) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
@ -154,11 +154,13 @@ const handleSave = async () => {
|
|||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
return res
|
return res
|
||||||
} else if (res.code === 3006) {
|
} else if (res.code === 3006) {
|
||||||
ElMessageBox.alert('当前问卷已在其它页面开启编辑,点击“抢占”以获取保存权限。', '提示', {
|
ElMessageBox.alert(res.errmsg, '提示', {
|
||||||
confirmButtonText: '抢占',
|
confirmButtonText: '刷新同步',
|
||||||
callback: () => {
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
seize();
|
seize();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.errmsg)
|
ElMessage.error(res.errmsg)
|
||||||
|
@ -187,7 +187,7 @@ const handleLogout = () => {
|
|||||||
}
|
}
|
||||||
// 下载页面
|
// 下载页面
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
router.push('/survey/downloadTask/')
|
router.push({ name: 'download' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
|||||||
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
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'
|
||||||
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/button.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/overlay.scss'
|
||||||
import { useUserStore } from '@/management/stores/user'
|
import { useUserStore } from '@/management/stores/user'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
|
||||||
@ -27,7 +30,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/survey/downloadTask/',
|
path: '/download',
|
||||||
name: 'download',
|
name: 'download',
|
||||||
component: () => import('../pages/downloadTask/TaskList.vue'),
|
component: () => import('../pages/downloadTask/TaskList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
|
Loading…
Reference in New Issue
Block a user