Merge branch 'feature/peking' of github.com:didi/xiaoju-survey into feature/peking
This commit is contained in:
commit
aaedaef3bc
@ -22,7 +22,7 @@
|
|||||||
<img src="https://img.shields.io/badge/help-%E5%AE%98%E7%BD%91-blue" alt="docs">
|
<img src="https://img.shields.io/badge/help-%E5%AE%98%E7%BD%91-blue" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/didi/xiaoju-survey/blob/main/README_EN.md">
|
<a href="https://github.com/didi/xiaoju-survey/blob/main/README_EN.md">
|
||||||
<img src="https://img.shields.io/badge/help-%E8%8B%B1%E6%96%87README-50c62a" alt="docs">
|
<img src="https://img.shields.io/badge/help-README_EN-50c62a" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,10 +19,10 @@
|
|||||||
<img src="https://img.shields.io/badge/PRs-welcome-%23ffa600" alt="pr">
|
<img src="https://img.shields.io/badge/PRs-welcome-%23ffa600" alt="pr">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://xiaojusurvey.didi.cn">
|
<a href="https://xiaojusurvey.didi.cn">
|
||||||
<img src="https://img.shields.io/badge/help-%E5%AE%98%E7%BD%91-blue" alt="docs">
|
<img src="https://img.shields.io/badge/help-website-blue" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/didi/xiaoju-survey/blob/main/README.md">
|
<a href="https://github.com/didi/xiaoju-survey/blob/main/README.md">
|
||||||
<img src="https://img.shields.io/badge/help-README-50c62a" alt="docs">
|
<img src="https://img.shields.io/badge/help-README_ZH-50c62a" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,6 +39,11 @@ http {
|
|||||||
try_files $uri $uri/ /management.html;
|
try_files $uri $uri/ /management.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /management/preview/ {
|
||||||
|
try_files $uri $uri/ /render.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
location /render/ {
|
location /render/ {
|
||||||
try_files $uri $uri/ /render.html;
|
try_files $uri $uri/ /render.html;
|
||||||
}
|
}
|
||||||
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"node-cron": "^3.0.3"
|
||||||
|
}
|
||||||
|
}
|
@ -97,5 +97,9 @@
|
|||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^src/(.*)$": "<rootDir>/$1"
|
"^src/(.*)$": "<rootDir>/$1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
|||||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
|
import { SurveyDownload } from './models/surveyDownload.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -81,6 +82,7 @@ import { Logger } from './logger';
|
|||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
Collaborator,
|
Collaborator,
|
||||||
|
SurveyDownload,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
21
server/src/config/index.ts
Normal file
21
server/src/config/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const mongo = {
|
||||||
|
url: process.env.XIAOJU_SURVEY_MONGO_URL || 'mongodb://localhost:27017',
|
||||||
|
dbName: process.env.XIAOJU_SURVER_MONGO_DBNAME || 'xiaojuSurvey',
|
||||||
|
};
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
expireTime:
|
||||||
|
parseInt(process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN) || 8 * 3600 * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encrypt = {
|
||||||
|
type: process.env.XIAOJU_SURVEY_ENCRYPT_TYPE || 'aes',
|
||||||
|
aesCodelength: parseInt(process.env.XIAOJU_SURVEY_ENCRYPT_TYPE_LEN) || 10, //aes密钥长度
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwt = {
|
||||||
|
secret: process.env.XIAOJU_SURVEY_JWT_SECRET || 'xiaojuSurveyJwtSecret',
|
||||||
|
expiresIn: process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN || '8h',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { mongo, session, encrypt, jwt };
|
@ -6,6 +6,7 @@ export enum RECORD_STATUS {
|
|||||||
PUBLISHED = 'published', // 发布
|
PUBLISHED = 'published', // 发布
|
||||||
REMOVED = 'removed', // 删除
|
REMOVED = 'removed', // 删除
|
||||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||||
|
COMOPUTETING = 'computing', // 计算中
|
||||||
}
|
}
|
||||||
|
|
||||||
// 历史类型
|
// 历史类型
|
||||||
|
46
server/src/models/surveyDownload.entity.ts
Normal file
46
server/src/models/surveyDownload.entity.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Entity, Column, BeforeInsert, AfterLoad } from 'typeorm';
|
||||||
|
import pluginManager from '../securityPlugin/pluginManager';
|
||||||
|
import { BaseEntity } from './base.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'surveyDownload' })
|
||||||
|
export class SurveyDownload extends BaseEntity {
|
||||||
|
@Column()
|
||||||
|
pageId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
surveyPath: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
filePath: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
onwer: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
filename: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
fileSize: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
fileType: string;
|
||||||
|
|
||||||
|
// @Column()
|
||||||
|
// ownerId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
downloadTime: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
async onDataInsert() {
|
||||||
|
return await pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterLoad()
|
||||||
|
async onDataLoaded() {
|
||||||
|
return await pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ 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 { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ -30,6 +31,8 @@ export class DataStatisticController {
|
|||||||
private readonly dataStatisticService: DataStatisticService,
|
private readonly dataStatisticService: DataStatisticService,
|
||||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
//
|
||||||
|
private readonly surveyDownloadService: SurveyDownloadService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/dataTable')
|
@Get('/dataTable')
|
||||||
|
@ -223,6 +223,38 @@ export class SurveyController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/getPreviewSchema')
|
||||||
|
@HttpCode(200)
|
||||||
|
async getPreviewSchema(
|
||||||
|
@Query()
|
||||||
|
queryInfo: {
|
||||||
|
surveyPath: string;
|
||||||
|
},
|
||||||
|
@Request()
|
||||||
|
req,
|
||||||
|
) {
|
||||||
|
const { value, error } = Joi.object({
|
||||||
|
surveyId: Joi.string().required(),
|
||||||
|
}).validate({ surveyId: queryInfo.surveyPath });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(error.message, { req });
|
||||||
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
const surveyId = value.surveyId;
|
||||||
|
const surveyConf =
|
||||||
|
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||||
|
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
...surveyConf,
|
||||||
|
title: surveyMeta?.title,
|
||||||
|
surveyPath: surveyMeta?.surveyPath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post('/publishSurvey')
|
@Post('/publishSurvey')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(SurveyGuard)
|
@UseGuards(SurveyGuard)
|
||||||
|
@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
UseGuards,
|
||||||
|
SetMetadata,
|
||||||
|
Request,
|
||||||
|
Res,
|
||||||
|
// Response,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||||
|
|
||||||
|
import { Authentication } from 'src/guards/authentication.guard';
|
||||||
|
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||||
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
import { Logger } from 'src/logger';
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
//后添加
|
||||||
|
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||||
|
import {
|
||||||
|
DownloadFileByNameDto,
|
||||||
|
GetDownloadDto,
|
||||||
|
GetDownloadListDto,
|
||||||
|
} from '../dto/getdownload.dto';
|
||||||
|
import { join } from 'path';
|
||||||
|
import * as util from 'util';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { MessageService } from '../services/message.service';
|
||||||
|
|
||||||
|
@ApiTags('survey')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('/api/survey/surveyDownload')
|
||||||
|
export class SurveyDownloadController {
|
||||||
|
constructor(
|
||||||
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
|
private readonly surveyDownloadService: SurveyDownloadService,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly messageService: MessageService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('/download')
|
||||||
|
@HttpCode(200)
|
||||||
|
@UseGuards(SurveyGuard)
|
||||||
|
@SetMetadata('surveyId', 'query.surveyId')
|
||||||
|
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||||
|
@UseGuards(Authentication)
|
||||||
|
async download(
|
||||||
|
@Query()
|
||||||
|
queryInfo: GetDownloadDto,
|
||||||
|
@Request() req,
|
||||||
|
) {
|
||||||
|
const { value, error } = GetDownloadDto.validate(queryInfo);
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(error.message, { req });
|
||||||
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
const { surveyId, isDesensitive } = value;
|
||||||
|
const responseSchema =
|
||||||
|
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||||
|
const id = await this.surveyDownloadService.createDownload({
|
||||||
|
surveyId,
|
||||||
|
responseSchema,
|
||||||
|
});
|
||||||
|
this.messageService.addMessage({
|
||||||
|
responseSchema,
|
||||||
|
surveyId,
|
||||||
|
isDesensitive,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: { message: '正在生成下载文件,请稍后查看' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@Get('/getdownloadList')
|
||||||
|
@HttpCode(200)
|
||||||
|
@UseGuards(SurveyGuard)
|
||||||
|
@SetMetadata('surveyId', 'query.surveyId')
|
||||||
|
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||||
|
@UseGuards(Authentication)
|
||||||
|
async downloadList(
|
||||||
|
@Query()
|
||||||
|
queryInfo: GetDownloadListDto,
|
||||||
|
@Request() req,
|
||||||
|
) {
|
||||||
|
const { value, error } = GetDownloadListDto.validate(queryInfo);
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(error.message, { req });
|
||||||
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
const { ownerId, page, pageSize } = value;
|
||||||
|
const { total, listBody } =
|
||||||
|
await this.surveyDownloadService.getDownloadList({
|
||||||
|
ownerId,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
total: total,
|
||||||
|
listBody: listBody.map((data) => {
|
||||||
|
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let unitIndex = 0;
|
||||||
|
let size = Number(data.fileSize);
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
data.downloadTime = moment(Number(data.downloadTime)).format(fmt);
|
||||||
|
data.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/getdownloadfileByName')
|
||||||
|
// @HttpCode(200)
|
||||||
|
// @UseGuards(SurveyGuard)
|
||||||
|
// @SetMetadata('surveyId', 'query.surveyId')
|
||||||
|
// @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||||
|
// @UseGuards(Authentication)
|
||||||
|
async getDownloadfileByName(
|
||||||
|
@Query() queryInfo: DownloadFileByNameDto,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const { value, error } = DownloadFileByNameDto.validate(queryInfo);
|
||||||
|
if (error) {
|
||||||
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { owner, fileName } = value;
|
||||||
|
const rootDir = process.cwd(); // 获取当前工作目录
|
||||||
|
const filePath = join(rootDir, 'download', owner, fileName);
|
||||||
|
|
||||||
|
// 使用 util.promisify 将 fs.access 转换为返回 Promise 的函数
|
||||||
|
const access = util.promisify(fs.access);
|
||||||
|
try {
|
||||||
|
console.log('检查文件路径:', filePath);
|
||||||
|
await access(filePath, fs.constants.F_OK);
|
||||||
|
|
||||||
|
// 文件存在,设置响应头并流式传输文件
|
||||||
|
res.setHeader('Content-Type', 'application/octet-stream');
|
||||||
|
console.log('文件存在,设置响应头');
|
||||||
|
const encodedFileName = encodeURIComponent(fileName);
|
||||||
|
const contentDisposition = `attachment; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`;
|
||||||
|
res.setHeader('Content-Disposition', contentDisposition);
|
||||||
|
console.log('设置响应头成功,文件名:', encodedFileName);
|
||||||
|
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
console.log('创建文件流成功');
|
||||||
|
fileStream.pipe(res);
|
||||||
|
|
||||||
|
fileStream.on('end', () => {
|
||||||
|
console.log('文件传输完成');
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on('error', (streamErr) => {
|
||||||
|
console.error('文件流错误:', streamErr);
|
||||||
|
res.status(500).send('文件传输中出现错误');
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('文件不存在:', filePath);
|
||||||
|
res.status(404).send('文件不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/deletefileByName')
|
||||||
|
@HttpCode(200)
|
||||||
|
@UseGuards(SurveyGuard)
|
||||||
|
@SetMetadata('surveyId', 'query.surveyId')
|
||||||
|
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||||
|
@UseGuards(Authentication)
|
||||||
|
async deleteFileByName(
|
||||||
|
@Query() queryInfo: DownloadFileByNameDto,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const { value, error } = DownloadFileByNameDto.validate(queryInfo);
|
||||||
|
if (error) {
|
||||||
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
const { owner, fileName } = value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.surveyDownloadService.deleteDownloadFile({
|
||||||
|
owner,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据 deleteDownloadFile 的返回值执行不同操作
|
||||||
|
if (result === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
code: 404,
|
||||||
|
message: '文件状态已删除或文件不存在',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
code: 200,
|
||||||
|
message: '文件删除成功',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({
|
||||||
|
code: 500,
|
||||||
|
message: '删除文件时出错',
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
server/src/modules/survey/dto/getdownload.dto.ts
Normal file
43
server/src/modules/survey/dto/getdownload.dto.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import Joi from 'joi';
|
||||||
|
|
||||||
|
export class GetDownloadDto {
|
||||||
|
@ApiProperty({ description: '问卷id', required: true })
|
||||||
|
surveyId: string;
|
||||||
|
@ApiProperty({ description: '是否脱密', required: true })
|
||||||
|
isDesensitive: boolean;
|
||||||
|
|
||||||
|
static validate(data) {
|
||||||
|
return Joi.object({
|
||||||
|
surveyId: Joi.string().required(),
|
||||||
|
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||||
|
}).validate(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class GetDownloadListDto {
|
||||||
|
@ApiProperty({ description: '拥有者id', required: true })
|
||||||
|
ownerId: string;
|
||||||
|
@ApiProperty({ description: '当前页', required: false })
|
||||||
|
page: number;
|
||||||
|
@ApiProperty({ description: '一页大小', required: false })
|
||||||
|
pageSize: number;
|
||||||
|
|
||||||
|
static validate(data) {
|
||||||
|
return Joi.object({
|
||||||
|
ownerId: Joi.string().required(),
|
||||||
|
page: Joi.number().default(1),
|
||||||
|
pageSize: Joi.number().default(20),
|
||||||
|
}).validate(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class DownloadFileByNameDto {
|
||||||
|
@ApiProperty({ description: '文件名', required: true })
|
||||||
|
fileName: string;
|
||||||
|
owner: string;
|
||||||
|
static validate(data) {
|
||||||
|
return Joi.object({
|
||||||
|
fileName: Joi.string().required(),
|
||||||
|
owner: Joi.string().required(),
|
||||||
|
}).validate(data);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ 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 } from '../utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataStatisticService {
|
export class DataStatisticService {
|
||||||
private radioType = ['radio-star', 'radio-nps'];
|
private radioType = ['radio-star', 'radio-nps'];
|
||||||
|
87
server/src/modules/survey/services/message.service.ts
Normal file
87
server/src/modules/survey/services/message.service.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { SurveyDownloadService } from './surveyDownload.service';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
|
|
||||||
|
interface QueueItem {
|
||||||
|
surveyId: string;
|
||||||
|
responseSchema: ResponseSchema;
|
||||||
|
isDesensitive: boolean;
|
||||||
|
id: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessageService extends EventEmitter {
|
||||||
|
private queue: QueueItem[];
|
||||||
|
private concurrency: number;
|
||||||
|
private processing: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('NumberToken') concurrency: number,
|
||||||
|
private readonly surveyDownloadService: SurveyDownloadService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.queue = [];
|
||||||
|
this.concurrency = concurrency;
|
||||||
|
this.processing = 0;
|
||||||
|
this.on('messageAdded', this.processMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMessage({
|
||||||
|
surveyId,
|
||||||
|
responseSchema,
|
||||||
|
isDesensitive,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
surveyId: string;
|
||||||
|
responseSchema: ResponseSchema;
|
||||||
|
isDesensitive: boolean;
|
||||||
|
id: object;
|
||||||
|
}) {
|
||||||
|
const message = {
|
||||||
|
surveyId,
|
||||||
|
responseSchema,
|
||||||
|
isDesensitive,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
this.queue.push(message);
|
||||||
|
this.emit('messageAdded');
|
||||||
|
}
|
||||||
|
|
||||||
|
private processMessages = async (): Promise<void> => {
|
||||||
|
if (this.processing >= this.concurrency || this.queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesToProcess = Math.min(
|
||||||
|
this.queue.length,
|
||||||
|
this.concurrency - this.processing,
|
||||||
|
);
|
||||||
|
const messages = this.queue.splice(0, messagesToProcess);
|
||||||
|
|
||||||
|
this.processing += messagesToProcess;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
messages.map(async (message) => {
|
||||||
|
console.log(`开始计算: ${message}`);
|
||||||
|
await this.handleMessage(message);
|
||||||
|
this.emit('messageProcessed', message);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.processing -= messagesToProcess;
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
setImmediate(() => this.processMessages());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async handleMessage(message: QueueItem) {
|
||||||
|
const { surveyId, responseSchema, isDesensitive, id } = message;
|
||||||
|
await this.surveyDownloadService.getDownloadPath({
|
||||||
|
responseSchema,
|
||||||
|
surveyId,
|
||||||
|
isDesensitive,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
365
server/src/modules/survey/services/surveyDownload.service.ts
Normal file
365
server/src/modules/survey/services/surveyDownload.service.ts
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { MongoRepository } from 'typeorm';
|
||||||
|
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import { keyBy } from 'lodash';
|
||||||
|
import { DataItem } from 'src/interfaces/survey';
|
||||||
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
|
import { getListHeadByDataList } from '../utils';
|
||||||
|
//后添加
|
||||||
|
import { promises } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { SurveyDownload } from 'src/models/surveyDownload.entity';
|
||||||
|
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||||
|
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||||
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
import * as cron from 'node-cron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurveyDownloadService implements OnModuleInit {
|
||||||
|
private radioType = ['radio-star', 'radio-nps'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SurveyResponse)
|
||||||
|
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||||
|
@InjectRepository(SurveyDownload)
|
||||||
|
private readonly SurveyDownloadRepository: MongoRepository<SurveyDownload>,
|
||||||
|
@InjectRepository(SurveyMeta)
|
||||||
|
private readonly SurveyDmetaRepository: MongoRepository<SurveyMeta>,
|
||||||
|
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||||
|
) {}
|
||||||
|
//初始化一个自动删除过期文件的方法
|
||||||
|
async onModuleInit() {
|
||||||
|
cron.schedule('0 0 * * *', async () => {
|
||||||
|
try {
|
||||||
|
const files = await this.SurveyDownloadRepository.find({
|
||||||
|
where: {
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.downloadTime || !file.filePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSaveDate = Number(file.downloadTime);
|
||||||
|
const diffDays = (now - fileSaveDate) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
if (diffDays > 10) {
|
||||||
|
this.deleteDownloadFile({
|
||||||
|
owner: file.onwer,
|
||||||
|
fileName: file.filename,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('删除文件错误', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDownload({
|
||||||
|
surveyId,
|
||||||
|
responseSchema,
|
||||||
|
}: {
|
||||||
|
surveyId: string;
|
||||||
|
responseSchema: ResponseSchema;
|
||||||
|
}) {
|
||||||
|
const [surveyMeta] = await this.SurveyDmetaRepository.find({
|
||||||
|
where: {
|
||||||
|
surveyPath: responseSchema.surveyPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const newSurveyDownload = this.SurveyDownloadRepository.create({
|
||||||
|
pageId: surveyId,
|
||||||
|
surveyPath: responseSchema.surveyPath,
|
||||||
|
title: responseSchema.title,
|
||||||
|
fileSize: '计算中',
|
||||||
|
downloadTime: String(Date.now()),
|
||||||
|
onwer: surveyMeta.owner,
|
||||||
|
});
|
||||||
|
newSurveyDownload.curStatus = {
|
||||||
|
status: RECORD_STATUS.COMOPUTETING,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
return (await this.SurveyDownloadRepository.save(newSurveyDownload))._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 getDownloadPath({
|
||||||
|
surveyId,
|
||||||
|
responseSchema,
|
||||||
|
isDesensitive,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
surveyId: string;
|
||||||
|
responseSchema: ResponseSchema;
|
||||||
|
isDesensitive: boolean;
|
||||||
|
id: object;
|
||||||
|
}) {
|
||||||
|
const dataList = responseSchema?.code?.dataConf?.dataList || [];
|
||||||
|
const Head = getListHeadByDataList(dataList);
|
||||||
|
const listHead = this.formatHead(Head);
|
||||||
|
const dataListMap = keyBy(dataList, 'field');
|
||||||
|
const where = {
|
||||||
|
pageId: surveyId,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: 'removed',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [surveyResponseList] =
|
||||||
|
await this.surveyResponseRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: {
|
||||||
|
createDate: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [surveyMeta] = await this.SurveyDmetaRepository.find({
|
||||||
|
where: {
|
||||||
|
surveyPath: responseSchema.surveyPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const listBody = surveyResponseList.map((submitedData) => {
|
||||||
|
const data = submitedData.data;
|
||||||
|
const dataKeys = Object.keys(data);
|
||||||
|
|
||||||
|
for (const itemKey of dataKeys) {
|
||||||
|
if (typeof itemKey !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (itemKey.indexOf('data') !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 获取题目id
|
||||||
|
const itemConfigKey = itemKey.split('_')[0];
|
||||||
|
// 获取题目
|
||||||
|
const itemConfig: DataItem = dataListMap[itemConfigKey];
|
||||||
|
// 题目删除会出现,数据列表报错
|
||||||
|
if (!itemConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 处理选项的更多输入框
|
||||||
|
if (
|
||||||
|
this.radioType.includes(itemConfig.type) &&
|
||||||
|
!data[`${itemConfigKey}_custom`]
|
||||||
|
) {
|
||||||
|
data[`${itemConfigKey}_custom`] =
|
||||||
|
data[`${itemConfigKey}_${data[itemConfigKey]}`];
|
||||||
|
}
|
||||||
|
// 将选项id还原成选项文案
|
||||||
|
if (
|
||||||
|
Array.isArray(itemConfig.options) &&
|
||||||
|
itemConfig.options.length > 0
|
||||||
|
) {
|
||||||
|
const optionTextMap = keyBy(itemConfig.options, 'hash');
|
||||||
|
data[itemKey] = Array.isArray(data[itemKey])
|
||||||
|
? data[itemKey]
|
||||||
|
.map((item) => optionTextMap[item]?.text || item)
|
||||||
|
.join(',')
|
||||||
|
: optionTextMap[data[itemKey]]?.text || data[itemKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||||
|
createDate: moment(submitedData.createDate).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (isDesensitive) {
|
||||||
|
// 脱敏
|
||||||
|
listBody.forEach((item) => {
|
||||||
|
this.pluginManager.triggerHook('desensitiveData', item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let titlesCsv =
|
||||||
|
listHead
|
||||||
|
.map((question) => `"${question.title.replace(/<[^>]*>/g, '')}"`)
|
||||||
|
.join(',') + '\n';
|
||||||
|
// 获取工作区根目录的路径
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const filePath = join(
|
||||||
|
rootDir,
|
||||||
|
'download',
|
||||||
|
`${surveyMeta.owner}`,
|
||||||
|
`${surveyMeta.title}_${timestamp}.csv`,
|
||||||
|
);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
listBody.forEach((row) => {
|
||||||
|
const rowValues = listHead.map((head) => {
|
||||||
|
const value = row[head.field];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 处理字符串中的特殊字符
|
||||||
|
return `"${value.replace(/"/g, '""').replace(/<[^>]*>/g, '')}"`;
|
||||||
|
}
|
||||||
|
return `"${value}"`; // 其他类型的值(数字、布尔等)直接转换为字符串
|
||||||
|
});
|
||||||
|
titlesCsv += rowValues.join(',') + '\n';
|
||||||
|
});
|
||||||
|
const BOM = '\uFEFF';
|
||||||
|
let size = 0;
|
||||||
|
const newSurveyDownload = await this.SurveyDownloadRepository.findOne({
|
||||||
|
where: {
|
||||||
|
_id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.writeFile(filePath, BOM + titlesCsv, { encoding: 'utf8' }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('保存文件时出错:', err);
|
||||||
|
} else {
|
||||||
|
console.log('文件已保存:', filePath);
|
||||||
|
fs.stat(filePath, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('获取文件大小时出错:', err);
|
||||||
|
} else {
|
||||||
|
console.log('文件大小:', stats.size);
|
||||||
|
size = stats.size;
|
||||||
|
const filename = `${surveyMeta.title}_${timestamp}.csv`;
|
||||||
|
const fileType = 'csv';
|
||||||
|
(newSurveyDownload.pageId = surveyId),
|
||||||
|
(newSurveyDownload.surveyPath = responseSchema.surveyPath),
|
||||||
|
(newSurveyDownload.title = responseSchema.title),
|
||||||
|
(newSurveyDownload.filePath = filePath),
|
||||||
|
(newSurveyDownload.filename = filename),
|
||||||
|
(newSurveyDownload.fileType = fileType),
|
||||||
|
(newSurveyDownload.fileSize = String(size)),
|
||||||
|
(newSurveyDownload.downloadTime = String(Date.now())),
|
||||||
|
(newSurveyDownload.onwer = surveyMeta.owner);
|
||||||
|
newSurveyDownload.curStatus = {
|
||||||
|
status: RECORD_STATUS.NEW,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.SurveyDownloadRepository.save(newSurveyDownload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDownloadList({
|
||||||
|
ownerId,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
}: {
|
||||||
|
ownerId: string;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
const where = {
|
||||||
|
onwer: ownerId,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [surveyDownloadList, total] =
|
||||||
|
await this.SurveyDownloadRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
take: pageSize,
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
order: {
|
||||||
|
createDate: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const listBody = surveyDownloadList.map((data) => {
|
||||||
|
return {
|
||||||
|
_id: data._id,
|
||||||
|
filename: data.filename,
|
||||||
|
fileType: data.fileType,
|
||||||
|
fileSize: data.fileSize,
|
||||||
|
downloadTime: data.downloadTime,
|
||||||
|
curStatus: data.curStatus.status,
|
||||||
|
owner: data.onwer,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
listBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async test({}: { fileName: string }) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDownloadFile({
|
||||||
|
owner,
|
||||||
|
fileName,
|
||||||
|
}: {
|
||||||
|
owner: string;
|
||||||
|
fileName: string;
|
||||||
|
}) {
|
||||||
|
const where = {
|
||||||
|
filename: fileName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [surveyDownloadList] = await this.SurveyDownloadRepository.find({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
if (surveyDownloadList.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatusInfo = {
|
||||||
|
status: RECORD_STATUS.REMOVED,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
surveyDownloadList.curStatus = newStatusInfo;
|
||||||
|
// if (Array.isArray(survey.statusList)) {
|
||||||
|
// survey.statusList.push(newStatusInfo);
|
||||||
|
// } else {
|
||||||
|
// survey.statusList = [newStatusInfo];
|
||||||
|
// }
|
||||||
|
const rootDir = process.cwd(); // 获取当前工作目录
|
||||||
|
const filePath = join(rootDir, 'download', owner, fileName);
|
||||||
|
try {
|
||||||
|
await promises.unlink(filePath);
|
||||||
|
console.log(`File at ${filePath} has been successfully deleted.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete file at ${filePath}:`, error);
|
||||||
|
}
|
||||||
|
await this.SurveyDownloadRepository.save(surveyDownloadList);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
message: '删除成功',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,11 @@ import { SurveyHistoryService } from './services/surveyHistory.service';
|
|||||||
import { SurveyMetaService } from './services/surveyMeta.service';
|
import { SurveyMetaService } from './services/surveyMeta.service';
|
||||||
import { ContentSecurityService } from './services/contentSecurity.service';
|
import { ContentSecurityService } from './services/contentSecurity.service';
|
||||||
import { CollaboratorService } from './services/collaborator.service';
|
import { CollaboratorService } from './services/collaborator.service';
|
||||||
|
//后添加
|
||||||
|
import { SurveyDownload } from 'src/models/surveyDownload.entity';
|
||||||
|
import { SurveyDownloadService } from './services/surveyDownload.service';
|
||||||
|
import { SurveyDownloadController } from './controllers/surveyDownload.controller';
|
||||||
|
import { MessageService } from './services/message.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -39,6 +44,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
SurveyResponse,
|
SurveyResponse,
|
||||||
Word,
|
Word,
|
||||||
Collaborator,
|
Collaborator,
|
||||||
|
//后添加
|
||||||
|
SurveyDownload,
|
||||||
]),
|
]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
SurveyResponseModule,
|
SurveyResponseModule,
|
||||||
@ -52,6 +59,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
SurveyMetaController,
|
SurveyMetaController,
|
||||||
SurveyUIController,
|
SurveyUIController,
|
||||||
CollaboratorController,
|
CollaboratorController,
|
||||||
|
//后添加
|
||||||
|
SurveyDownloadController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DataStatisticService,
|
DataStatisticService,
|
||||||
@ -62,6 +71,13 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
ContentSecurityService,
|
ContentSecurityService,
|
||||||
CollaboratorService,
|
CollaboratorService,
|
||||||
LoggerProvider,
|
LoggerProvider,
|
||||||
|
//后添加
|
||||||
|
SurveyDownloadService,
|
||||||
|
MessageService,
|
||||||
|
{
|
||||||
|
provide: 'NumberToken', // 使用一个唯一的标识符
|
||||||
|
useValue: 10, // 假设这是你想提供的值
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SurveyModule {}
|
export class SurveyModule {}
|
||||||
|
6
web/components.d.ts
vendored
6
web/components.d.ts
vendored
@ -28,7 +28,6 @@ declare module 'vue' {
|
|||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
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']
|
|
||||||
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']
|
||||||
@ -46,10 +45,11 @@ declare module 'vue' {
|
|||||||
IEpClose: typeof import('~icons/ep/close')['default']
|
IEpClose: typeof import('~icons/ep/close')['default']
|
||||||
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
||||||
IEpDelete: typeof import('~icons/ep/delete')['default']
|
IEpDelete: typeof import('~icons/ep/delete')['default']
|
||||||
|
IEpIphone: typeof import('~icons/ep/iphone')['default']
|
||||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
IEpLoading: typeof import('~icons/ep/loading')['default']
|
||||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
IEpMinus: typeof import('~icons/ep/minus')['default']
|
||||||
|
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
||||||
IEpMore: typeof import('~icons/ep/more')['default']
|
IEpMore: typeof import('~icons/ep/more')['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']
|
||||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||||
@ -58,6 +58,8 @@ declare module 'vue' {
|
|||||||
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
|
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
|
||||||
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
|
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
|
||||||
IEpTop: typeof import('~icons/ep/top')['default']
|
IEpTop: typeof import('~icons/ep/top')['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']
|
||||||
}
|
}
|
||||||
|
BIN
web/public/imgs/preview-phone.png
Normal file
BIN
web/public/imgs/preview-phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
50
web/src/common/typeEnum.ts
Normal file
50
web/src/common/typeEnum.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 题型枚举
|
||||||
|
export enum QUESTION_TYPE {
|
||||||
|
TEXT = 'text',
|
||||||
|
TEXTAREA = 'textarea',
|
||||||
|
RADIO = 'radio',
|
||||||
|
CHECKBOX = 'checkbox',
|
||||||
|
BINARY_CHOICE = 'binary-choice',
|
||||||
|
RADIO_STAR = 'radio-star',
|
||||||
|
RADIO_NPS = 'radio-nps',
|
||||||
|
VOTE = 'vote',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 题目类型标签映射对象
|
||||||
|
export const typeTagLabels: Record<QUESTION_TYPE, string> = {
|
||||||
|
[QUESTION_TYPE.TEXT]: '单行输入框',
|
||||||
|
[QUESTION_TYPE.TEXTAREA]: '多行输入框',
|
||||||
|
[QUESTION_TYPE.RADIO]: '单选',
|
||||||
|
[QUESTION_TYPE.CHECKBOX]: '多选',
|
||||||
|
[QUESTION_TYPE.BINARY_CHOICE]: '判断',
|
||||||
|
[QUESTION_TYPE.RADIO_STAR]: '评分',
|
||||||
|
[QUESTION_TYPE.RADIO_NPS]: 'NPS评分',
|
||||||
|
[QUESTION_TYPE.VOTE]: '投票'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入类题型
|
||||||
|
export const INPUT = [
|
||||||
|
QUESTION_TYPE.TEXT,
|
||||||
|
QUESTION_TYPE.TEXTAREA
|
||||||
|
]
|
||||||
|
|
||||||
|
// 选择类题型分类
|
||||||
|
export const NORMAL_CHOICES = [
|
||||||
|
QUESTION_TYPE.RADIO,
|
||||||
|
QUESTION_TYPE.CHECKBOX
|
||||||
|
]
|
||||||
|
|
||||||
|
// 选择类题型分类
|
||||||
|
export const CHOICES = [
|
||||||
|
QUESTION_TYPE.RADIO,
|
||||||
|
QUESTION_TYPE.CHECKBOX,
|
||||||
|
QUESTION_TYPE.BINARY_CHOICE,
|
||||||
|
QUESTION_TYPE.VOTE
|
||||||
|
]
|
||||||
|
|
||||||
|
// 评分题题型分类
|
||||||
|
export const RATES = [
|
||||||
|
QUESTION_TYPE.RADIO_STAR,
|
||||||
|
QUESTION_TYPE.RADIO_NPS
|
||||||
|
]
|
||||||
|
|
@ -8,3 +8,12 @@ export const getRecycleList = (data) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//问卷下载
|
||||||
|
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
|
||||||
|
return axios.get('/survey/surveyDownload/download', {
|
||||||
|
params: {
|
||||||
|
surveyId,
|
||||||
|
isDesensitive
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
34
web/src/management/api/download.js
Normal file
34
web/src/management/api/download.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import axios from './base'
|
||||||
|
|
||||||
|
//问卷列表
|
||||||
|
export const getDownloadList = ({ ownerId, page, pageSize }) => {
|
||||||
|
return axios.get('/survey/surveyDownload/getdownloadList', {
|
||||||
|
params: {
|
||||||
|
ownerId,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//问卷下载
|
||||||
|
export const getDownloadFileByName = (fileName) => {
|
||||||
|
return axios
|
||||||
|
.get('/survey/surveyDownload/getdownloadfileByName', {
|
||||||
|
params: {
|
||||||
|
fileName
|
||||||
|
},
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//问卷删除
|
||||||
|
export const deleteDownloadFile = (owner, fileName) => {
|
||||||
|
return axios.get('/survey/surveyDownload/deletefileByName', {
|
||||||
|
params: {
|
||||||
|
owner,
|
||||||
|
fileName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import axios from './base'
|
import axios from './base'
|
||||||
|
|
||||||
// 空间
|
// 空间
|
||||||
export const createSpace = ({ name, description, members }: any) => {
|
export const createSpace = ({ name, description, members }: any) => {
|
||||||
return axios.post('/workspace', { name, description, members })
|
return axios.post('/workspace', { name, description, members })
|
||||||
@ -29,7 +28,7 @@ export const getUserList = (username: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 协作权限列表
|
// 获取协作权限下拉框枚举
|
||||||
export const getPermissionList = () => {
|
export const getPermissionList = () => {
|
||||||
return axios.get('collaborator/getPermissionList')
|
return axios.get('collaborator/getPermissionList')
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -58,18 +58,18 @@ const tabArr = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
const tabs = ref([])
|
const tabs = ref([])
|
||||||
onMounted(async () => {
|
watch(() => store.state.cooperPermissions, (newVal) => {
|
||||||
await store.dispatch('fetchCooperPermissions', route.params.id)
|
tabs.value = []
|
||||||
// 如果有问卷管理权限,则加入问卷编辑和投放菜单
|
// 如果有问卷管理权限,则加入问卷编辑和投放菜单
|
||||||
if (store.state.cooperPermissions.includes(SurveyPermissions.SurveyManage)) {
|
if (newVal.includes(SurveyPermissions.SurveyManage)) {
|
||||||
tabs.value.push(tabArr[0])
|
tabs.value.push(tabArr[0])
|
||||||
tabs.value.push(tabArr[1])
|
tabs.value.push(tabArr[1])
|
||||||
}
|
}
|
||||||
// 如果有数据分析权限,则加入数据分析菜单
|
// 如果有数据分析权限,则加入数据分析菜单
|
||||||
if (store.state.cooperPermissions.includes(SurveyPermissions.DataManage)) {
|
if (newVal.includes(SurveyPermissions.DataManage)) {
|
||||||
tabs.value.push(tabArr[2])
|
tabs.value.push(tabArr[2])
|
||||||
}
|
}
|
||||||
})
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nav {
|
.nav {
|
||||||
|
@ -11,6 +11,17 @@
|
|||||||
@input="onIsShowOriginChange"
|
@input="onIsShowOriginChange"
|
||||||
>
|
>
|
||||||
</el-switch>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -38,7 +49,7 @@ import 'element-plus/theme-chalk/src/message.scss'
|
|||||||
|
|
||||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||||
import LeftMenu from '@/management/components/LeftMenu.vue'
|
import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||||
import { getRecycleList } from '@/management/api/analysis'
|
import { getRecycleList, downloadSurvey } from '@/management/api/analysis'
|
||||||
|
|
||||||
import DataTable from './components/DataTable.vue'
|
import DataTable from './components/DataTable.vue'
|
||||||
|
|
||||||
@ -59,7 +70,8 @@ export default {
|
|||||||
},
|
},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
isShowOriginData: false,
|
isShowOriginData: false,
|
||||||
tmpIsShowOriginData: false
|
tmpIsShowOriginData: false,
|
||||||
|
isDownloadDesensitive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
@ -89,6 +101,34 @@ export default {
|
|||||||
ElMessage.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) {
|
handleCurrentChange(current) {
|
||||||
if (this.mainTableLoading) {
|
if (this.mainTableLoading) {
|
||||||
return
|
return
|
||||||
@ -125,6 +165,29 @@ export default {
|
|||||||
this.tmpIsShowOriginData = data
|
this.tmpIsShowOriginData = data
|
||||||
await this.init()
|
await this.init()
|
||||||
this.isShowOriginData = data
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -158,6 +221,8 @@ export default {
|
|||||||
|
|
||||||
.menus {
|
.menus {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
|
129
web/src/management/pages/download/SurveyDownloadPage.vue
Normal file
129
web/src/management/pages/download/SurveyDownloadPage.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-list-root">
|
||||||
|
<div class="top-nav">
|
||||||
|
<div class="left">
|
||||||
|
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
||||||
|
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
||||||
|
<el-menu-item index="1" @click="handleSurvey">问卷列表</el-menu-item>
|
||||||
|
<el-menu-item index="2">下载页面</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
<div class="login-info">
|
||||||
|
您好,{{ userInfo?.username }}
|
||||||
|
<img class="login-info-img" src="/imgs/avatar.webp" />
|
||||||
|
<span class="logout" @click="handleLogout">退出</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<DownloadList
|
||||||
|
:loading="loading"
|
||||||
|
:data="surveyList"
|
||||||
|
:total="surveyTotal"
|
||||||
|
@reflush="fetchSurveyList"
|
||||||
|
></DownloadList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import DownloadList from './components/DownloadList.vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const userInfo = computed(() => {
|
||||||
|
return store.state.user.userInfo
|
||||||
|
})
|
||||||
|
const surveyList = computed(() => {
|
||||||
|
return store.state.download.surveyList
|
||||||
|
})
|
||||||
|
const surveyTotal = computed(() => {
|
||||||
|
return store.state.download.surveyTotal
|
||||||
|
})
|
||||||
|
const handleSurvey = () => {
|
||||||
|
router.push('/survey')
|
||||||
|
}
|
||||||
|
const handleLogout = () => {
|
||||||
|
store.dispatch('user/logout')
|
||||||
|
router.replace({ name: 'login' })
|
||||||
|
}
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSurveyList()
|
||||||
|
})
|
||||||
|
const fetchSurveyList = async (params?: any) => {
|
||||||
|
if (!params) {
|
||||||
|
params = {
|
||||||
|
pageSize: 15,
|
||||||
|
curPage: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(params.ownerId = store.state.user.userInfo.username), (loading.value = true)
|
||||||
|
await store.dispatch('download/getDownloadList', params)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
const activeIndex = ref('2')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.question-list-root {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f6f7f9;
|
||||||
|
.top-nav {
|
||||||
|
background: #fff;
|
||||||
|
color: #4a4c5b;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% - 200px);
|
||||||
|
.logo-img {
|
||||||
|
width: 90px;
|
||||||
|
height: fit-content;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.el-menu {
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
border: none !important;
|
||||||
|
:deep(.el-menu-item, .is-active) {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.login-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.login-info-img {
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 30px;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #faa600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.table-container {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%; /* 确保容器宽度为100% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
273
web/src/management/pages/download/components/DownloadList.vue
Normal file
273
web/src/management/pages/download/components/DownloadList.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-wrapper" v-if="total">
|
||||||
|
<el-table
|
||||||
|
v-if="total"
|
||||||
|
ref="multipleListTable"
|
||||||
|
class="list-table"
|
||||||
|
:data="dataList"
|
||||||
|
empty-text="暂无数据"
|
||||||
|
row-key="_id"
|
||||||
|
header-row-class-name="tableview-header"
|
||||||
|
row-class-name="tableview-row"
|
||||||
|
cell-class-name="tableview-cell"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
v-for="field in fieldList"
|
||||||
|
:key="field.key"
|
||||||
|
:prop="field.key"
|
||||||
|
:label="field.title"
|
||||||
|
:width="field.width"
|
||||||
|
class-name="link"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template v-slot="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="handleDownload(row)"> 下载 </el-button>
|
||||||
|
<el-button type="text" size="small" @click="openDeleteDialog(row)"> 删除 </el-button>
|
||||||
|
<el-dialog v-model="centerDialogVisible" title="Warning" 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>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="small-text">文件有效期为十天,过期或删除将从下载页面移除,请及时下载.</div>
|
||||||
|
<div class="list-pagination" v-if="total">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { get, map } from 'lodash-es'
|
||||||
|
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
|
|
||||||
|
import moment from 'moment'
|
||||||
|
// 引入中文
|
||||||
|
import 'moment/locale/zh-cn'
|
||||||
|
// 设置中文
|
||||||
|
moment.locale('zh-cn')
|
||||||
|
|
||||||
|
import { deleteDownloadFile } from '@/management/api/download'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
interface DownloadItem {
|
||||||
|
downloadTime: number // 根据实际情况可能需要调整类型
|
||||||
|
[key: string]: any // 允许其他任意属性
|
||||||
|
}
|
||||||
|
const store = useStore()
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const centerDialogVisible = ref(false)
|
||||||
|
// 下载文件
|
||||||
|
const handleDownload = async (row: any) => {
|
||||||
|
if (row.curStatus == 'removed') {
|
||||||
|
ElMessage.error('文件已删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fileName = row.filename
|
||||||
|
const owner = row.owner
|
||||||
|
axios({
|
||||||
|
method: 'get',
|
||||||
|
url:
|
||||||
|
'/api/survey/surveyDownload/getdownloadfileByName?fileName=' + fileName + '&owner=' + owner,
|
||||||
|
responseType: 'blob' // 设置响应类型为 Blob
|
||||||
|
})
|
||||||
|
.then((response: { data: BlobPart }) => {
|
||||||
|
const blob = new Blob([response.data])
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = blobUrl
|
||||||
|
link.download = fileName
|
||||||
|
|
||||||
|
// 添加到文档中
|
||||||
|
document.body.appendChild(link)
|
||||||
|
|
||||||
|
// 模拟点击链接来触发文件下载
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.error('下载文件时出错:', error)
|
||||||
|
})
|
||||||
|
// try {
|
||||||
|
// // 获取文件内容
|
||||||
|
// const response = await getDownloadFileByName(fileName);
|
||||||
|
// console.log('Response from server:', response);
|
||||||
|
|
||||||
|
// // 解析文件名获取 MIME 类型
|
||||||
|
// let mimeType = '';
|
||||||
|
// if (fileName.endsWith('.csv')) {
|
||||||
|
// mimeType = 'text/csv; charset=utf-8'; // 指定编码方式为 UTF-8
|
||||||
|
// } else if (fileName.endsWith('.xls')) {
|
||||||
|
// mimeType = 'application/vnd.ms-excel';
|
||||||
|
// } else if (fileName.endsWith('.xlsx')) {
|
||||||
|
// mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
// } else {
|
||||||
|
// throw new Error('不支持的文件类型');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const blob = new Blob(response.data, { type: mimeType });
|
||||||
|
// console.log('Blob:', blob); // Check the Blob object
|
||||||
|
|
||||||
|
// const url = window.URL.createObjectURL(blob);
|
||||||
|
// const link = document.createElement('a');
|
||||||
|
// link.href = url;
|
||||||
|
// link.download = fileName;
|
||||||
|
// link.click();
|
||||||
|
// window.URL.revokeObjectURL(url);
|
||||||
|
// if (link.parentNode)
|
||||||
|
// link.parentNode.removeChild(link);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('下载文件时出错:', error);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
// 删除文件
|
||||||
|
const openDeleteDialog = (row: any) => {
|
||||||
|
centerDialogVisible.value = true
|
||||||
|
store.dispatch('download/setRow', row)
|
||||||
|
}
|
||||||
|
const handleDelete = async (row: any, callback: { (): void; (): void }) => {
|
||||||
|
try {
|
||||||
|
console.log('Delete file:', row.filename)
|
||||||
|
const fileName = row.filename
|
||||||
|
const owner = row.owner
|
||||||
|
await deleteDownloadFile(owner, fileName)
|
||||||
|
row.curStatus = 'removed'
|
||||||
|
if (callback) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除文件时出错:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 确认删除文件
|
||||||
|
const confirmDelete = () => {
|
||||||
|
handleDelete(store.state.download.currentRow, () => {
|
||||||
|
centerDialogVisible.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = ['filename', 'fileType', 'fileSize', 'downloadTime', 'curStatus']
|
||||||
|
const total = computed(() => {
|
||||||
|
return props.total
|
||||||
|
})
|
||||||
|
const data = computed(() => {
|
||||||
|
return props.data
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataList = computed(() => {
|
||||||
|
return (data.value as DownloadItem[]).map((item: DownloadItem) => {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
return {
|
||||||
|
...item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const fieldList = computed(() => {
|
||||||
|
return map(fields, (f) => {
|
||||||
|
return get(downloadListConfig, f)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const downloadListConfig = {
|
||||||
|
filename: {
|
||||||
|
title: '文件名称',
|
||||||
|
key: 'filename',
|
||||||
|
width: 340,
|
||||||
|
tip: true
|
||||||
|
},
|
||||||
|
fileType: {
|
||||||
|
title: '格式',
|
||||||
|
key: 'fileType',
|
||||||
|
width: 200,
|
||||||
|
tip: true
|
||||||
|
},
|
||||||
|
fileSize: {
|
||||||
|
title: '预估大小',
|
||||||
|
key: 'fileSize',
|
||||||
|
width: 140
|
||||||
|
},
|
||||||
|
downloadTime: {
|
||||||
|
title: '下载时间',
|
||||||
|
key: 'downloadTime',
|
||||||
|
width: 240
|
||||||
|
},
|
||||||
|
curStatus: {
|
||||||
|
title: '状态',
|
||||||
|
key: 'curStatus',
|
||||||
|
comp: 'StateModule'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
const params = {
|
||||||
|
pageSize: 15,
|
||||||
|
page: val,
|
||||||
|
ownerId: store.state.user.userInfo.username
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch('download/getDownloadList', params)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.question-list-root {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f6f7f9;
|
||||||
|
|
||||||
|
.list-wrapper {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.list-table {
|
||||||
|
min-height: 620px;
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.small-text {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.list-pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
:deep(.el-pagination) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,6 +8,7 @@
|
|||||||
<NavPanel></NavPanel>
|
<NavPanel></NavPanel>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-group">
|
<div class="right-group">
|
||||||
|
<PreviewPanel></PreviewPanel>
|
||||||
<HistoryPanel></HistoryPanel>
|
<HistoryPanel></HistoryPanel>
|
||||||
<SavePanel></SavePanel>
|
<SavePanel></SavePanel>
|
||||||
<PublishPanel></PublishPanel>
|
<PublishPanel></PublishPanel>
|
||||||
@ -23,6 +24,7 @@ import BackPanel from '../modules/generalModule/BackPanel.vue'
|
|||||||
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
||||||
import NavPanel from '../modules/generalModule/NavPanel.vue'
|
import NavPanel from '../modules/generalModule/NavPanel.vue'
|
||||||
import HistoryPanel from '../modules/contentModule/HistoryPanel.vue'
|
import HistoryPanel from '../modules/contentModule/HistoryPanel.vue'
|
||||||
|
import PreviewPanel from '../modules/contentModule/PreviewPanel.vue'
|
||||||
import SavePanel from '../modules/contentModule/SavePanel.vue'
|
import SavePanel from '../modules/contentModule/SavePanel.vue'
|
||||||
import PublishPanel from '../modules/contentModule/PublishPanel.vue'
|
import PublishPanel from '../modules/contentModule/PublishPanel.vue'
|
||||||
|
|
||||||
|
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="btn preview-btn" @click="dialogTableVisible = true">
|
||||||
|
<i-ep-view class="view-icon" :size="20" />
|
||||||
|
<span class="btn-txt">预览</span>
|
||||||
|
</div>
|
||||||
|
<el-dialog
|
||||||
|
:z-index="99999"
|
||||||
|
top="50px"
|
||||||
|
class="preview-config-wrapper"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:show-close="false"
|
||||||
|
@open="openDialog"
|
||||||
|
@closed="closedDialog"
|
||||||
|
v-model="dialogTableVisible"
|
||||||
|
:width="`${previewTab == 1 ? '398' : '1290'}`"
|
||||||
|
>
|
||||||
|
<div class="ml75">
|
||||||
|
<div class="preview-tab">
|
||||||
|
<div
|
||||||
|
:class="`preview-tab-item ${previewTab == 1 ? 'active' : ''}`"
|
||||||
|
@click="previewTab = 1"
|
||||||
|
>
|
||||||
|
<i-ep-iphone />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="`preview-tab-item ${previewTab == 2 ? 'active' : ''}`"
|
||||||
|
@click="previewTab = 2"
|
||||||
|
>
|
||||||
|
<i-ep-monitor />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`preview-panel ${previewTab == 1 ? 'phone' : 'pc'}`">
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="tips-wrapper">
|
||||||
|
<i-ep-WarningFilled /> <span>用户预览模式,数据不保存!</span>
|
||||||
|
</div>
|
||||||
|
<div v-loading="loading" element-loading-text="加载中..." style="height: 100%">
|
||||||
|
<iframe
|
||||||
|
v-loading="loading"
|
||||||
|
id="iframe-preview"
|
||||||
|
:src="`/management/preview/${surveyId}`"
|
||||||
|
frameborder="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const dialogTableVisible = ref(false)
|
||||||
|
const previewTab = ref(1)
|
||||||
|
const surveyId = route.params.id
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
const openDialog = () => {
|
||||||
|
const iframePreview = document.getElementById('iframe-preview')
|
||||||
|
if (!iframePreview) return
|
||||||
|
iframePreview.onload = function () {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closedDialog = () => {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import url('@/management/styles/edit-btn.scss');
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
:deep(.preview-config-wrapper) {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.ml75 {
|
||||||
|
margin-left: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
height: 29px;
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.border-right-none {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border-color: $primary-color;
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-none {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(227, 228, 232, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
border-color: $primary-color;
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.preview-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
&.pc {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0px 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||||
|
height: 726px;
|
||||||
|
.wrapper {
|
||||||
|
width: 636px;
|
||||||
|
height: 704px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.phone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
background: url('/imgs/preview-phone.png') no-repeat;
|
||||||
|
width: 328px;
|
||||||
|
height: 678px;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
padding: 0 14px;
|
||||||
|
padding-top: 58px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
border-radius: 0px 0px 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tips-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: $primary-bg-color;
|
||||||
|
color: $primary-color;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
padding-left: 9px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,7 +5,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const handleNavigateHome = () => window.open('/survey', '_self')
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const handleNavigateHome = () => {
|
||||||
|
router.push({
|
||||||
|
name: 'survey'
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.back-btn {
|
.back-btn {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="condition-wrapper"
|
class="condition-wrapper"
|
||||||
:class="{ 'is-last': isLastCondition }"
|
:class="{ 'is-last': isLastCondition }"
|
||||||
:data-content-before="!isLastCondition ? '且' : ''"
|
:data-content-before="!isLastCondition ? '且' : ''"
|
||||||
>
|
>
|
||||||
<span class="desc">如果</span>
|
<span class="desc">如果</span>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
@ -15,16 +15,9 @@
|
|||||||
placeholder="请选择题目"
|
placeholder="请选择题目"
|
||||||
@change="(val: any) => handleChange(conditionNode, 'field', val)"
|
@change="(val: any) => handleChange(conditionNode, 'field', val)"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option v-for="{ label, value } in fieldList" :key="value" :label="label" :value="value">
|
||||||
v-for="{ label, value } in fieldList"
|
|
||||||
:key="value"
|
|
||||||
:label="label"
|
|
||||||
:value="value"
|
|
||||||
>
|
|
||||||
</el-option>
|
</el-option>
|
||||||
<template #empty>
|
<template #empty> 无数据 </template>
|
||||||
无数据
|
|
||||||
</template>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<span class="desc">选择了</span>
|
<span class="desc">选择了</span>
|
||||||
@ -45,9 +38,7 @@
|
|||||||
:label="label"
|
:label="label"
|
||||||
:value="value"
|
:value="value"
|
||||||
></el-option>
|
></el-option>
|
||||||
<template #empty>
|
<template #empty> 无数据 </template>
|
||||||
无数据
|
|
||||||
</template>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<span class="desc">中的任一选项 </span>
|
<span class="desc">中的任一选项 </span>
|
||||||
@ -65,7 +56,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, type ComputedRef } from 'vue'
|
import { computed, inject, ref, type ComputedRef } from 'vue'
|
||||||
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
|
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { qAbleList } from '@/management/utils/constant.js'
|
import { CHOICES } from '@/common/typeEnum'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -94,7 +85,7 @@ const fieldList = computed(() => {
|
|||||||
const currentIndex = renderData.value.findIndex((item) => item.field === props.ruleNode.target)
|
const currentIndex = renderData.value.findIndex((item) => item.field === props.ruleNode.target)
|
||||||
return renderData.value
|
return renderData.value
|
||||||
.slice(0, currentIndex)
|
.slice(0, currentIndex)
|
||||||
.filter((question: any) => qAbleList.includes(question.type))
|
.filter((question: any) => CHOICES.includes(question.type))
|
||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
|
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
|
||||||
|
@ -36,9 +36,7 @@
|
|||||||
:value="value"
|
:value="value"
|
||||||
>
|
>
|
||||||
</el-option>
|
</el-option>
|
||||||
<template #empty>
|
<template #empty> 无数据 </template>
|
||||||
无数据
|
|
||||||
</template>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,7 @@ import { getQuestionByType } from '@/management/utils/index'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { get as _get, isNumber as _isNumber } from 'lodash-es'
|
import { get as _get, isNumber as _isNumber } from 'lodash-es'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const activeNames = ref([0, 1])
|
const activeNames = ref([0, 1])
|
||||||
@ -70,8 +70,8 @@ const getNewQuestion = ({ type }) => {
|
|||||||
const fields = questionDataList.value.map((item) => item.field)
|
const fields = questionDataList.value.map((item) => item.field)
|
||||||
const newQuestion = getQuestionByType(type, fields)
|
const newQuestion = getQuestionByType(type, fields)
|
||||||
newQuestion.title = newQuestion.title = `标题${newQuestionIndex.value + 1}`
|
newQuestion.title = newQuestion.title = `标题${newQuestionIndex.value + 1}`
|
||||||
if (type === 'vote') {
|
if (type === QUESTION_TYPE.VOTE) {
|
||||||
newQuestion.innerType = 'radio'
|
newQuestion.innerType = QUESTION_TYPE.RADIO
|
||||||
}
|
}
|
||||||
return newQuestion
|
return newQuestion
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ moment.locale('zh-cn')
|
|||||||
|
|
||||||
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||||
import { CODE_MAP } from '@/management/api/base'
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
import { QOP_MAP } from '@/management/utils/constant'
|
import { QOP_MAP } from '@/management/utils/constant.ts'
|
||||||
import { deleteSurvey } from '@/management/api/survey'
|
import { deleteSurvey } from '@/management/api/survey'
|
||||||
import ModifyDialog from './ModifyDialog.vue'
|
import ModifyDialog from './ModifyDialog.vue'
|
||||||
import TagModule from './TagModule.vue'
|
import TagModule from './TagModule.vue'
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
row-class-name="tableview-row"
|
row-class-name="tableview-row"
|
||||||
cell-class-name="tableview-cell"
|
cell-class-name="tableview-cell"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@row-click="onRowClick"
|
|
||||||
>
|
>
|
||||||
<el-table-column column-key="space" width="20" />
|
<el-table-column column-key="space" width="20" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@ -98,9 +97,7 @@ const isAdmin = (id: string) => {
|
|||||||
UserRole.Admin
|
UserRole.Admin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const onRowClick = () => {
|
|
||||||
console.log('onRowClick')
|
|
||||||
}
|
|
||||||
const handleModify = async (id: string) => {
|
const handleModify = async (id: string) => {
|
||||||
await store.dispatch('list/getSpaceDetail', id)
|
await store.dispatch('list/getSpaceDetail', id)
|
||||||
modifyType.value = 'edit'
|
modifyType.value = 'edit'
|
||||||
|
@ -48,7 +48,6 @@ import { useStore } from 'vuex'
|
|||||||
import { pick as _pick } from 'lodash-es'
|
import { pick as _pick } from 'lodash-es'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
import { QOP_MAP } from '@/management/utils/constant'
|
import { QOP_MAP } from '@/management/utils/constant'
|
||||||
import MemberSelect from './MemberSelect.vue'
|
import MemberSelect from './MemberSelect.vue'
|
||||||
import { type IMember, type IWorkspace, UserRole } from '@/management/utils/types/workSpace'
|
import { type IMember, type IWorkspace, UserRole } from '@/management/utils/types/workSpace'
|
||||||
|
@ -92,7 +92,7 @@ export const statusMaps = {
|
|||||||
new: '未发布',
|
new: '未发布',
|
||||||
editing: '修改中',
|
editing: '修改中',
|
||||||
published: '已发布',
|
published: '已发布',
|
||||||
removed: '',
|
removed: '已删除',
|
||||||
pausing: ''
|
pausing: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
||||||
<el-menu-item index="1">问卷列表</el-menu-item>
|
<el-menu-item index="1">问卷列表</el-menu-item>
|
||||||
|
<el-menu-item index="2" @click="handleDownload">下载页面</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-info">
|
<div class="login-info">
|
||||||
@ -155,6 +156,10 @@ const handleLogout = () => {
|
|||||||
store.dispatch('user/logout')
|
store.dispatch('user/logout')
|
||||||
router.replace({ name: 'login' })
|
router.replace({ name: 'login' })
|
||||||
}
|
}
|
||||||
|
// 下载页面
|
||||||
|
const handleDownload = () => {
|
||||||
|
router.push('/survey/download')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -51,7 +51,7 @@ const mainChannel = computed(() => {
|
|||||||
let fullUrl = ''
|
let fullUrl = ''
|
||||||
|
|
||||||
if (metaData.value) {
|
if (metaData.value) {
|
||||||
fullUrl = `${location.origin}/render/${metaData.value.surveyPath}`
|
fullUrl = `${location.origin}/render/${metaData.value.surveyPath}?t=${Date.now()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return { fullUrl }
|
return { fullUrl }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory, type RouteLocationNormalized, type NavigationGuardNext } from 'vue-router'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore, type Store } from 'vuex'
|
||||||
import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||||
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'
|
||||||
@ -19,11 +19,19 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '问卷列表'
|
title: '问卷列表'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/survey/download/',
|
||||||
|
name: 'download',
|
||||||
|
component: () => import('../pages/download/SurveyDownloadPage.vue'),
|
||||||
|
meta: {
|
||||||
|
needLogin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/survey/:id/edit',
|
path: '/survey/:id/edit',
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true,
|
needLogin: true,
|
||||||
premissions: [SurveyPermissions.SurveyManage]
|
permissions: [SurveyPermissions.SurveyManage]
|
||||||
},
|
},
|
||||||
name: 'QuestionEdit',
|
name: 'QuestionEdit',
|
||||||
component: () => import('../pages/edit/index.vue'),
|
component: () => import('../pages/edit/index.vue'),
|
||||||
@ -94,7 +102,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'analysisPage',
|
name: 'analysisPage',
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true,
|
needLogin: true,
|
||||||
premissions: [SurveyPermissions.DataManage]
|
permissions: [SurveyPermissions.DataManage]
|
||||||
},
|
},
|
||||||
component: () => import('../pages/analysis/AnalysisPage.vue')
|
component: () => import('../pages/analysis/AnalysisPage.vue')
|
||||||
},
|
},
|
||||||
@ -103,7 +111,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'publish',
|
name: 'publish',
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true,
|
needLogin: true,
|
||||||
premissions: [SurveyPermissions.SurveyManage]
|
permissions: [SurveyPermissions.SurveyManage]
|
||||||
},
|
},
|
||||||
component: () => import('../pages/publish/PublishPage.vue')
|
component: () => import('../pages/publish/PublishPage.vue')
|
||||||
},
|
},
|
||||||
@ -132,57 +140,60 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
|
// 初始化用户信息
|
||||||
if (!store.state.user?.initialized) {
|
if (!store.state.user?.initialized) {
|
||||||
store?.dispatch('user/init')
|
await store.dispatch('user/init');
|
||||||
}
|
}
|
||||||
|
// 更新页面标题
|
||||||
if (to.meta.title) {
|
if (to.meta.title) {
|
||||||
document.title = to.meta.title as string
|
document.title = to.meta.title as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.needLogin) {
|
if (to.meta.needLogin) {
|
||||||
if (store?.state?.user?.hasLogined) {
|
await handleLoginGuard(to, from, next, store);
|
||||||
if (to.meta.premissions) {
|
} else {
|
||||||
const params = to.params
|
next();
|
||||||
await store.dispatch('fetchCooperPermissions', params.id)
|
}
|
||||||
if (
|
});
|
||||||
(to.meta.premissions as []).some((permission) =>
|
|
||||||
store.state?.cooperPermissions?.includes(permission)
|
async function handleLoginGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, store: Store<any>) {
|
||||||
)
|
if (store.state.user?.hasLogined) {
|
||||||
) {
|
await handlePermissionsGuard(to, from, next, store);
|
||||||
next()
|
} else {
|
||||||
} else {
|
next({
|
||||||
ElMessage.warning('您没有该问卷的相关协作权限')
|
name: 'login',
|
||||||
next({
|
query: { redirect: encodeURIComponent(to.path) },
|
||||||
name: 'survey'
|
});
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePermissionsGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, store: Store<any>) {
|
||||||
|
const currSurveyId = to?.params?.id || ''
|
||||||
|
const prevSurveyId = from?.params?.id || ''
|
||||||
|
// 如果跳转页面不存在surveyId 或者不需要页面权限,则直接跳转
|
||||||
|
if (!to.meta.permissions || !currSurveyId) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
// 如果跳转编辑页面,且跳转页面和上一页的surveyId不同,判断是否有对应页面权限
|
||||||
|
if (currSurveyId !== prevSurveyId) {
|
||||||
|
await store.dispatch('fetchCooperPermissions', currSurveyId)
|
||||||
|
if (hasRequiredPermissions(to.meta.permissions as string[], store.state.cooperPermissions)) {
|
||||||
|
next();
|
||||||
} else {
|
} else {
|
||||||
next()
|
ElMessage.warning('您没有该问卷的相关协作权限');
|
||||||
|
next({ name: 'survey' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
next({
|
next();
|
||||||
name: 'login',
|
|
||||||
query: {
|
|
||||||
redirect: encodeURIComponent(to.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
function hasRequiredPermissions(requiredPermissions: string[], userPermissions: string[]) {
|
||||||
|
return requiredPermissions.some(permission => userPermissions.includes(permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// router.afterEach(async (to, from) => {
|
|
||||||
// const store = useStore()
|
|
||||||
// if (to.meta.premissions) {
|
|
||||||
// const params = to.params
|
|
||||||
// await store.dispatch('fetchCooperPermissions', params.id)
|
|
||||||
// if (!(to.meta.premissions as []).some((permission) => store.state?.cooperPermissions?.includes(permission))) {
|
|
||||||
// ElMessage.warning('您没有该问卷的相关协作权限')
|
|
||||||
// router.push({
|
|
||||||
// name: 'survey'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
export default router
|
export default router
|
||||||
|
@ -14,7 +14,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchCooperPermissions({ commit }, id) {
|
async fetchCooperPermissions({ commit }, id) {
|
||||||
const res = await getCollaboratorPermissions(id)
|
const res = await getCollaboratorPermissions(id)
|
||||||
console.log(res.data)
|
|
||||||
if (res.code === CODE_MAP.SUCCESS) {
|
if (res.code === CODE_MAP.SUCCESS) {
|
||||||
commit('setCooperPermissions', res.data.permissions)
|
commit('setCooperPermissions', res.data.permissions)
|
||||||
}
|
}
|
||||||
|
54
web/src/management/store/download/index.js
Normal file
54
web/src/management/store/download/index.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import { getDownloadList, deleteDownloadFile } from '@/management/api/download'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
surveyList: [],
|
||||||
|
surveyTotal: 0,
|
||||||
|
currentRow: []
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setSurveyList(state, list) {
|
||||||
|
state.surveyList = list
|
||||||
|
},
|
||||||
|
setSurveyTotal(state, total) {
|
||||||
|
state.surveyTotal = total
|
||||||
|
},
|
||||||
|
setCurrentRow(state, row) {
|
||||||
|
state.currentRow = row
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async setRow({ commit }, payload) {
|
||||||
|
commit('setCurrentRow', payload)
|
||||||
|
},
|
||||||
|
async getDownloadList({ commit }, payload) {
|
||||||
|
let params = {
|
||||||
|
ownerId: payload.ownerId,
|
||||||
|
page: payload.page ? payload.page : 1,
|
||||||
|
pageSize: payload.pageSize ? payload.pageSize : 15
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await getDownloadList(params)
|
||||||
|
console.log(data)
|
||||||
|
commit('setSurveyList', data.listBody)
|
||||||
|
commit('setSurveyTotal', data.total)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async DownloadFileByName(payload) {
|
||||||
|
let params = {
|
||||||
|
fileName: payload.fileName
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await deleteDownloadFile(params)
|
||||||
|
console.log(data)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {}
|
||||||
|
}
|
@ -64,5 +64,8 @@ export default {
|
|||||||
},
|
},
|
||||||
setQuestionDataList(state, data) {
|
setQuestionDataList(state, data) {
|
||||||
state.schema.questionDataList = data
|
state.schema.questionDataList = data
|
||||||
|
},
|
||||||
|
setDownloadPath(state, data) {
|
||||||
|
state.downloadPath = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,5 +56,6 @@ export default {
|
|||||||
logicConf: {
|
logicConf: {
|
||||||
showLogicConf: []
|
showLogicConf: []
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
downloadPath: ''
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import list from './list'
|
|||||||
import actions from './actions'
|
import actions from './actions'
|
||||||
import mutations from './mutations'
|
import mutations from './mutations'
|
||||||
import state from './state'
|
import state from './state'
|
||||||
|
import download from './download'
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state,
|
state,
|
||||||
@ -14,6 +15,7 @@ export default createStore({
|
|||||||
modules: {
|
modules: {
|
||||||
edit,
|
edit,
|
||||||
user,
|
user,
|
||||||
list
|
list,
|
||||||
|
download
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
// 问卷操作枚举
|
// 问卷操作枚举
|
||||||
export const QOP_MAP = {
|
export enum QOP_MAP {
|
||||||
ADD: 'add',
|
ADD = 'add',
|
||||||
COPY: 'copy',
|
COPY = 'copy',
|
||||||
EDIT: 'edit'
|
EDIT = 'edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const qAbleList = ['radio', 'checkbox', 'binary-choice', 'vote']
|
|
||||||
export const operatorOptions = [
|
export const operatorOptions = [
|
||||||
{
|
{
|
||||||
label: '选择了',
|
label: '选择了',
|
@ -1,5 +1,6 @@
|
|||||||
import { defaultQuestionConfig } from '../config/questionConfig'
|
import { defaultQuestionConfig } from '../config/questionConfig'
|
||||||
import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es'
|
import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
const generateQuestionField = () => {
|
const generateQuestionField = () => {
|
||||||
const num = Math.floor(Math.random() * 1000)
|
const num = Math.floor(Math.random() * 1000)
|
||||||
return `data${num}`
|
return `data${num}`
|
||||||
@ -24,7 +25,7 @@ const generateHash = (hashList) => {
|
|||||||
|
|
||||||
function getOptions(type) {
|
function getOptions(type) {
|
||||||
const options = [].concat({ ..._cloneDeep(defaultQuestionConfig) }.options)
|
const options = [].concat({ ..._cloneDeep(defaultQuestionConfig) }.options)
|
||||||
if (type === 'binary-choice') {
|
if (type === QUESTION_TYPE.BINARY_CHOICE) {
|
||||||
options[0].text = '对'
|
options[0].text = '对'
|
||||||
options[1].text = '错'
|
options[1].text = '错'
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
const config = {
|
|
||||||
mob: '手机号',
|
|
||||||
text: '单行输入框',
|
|
||||||
textarea: '多行输入框',
|
|
||||||
radio: '单选',
|
|
||||||
checkbox: '多选',
|
|
||||||
'radio-star': '评分',
|
|
||||||
'radio-nps': 'NPS评分',
|
|
||||||
city: '城市选择',
|
|
||||||
vote: '投票',
|
|
||||||
'binary-choice': '判断',
|
|
||||||
fillin: '填空'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
@ -54,6 +54,7 @@ import { ref, computed, inject } from 'vue'
|
|||||||
import OptionConfig from '../AdvancedConfig/OptionConfig.vue'
|
import OptionConfig from '../AdvancedConfig/OptionConfig.vue'
|
||||||
import RateConfig from '../AdvancedConfig/RateConfig.vue'
|
import RateConfig from '../AdvancedConfig/RateConfig.vue'
|
||||||
import ExtraIcon from '../ExtraIcon/index.vue'
|
import ExtraIcon from '../ExtraIcon/index.vue'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
optionList: {
|
optionList: {
|
||||||
@ -78,7 +79,6 @@ const emit = defineEmits(['addOther', 'optionChange', 'change'])
|
|||||||
const moduleConfig = inject('moduleConfig')
|
const moduleConfig = inject('moduleConfig')
|
||||||
const optionConfigVisible = ref(false)
|
const optionConfigVisible = ref(false)
|
||||||
const openOptionConfig = () => {
|
const openOptionConfig = () => {
|
||||||
console.log('open')
|
|
||||||
optionConfigVisible.value = true
|
optionConfigVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ const onVisibleChange = (val) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isNps = computed(() => {
|
const isNps = computed(() => {
|
||||||
return moduleConfig.value.type === 'radio-nps'
|
return moduleConfig.value.type === QUESTION_TYPE.RADIO_NPS
|
||||||
})
|
})
|
||||||
|
|
||||||
const min = computed(() => {
|
const min = computed(() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineComponent, watch, ref, computed } from 'vue'
|
import { defineComponent, watch, ref, computed } from 'vue'
|
||||||
import { filterXSS } from '@/common/xss'
|
import { filterXSS } from '@/common/xss'
|
||||||
import tagList from '@materials/questions/common/config/tagList'
|
import { typeTagLabels } from '@/common/typeEnum.ts'
|
||||||
|
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export default defineComponent({
|
|||||||
let ret = ''
|
let ret = ''
|
||||||
types.forEach((t) => {
|
types.forEach((t) => {
|
||||||
if (ret) return
|
if (ret) return
|
||||||
const tv = tagList && tagList[t]
|
const tv = typeTagLabels && typeTagLabels[t]
|
||||||
if (tv && typeof tv === 'string') {
|
if (tv && typeof tv === 'string') {
|
||||||
ret = tv.trim()
|
ret = tv.trim()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { computed, defineComponent } from 'vue'
|
import { computed, defineComponent } from 'vue'
|
||||||
import { includes } from 'lodash-es'
|
import { includes } from 'lodash-es'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum'
|
||||||
import AnswerProcess from './AnswerProcess/index.vue'
|
import AnswerProcess from './AnswerProcess/index.vue'
|
||||||
import BaseChoice from '../BaseChoice'
|
import BaseChoice from '../BaseChoice'
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
const myOptions = computed(() => {
|
const myOptions = computed(() => {
|
||||||
const { options } = props
|
const { options } = props
|
||||||
if (props.innerType === 'checkbox') {
|
if (props.innerType === QUESTION_TYPE.CHECKBOX) {
|
||||||
return options.map((item) => {
|
return options.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
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,7 +25,7 @@ interface Emit {
|
|||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
const emit = defineEmits<Emit>()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const setterTypes = ['checkbox', 'vote']
|
const setterTypes = [QUESTION_TYPE.CHECKBOX, QUESTION_TYPE.VOTE]
|
||||||
const modelValue = ref(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
|
||||||
|
@ -1,49 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<Component
|
<router-view></router-view>
|
||||||
v-if="store.state.router"
|
|
||||||
:is="
|
|
||||||
components[
|
|
||||||
upperFirst(store.state.router) as 'IndexPage' | 'EmptyPage' | 'ErrorPage' | 'SuccessPage'
|
|
||||||
]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</Component>
|
|
||||||
<LogoIcon
|
|
||||||
v-if="!['successPage', 'indexPage'].includes(store.state.router)"
|
|
||||||
:logo-conf="logoConf"
|
|
||||||
:readonly="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch, onMounted } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
|
import { get as _get } from 'lodash-es'
|
||||||
import { getPublishedSurveyInfo } from './api/survey'
|
|
||||||
import useCommandComponent from './hooks/useCommandComponent'
|
|
||||||
|
|
||||||
import EmptyPage from './pages/EmptyPage.vue'
|
|
||||||
import IndexPage from './pages/IndexPage.vue'
|
|
||||||
import ErrorPage from './pages/ErrorPage.vue'
|
|
||||||
import SuccessPage from './pages/SuccessPage.vue'
|
|
||||||
import AlertDialog from './components/AlertDialog.vue'
|
|
||||||
// @ts-ignore
|
|
||||||
import communalLoader from '@materials/communals/communalLoader.js'
|
|
||||||
import { get as _get, upperFirst } from 'lodash-es'
|
|
||||||
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
|
||||||
|
|
||||||
const LogoIcon = communalLoader.loadComponent('LogoIcon')
|
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const logoConf = computed(() => store.state?.bottomConf || {})
|
|
||||||
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
|
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
|
||||||
const components = {
|
|
||||||
EmptyPage,
|
|
||||||
IndexPage,
|
|
||||||
ErrorPage,
|
|
||||||
SuccessPage
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSkinConfig = (value: any) => {
|
const updateSkinConfig = (value: any) => {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
@ -68,49 +34,6 @@ const updateSkinConfig = (value: any) => {
|
|||||||
watch(skinConf, (value) => {
|
watch(skinConf, (value) => {
|
||||||
updateSkinConfig(value)
|
updateSkinConfig(value)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const surveyPath = location.pathname.split('/').pop()
|
|
||||||
|
|
||||||
if (!surveyPath) {
|
|
||||||
store.commit('setRouter', 'EmptyPage')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const alert = useCommandComponent(AlertDialog)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res: any = await getPublishedSurveyInfo({ surveyPath })
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
const data = res.data
|
|
||||||
const { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf, logicConf } =
|
|
||||||
data.code
|
|
||||||
const questionData = {
|
|
||||||
bannerConf,
|
|
||||||
baseConf,
|
|
||||||
bottomConf,
|
|
||||||
dataConf,
|
|
||||||
skinConf,
|
|
||||||
submitConf
|
|
||||||
}
|
|
||||||
|
|
||||||
document.title = data.title
|
|
||||||
|
|
||||||
updateSkinConfig(skinConf)
|
|
||||||
|
|
||||||
store.commit('setSurveyPath', surveyPath)
|
|
||||||
store.dispatch('init', questionData)
|
|
||||||
store.dispatch('getEncryptInfo')
|
|
||||||
initRuleEngine(logicConf?.showLogicConf)
|
|
||||||
} else {
|
|
||||||
throw new Error(res.errmsg)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log(error)
|
|
||||||
alert({ title: error.message || '获取问卷失败' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import url('./styles/icon.scss');
|
@import url('./styles/icon.scss');
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// 定义提交的数据结构:{ field1: '', field2: [], field1_hash1: '', }
|
// 定义提交的数据结构:{ field1: '', field2: [], field1_hash1: '', }
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
export default function ({ dataConf }) {
|
export default function ({ dataConf }) {
|
||||||
const dataList = dataConf.dataList
|
const dataList = dataConf.dataList
|
||||||
const formValues = {}
|
const formValues = {}
|
||||||
@ -17,7 +18,7 @@ export default function ({ dataConf }) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||||
if (/checkbox/.test(type) || innerType === 'checkbox') {
|
if (/checkbox/.test(type) || innerType === QUESTION_TYPE.CHECKBOX) {
|
||||||
value = value ? [value] : []
|
value = value ? [value] : []
|
||||||
}
|
}
|
||||||
formValues[key] = value
|
formValues[key] = value
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { get as _get, map as _map } from 'lodash-es'
|
import { get as _get, map as _map } from 'lodash-es'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
// 处理选择题的options
|
// 处理选择题的options
|
||||||
function handleOptions(item) {
|
function handleOptions(item) {
|
||||||
const { type } = item
|
const { type } = item
|
||||||
@ -13,7 +13,7 @@ function handleOptions(item) {
|
|||||||
const cleanOption = {}
|
const cleanOption = {}
|
||||||
|
|
||||||
// 投票逻辑处理
|
// 投票逻辑处理
|
||||||
if (type.indexOf('vote') > -1) {
|
if (type.indexOf(QUESTION_TYPE.VOTE) > -1) {
|
||||||
cleanOption.voteCount = 0
|
cleanOption.voteCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
keys as _keys,
|
keys as _keys,
|
||||||
set as _set
|
set as _set
|
||||||
} from 'lodash-es'
|
} from 'lodash-es'
|
||||||
|
import { INPUT, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
|
|
||||||
const regexpMap = {
|
const regexpMap = {
|
||||||
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
||||||
@ -28,13 +29,11 @@ const msgMap = {
|
|||||||
e: '请输入邮箱',
|
e: '请输入邮箱',
|
||||||
licensePlate: '请输入车牌号'
|
licensePlate: '请输入车牌号'
|
||||||
}
|
}
|
||||||
const inputType = ['text', 'textarea']
|
|
||||||
const checkBoxTip = '至少选择#min#项,少选择了#less#项'
|
const checkBoxTip = '至少选择#min#项,少选择了#less#项'
|
||||||
const checkBoxTipSame = '请选择#min#项,少选择了#less#项'
|
const checkBoxTipSame = '请选择#min#项,少选择了#less#项'
|
||||||
const textRangeMinTip = '至少输入#min#字'
|
const textRangeMinTip = '至少输入#min#字'
|
||||||
const numberRangeMinTip = '数字最小为#min#'
|
const numberRangeMinTip = '数字最小为#min#'
|
||||||
const numberRangeMaxTip = '数字最大为#max#'
|
const numberRangeMaxTip = '数字最大为#max#'
|
||||||
const radioType = ['radio-star', 'radio-nps']
|
|
||||||
|
|
||||||
// 多选题的选项数目限制
|
// 多选题的选项数目限制
|
||||||
export function optionValidator(value, minNum, maxNum) {
|
export function optionValidator(value, minNum, maxNum) {
|
||||||
@ -88,7 +87,7 @@ export function generateValidArr(
|
|||||||
numberRangeMax
|
numberRangeMax
|
||||||
) {
|
) {
|
||||||
const validArr = []
|
const validArr = []
|
||||||
const isInput = inputType.indexOf(type) !== -1
|
const isInput = INPUT.indexOf(type) !== -1
|
||||||
if (isRequired || valid === '*') {
|
if (isRequired || valid === '*') {
|
||||||
// 输入框的必填校验做trim
|
// 输入框的必填校验做trim
|
||||||
if (!isInput) {
|
if (!isInput) {
|
||||||
@ -199,14 +198,14 @@ const generateOthersKeyMap = (question) => {
|
|||||||
const { type, field, options, rangeConfig } = question
|
const { type, field, options, rangeConfig } = question
|
||||||
let othersKeyMap = undefined
|
let othersKeyMap = undefined
|
||||||
|
|
||||||
if (radioType.includes(type)) {
|
if (RATES.includes(type)) {
|
||||||
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('radio') || type.includes('checkbox')) {
|
} else if (type.includes(QUESTION_TYPE.RADIO) || type.includes(QUESTION_TYPE.CHECKBOX)) {
|
||||||
othersKeyMap = {}
|
othersKeyMap = {}
|
||||||
options
|
options
|
||||||
.filter((op) => op.others)
|
.filter((op) => op.others)
|
||||||
@ -258,7 +257,7 @@ export default function (questionConfig) {
|
|||||||
|
|
||||||
// 对于选择题支持填写更多信息的,需要做是否必填的校验
|
// 对于选择题支持填写更多信息的,需要做是否必填的校验
|
||||||
if (_keys(othersKeyMap).length) {
|
if (_keys(othersKeyMap).length) {
|
||||||
if (radioType.includes(type)) {
|
if (RATES.includes(type)) {
|
||||||
if (rangeConfig) {
|
if (rangeConfig) {
|
||||||
for (const key in rangeConfig) {
|
for (const key in rangeConfig) {
|
||||||
if (rangeConfig[key].isShowInput && rangeConfig[key].required) {
|
if (rangeConfig[key].isShowInput && rangeConfig[key].required) {
|
||||||
|
@ -8,6 +8,14 @@ export const getPublishedSurveyInfo = ({ surveyPath }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPreviewSchema = ({ surveyPath }) => {
|
||||||
|
return axios.get('/survey/getPreviewSchema', {
|
||||||
|
params: {
|
||||||
|
surveyPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const submitForm = (data) => {
|
export const submitForm = (data) => {
|
||||||
return axios.post('/surveyResponse/createResponse', data)
|
return axios.post('/surveyResponse/createResponse', data)
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,8 @@
|
|||||||
<div v-for="item in renderData" :key="item.field">
|
<div v-for="item in renderData" :key="item.field">
|
||||||
<QuestionWrapper
|
<QuestionWrapper
|
||||||
class="gap"
|
class="gap"
|
||||||
v-bind="$attrs"
|
|
||||||
:moduleConfig="item"
|
:moduleConfig="item"
|
||||||
:qIndex="item.qIndex"
|
|
||||||
:indexNumber="item.indexNumber"
|
:indexNumber="item.indexNumber"
|
||||||
:showTitle="true"
|
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
></QuestionWrapper>
|
></QuestionWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuestionRuleContainer
|
<QuestionRuleContainer
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
v-bind="$attrs"
|
|
||||||
:moduleConfig="questionConfig"
|
:moduleConfig="questionConfig"
|
||||||
:indexNumber="indexNumber"
|
:indexNumber="indexNumber"
|
||||||
:showTitle="true"
|
:showTitle="true"
|
||||||
@ -17,7 +16,8 @@ import { useShowInput } from '@/render/hooks/useShowInput'
|
|||||||
import store from '@/render/store'
|
import store from '@/render/store'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { ruleEngine } from '@/render/hooks/useRuleEngine.js'
|
import { ruleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||||
import { QUESTION_TYPE } from '@/render/constant/index'
|
|
||||||
|
import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
indexNumber: {
|
indexNumber: {
|
||||||
@ -48,7 +48,7 @@ const questionConfig = computed(() => {
|
|||||||
moduleConfig.voteTotal = unref(voteTotal)
|
moduleConfig.voteTotal = unref(voteTotal)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
QUESTION_TYPE.CHOICES.includes(type) &&
|
NORMAL_CHOICES.includes(type) &&
|
||||||
options.filter((optionItem) => optionItem.others).length > 0
|
options.filter((optionItem) => optionItem.others).length > 0
|
||||||
) {
|
) {
|
||||||
let { options, othersValue } = useShowOthers(field)
|
let { options, othersValue } = useShowOthers(field)
|
||||||
@ -57,7 +57,7 @@ const questionConfig = computed(() => {
|
|||||||
moduleConfig.othersValue = unref(othersValue)
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
QUESTION_TYPE.RATES.includes(type) &&
|
RATES.includes(type) &&
|
||||||
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)
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
export const QUESTION_TYPE = {
|
|
||||||
VOTE: 'vote',
|
|
||||||
CHECKBOX: 'checkbox',
|
|
||||||
CHOICES: [
|
|
||||||
// 选择类题型分类
|
|
||||||
'radio',
|
|
||||||
'checkbox'
|
|
||||||
],
|
|
||||||
RATES: [
|
|
||||||
// 评分题题型分类
|
|
||||||
'radio-star',
|
|
||||||
'radio-nps'
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import EventBus from './utils/eventbus'
|
import EventBus from './utils/eventbus'
|
||||||
|
import router from './router'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@ -10,7 +10,7 @@ const $bus = new EventBus()
|
|||||||
app.provide('$bus', $bus)
|
app.provide('$bus', $bus)
|
||||||
// 挂载到this上
|
// 挂载到this上
|
||||||
app.config.globalProperties.$bus = $bus
|
app.config.globalProperties.$bus = $bus
|
||||||
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -7,10 +7,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
|
||||||
name: 'NotFound'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="result-page-wrap">
|
<div class="result-page-wrap">
|
||||||
<div class="result-page">
|
<div class="result-page">
|
||||||
<div class="error-wrapper">
|
<div class="page-content">
|
||||||
<img class="error-img" :src="errorImageUrl" />
|
<img class="img" :src="errorImageUrl" />
|
||||||
<div class="bottom-word" v-html="errorMsg"></div>
|
<div class="msg" v-html="errorMsg"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<LogoIcon :logo-conf="logoConf" :readonly="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
|
// @ts-ignore
|
||||||
|
import communalLoader from '@materials/communals/communalLoader.js'
|
||||||
|
|
||||||
|
const LogoIcon = communalLoader.loadComponent('LogoIcon')
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -24,7 +29,8 @@ const errorImageUrl = computed(() => {
|
|||||||
return imageMap[errorType as 'overTime'] || imageMap.default
|
return imageMap[errorType as 'overTime'] || imageMap.default
|
||||||
})
|
})
|
||||||
|
|
||||||
const errorMsg = computed(() => store.state?.errorInfo?.errorMsg)
|
const errorMsg = computed(() => store.state?.errorInfo?.errorMsg || '提交失败')
|
||||||
|
const logoConf = computed(() => store.state?.bottomConf || {})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.result-page-wrap {
|
.result-page-wrap {
|
||||||
@ -42,25 +48,27 @@ const errorMsg = computed(() => store.state?.errorInfo?.errorMsg)
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.error-wrapper {
|
.page-content {
|
||||||
text-align: center;
|
position: relative;
|
||||||
font-size: 14px;
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 2rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
p {
|
.img {
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-img {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-word {
|
.msg {
|
||||||
color: #999;
|
font-size: 0.32rem;
|
||||||
margin-top: 10px;
|
color: #4a4c5b;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,156 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="index">
|
<router-view></router-view>
|
||||||
<ProgressBar />
|
|
||||||
<div class="wrapper" ref="boxRef">
|
|
||||||
<HeaderContent :bannerConf="bannerConf" :readonly="true" />
|
|
||||||
<div class="content">
|
|
||||||
<MainTitle :bannerConf="bannerConf" :readonly="true"></MainTitle>
|
|
||||||
<MainRenderer ref="mainRef"></MainRenderer>
|
|
||||||
<SubmitButton
|
|
||||||
:validate="validate"
|
|
||||||
:submitConf="submitConf"
|
|
||||||
:readonly="true"
|
|
||||||
:renderData="renderData"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
></SubmitButton>
|
|
||||||
<LogoIcon :logo-conf="logoConf" :readonly="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
// @ts-ignore
|
import { useRoute } from 'vue-router'
|
||||||
import communalLoader from '@materials/communals/communalLoader.js'
|
|
||||||
import MainRenderer from '../components/MainRenderer.vue'
|
|
||||||
import AlertDialog from '../components/AlertDialog.vue'
|
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
|
||||||
import ProgressBar from '../components/ProgressBar.vue'
|
|
||||||
|
|
||||||
import { submitForm } from '../api/survey'
|
|
||||||
import encrypt from '../utils/encrypt'
|
|
||||||
|
|
||||||
|
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
|
||||||
import useCommandComponent from '../hooks/useCommandComponent'
|
import useCommandComponent from '../hooks/useCommandComponent'
|
||||||
|
|
||||||
interface Props {
|
import AlertDialog from '../components/AlertDialog.vue'
|
||||||
questionInfo?: any
|
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||||
isMobile?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
questionInfo: {},
|
|
||||||
isMobile: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const HeaderContent = communalLoader.loadComponent('HeaderContent')
|
|
||||||
const MainTitle = communalLoader.loadComponent('MainTitle')
|
|
||||||
const SubmitButton = communalLoader.loadComponent('SubmitButton')
|
|
||||||
const LogoIcon = communalLoader.loadComponent('LogoIcon')
|
|
||||||
|
|
||||||
const mainRef = ref<any>()
|
|
||||||
const boxRef = ref<HTMLElement>()
|
|
||||||
|
|
||||||
const alert = useCommandComponent(AlertDialog)
|
|
||||||
const confirm = useCommandComponent(ConfirmDialog)
|
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
const bannerConf = computed(() => store.state?.bannerConf || {})
|
const loadData = (res: any, surveyPath: string) => {
|
||||||
const renderData = computed(() => store.getters.renderData)
|
if (res.code === 200) {
|
||||||
const submitConf = computed(() => store.state?.submitConf || {})
|
const data = res.data
|
||||||
const logoConf = computed(() => store.state?.bottomConf || {})
|
const {
|
||||||
|
bannerConf,
|
||||||
const validate = (cbk: (v: boolean) => void) => {
|
baseConf,
|
||||||
const index = 0
|
bottomConf,
|
||||||
mainRef.value.$refs.formGroup[index].validate(cbk)
|
dataConf,
|
||||||
}
|
skinConf,
|
||||||
|
submitConf,
|
||||||
const normalizationRequestBody = () => {
|
logicConf
|
||||||
const enterTime = store.state.enterTime
|
} = data.code
|
||||||
const encryptInfo = store.state.encryptInfo
|
const questionData = {
|
||||||
const formValues = store.state.formValues
|
bannerConf,
|
||||||
const surveyPath = store.state.surveyPath
|
baseConf,
|
||||||
|
bottomConf,
|
||||||
const result: any = {
|
dataConf,
|
||||||
surveyPath,
|
skinConf,
|
||||||
data: JSON.stringify(formValues),
|
submitConf
|
||||||
difTime: Date.now() - enterTime,
|
|
||||||
clientTime: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encryptInfo?.encryptType) {
|
|
||||||
result.encryptType = encryptInfo?.encryptType
|
|
||||||
result.data = encrypt[result.encryptType as 'rsa']({
|
|
||||||
data: result.data,
|
|
||||||
secretKey: encryptInfo?.data?.secretKey
|
|
||||||
})
|
|
||||||
if (encryptInfo?.data?.sessionId) {
|
|
||||||
result.sessionId = encryptInfo.data.sessionId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.title = data.title
|
||||||
|
|
||||||
|
store.commit('setSurveyPath', surveyPath)
|
||||||
|
store.dispatch('init', questionData)
|
||||||
|
initRuleEngine(logicConf?.showLogicConf)
|
||||||
} else {
|
} else {
|
||||||
result.data = JSON.stringify(result.data)
|
throw new Error(res.errmsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
const surveyId = route.params.surveyId
|
||||||
|
console.log({ surveyId })
|
||||||
|
store.commit('setSurveyPath', surveyId)
|
||||||
|
getDetail(surveyId as string)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDetail = async (surveyPath: string) => {
|
||||||
|
const alert = useCommandComponent(AlertDialog)
|
||||||
|
|
||||||
const submitSurver = async () => {
|
|
||||||
try {
|
try {
|
||||||
const params = normalizationRequestBody()
|
if (surveyPath.length > 8) {
|
||||||
console.log(params)
|
const res: any = await getPreviewSchema({ surveyPath })
|
||||||
const res: any = await submitForm(params)
|
loadData(res, surveyPath)
|
||||||
if (res.code === 200) {
|
|
||||||
store.commit('setRouter', 'successPage')
|
|
||||||
} else {
|
} else {
|
||||||
alert({
|
const res: any = await getPublishedSurveyInfo({ surveyPath })
|
||||||
title: res.errmsg || '提交失败'
|
loadData(res, surveyPath)
|
||||||
})
|
store.dispatch('getEncryptInfo')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
alert({ title: error.message || '获取问卷失败' })
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
const confirmAgain = store.state.submitConf.confirmAgain
|
|
||||||
const { again_text, is_again } = confirmAgain
|
|
||||||
|
|
||||||
if (is_again) {
|
|
||||||
confirm({
|
|
||||||
title: again_text,
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
submitSurver()
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
} finally {
|
|
||||||
confirm.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
submitSurver()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
|
||||||
.index {
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
background-color: var(--primary-background-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 0.3rem;
|
|
||||||
background: rgba(255, 255, 255, var(--opacity));
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
162
web/src/render/pages/RenderPage.vue
Normal file
162
web/src/render/pages/RenderPage.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="index">
|
||||||
|
<ProgressBar />
|
||||||
|
<div class="wrapper" ref="boxRef">
|
||||||
|
<HeaderContent :bannerConf="bannerConf" :readonly="true" />
|
||||||
|
<div class="content">
|
||||||
|
<MainTitle :bannerConf="bannerConf" :readonly="true"></MainTitle>
|
||||||
|
<MainRenderer ref="mainRef"></MainRenderer>
|
||||||
|
<SubmitButton
|
||||||
|
:validate="validate"
|
||||||
|
:submitConf="submitConf"
|
||||||
|
:readonly="true"
|
||||||
|
:renderData="renderData"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
></SubmitButton>
|
||||||
|
<LogoIcon :logo-conf="logoConf" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
// @ts-ignore
|
||||||
|
import communalLoader from '@materials/communals/communalLoader.js'
|
||||||
|
import MainRenderer from '../components/MainRenderer.vue'
|
||||||
|
import AlertDialog from '../components/AlertDialog.vue'
|
||||||
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
|
import ProgressBar from '../components/ProgressBar.vue'
|
||||||
|
|
||||||
|
import { submitForm } from '../api/survey'
|
||||||
|
import encrypt from '../utils/encrypt'
|
||||||
|
|
||||||
|
import useCommandComponent from '../hooks/useCommandComponent'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
questionInfo?: any
|
||||||
|
isMobile?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
questionInfo: {},
|
||||||
|
isMobile: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const HeaderContent = communalLoader.loadComponent('HeaderContent')
|
||||||
|
const MainTitle = communalLoader.loadComponent('MainTitle')
|
||||||
|
const SubmitButton = communalLoader.loadComponent('SubmitButton')
|
||||||
|
const LogoIcon = communalLoader.loadComponent('LogoIcon')
|
||||||
|
|
||||||
|
const mainRef = ref<any>()
|
||||||
|
const boxRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const alert = useCommandComponent(AlertDialog)
|
||||||
|
const confirm = useCommandComponent(ConfirmDialog)
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const bannerConf = computed(() => store.state?.bannerConf || {})
|
||||||
|
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 validate = (cbk: (v: boolean) => void) => {
|
||||||
|
const index = 0
|
||||||
|
mainRef.value.$refs.formGroup[index].validate(cbk)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizationRequestBody = () => {
|
||||||
|
const enterTime = store.state.enterTime
|
||||||
|
const encryptInfo = store.state.encryptInfo
|
||||||
|
const formValues = store.state.formValues
|
||||||
|
|
||||||
|
const result: any = {
|
||||||
|
surveyPath: surveyPath.value,
|
||||||
|
data: JSON.stringify(formValues),
|
||||||
|
difTime: Date.now() - enterTime,
|
||||||
|
clientTime: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encryptInfo?.encryptType) {
|
||||||
|
result.encryptType = encryptInfo?.encryptType
|
||||||
|
result.data = encrypt[result.encryptType as 'rsa']({
|
||||||
|
data: result.data,
|
||||||
|
secretKey: encryptInfo?.data?.secretKey
|
||||||
|
})
|
||||||
|
if (encryptInfo?.data?.sessionId) {
|
||||||
|
result.sessionId = encryptInfo.data.sessionId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.data = JSON.stringify(result.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSurver = async () => {
|
||||||
|
if (surveyPath.value.length > 8) {
|
||||||
|
router.push({ name: 'successPage' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const params = normalizationRequestBody()
|
||||||
|
console.log(params)
|
||||||
|
const res: any = await submitForm(params)
|
||||||
|
if (res.code === 200) {
|
||||||
|
router.push({ name: 'successPage' })
|
||||||
|
} else {
|
||||||
|
alert({
|
||||||
|
title: res.errmsg || '提交失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const confirmAgain = store.state.submitConf.confirmAgain
|
||||||
|
const { again_text, is_again } = confirmAgain
|
||||||
|
|
||||||
|
if (is_again) {
|
||||||
|
confirm({
|
||||||
|
title: again_text,
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
submitSurver()
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
confirm.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
submitSurver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.index {
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 0.3rem;
|
||||||
|
background: rgba(255, 255, 255, var(--opacity));
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
38
web/src/render/router/index.ts
Normal file
38
web/src/render/router/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/:surveyId',
|
||||||
|
component: () => import('../pages/IndexPage.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'renderPage',
|
||||||
|
component: () => import('../pages/RenderPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'success',
|
||||||
|
name: 'successPage',
|
||||||
|
component: () => import('../pages/SuccessPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'error',
|
||||||
|
name: 'errorPage',
|
||||||
|
component: () => import('../pages/ErrorPage.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:catchAll(.*)',
|
||||||
|
name: 'emptyPage',
|
||||||
|
component: () => import('../pages/EmptyPage.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// 兼容预览模式
|
||||||
|
const base = window.location.pathname.includes('preview') ? 'management/preview' : 'render'
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(base),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
@ -15,16 +15,19 @@ const CODE_MAP = {
|
|||||||
NO_AUTH: 403
|
NO_AUTH: 403
|
||||||
}
|
}
|
||||||
const VOTE_INFO_KEY = 'voteinfo'
|
const VOTE_INFO_KEY = 'voteinfo'
|
||||||
|
import router from '../router'
|
||||||
export default {
|
export default {
|
||||||
// 初始化
|
// 初始化
|
||||||
init({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }) {
|
init(
|
||||||
|
{ commit, dispatch },
|
||||||
|
{ bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }
|
||||||
|
) {
|
||||||
commit('setEnterTime')
|
commit('setEnterTime')
|
||||||
const { begTime, endTime, answerBegTime, answerEndTime } = baseConf
|
const { begTime, endTime, answerBegTime, answerEndTime } = baseConf
|
||||||
const { msgContent } = submitConf
|
const { msgContent } = submitConf
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now < new Date(begTime).getTime()) {
|
if (now < new Date(begTime).getTime()) {
|
||||||
commit('setRouter', 'errorPage')
|
router.push({ name: 'errorPage' })
|
||||||
commit('setErrorInfo', {
|
commit('setErrorInfo', {
|
||||||
errorType: 'overTime',
|
errorType: 'overTime',
|
||||||
errorMsg: `<p>问卷未到开始填写时间,暂时无法进行填写<p/>
|
errorMsg: `<p>问卷未到开始填写时间,暂时无法进行填写<p/>
|
||||||
@ -32,7 +35,7 @@ export default {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (now > new Date(endTime).getTime()) {
|
} else if (now > new Date(endTime).getTime()) {
|
||||||
commit('setRouter', 'errorPage')
|
router.push({ name: 'errorPage' })
|
||||||
commit('setErrorInfo', {
|
commit('setErrorInfo', {
|
||||||
errorType: 'overTime',
|
errorType: 'overTime',
|
||||||
errorMsg: msgContent.msg_9001 || '您来晚了,感谢支持问卷~'
|
errorMsg: msgContent.msg_9001 || '您来晚了,感谢支持问卷~'
|
||||||
@ -44,7 +47,7 @@ export default {
|
|||||||
const momentStartTime = moment(`${todayStr} ${answerBegTime}`)
|
const momentStartTime = moment(`${todayStr} ${answerBegTime}`)
|
||||||
const momentEndTime = moment(`${todayStr} ${answerEndTime}`)
|
const momentEndTime = moment(`${todayStr} ${answerEndTime}`)
|
||||||
if (momentNow.isBefore(momentStartTime) || momentNow.isAfter(momentEndTime)) {
|
if (momentNow.isBefore(momentStartTime) || momentNow.isAfter(momentEndTime)) {
|
||||||
commit('setRouter', 'errorPage')
|
router.push({ name: 'errorPage' })
|
||||||
commit('setErrorInfo', {
|
commit('setErrorInfo', {
|
||||||
errorType: 'overTime',
|
errorType: 'overTime',
|
||||||
errorMsg: `<p>不在答题时间范围内,暂时无法进行填写<p/>
|
errorMsg: `<p>不在答题时间范围内,暂时无法进行填写<p/>
|
||||||
@ -53,7 +56,6 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commit('setRouter', 'indexPage')
|
|
||||||
|
|
||||||
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
|
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
|
||||||
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
|
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
|
||||||
|
@ -10,7 +10,6 @@ export default {
|
|||||||
const questionArr = []
|
const questionArr = []
|
||||||
|
|
||||||
item.forEach((questionKey) => {
|
item.forEach((questionKey) => {
|
||||||
console.log('题目重新计算')
|
|
||||||
const question = { ...questionData[questionKey] }
|
const question = { ...questionData[questionKey] }
|
||||||
// 开启显示序号
|
// 开启显示序号
|
||||||
if (question.showIndex) {
|
if (question.showIndex) {
|
||||||
|
@ -9,9 +9,6 @@ export default {
|
|||||||
setQuestionData(state, data) {
|
setQuestionData(state, data) {
|
||||||
state.questionData = data
|
state.questionData = data
|
||||||
},
|
},
|
||||||
setRouter(state, data) {
|
|
||||||
state.router = data
|
|
||||||
},
|
|
||||||
setErrorInfo(state, { errorType, errorMsg }) {
|
setErrorInfo(state, { errorType, errorMsg }) {
|
||||||
state.errorInfo = {
|
state.errorInfo = {
|
||||||
errorType,
|
errorType,
|
||||||
|
@ -3,7 +3,6 @@ import { isMobile } from '../utils/index'
|
|||||||
export default {
|
export default {
|
||||||
surveyPath: '',
|
surveyPath: '',
|
||||||
questionData: null,
|
questionData: null,
|
||||||
router: '',
|
|
||||||
isMobile: isMobile(),
|
isMobile: isMobile(),
|
||||||
errorInfo: {
|
errorInfo: {
|
||||||
errorType: '',
|
errorType: '',
|
||||||
|
@ -34,6 +34,10 @@ const mpaPlugin = createMpaPlugin({
|
|||||||
from: /render/,
|
from: /render/,
|
||||||
to: () => normalizePath('/src/render/index.html')
|
to: () => normalizePath('/src/render/index.html')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
from: /management\/preview/,
|
||||||
|
to: () => normalizePath('/src/render/index.html')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
from: /\/|\/management\/.?/,
|
from: /\/|\/management\/.?/,
|
||||||
to: () => normalizePath('/src/management/index.html')
|
to: () => normalizePath('/src/management/index.html')
|
||||||
|
Loading…
Reference in New Issue
Block a user