【Feature】:北大实践课作业 (#424)
* 【北大开源实践】增加数据导出功能 (#294) * feat:添加了一个文件数据导出的功能和相应前端页面 * fix lint * fix conflict --------- Co-authored-by: dayou <853094838@qq.com> * fix: components.d.ts文件ignore * feat: Update README_EN.md * feat: Update README.md * feat:新增预览功能 (#257) * feat:问卷预览功能 * feat:修复样式问题 * fix: 优化预览展示 * refactor: 重构vue3组合式API写法 (#265) * feat: 抽离题型枚举 (#272) * feat: 抽离题型枚举 * fix: 投放的链接加时间戳去掉ifream缓存 * feat: serve端的node engines * feat: 权限接口请求优化以及修复其他问题 (#290) * feat: c端路由改造 (#296) * 【北大开源实践】增加数据导出功能 (#294) * feat:添加了一个文件数据导出的功能和相应前端页面 * fix lint * fix conflict --------- Co-authored-by: dayou <853094838@qq.com> * fix: 删除components.d.ts文件 * 【北大开源实践】- 问卷断点续答 - 前端 (#282) * feat:增加断点续答功能 * feat:增加断点续答功能 * fix: 同步代码并且解决冲突 --------- Co-authored-by: dayou <853094838@qq.com> * fix: 删除components.d.ts文件最终 * 【北大开源实践】-选项限制 (#284) * format: 代码格式化 (#160) * feat: 选项限制 * fix: 同步代码并解决冲突 * fix conflict * fix conflict * fix lint * fix server lint --------- Co-authored-by: dayou <853094838@qq.com> Co-authored-by: XiaoYuan <2521510174@qq.com> * feat: 登录失效检测 & 协作冲突检测 (#287) Co-authored-by: Liuxinyi <liuxy0406@163.com> Co-authored-by: dayou <853094838@qq.com> * fix: peking分支同步develop并解决冲突 * fix: 修正颜色不统一 (#338) * fix: 修正颜色不统一 * fix: 删除server下的lock文件 * 编辑冲突检测 (#351) * perl: 选项配额优化 * fix: pinia改写 * feat: 完善北大课程相关的内容 * fix: 修复断点续答以及样式问题 (#420) * feat: 修改readme * [Feature]: 密码复杂度检测 (#407) * feat: 密码复杂度检测 * chore: 改为服务端校验 * feat: 优化展示 * fix:修复编辑页在不同element版本下表现不一致问题 (#406) * fix: 通过声明element最低版本来确定tab样式表现 * fix lint * feat(选项设置扩展):选择类题型增加选项排列配置 (#403) * build: add optimizeDeps packages * feat(选项设置扩展):选择类题型增加选项排列配置 * feat(选项设置扩展): 验收问题修复 --------- Co-authored-by: jiangchunfu <jiangchunfu@kaike.la> * fix: 删除多余内容 * feat: 优化登录窗口 * fix: 修复断点续答以及样式问题 fix: 修复选项引用验收bug fix: 修复断点续答问题 fix: 修复断点续答 fix: ignore fix: 修复投票题默认值 fix: 优化断点续答逻辑 fix: 选中图标适应高度 fix: 回退最大最小选择 fix: 修复断点续答 fix: 修复elswitch不更新问题 fix: 修复访问密码更新不生效问题 fix: 修复样式 fix: 修复多选题最大最小限制 fix: 优化断点续答问题 修复多选题命中最多选择后无法取消问题 fix: 修复服务端的富文本解析 fix: lint fix: min error fix: 修复最少最多选择 fix: 修复投票问卷的最少最多选择 fix: 兼容断点续答情况下选项配额为0的情况 fix: 兼容断点续答情况下选项配额为0的情况 fix: 兼容单选题的断点续答下的选项配额 fix: 修复添加选项问题 fix: 前端提示服务的配额已满 fix: 更新填写的过程中配额减少情况 --------- Co-authored-by: sudoooooo <zjbbabybaby@gmail.com> Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com> Co-authored-by: Jiangchunfu <mrj_kevin@163.com> Co-authored-by: jiangchunfu <jiangchunfu@kaike.la> * feat: 修改验收问题 (#421) * fix lint --------- Co-authored-by: Oseast <162945153+Oseast@users.noreply.github.com> Co-authored-by: sudoooooo <zjbbabybaby@gmail.com> Co-authored-by: chaorenluo <1243357953@qq.com> Co-authored-by: Realabiha <48506355+Realabiha@users.noreply.github.com> Co-authored-by: shiyiting763 <70299297+shiyiting763@users.noreply.github.com> Co-authored-by: yiyeah <68832436+yiyeah@users.noreply.github.com> Co-authored-by: XiaoYuan <2521510174@qq.com> Co-authored-by: Xinyi Liu <74805961+colmon46@users.noreply.github.com> Co-authored-by: Liuxinyi <liuxy0406@163.com> Co-authored-by: nil <wangweiguo2013@icloud.com> Co-authored-by: 王晓聪 <wang86976110@126.com> Co-authored-by: taoshuang <taoshuang@didiglobal.com> Co-authored-by: luch1994 <1097650398@qq.com> Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com> Co-authored-by: Jiangchunfu <mrj_kevin@163.com> Co-authored-by: jiangchunfu <jiangchunfu@kaike.la> Co-authored-by: luch <32321690+luch1994@users.noreply.github.com>
This commit is contained in:
parent
43001a12c7
commit
b749cfa6f6
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,6 +3,7 @@ node_modules
|
||||
dist
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@ -25,7 +26,10 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
|
||||
components.d.ts
|
||||
|
||||
# 默认的上传文件夹
|
||||
userUpload
|
||||
exportfile
|
||||
yarn.lock
|
@ -52,6 +52,9 @@ http {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
location /exportfile {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
# 静态文件的默认存储文件夹
|
||||
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
|
||||
location /userUpload {
|
||||
|
12
server/.env
12
server/.env
@ -1,9 +1,15 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
XIAOJU_SURVEY_MONGO_URL= # mongodb://127.0.0.1:27017 # 建议设置强密码
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY= # dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
|
5
server/.gitignore
vendored
5
server/.gitignore
vendored
@ -13,6 +13,7 @@ pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@ -37,4 +38,6 @@ lerna-debug.log*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
tmp
|
||||
tmp
|
||||
exportfile
|
||||
userUpload
|
@ -27,10 +27,11 @@
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/typeorm": "^10.0.1",
|
||||
"ali-oss": "^6.20.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
@ -41,11 +42,14 @@
|
||||
"nanoid": "^3.3.7",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-xlsx": "^0.24.0",
|
||||
"qiniu": "^7.11.1",
|
||||
"redlock": "^5.0.0-beta.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.19"
|
||||
"typeorm": "^0.3.19",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
@ -70,6 +74,7 @@
|
||||
"jest": "^29.5.0",
|
||||
"mongodb-memory-server": "^9.1.4",
|
||||
"prettier": "^3.0.0",
|
||||
"redis-memory-server": "^0.11.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
import { spawn } from 'child_process';
|
||||
import { RedisMemoryServer } from 'redis-memory-server';
|
||||
|
||||
async function startServerAndRunScript() {
|
||||
// 启动 MongoDB 内存服务器
|
||||
@ -8,12 +9,19 @@ async function startServerAndRunScript() {
|
||||
|
||||
console.log('MongoDB Memory Server started:', mongoUri);
|
||||
|
||||
const redisServer = new RedisMemoryServer();
|
||||
const redisHost = await redisServer.getHost();
|
||||
const redisPort = await redisServer.getPort();
|
||||
|
||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||
const tsnode = spawn(
|
||||
'cross-env',
|
||||
[
|
||||
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||
`XIAOJU_SURVEY_REDIS_HOST=${redisHost}`,
|
||||
`XIAOJU_SURVEY_REDIS_PORT=${redisPort}`,
|
||||
'NODE_ENV=development',
|
||||
'SERVER_ENV=local',
|
||||
'npm',
|
||||
'run',
|
||||
'start:dev',
|
||||
@ -31,9 +39,10 @@ async function startServerAndRunScript() {
|
||||
console.error(data);
|
||||
});
|
||||
|
||||
tsnode.on('close', (code) => {
|
||||
tsnode.on('close', async (code) => {
|
||||
console.log(`Nodemon process exited with code ${code}`);
|
||||
mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
await mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
await redisServer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,9 @@ import { LoggerProvider } from './logger/logger.provider';
|
||||
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||
import { Logger } from './logger';
|
||||
import { XiaojuSurveyLogger } from './logger';
|
||||
import { DownloadTask } from './models/downloadTask.entity';
|
||||
import { Session } from './models/session.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -81,6 +83,8 @@ import { Logger } from './logger';
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Collaborator,
|
||||
DownloadTask,
|
||||
Session,
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -128,7 +132,7 @@ export class AppModule {
|
||||
),
|
||||
new SurveyUtilPlugin(),
|
||||
);
|
||||
Logger.init({
|
||||
XiaojuSurveyLogger.init({
|
||||
filename: this.configService.get<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
|
||||
});
|
||||
}
|
||||
|
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 };
|
@ -12,6 +12,7 @@ export enum EXCEPTION_CODE {
|
||||
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误
|
||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||
SURVEY_SAVE_CONFLICT = 3006, // 问卷冲突
|
||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||
|
||||
|
@ -6,6 +6,9 @@ export enum RECORD_STATUS {
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
COMOPUTETING = 'computing', // 计算中
|
||||
FINISHED = 'finished', // 已完成
|
||||
ERROR = 'error', // 错误
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
94
server/src/guards/session.guard.ts
Normal file
94
server/src/guards/session.guard.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
|
||||
@Injectable()
|
||||
export class SessionGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const sessionIdKey = this.reflector.get<string>(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
const sessionId = get(request, sessionIdKey);
|
||||
|
||||
if (!sessionId) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const saveSession = await this.sessionService.findOne(sessionId);
|
||||
|
||||
request.saveSession = saveSession;
|
||||
|
||||
const surveyId = saveSession.surveyId;
|
||||
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
throw new SurveyNotFoundException('问卷不存在');
|
||||
}
|
||||
|
||||
request.surveyMeta = surveyMeta;
|
||||
|
||||
// 兼容老的问卷没有ownerId
|
||||
if (
|
||||
surveyMeta.ownerId === user._id.toString() ||
|
||||
surveyMeta.owner === user.username
|
||||
) {
|
||||
// 问卷的owner,可以访问和操作问卷
|
||||
return true;
|
||||
}
|
||||
|
||||
if (surveyMeta.workspaceId) {
|
||||
const memberInfo = await this.workspaceMemberService.findOne({
|
||||
workspaceId: surveyMeta.workspaceId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
if (!memberInfo) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = this.reflector.get<string[]>(
|
||||
'surveyPermission',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const info = await this.collaboratorService.getCollaborator({
|
||||
surveyId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
request.collaborator = info;
|
||||
if (
|
||||
permissions.some((permission) => info.permissions.includes(permission))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
|
@ -60,6 +60,7 @@ export interface DataItem {
|
||||
rangeConfig?: any;
|
||||
starStyle?: string;
|
||||
innerType?: string;
|
||||
quotaNoDisplay?: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@ -69,6 +70,7 @@ export interface Option {
|
||||
othersKey?: string;
|
||||
placeholderDesc: string;
|
||||
hash: string;
|
||||
quota?: number;
|
||||
}
|
||||
|
||||
export interface DataConf {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
import { Request } from 'express';
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
export class Logger {
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class XiaojuSurveyLogger {
|
||||
private static inited = false;
|
||||
|
||||
constructor() {}
|
||||
private traceId: string;
|
||||
|
||||
static init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
if (XiaojuSurveyLogger.inited) {
|
||||
return;
|
||||
}
|
||||
log4js.configure({
|
||||
@ -30,25 +30,28 @@ export class Logger {
|
||||
default: { appenders: ['app'], level: 'trace' },
|
||||
},
|
||||
});
|
||||
XiaojuSurveyLogger.inited = true;
|
||||
}
|
||||
|
||||
_log(message, options: { dltag?: string; level: string; req?: Request }) {
|
||||
_log(message, options: { dltag?: string; level: string }) {
|
||||
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
const level = options?.level;
|
||||
const dltag = options?.dltag ? `${options.dltag}||` : '';
|
||||
const traceIdStr = options?.req?.['traceId']
|
||||
? `traceid=${options?.req?.['traceId']}||`
|
||||
: '';
|
||||
const traceIdStr = this.traceId ? `traceid=${this.traceId}||` : '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string; req?: Request }) {
|
||||
setTraceId(traceId: string) {
|
||||
this.traceId = traceId;
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'info' });
|
||||
}
|
||||
|
||||
error(message, options: { dltag?: string; req?: Request }) {
|
||||
error(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'error' });
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
import { Logger } from './index';
|
||||
import { XiaojuSurveyLogger } from './index';
|
||||
|
||||
export const LoggerProvider: Provider = {
|
||||
provide: Logger,
|
||||
useClass: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useClass: XiaojuSurveyLogger,
|
||||
};
|
||||
|
@ -10,9 +10,9 @@ const getCountStr = () => {
|
||||
|
||||
export const genTraceId = ({ ip }) => {
|
||||
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
||||
ip = ip.replace('::ffff:', '');
|
||||
ip = ip.replace('::ffff:', '').replace('::1', '');
|
||||
let ipArr;
|
||||
if (ip.indexOf(':') > 0) {
|
||||
if (ip.indexOf(':') >= 0) {
|
||||
ipArr = ip.split(':').map((segment) => {
|
||||
// 将IPv6每个段转为16位,并补0到长度为4
|
||||
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
||||
|
@ -1,26 +1,25 @@
|
||||
// logger.middleware.ts
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
import { XiaojuSurveyLogger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
import { genTraceId } from '../logger/util';
|
||||
|
||||
@Injectable()
|
||||
export class LogRequestMiddleware implements NestMiddleware {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
constructor(private readonly logger: XiaojuSurveyLogger) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const { method, originalUrl, ip } = req;
|
||||
const userAgent = req.get('user-agent') || '';
|
||||
const startTime = Date.now();
|
||||
const traceId = genTraceId({ ip });
|
||||
req['traceId'] = traceId;
|
||||
this.logger.setTraceId(traceId);
|
||||
const query = JSON.stringify(req.query);
|
||||
const body = JSON.stringify(req.body);
|
||||
this.logger.info(
|
||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||
{
|
||||
dltag: 'request_in',
|
||||
req,
|
||||
},
|
||||
);
|
||||
|
||||
@ -30,7 +29,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||
{
|
||||
dltag: 'request_out',
|
||||
req,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -5,8 +5,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'captcha' })
|
||||
export class Captcha extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
@ -6,8 +6,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'clientEncrypt' })
|
||||
export class ClientEncrypt extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
34
server/src/models/downloadTask.entity.ts
Normal file
34
server/src/models/downloadTask.entity.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'downloadTask' })
|
||||
export class DownloadTask extends BaseEntity {
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
// 文件路径
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
// 文件key
|
||||
@Column()
|
||||
fileKey: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
// 文件名
|
||||
@Column()
|
||||
filename: string;
|
||||
|
||||
// 文件大小
|
||||
@Column()
|
||||
fileSize: string;
|
||||
|
||||
@Column()
|
||||
params: string;
|
||||
}
|
18
server/src/models/session.entity.ts
Normal file
18
server/src/models/session.entity.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'session' })
|
||||
export class Session extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
}
|
@ -19,4 +19,7 @@ export class SurveyHistory extends BaseEntity {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@Column('string')
|
||||
sessionId: string;
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Controller, Get, Query, HttpCode, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
@ -43,4 +50,16 @@ export class UserController {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authentication)
|
||||
@Get('/getUserInfo')
|
||||
async getUserInfo(@Request() req) {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
userId: req.user._id.toString(),
|
||||
username: req.user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,13 @@ export class AuthService {
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async expiredCheck(token: string) {
|
||||
try {
|
||||
verify(token, this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'));
|
||||
} catch (err) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,18 @@ export class FileService {
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix,
|
||||
keepOriginFilename,
|
||||
}: {
|
||||
configKey: string;
|
||||
file: Express.Multer.File;
|
||||
pathPrefix: string;
|
||||
keepOriginFilename?: boolean;
|
||||
}) {
|
||||
const handler = this.getHandler(configKey);
|
||||
const { key } = await handler.upload(file, { pathPrefix });
|
||||
const { key } = await handler.upload(file, {
|
||||
pathPrefix,
|
||||
keepOriginFilename,
|
||||
});
|
||||
const url = await handler.getUrl(key);
|
||||
return {
|
||||
key,
|
||||
|
@ -12,9 +12,14 @@ export class LocalHandler implements FileUploadHandler {
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
options?: { pathPrefix?: string; keepOriginFilename?: boolean },
|
||||
): Promise<{ key: string }> {
|
||||
const filename = await generateUniqueFilename(file.originalname);
|
||||
let filename;
|
||||
if (options?.keepOriginFilename) {
|
||||
filename = file.originalname;
|
||||
} else {
|
||||
filename = await generateUniqueFilename(file.originalname);
|
||||
}
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
@ -35,6 +40,10 @@ export class LocalHandler implements FileUploadHandler {
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
if (process.env.SERVER_ENV === 'local') {
|
||||
const port = process.env.PORT || 3000;
|
||||
return `http://localhost:${port}/${key}`;
|
||||
}
|
||||
return `/${key}`;
|
||||
}
|
||||
}
|
||||
|
9
server/src/modules/redis/redis.module.ts
Normal file
9
server/src/modules/redis/redis.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// src/redis/redis.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
32
server/src/modules/redis/redis.service.ts
Normal file
32
server/src/modules/redis/redis.service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
import Redlock, { Lock } from 'redlock';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
private readonly redisClient: Redis;
|
||||
private readonly redlock: Redlock;
|
||||
|
||||
constructor() {
|
||||
this.redisClient = new Redis({
|
||||
host: process.env.XIAOJU_SURVEY_REDIS_HOST,
|
||||
port: parseInt(process.env.XIAOJU_SURVEY_REDIS_PORT),
|
||||
password: process.env.XIAOJU_SURVEY_REDIS_PASSWORD || undefined,
|
||||
username: process.env.XIAOJU_SURVEY_REDIS_USERNAME || undefined,
|
||||
db: parseInt(process.env.XIAOJU_SURVEY_REDIS_DB) || 0,
|
||||
});
|
||||
this.redlock = new Redlock([this.redisClient], {
|
||||
retryCount: 10,
|
||||
retryDelay: 200, // ms
|
||||
retryJitter: 200, // ms
|
||||
});
|
||||
}
|
||||
|
||||
async lockResource(resource: string, ttl: number): Promise<Lock> {
|
||||
return this.redlock.acquire([resource], ttl);
|
||||
}
|
||||
|
||||
async unlockResource(lock: Lock): Promise<void> {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CollaboratorController } from '../controllers/collaborator.controller';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
@ -25,7 +25,7 @@ jest.mock('src/guards/workspace.guard');
|
||||
describe('CollaboratorController', () => {
|
||||
let controller: CollaboratorController;
|
||||
let collaboratorService: CollaboratorService;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
let userService: UserService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let workspaceMemberServie: WorkspaceMemberService;
|
||||
@ -50,7 +50,7 @@ describe('CollaboratorController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
@ -84,7 +84,7 @@ describe('CollaboratorController', () => {
|
||||
|
||||
controller = module.get<CollaboratorController>(CollaboratorController);
|
||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
userService = module.get<UserService>(UserService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||
@ -191,7 +191,6 @@ describe('CollaboratorController', () => {
|
||||
describe('getSurveyCollaboratorList', () => {
|
||||
it('should return collaborator list', async () => {
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = [
|
||||
{ _id: 'collaboratorId', userId: 'userId', username: '' },
|
||||
];
|
||||
@ -202,7 +201,7 @@ describe('CollaboratorController', () => {
|
||||
|
||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
|
||||
|
||||
const response = await controller.getSurveyCollaboratorList(query, req);
|
||||
const response = await controller.getSurveyCollaboratorList(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -214,11 +213,10 @@ describe('CollaboratorController', () => {
|
||||
const query: GetSurveyCollaboratorListDto = {
|
||||
surveyId: '',
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(
|
||||
controller.getSurveyCollaboratorList(query, req),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.getSurveyCollaboratorList(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -230,14 +228,13 @@ describe('CollaboratorController', () => {
|
||||
userId: 'userId',
|
||||
permissions: ['read'],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = { _id: 'userId', permissions: ['read'] };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'changeUserPermission')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.changeUserPermission(reqBody, req);
|
||||
const response = await controller.changeUserPermission(reqBody);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -251,11 +248,10 @@ describe('CollaboratorController', () => {
|
||||
userId: '',
|
||||
permissions: ['surveyManage'],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(
|
||||
controller.changeUserPermission(reqBody, req),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.changeUserPermission(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -263,14 +259,13 @@ describe('CollaboratorController', () => {
|
||||
describe('deleteCollaborator', () => {
|
||||
it('should delete collaborator successfully', async () => {
|
||||
const query = { surveyId: 'surveyId', userId: 'userId' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = { acknowledged: true, deletedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'deleteCollaborator')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.deleteCollaborator(query, req);
|
||||
const response = await controller.deleteCollaborator(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -280,9 +275,8 @@ describe('CollaboratorController', () => {
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const query = { surveyId: '', userId: '' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(controller.deleteCollaborator(query, req)).rejects.toThrow(
|
||||
await expect(controller.deleteCollaborator(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
|
@ -3,13 +3,13 @@ import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { InsertManyResult, ObjectId } from 'mongodb';
|
||||
|
||||
describe('CollaboratorService', () => {
|
||||
let service: CollaboratorService;
|
||||
let repository: MongoRepository<Collaborator>;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -20,7 +20,7 @@ describe('CollaboratorService', () => {
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
},
|
||||
@ -32,7 +32,7 @@ describe('CollaboratorService', () => {
|
||||
repository = module.get<MongoRepository<Collaborator>>(
|
||||
getRepositoryToken(Collaborator),
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
|
@ -9,7 +9,7 @@ import { ResponseSchemaService } from '../../surveyResponse/services/responseSch
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
@ -28,7 +28,7 @@ describe('DataStatisticController', () => {
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -56,7 +56,7 @@ describe('DataStatisticController', () => {
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
@ -73,7 +73,7 @@ describe('DataStatisticController', () => {
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
@ -123,7 +123,7 @@ describe('DataStatisticController', () => {
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -169,7 +169,7 @@ describe('DataStatisticController', () => {
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -187,9 +187,9 @@ describe('DataStatisticController', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
controller.data(mockRequest.query, mockRequest),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.data(mockRequest.query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
@ -49,7 +49,7 @@ describe('SurveyHistoryController', () => {
|
||||
useClass: jest.fn().mockImplementation(() => ({})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
@ -66,7 +66,7 @@ describe('SurveyHistoryController', () => {
|
||||
it('should return history list when query is valid', async () => {
|
||||
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
||||
|
||||
await controller.getList(queryInfo, {});
|
||||
await controller.getList(queryInfo);
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
|
@ -78,7 +78,13 @@ describe('SurveyHistoryService', () => {
|
||||
.spyOn(repository, 'save')
|
||||
.mockResolvedValueOnce({} as SurveyHistory);
|
||||
|
||||
await service.addHistory({ surveyId, schema, type, user });
|
||||
await service.addHistory({
|
||||
surveyId,
|
||||
schema,
|
||||
type,
|
||||
user,
|
||||
sessionId: '',
|
||||
});
|
||||
|
||||
expect(spyCreate).toHaveBeenCalledWith({
|
||||
pageId: surveyId,
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
SURVEY_PERMISSION,
|
||||
SURVEY_PERMISSION_DESCRIPTION,
|
||||
} from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
@ -40,7 +40,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
export class CollaboratorController {
|
||||
constructor(
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly userService: UserService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberServie: WorkspaceMemberService,
|
||||
@ -69,7 +69,7 @@ export class CollaboratorController {
|
||||
) {
|
||||
const { error, value } = CreateCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -124,7 +124,7 @@ export class CollaboratorController {
|
||||
) {
|
||||
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -184,7 +184,7 @@ export class CollaboratorController {
|
||||
neIdList: collaboratorIdList,
|
||||
userIdList: newCollaboratorUserIdList,
|
||||
});
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes), { req });
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes));
|
||||
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
||||
const insertRes = await this.collaboratorService.batchCreate({
|
||||
surveyId: value.surveyId,
|
||||
@ -208,7 +208,7 @@ export class CollaboratorController {
|
||||
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
|
||||
value.surveyId,
|
||||
);
|
||||
this.logger.info(JSON.stringify(delRes), { req });
|
||||
this.logger.info(JSON.stringify(delRes));
|
||||
}
|
||||
|
||||
return {
|
||||
@ -225,11 +225,10 @@ export class CollaboratorController {
|
||||
])
|
||||
async getSurveyCollaboratorList(
|
||||
@Query() query: GetSurveyCollaboratorListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -263,17 +262,14 @@ export class CollaboratorController {
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async changeUserPermission(
|
||||
@Body() reqBody: ChangeUserPermissionDto,
|
||||
@Request() req,
|
||||
) {
|
||||
async changeUserPermission(@Body() reqBody: ChangeUserPermissionDto) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
permissions: Joi.array().items(Joi.string().required()),
|
||||
}).validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -292,13 +288,13 @@ export class CollaboratorController {
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async deleteCollaborator(@Query() query, @Request() req) {
|
||||
async deleteCollaborator(@Query() query) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
}).validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -319,7 +315,7 @@ export class CollaboratorController {
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
this.logger.error(`问卷不存在: ${surveyId}`, { req });
|
||||
this.logger.error(`问卷不存在: ${surveyId}`);
|
||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
@ -17,7 +16,7 @@ import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
||||
@ -32,7 +31,7 @@ export class DataStatisticController {
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('/dataTable')
|
||||
@ -44,7 +43,6 @@ export class DataStatisticController {
|
||||
async data(
|
||||
@Query()
|
||||
queryInfo,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
@ -53,7 +51,7 @@ export class DataStatisticController {
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isDesensitive, page, pageSize } = value;
|
||||
|
188
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
188
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
Post,
|
||||
Body,
|
||||
// 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 { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
//后添加
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import {
|
||||
GetDownloadTaskDto,
|
||||
CreateDownloadDto,
|
||||
GetDownloadTaskListDto,
|
||||
DeleteDownloadTaskDto,
|
||||
} from '../dto/downloadTask.dto';
|
||||
import moment from 'moment';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
@ApiTags('downloadTask')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/downloadTask')
|
||||
export class DownloadTaskController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly downloadTaskService: DownloadTaskService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Post('/createTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async createTask(
|
||||
@Body()
|
||||
reqBody: CreateDownloadDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = CreateDownloadDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isDesensitive } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const id = await this.downloadTaskService.createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
operatorId: req.user._id.toString(),
|
||||
params: { isDesensitive },
|
||||
});
|
||||
this.downloadTaskService.processDownloadTask({ taskId: id });
|
||||
return {
|
||||
code: 200,
|
||||
data: { taskId: id },
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTaskList')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async downloadList(
|
||||
@Query()
|
||||
queryInfo: GetDownloadTaskListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = GetDownloadTaskListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { pageIndex, pageSize } = value;
|
||||
const { total, list } = await this.downloadTaskService.getDownloadTaskList({
|
||||
ownerId: req.user._id.toString(),
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: total,
|
||||
list: list.map((data) => {
|
||||
const item: Record<string, any> = {};
|
||||
item.taskId = data._id.toString();
|
||||
item.curStatus = data.curStatus;
|
||||
item.filename = data.filename;
|
||||
item.url = data.url;
|
||||
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = Number(data.fileSize);
|
||||
if (isNaN(size)) {
|
||||
item.fileSize = data.fileSize;
|
||||
} else {
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
item.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
|
||||
}
|
||||
item.createDate = moment(Number(data.createDate)).format(fmt);
|
||||
return item;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async getDownloadTask(@Query() query: GetDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = GetDownloadTaskDto.validate(query);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId: value.taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.ownerId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
const res: Record<string, any> = {
|
||||
...taskInfo,
|
||||
};
|
||||
res.taskId = taskInfo._id.toString();
|
||||
delete res._id;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/deleteDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async deleteFileByName(@Body() body: DeleteDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = DeleteDownloadTaskDto.validate(body);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { taskId } = value;
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.ownerId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const delRes = await this.downloadTaskService.deleteDownloadTask({
|
||||
taskId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: delRes.modifiedCount === 1,
|
||||
};
|
||||
}
|
||||
}
|
89
server/src/modules/survey/controllers/session.controller.ts
Normal file
89
server/src/modules/survey/controllers/session.controller.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionService } from '../services/session.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SessionGuard } from 'src/guards/session.guard';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/session')
|
||||
export class SessionController {
|
||||
constructor(
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Post('/create')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async create(
|
||||
@Body()
|
||||
reqBody: {
|
||||
surveyId: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(reqBody);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const surveyId = value.surveyId;
|
||||
const session = await this.sessionService.create({
|
||||
surveyId,
|
||||
userId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
sessionId: session._id.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/seize')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SessionGuard)
|
||||
@SetMetadata('sessionId', 'body.sessionId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async seize(
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const saveSession = req.saveSession;
|
||||
|
||||
await this.sessionService.updateSessionToEditing({
|
||||
sessionId: saveSession._id.toString(),
|
||||
surveyId: saveSession.surveyId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import { SurveyConfService } from '../services/surveyConf.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { CounterService } from 'src/modules/surveyResponse/services/counter.service';
|
||||
|
||||
import BannerData from '../template/banner/index.json';
|
||||
import { CreateSurveyDto } from '../dto/createSurvey.dto';
|
||||
@ -25,13 +26,15 @@ import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { HISTORY_TYPE } from 'src/enums';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/survey')
|
||||
@ -42,7 +45,10 @@ export class SurveyController {
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly contentSecurityService: ContentSecurityService,
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Get('/getBannerData')
|
||||
@ -71,9 +77,7 @@ export class SurveyController {
|
||||
) {
|
||||
const { error, value } = CreateSurveyDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -129,13 +133,41 @@ export class SurveyController {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
configData: Joi.any().required(),
|
||||
sessionId: Joi.string().required(),
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const username = req.user.username;
|
||||
const sessionId = value.sessionId;
|
||||
const surveyId = value.surveyId;
|
||||
const latestEditingOne = await this.sessionService.findLatestEditingOne({
|
||||
surveyId,
|
||||
});
|
||||
|
||||
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
|
||||
const curSession = await this.sessionService.findOne(sessionId);
|
||||
if (curSession.createDate <= latestEditingOne.updateDate) {
|
||||
// 在当前用户打开之后,被其他页面保存过了
|
||||
const isSameOperator =
|
||||
latestEditingOne.userId === req.user._id.toString();
|
||||
let preOperator;
|
||||
if (!isSameOperator) {
|
||||
preOperator = await this.userService.getUserById(
|
||||
latestEditingOne.userId,
|
||||
);
|
||||
}
|
||||
return {
|
||||
code: EXCEPTION_CODE.SURVEY_SAVE_CONFLICT,
|
||||
errmsg: isSameOperator
|
||||
? '当前问卷已在其它页面开启编辑,刷新以获取最新内容'
|
||||
: `当前问卷已由 ${preOperator.username} 编辑,刷新以获取最新内容`,
|
||||
};
|
||||
}
|
||||
}
|
||||
await this.sessionService.updateSessionToEditing({ sessionId, surveyId });
|
||||
|
||||
const username = req.user.username;
|
||||
|
||||
const configData = value.configData;
|
||||
await this.surveyConfService.saveSurveyConf({
|
||||
@ -198,7 +230,7 @@ export class SurveyController {
|
||||
}).validate(queryInfo);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -241,15 +273,13 @@ export class SurveyController {
|
||||
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 });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const surveyId = value.surveyId;
|
||||
@ -282,7 +312,7 @@ export class SurveyController {
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const username = req.user.username;
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
@ -15,16 +14,15 @@ import { SurveyHistoryService } from '../services/surveyHistory.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 { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/surveyHisotry')
|
||||
export class SurveyHistoryController {
|
||||
constructor(
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('/getList')
|
||||
@ -43,7 +41,6 @@ export class SurveyHistoryController {
|
||||
surveyId: string;
|
||||
historyType: string;
|
||||
},
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
@ -51,7 +48,7 @@ export class SurveyHistoryController {
|
||||
}).validate(queryInfo);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import { getFilter, getOrder } from 'src/utils/surveyUtil';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
@ -33,7 +33,7 @@ import { CollaboratorService } from '../services/collaborator.service';
|
||||
export class SurveyMetaController {
|
||||
constructor(
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
) {}
|
||||
|
||||
@ -51,9 +51,7 @@ export class SurveyMetaController {
|
||||
}).validate(reqBody, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const survey = req.surveyMeta;
|
||||
@ -81,7 +79,7 @@ export class SurveyMetaController {
|
||||
) {
|
||||
const { value, error } = GetSurveyListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { curPage, pageSize, workspaceId } = value;
|
||||
@ -91,14 +89,14 @@ export class SurveyMetaController {
|
||||
try {
|
||||
filter = getFilter(JSON.parse(decodeURIComponent(value.filter)));
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
if (value.order) {
|
||||
try {
|
||||
order = order = getOrder(JSON.parse(decodeURIComponent(value.order)));
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
const userId = req.user._id.toString();
|
||||
|
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal file
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
|
||||
export class CreateDownloadDto {
|
||||
@ApiProperty({ description: '问卷id', required: true })
|
||||
surveyId: string;
|
||||
@ApiProperty({ description: '是否脱敏', required: false })
|
||||
isDesensitive: boolean;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().allow(null).default(false),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
export class GetDownloadTaskListDto {
|
||||
@ApiProperty({ description: '当前页', required: false })
|
||||
pageIndex: number;
|
||||
@ApiProperty({ description: '一页大小', required: false })
|
||||
pageSize: number;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
pageIndex: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(20),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class GetDownloadTaskDto {
|
||||
@ApiProperty({ description: '任务id', required: true })
|
||||
taskId: string;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
taskId: Joi.string().required(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteDownloadTaskDto {
|
||||
@ApiProperty({ description: '任务id', required: true })
|
||||
taskId: string;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
taskId: Joi.string().required(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
@ -3,14 +3,14 @@ import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
@Injectable()
|
||||
export class CollaboratorService {
|
||||
constructor(
|
||||
@InjectRepository(Collaborator)
|
||||
private readonly collaboratorRepository: MongoRepository<Collaborator>,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
async create({ surveyId, userId, permissions }) {
|
||||
|
280
server/src/modules/survey/services/downloadTask.service.ts
Normal file
280
server/src/modules/survey/services/downloadTask.service.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { DataStatisticService } from './dataStatistic.service';
|
||||
import xlsx from 'node-xlsx';
|
||||
import { load } from 'cheerio';
|
||||
import { get } from 'lodash';
|
||||
import { FileService } from 'src/modules/file/services/file.service';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
export class DownloadTaskService {
|
||||
private static taskList: Array<any> = [];
|
||||
private static isExecuting: boolean = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(DownloadTask)
|
||||
private readonly downloadTaskRepository: MongoRepository<DownloadTask>,
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
@InjectRepository(SurveyResponse)
|
||||
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
async createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
operatorId,
|
||||
params,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
operatorId: string;
|
||||
params: any;
|
||||
}) {
|
||||
const filename = `${responseSchema.title}-${params.isDesensitive ? '脱敏' : '原'}回收数据-${moment().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||
const downloadTask = this.downloadTaskRepository.create({
|
||||
surveyId,
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
fileSize: '计算中',
|
||||
ownerId: operatorId,
|
||||
params: {
|
||||
...params,
|
||||
title: responseSchema.title,
|
||||
},
|
||||
filename,
|
||||
});
|
||||
await this.downloadTaskRepository.save(downloadTask);
|
||||
return downloadTask._id.toString();
|
||||
}
|
||||
|
||||
async getDownloadTaskList({
|
||||
ownerId,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}: {
|
||||
ownerId: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
const where = {
|
||||
ownerId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
};
|
||||
const [surveyDownloadList, total] =
|
||||
await this.downloadTaskRepository.findAndCount({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (pageIndex - 1) * pageSize,
|
||||
order: {
|
||||
createDate: -1,
|
||||
},
|
||||
});
|
||||
return {
|
||||
total,
|
||||
list: surveyDownloadList,
|
||||
};
|
||||
}
|
||||
|
||||
async getDownloadTaskById({ taskId }) {
|
||||
const res = await this.downloadTaskRepository.find({
|
||||
where: {
|
||||
_id: new ObjectId(taskId),
|
||||
},
|
||||
});
|
||||
if (Array.isArray(res) && res.length > 0) {
|
||||
return res[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteDownloadTask({ taskId }: { taskId: string }) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
return this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
processDownloadTask({ taskId }) {
|
||||
DownloadTaskService.taskList.push(taskId);
|
||||
if (!DownloadTaskService.isExecuting) {
|
||||
this.executeTask();
|
||||
DownloadTaskService.isExecuting = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeTask() {
|
||||
try {
|
||||
for (const taskId of DownloadTaskService.taskList) {
|
||||
const taskInfo = await this.getDownloadTaskById({ taskId });
|
||||
if (!taskInfo || taskInfo.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||
// 不存在或者已删除的,不处理
|
||||
continue;
|
||||
}
|
||||
await this.handleDownloadTask({ taskInfo });
|
||||
}
|
||||
} finally {
|
||||
DownloadTaskService.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadTask({ taskInfo }) {
|
||||
try {
|
||||
// 更新任务状态为计算中
|
||||
const updateRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.COMOPUTETING,
|
||||
date: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.info(JSON.stringify(updateRes));
|
||||
|
||||
// 开始计算任务
|
||||
const surveyId = taskInfo.surveyId;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
},
|
||||
};
|
||||
const total = await this.surveyResponseRepository.count(where);
|
||||
const pageSize = 200;
|
||||
const pageTotal = Math.ceil(total / pageSize);
|
||||
const xlsxHead = [];
|
||||
const xlsxBody = [];
|
||||
for (let pageIndex = 1; pageIndex <= pageTotal; pageIndex++) {
|
||||
const { listHead, listBody } =
|
||||
await this.dataStatisticService.getDataTable({
|
||||
surveyId,
|
||||
pageNum: pageIndex,
|
||||
pageSize,
|
||||
responseSchema,
|
||||
});
|
||||
if (xlsxHead.length === 0) {
|
||||
for (const item of listHead) {
|
||||
const $ = load(item.title);
|
||||
const text = $.text();
|
||||
xlsxHead.push(text);
|
||||
}
|
||||
}
|
||||
for (const bodyItem of listBody) {
|
||||
const bodyData = [];
|
||||
for (const headItem of listHead) {
|
||||
const field = headItem.field;
|
||||
const val = get(bodyItem, field, '');
|
||||
const $ = load(val);
|
||||
const text = $.text();
|
||||
bodyData.push(text);
|
||||
}
|
||||
xlsxBody.push(bodyData);
|
||||
}
|
||||
}
|
||||
const xlsxData = [xlsxHead, ...xlsxBody];
|
||||
const buffer = await xlsx.build([
|
||||
{ name: 'sheet1', data: xlsxData, options: {} },
|
||||
]);
|
||||
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: taskInfo.filename,
|
||||
encoding: '7bit',
|
||||
mimetype: 'application/octet-stream',
|
||||
filename: taskInfo.filename,
|
||||
size: buffer.length,
|
||||
buffer: buffer,
|
||||
stream: null,
|
||||
destination: null,
|
||||
path: '',
|
||||
};
|
||||
const { url, key } = await this.fileService.upload({
|
||||
configKey: 'SERVER_LOCAL_CONFIG',
|
||||
file,
|
||||
pathPrefix: 'exportfile',
|
||||
keepOriginFilename: true,
|
||||
});
|
||||
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.FINISHED,
|
||||
date: Date.now(),
|
||||
};
|
||||
|
||||
// 更新计算结果
|
||||
const updateFinishRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
url,
|
||||
fileKey: key,
|
||||
fileSize: buffer.length,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.info(JSON.stringify(updateFinishRes));
|
||||
} catch (error) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.ERROR,
|
||||
date: Date.now(),
|
||||
};
|
||||
await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.error(
|
||||
`导出文件失败 taskId: ${taskInfo._id.toString()}, surveyId: ${taskInfo.surveyId}, message: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
80
server/src/modules/survey/services/session.service.ts
Normal file
80
server/src/modules/survey/services/session.service.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Session } from 'src/models/session.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
constructor(
|
||||
@InjectRepository(Session)
|
||||
private readonly sessionRepository: MongoRepository<Session>,
|
||||
) {}
|
||||
|
||||
create({ surveyId, userId }) {
|
||||
const session = this.sessionRepository.create({
|
||||
surveyId,
|
||||
userId,
|
||||
});
|
||||
return this.sessionRepository.save(session);
|
||||
}
|
||||
|
||||
findOne(sessionId) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findLatestEditingOne({ surveyId }) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.NEW,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSessionToEditing({ sessionId, surveyId }) {
|
||||
const now = Date.now();
|
||||
const editingStatus = {
|
||||
status: RECORD_STATUS.EDITING,
|
||||
date: now,
|
||||
};
|
||||
const newStatus = {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: now,
|
||||
};
|
||||
return Promise.all([
|
||||
this.sessionRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: editingStatus,
|
||||
updateDate: now,
|
||||
},
|
||||
},
|
||||
),
|
||||
this.sessionRepository.updateMany(
|
||||
{
|
||||
surveyId,
|
||||
_id: {
|
||||
$ne: new ObjectId(sessionId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: newStatus,
|
||||
updateDate: now,
|
||||
},
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { FileModule } from '../file/file.module';
|
||||
|
||||
import { DataStatisticController } from './controllers/dataStatistic.controller';
|
||||
import { SurveyController } from './controllers/survey.controller';
|
||||
@ -14,6 +15,8 @@ import { SurveyHistoryController } from './controllers/surveyHistory.controller'
|
||||
import { SurveyMetaController } from './controllers/surveyMeta.controller';
|
||||
import { SurveyUIController } from './controllers/surveyUI.controller';
|
||||
import { CollaboratorController } from './controllers/collaborator.controller';
|
||||
import { DownloadTaskController } from './controllers/downloadTask.controller';
|
||||
import { SessionController } from './controllers/session.controller';
|
||||
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { SurveyHistory } from 'src/models/surveyHistory.entity';
|
||||
@ -21,14 +24,21 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Word } from 'src/models/word.entity';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { DataStatisticService } from './services/dataStatistic.service';
|
||||
import { SurveyConfService } from './services/surveyConf.service';
|
||||
import { SurveyHistoryService } from './services/surveyHistory.service';
|
||||
import { SurveyMetaService } from './services/surveyMeta.service';
|
||||
import { ContentSecurityService } from './services/contentSecurity.service';
|
||||
import { CollaboratorService } from './services/collaborator.service';
|
||||
import { Counter } from 'src/models/counter.entity';
|
||||
import { CounterService } from '../surveyResponse/services/counter.service';
|
||||
import { FileService } from '../file/services/file.service';
|
||||
import { DownloadTaskService } from './services/downloadTask.service';
|
||||
import { SessionService } from './services/session.service';
|
||||
import { Session } from 'src/models/session.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -39,11 +49,15 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyResponse,
|
||||
Word,
|
||||
Collaborator,
|
||||
Counter,
|
||||
DownloadTask,
|
||||
Session,
|
||||
]),
|
||||
ConfigModule,
|
||||
SurveyResponseModule,
|
||||
AuthModule,
|
||||
WorkspaceModule,
|
||||
FileModule,
|
||||
],
|
||||
controllers: [
|
||||
DataStatisticController,
|
||||
@ -52,6 +66,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyMetaController,
|
||||
SurveyUIController,
|
||||
CollaboratorController,
|
||||
DownloadTaskController,
|
||||
SessionController,
|
||||
],
|
||||
providers: [
|
||||
DataStatisticService,
|
||||
@ -62,6 +78,10 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
ContentSecurityService,
|
||||
CollaboratorService,
|
||||
LoggerProvider,
|
||||
CounterService,
|
||||
DownloadTaskService,
|
||||
FileService,
|
||||
SessionService,
|
||||
],
|
||||
})
|
||||
export class SurveyModule {}
|
||||
|
@ -48,7 +48,8 @@
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115019"
|
||||
"hash": "115019",
|
||||
"quota": "0"
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
@ -57,9 +58,11 @@
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115020"
|
||||
"hash": "115020",
|
||||
"quota": "0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"quotaNoDisplay": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -41,8 +41,8 @@
|
||||
"innerType": "radio",
|
||||
"field": "data606",
|
||||
"title": "标题2",
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"minNum": 0,
|
||||
"maxNum": 0,
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
|
@ -20,7 +20,7 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
|
||||
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
@ -122,7 +122,7 @@ describe('SurveyResponseController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
@ -220,7 +220,8 @@ describe('SurveyResponseController', () => {
|
||||
jest
|
||||
.spyOn(clientEncryptService, 'deleteEncryptInfo')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const result = await controller.createResponse(reqBody, {});
|
||||
|
||||
const result = await controller.createResponse(reqBody);
|
||||
|
||||
expect(result).toEqual({ code: 200, msg: '提交成功' });
|
||||
expect(
|
||||
@ -267,7 +268,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
SurveyNotFoundException,
|
||||
);
|
||||
});
|
||||
@ -276,7 +277,7 @@ describe('SurveyResponseController', () => {
|
||||
const reqBody = cloneDeep(mockSubmitData);
|
||||
delete reqBody.sign;
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
@ -289,7 +290,7 @@ describe('SurveyResponseController', () => {
|
||||
const reqBody = cloneDeep(mockDecryptErrorBody);
|
||||
reqBody.sign = 'mock sign';
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
@ -305,7 +306,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
@ -317,7 +318,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
@ -343,7 +344,7 @@ describe('SurveyResponseController', () => {
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
@ -24,7 +24,7 @@ import { WorkspaceMemberService } from 'src/modules/workspace/services/workspace
|
||||
export class ResponseSchemaController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Body, HttpCode, Request } from '@nestjs/common';
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { checkSign } from 'src/utils/checkSign';
|
||||
@ -7,37 +7,48 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { getPushingData } from 'src/utils/messagePushing';
|
||||
|
||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { SurveyResponseService } from '../services/surveyResponse.service';
|
||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service';
|
||||
import { RedisService } from 'src/modules/redis/redis.service';
|
||||
|
||||
import moment from 'moment';
|
||||
import * as Joi from 'joi';
|
||||
import * as forge from 'node-forge';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
|
||||
const optionQuestionType: Array<string> = [
|
||||
QUESTION_TYPE.RADIO,
|
||||
QUESTION_TYPE.CHECKBOX,
|
||||
QUESTION_TYPE.BINARY_CHOICE,
|
||||
QUESTION_TYPE.VOTE,
|
||||
];
|
||||
|
||||
@ApiTags('surveyResponse')
|
||||
@Controller('/api/surveyResponse')
|
||||
export class SurveyResponseController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly surveyResponseService: SurveyResponseService,
|
||||
private readonly clientEncryptService: ClientEncryptService,
|
||||
private readonly messagePushingTaskService: MessagePushingTaskService,
|
||||
private readonly logger: Logger,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Post('/createResponse')
|
||||
@HttpCode(200)
|
||||
async createResponse(@Body() reqBody, @Request() req) {
|
||||
async createResponse(@Body() reqBody) {
|
||||
// 检查签名
|
||||
checkSign(reqBody);
|
||||
// 校验参数
|
||||
@ -53,9 +64,7 @@ export class SurveyResponseController {
|
||||
}).validate(reqBody, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -205,6 +214,7 @@ export class SurveyResponseController {
|
||||
const optionTextAndId = dataList
|
||||
.filter((questionItem) => {
|
||||
return (
|
||||
optionQuestionType.includes(questionItem.type) &&
|
||||
Array.isArray(questionItem.options) &&
|
||||
questionItem.options.length > 0 &&
|
||||
decryptedData[questionItem.field]
|
||||
@ -214,38 +224,77 @@ export class SurveyResponseController {
|
||||
const arr = cur.options.map((optionItem) => ({
|
||||
hash: optionItem.hash,
|
||||
text: optionItem.text,
|
||||
quota: optionItem.quota,
|
||||
}));
|
||||
pre[cur.field] = arr;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
// 对用户提交的数据进行遍历处理
|
||||
for (const field in decryptedData) {
|
||||
const val = decryptedData[field];
|
||||
const vals = Array.isArray(val) ? val : [val];
|
||||
if (field in optionTextAndId) {
|
||||
// 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能
|
||||
const optionCountData: Record<string, any> =
|
||||
(await this.counterService.get({
|
||||
surveyPath,
|
||||
key: field,
|
||||
type: 'option',
|
||||
})) || { total: 0 };
|
||||
optionCountData.total++;
|
||||
for (const val of vals) {
|
||||
if (!optionCountData[val]) {
|
||||
optionCountData[val] = 1;
|
||||
} else {
|
||||
// 使用redis作为锁,校验选项配额
|
||||
const surveyId = responseSchema.pageId;
|
||||
const lockKey = `locks:optionSelectedCount:${surveyId}`;
|
||||
const lock = await this.redisService.lockResource(lockKey, 1000);
|
||||
this.logger.info(`lockKey: ${lockKey}`);
|
||||
try {
|
||||
const successParams = [];
|
||||
for (const field in decryptedData) {
|
||||
const value = decryptedData[field];
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
if (field in optionTextAndId) {
|
||||
const optionCountData =
|
||||
(await this.counterService.get({
|
||||
key: field,
|
||||
surveyPath,
|
||||
type: 'option',
|
||||
})) || {};
|
||||
|
||||
//遍历选项hash值
|
||||
for (const val of values) {
|
||||
const option = optionTextAndId[field].find(
|
||||
(opt) => opt['hash'] === val,
|
||||
);
|
||||
const quota = parseInt(option['quota']);
|
||||
if (
|
||||
quota &&
|
||||
optionCountData?.[val] &&
|
||||
quota <= optionCountData[val]
|
||||
) {
|
||||
return {
|
||||
code: EXCEPTION_CODE.RESPONSE_OVER_LIMIT,
|
||||
data: {
|
||||
field,
|
||||
optionHash: option.hash,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!optionCountData[val]) {
|
||||
optionCountData[val] = 0;
|
||||
}
|
||||
optionCountData[val]++;
|
||||
}
|
||||
if (!optionCountData['total']) {
|
||||
optionCountData['total'] = 1;
|
||||
} else {
|
||||
optionCountData['total']++;
|
||||
}
|
||||
successParams.push({
|
||||
key: field,
|
||||
surveyPath,
|
||||
type: 'option',
|
||||
data: optionCountData,
|
||||
});
|
||||
}
|
||||
this.counterService.set({
|
||||
surveyPath,
|
||||
key: field,
|
||||
data: optionCountData,
|
||||
type: 'option',
|
||||
});
|
||||
}
|
||||
// 校验通过后统一更新
|
||||
await Promise.all(
|
||||
successParams.map((item) => this.counterService.set(item)),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
await this.redisService.unlockResource(lock);
|
||||
this.logger.info(`unlockResource: ${lockKey}`);
|
||||
}
|
||||
|
||||
// 入库
|
||||
@ -259,7 +308,6 @@ export class SurveyResponseController {
|
||||
optionTextAndId,
|
||||
});
|
||||
|
||||
const surveyId = responseSchema.pageId;
|
||||
const sendData = getPushingData({
|
||||
surveyResponse,
|
||||
questionList: responseSchema?.code?.dataConf?.dataList || [],
|
||||
|
@ -1,19 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { MessageModule } from '../message/message.module';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
import { ResponseSchemaService } from './services/responseScheme.service';
|
||||
import { SurveyResponseService } from './services/surveyResponse.service';
|
||||
import { CounterService } from './services/counter.service';
|
||||
import { ClientEncryptService } from './services/clientEncrypt.service';
|
||||
import { RedisService } from '../redis/redis.service';
|
||||
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { Counter } from 'src/models/counter.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
|
||||
import { ClientEncryptController } from './controllers/clientEncrpt.controller';
|
||||
import { CounterController } from './controllers/counter.controller';
|
||||
@ -23,6 +22,9 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
@ -33,6 +35,7 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
]),
|
||||
ConfigModule,
|
||||
MessageModule,
|
||||
RedisModule,
|
||||
AuthModule,
|
||||
WorkspaceModule,
|
||||
],
|
||||
@ -48,7 +51,8 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
SurveyResponseService,
|
||||
CounterService,
|
||||
ClientEncryptService,
|
||||
Logger,
|
||||
LoggerProvider,
|
||||
RedisService,
|
||||
],
|
||||
exports: [
|
||||
ResponseSchemaService,
|
||||
|
@ -10,7 +10,7 @@ import { Workspace } from 'src/models/workspace.entity';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { User } from 'src/models/user.entity';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
@ -65,7 +65,7 @@ describe('WorkspaceController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
import { splitMembers } from '../utils/splitMember';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { Workspace } from 'src/models/workspace.entity';
|
||||
@ -46,7 +46,7 @@ export class WorkspaceController {
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
private readonly userService: UserService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('getRoleList')
|
||||
@ -64,10 +64,7 @@ export class WorkspaceController {
|
||||
async create(@Body() workspace: CreateWorkspaceDto, @Request() req) {
|
||||
const { value, error } = CreateWorkspaceDto.validate(workspace);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`CreateWorkspaceDto validate failed: ${error.message}`,
|
||||
{ req },
|
||||
);
|
||||
this.logger.error(`CreateWorkspaceDto validate failed: ${error.message}`);
|
||||
throw new HttpException(
|
||||
`参数错误: 请联系管理员`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -137,7 +134,6 @@ export class WorkspaceController {
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`GetWorkspaceListDto validate failed: ${error.message}`,
|
||||
{ req },
|
||||
);
|
||||
throw new HttpException(
|
||||
`参数错误: 请联系管理员`,
|
||||
|
53
server/src/utils/xss.ts
Normal file
53
server/src/utils/xss.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import xss from 'xss';
|
||||
|
||||
const myxss = new (xss as any).FilterXSS({
|
||||
onIgnoreTagAttr(tag, name, value) {
|
||||
if (name === 'style' || name === 'class') {
|
||||
return `${name}="${value}"`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
onIgnoreTag(tag, html) {
|
||||
// <xxx>过滤为空,否则不过滤为空
|
||||
const re1 = new RegExp('<.+?>', 'g');
|
||||
if (re1.test(html)) {
|
||||
return '';
|
||||
} else {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const cleanRichTextWithMediaTag = (text) => {
|
||||
if (!text) {
|
||||
return text === 0 ? 0 : '';
|
||||
}
|
||||
const html = transformHtmlTag(text)
|
||||
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
|
||||
.replace(/<video.*\/video>/g, '[视频]');
|
||||
const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '');
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export function escapeHtml(html) {
|
||||
return html.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
export const transformHtmlTag = (html) => {
|
||||
if (!html) return '';
|
||||
if (typeof html !== 'string') return html + '';
|
||||
return html
|
||||
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\\\n/g, '\\n');
|
||||
//.replace(/ /g, "")
|
||||
};
|
||||
|
||||
const filterXSSClone = myxss.process.bind(myxss);
|
||||
|
||||
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html));
|
||||
|
||||
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html));
|
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@ -8,6 +8,8 @@ node_modules
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
components.d.ts
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
@ -11,7 +11,7 @@
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logicflow/core": "2.0.0",
|
||||
@ -30,6 +30,7 @@
|
||||
"node-forge": "^1.3.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"vuedraggable": "^4.1.0",
|
||||
@ -43,6 +44,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
|
@ -2,10 +2,71 @@
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { get as _get } from 'lodash-es'
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage, type Action } from 'element-plus'
|
||||
|
||||
// 这里不需要自动跳转登录页面,所以单独引入axios
|
||||
import axios from 'axios'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
let timer: any
|
||||
|
||||
const showConfirmBox = () => {
|
||||
ElMessageBox.alert('登录状态已失效,请重新登陆。', '提示', {
|
||||
confirmButtonText: '确认',
|
||||
showClose: false,
|
||||
callback: (action: Action) => {
|
||||
if (action === 'confirm') {
|
||||
userStore.logout();
|
||||
router.replace({ name: 'login' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const token = _get(userStore, 'userInfo.token')
|
||||
|
||||
const res = await axios({
|
||||
url: '/api/user/getUserInfo',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (res.data.code !== 200) {
|
||||
showConfirmBox();
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
checkAuth()
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
const e = error as any
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
watch(() => userStore.hasLogined, (hasLogined) => {
|
||||
if (hasLogined) {
|
||||
timer = setTimeout(() => {
|
||||
checkAuth()
|
||||
}, 30 * 60 * 1000);
|
||||
} else {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -16,3 +16,4 @@ export const getStatisticList = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,9 @@ export const login = (data) => {
|
||||
return axios.post('/auth/login', data)
|
||||
}
|
||||
|
||||
export const getUserInfo = () => {
|
||||
return axios.get('/user/getUserInfo')
|
||||
}
|
||||
/** 获取密码强度 */
|
||||
export const getPasswordStrength = (password) => {
|
||||
return axios.get('/auth/register/password/strength', {
|
||||
|
29
web/src/management/api/downloadTask.js
Normal file
29
web/src/management/api/downloadTask.js
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from './base'
|
||||
|
||||
export const createDownloadSurveyResponseTask = ({ surveyId, isDesensitive }) => {
|
||||
return axios.post('/downloadTask/createTask', {
|
||||
surveyId,
|
||||
isDesensitive
|
||||
})
|
||||
}
|
||||
|
||||
export const getDownloadTask = taskId => {
|
||||
return axios.get('/downloadTask/getDownloadTask', { params: { taskId } })
|
||||
}
|
||||
|
||||
|
||||
export const getDownloadTaskList = ({ pageIndex, pageSize }) => {
|
||||
return axios.get('/downloadTask/getDownloadTaskList', {
|
||||
params: {
|
||||
pageIndex,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//问卷删除
|
||||
export const deleteDownloadTask = (taskId) => {
|
||||
return axios.post('/downloadTask/deleteDownloadTask', {
|
||||
taskId,
|
||||
})
|
||||
}
|
@ -20,8 +20,8 @@ export const getSurveyById = (id) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const saveSurvey = ({ surveyId, configData }) => {
|
||||
return axios.post('/survey/updateConf', { surveyId, configData })
|
||||
export const saveSurvey = ({ surveyId, configData, sessionId }) => {
|
||||
return axios.post('/survey/updateConf', { surveyId, configData, sessionId })
|
||||
}
|
||||
|
||||
export const publishSurvey = ({ surveyId }) => {
|
||||
@ -52,3 +52,11 @@ export const deleteSurvey = (surveyId) => {
|
||||
export const updateSurvey = (data) => {
|
||||
return axios.post('/survey/updateMeta', data)
|
||||
}
|
||||
|
||||
export const getSessionId = ({ surveyId }) => {
|
||||
return axios.post('/session/create', { surveyId })
|
||||
}
|
||||
|
||||
export const seizeSession = ({ sessionId }) => {
|
||||
return axios.post('/session/seize', { sessionId })
|
||||
}
|
@ -102,7 +102,7 @@ export const statusMaps = {
|
||||
new: '未发布',
|
||||
editing: '修改中',
|
||||
published: '已发布',
|
||||
removed: '',
|
||||
removed: '已删除',
|
||||
pausing: ''
|
||||
}
|
||||
|
||||
|
@ -41,13 +41,15 @@ export const defaultQuestionConfig = {
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: ''
|
||||
placeholderDesc: '',
|
||||
quota: '0'
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: ''
|
||||
placeholderDesc: '',
|
||||
quota: '0'
|
||||
}
|
||||
],
|
||||
star: 5,
|
||||
@ -74,5 +76,6 @@ export const defaultQuestionConfig = {
|
||||
placeholder: '500',
|
||||
value: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
quotaNoDisplay: false
|
||||
}
|
||||
|
@ -60,6 +60,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ImagePreview from './ImagePreview.vue'
|
||||
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||
|
||||
const props = defineProps({
|
||||
tableData: {
|
||||
@ -78,8 +79,16 @@ const popoverVirtualRef = ref()
|
||||
const popoverContent = ref('')
|
||||
|
||||
const getContent = (content) => {
|
||||
// const content = cleanRichText(value)
|
||||
return content === 0 ? 0 : content || '未知'
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(item => getContent(item)).join(',');
|
||||
}
|
||||
if (content === null || content === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof content !== 'string') {
|
||||
content = content + ''
|
||||
}
|
||||
return cleanRichTextWithMediaTag(content) || '未知'
|
||||
}
|
||||
const setPopoverContent = (content) => {
|
||||
popoverContent.value = content
|
||||
|
@ -2,7 +2,9 @@
|
||||
<div class="data-table-page">
|
||||
<template v-if="tableData.total">
|
||||
<div class="menus">
|
||||
<el-button type="primary" :loading="isDownloading" @click="onDownload">导出全部数据</el-button>
|
||||
<el-switch
|
||||
class="desensitive-switch"
|
||||
:model-value="isShowOriginData"
|
||||
active-text="是否展示原数据"
|
||||
@input="onIsShowOriginChange"
|
||||
@ -25,11 +27,42 @@
|
||||
<div v-else>
|
||||
<EmptyIndex :data="noDataConfig" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="downloadDialogVisible"
|
||||
title="导出确认"
|
||||
width="500"
|
||||
style="padding: 40px;"
|
||||
>
|
||||
<el-form :model="downloadForm" label-width="100px" label-position="left" >
|
||||
<el-form-item label="导出内容">
|
||||
<el-radio-group v-model="downloadForm.isDesensitive">
|
||||
<el-radio :value="true">脱敏数据</el-radio>
|
||||
<el-radio :value="false">原回收数据</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="download-tips">
|
||||
<div>注:</div>
|
||||
<div>
|
||||
<p>推荐优先下载脱敏数据,如手机号:1***3。</p>
|
||||
<p>原回收数据可能存在敏感信息,请谨慎下载。</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="downloadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmDownload()">
|
||||
确认
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import { reactive, toRefs, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
@ -37,6 +70,7 @@ import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
||||
import { getRecycleList } from '@/management/api/analysis'
|
||||
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||
import DataTable from '../components/DataTable.vue'
|
||||
import { createDownloadSurveyResponseTask, getDownloadTask } from '@/management/api/downloadTask'
|
||||
|
||||
const dataTableState = reactive({
|
||||
mainTableLoading: false,
|
||||
@ -47,10 +81,16 @@ const dataTableState = reactive({
|
||||
},
|
||||
currentPage: 1,
|
||||
isShowOriginData: false,
|
||||
tmpIsShowOriginData: false
|
||||
tmpIsShowOriginData: false,
|
||||
isDownloading: false,
|
||||
downloadDialogVisible: false,
|
||||
downloadForm: {
|
||||
isDesensitive: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { mainTableLoading, tableData, isShowOriginData } = toRefs(dataTableState)
|
||||
const { mainTableLoading, tableData, isShowOriginData, downloadDialogVisible, isDownloading } = toRefs(dataTableState)
|
||||
const downloadForm = dataTableState.downloadForm
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -115,8 +155,67 @@ const init = async () => {
|
||||
ElMessage.error('查询回收数据失败,请重试')
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
const onDownload = async () => {
|
||||
dataTableState.downloadDialogVisible = true
|
||||
}
|
||||
|
||||
const confirmDownload = async () => {
|
||||
if (isDownloading.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
isDownloading.value = true
|
||||
const createRes = await createDownloadSurveyResponseTask({ surveyId: route.params.id, isDesensitive: downloadForm.isDesensitive })
|
||||
dataTableState.downloadDialogVisible = false
|
||||
if (createRes.code === 200) {
|
||||
ElMessage.success(`下载文件计算中,可前往“下载中心”查看`)
|
||||
try {
|
||||
const taskInfo = await checkIsTaskFinished(createRes.data.taskId)
|
||||
if (taskInfo.url) {
|
||||
window.open(taskInfo.url)
|
||||
ElMessage.success("导出成功")
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导出失败,请重试')
|
||||
}
|
||||
|
||||
} else {
|
||||
ElMessage.error('导出失败,请重试')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导出失败,请重试')
|
||||
} finally {
|
||||
isDownloading.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const checkIsTaskFinished = (taskId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const run = () => {
|
||||
getDownloadTask(taskId).then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const status = res.data.curStatus.status
|
||||
if (status === 'new' || status === 'computing') {
|
||||
setTimeout(() => {
|
||||
run()
|
||||
}, 5000)
|
||||
} else {
|
||||
resolve(res.data)
|
||||
}
|
||||
} else {
|
||||
reject("导出失败");
|
||||
}
|
||||
})
|
||||
}
|
||||
run()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -126,6 +225,11 @@ init()
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.download-tips {
|
||||
display: flex;
|
||||
color: #ec4e29;
|
||||
}
|
||||
|
||||
.menus {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@ -139,4 +243,8 @@ init()
|
||||
.data-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.desensitive-switch {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
103
web/src/management/pages/downloadTask/TaskList.vue
Normal file
103
web/src/management/pages/downloadTask/TaskList.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<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">
|
||||
<DownloadTaskList></DownloadTaskList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DownloadTaskList from './components/DownloadTaskList.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const userInfo = computed(() => {
|
||||
return userStore.userInfo
|
||||
})
|
||||
|
||||
const handleSurvey = () => {
|
||||
router.push('/survey')
|
||||
}
|
||||
const handleLogout = () => {
|
||||
userStore.logout()
|
||||
router.replace({ name: 'login' })
|
||||
}
|
||||
|
||||
const activeIndex = ref('2')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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 {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%; /* 确保容器宽度为100% */
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div v-loading="loading" 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="[field.key]"
|
||||
:formatter="field.formatter"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template v-slot="{ row }">
|
||||
<span v-if="row.curStatus?.status === 'finished'" class="text-btn download-btn" @click="handleDownload(row)"> 下载 </span>
|
||||
<span class="text-btn delete-btn" @click="openDeleteDialog(row)"> 删除 </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="list-pagination" v-if="total">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="total"
|
||||
small
|
||||
:page-size="pageSize"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { get, map } from 'lodash-es'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { deleteDownloadTask, getDownloadTaskList } from '@/management/api/downloadTask'
|
||||
import { CODE_MAP } from '@/management/api/base'
|
||||
|
||||
import moment from 'moment'
|
||||
// 引入中文
|
||||
import 'moment/locale/zh-cn'
|
||||
// 设置中文
|
||||
moment.locale('zh-cn')
|
||||
|
||||
const loading = ref(false)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const dataList: Array<any> = reactive([])
|
||||
|
||||
onMounted(() => {
|
||||
getList({ pageIndex: 1 })
|
||||
})
|
||||
const getList = async ({ pageIndex }: { pageIndex: number }) => {
|
||||
if (!pageIndex) {
|
||||
pageIndex = 1
|
||||
}
|
||||
const params = {
|
||||
pageSize: pageSize.value,
|
||||
pageIndex,
|
||||
}
|
||||
|
||||
const res: Record<string, any> = await getDownloadTaskList(params)
|
||||
if (res.code === CODE_MAP.SUCCESS) {
|
||||
total.value = res.data.total
|
||||
const list = res.data.list as any
|
||||
dataList.splice(0, dataList.length, ...list);
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const statusTextMap: Record<string, string> = {
|
||||
new: '排队中',
|
||||
computing: '计算中',
|
||||
finished: '已完成',
|
||||
removed: '已删除',
|
||||
};
|
||||
|
||||
let currentDelRow: Record<string, any> = {}
|
||||
// 下载文件
|
||||
const handleDownload = async (row: any) => {
|
||||
if (row.curStatus.status === 'removed') {
|
||||
ElMessage.error('文件已删除')
|
||||
return
|
||||
}
|
||||
if (row.url) {
|
||||
window.open(row.url)
|
||||
}
|
||||
}
|
||||
// 删除文件
|
||||
const openDeleteDialog = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('是否确认删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
currentDelRow = row
|
||||
confirmDelete()
|
||||
} catch (error) {
|
||||
console.log('取消删除')
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除文件
|
||||
const confirmDelete = async () => {
|
||||
try {
|
||||
const res: Record<string, any> = await deleteDownloadTask(currentDelRow.taskId)
|
||||
if (res.code !== CODE_MAP.SUCCESS) {
|
||||
ElMessage.error(res.errmsg)
|
||||
} else {
|
||||
ElMessage.success('删除成功');
|
||||
await getList({ pageIndex: 1 })
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败,请刷新重试")
|
||||
}
|
||||
}
|
||||
|
||||
const fields = ['filename', 'fileSize', 'createDate', 'curStatus']
|
||||
|
||||
const fieldList = computed(() => {
|
||||
return map(fields, (f) => {
|
||||
return get(downloadListConfig, f)
|
||||
})
|
||||
})
|
||||
|
||||
const downloadListConfig = {
|
||||
filename: {
|
||||
title: '文件名称',
|
||||
key: 'filename',
|
||||
width: 340,
|
||||
tip: true
|
||||
},
|
||||
fileSize: {
|
||||
title: '预估大小',
|
||||
key: 'fileSize',
|
||||
width: 140
|
||||
},
|
||||
createDate: {
|
||||
title: '下载时间',
|
||||
key: 'createDate',
|
||||
width: 240
|
||||
},
|
||||
curStatus: {
|
||||
title: '状态',
|
||||
key: 'curStatus.status',
|
||||
formatter(row: Record<string, any>, column: Record<string, any>) {
|
||||
console.log({
|
||||
row,
|
||||
column,
|
||||
})
|
||||
return statusTextMap[get(row, column.rawColumnKey)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
getList({ pageIndex: val })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.question-list-root {
|
||||
height: 100%;
|
||||
background-color: #f6f7f9;
|
||||
|
||||
.list-wrapper {
|
||||
width: 90%;
|
||||
min-width: 1080px;
|
||||
padding: 10px 20px;
|
||||
background: #fff;
|
||||
margin: 0 auto;
|
||||
|
||||
.list-table {
|
||||
.cell {
|
||||
text-align: center;
|
||||
}
|
||||
.text-btn {
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.download-btn {
|
||||
color: $primary-color;
|
||||
}
|
||||
.delete-btn {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.small-text {
|
||||
color: red;
|
||||
}
|
||||
.list-pagination {
|
||||
margin-top: 20px;
|
||||
:deep(.el-pagination) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -68,15 +68,17 @@ const updateLogicConf = () => {
|
||||
}
|
||||
|
||||
const showLogicConf = showLogicEngine.value.toJson()
|
||||
|
||||
// 更新逻辑配置
|
||||
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||
|
||||
if(JSON.stringify(schema.logicConf.showLogicConf) !== JSON.stringify(showLogicConf)) {
|
||||
// 更新逻辑配置
|
||||
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
||||
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||
if(JSON.stringify(schema.logicConf.jumpLogicConf) !== JSON.stringify(jumpLogicConf)){
|
||||
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
@ -24,13 +24,16 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
import CommonTemplate from './components/CommonTemplate.vue'
|
||||
import Navbar from './components/ModuleNavbar.vue'
|
||||
|
||||
|
||||
const editStore = useEditStore()
|
||||
const { init, setSurveyId } = editStore
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
setSurveyId(route.params.id as string)
|
||||
const surveyId = route.params.id as string
|
||||
setSurveyId(surveyId)
|
||||
|
||||
try {
|
||||
await init()
|
||||
|
@ -4,14 +4,15 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
||||
import { publishSurvey, saveSurvey, seizeSession } from '@/management/api/survey'
|
||||
import buildData from './buildData'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { CODE_MAP } from '@/management/api/base'
|
||||
|
||||
interface Props {
|
||||
updateLogicConf: any
|
||||
@ -22,7 +23,21 @@ const props = defineProps<Props>()
|
||||
|
||||
const isPublishing = ref<boolean>(false)
|
||||
const editStore = useEditStore()
|
||||
const { schema, getSchemaFromRemote } = editStore
|
||||
const { getSchemaFromRemote } = editStore
|
||||
const { schema, sessionId } = storeToRefs(editStore)
|
||||
const saveData = computed(() => {
|
||||
return buildData(schema.value, sessionId.value)
|
||||
})
|
||||
|
||||
const seize = async () => {
|
||||
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
|
||||
if (seizeRes.code === 200) {
|
||||
location.reload();
|
||||
} else {
|
||||
ElMessage.error('获取权限失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const validate = () => {
|
||||
@ -45,6 +60,46 @@ const validate = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onSave = async () => {
|
||||
|
||||
if (!saveData.value.sessionId) {
|
||||
ElMessage.error('未获取到sessionId')
|
||||
return null
|
||||
}
|
||||
|
||||
if (!saveData.value.surveyId) {
|
||||
ElMessage.error('未获取到问卷id')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await saveSurvey(saveData.value)
|
||||
if(!res) {
|
||||
return null
|
||||
}
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('保存成功')
|
||||
return res
|
||||
} else if (res.code === 3006) {
|
||||
ElMessageBox.alert(res.errmsg, '提示', {
|
||||
confirmButtonText: '刷新同步',
|
||||
callback: (action: string) => {
|
||||
if (action === 'confirm') {
|
||||
seize();
|
||||
}
|
||||
}
|
||||
});
|
||||
return null
|
||||
} else {
|
||||
ElMessage.error(res.errmsg)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存问卷失败')
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
const handlePublish = async () => {
|
||||
if (isPublishing.value) {
|
||||
return
|
||||
@ -60,22 +115,12 @@ const handlePublish = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const saveData = buildData(schema)
|
||||
if (!saveData.surveyId) {
|
||||
isPublishing.value = false
|
||||
ElMessage.error('未获取到问卷id')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const saveRes: any = await saveSurvey(saveData)
|
||||
if (saveRes.code !== 200) {
|
||||
isPublishing.value = false
|
||||
ElMessage.error(saveRes.errmsg || '问卷保存失败')
|
||||
const saveRes: any = await onSave()
|
||||
if (!saveRes || saveRes.code !== CODE_MAP.SUCCESS) {
|
||||
return
|
||||
}
|
||||
|
||||
const publishRes: any = await publishSurvey({ surveyId: saveData.surveyId })
|
||||
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId })
|
||||
if (publishRes.code === 200) {
|
||||
ElMessage.success('发布成功')
|
||||
getSchemaFromRemote()
|
||||
|
@ -18,10 +18,10 @@ import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import { saveSurvey } from '@/management/api/survey'
|
||||
import { saveSurvey, seizeSession } from '@/management/api/survey'
|
||||
import buildData from './buildData'
|
||||
|
||||
interface Props {
|
||||
@ -44,8 +44,8 @@ const saveText = computed(
|
||||
)
|
||||
|
||||
const editStore = useEditStore()
|
||||
const { schemaUpdateTime } = storeToRefs(editStore)
|
||||
const { schema } = editStore
|
||||
const { schemaUpdateTime, schema, sessionId } = storeToRefs(editStore)
|
||||
|
||||
|
||||
const validate = () => {
|
||||
let checked = true
|
||||
@ -68,18 +68,32 @@ const validate = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveData = async () => {
|
||||
const saveData = buildData(schema)
|
||||
const onSave = async () => {
|
||||
const saveData = buildData(schema.value, sessionId.value);
|
||||
if (!saveData.sessionId) {
|
||||
ElMessage.error('sessionId有误')
|
||||
return null
|
||||
}
|
||||
|
||||
if (!saveData.surveyId) {
|
||||
ElMessage.error('未获取到问卷id')
|
||||
return null
|
||||
}
|
||||
|
||||
const res = await saveSurvey(saveData)
|
||||
const res: Record<string, any> = await saveSurvey(saveData)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const seize = async () => {
|
||||
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
|
||||
if (seizeRes.code === 200) {
|
||||
location.reload();
|
||||
} else {
|
||||
ElMessage.error('获取权限失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
const timerHandle = ref<NodeJS.Timeout | number | null>(null)
|
||||
const triggerAutoSave = () => {
|
||||
if (autoSaveStatus.value === 'saving') {
|
||||
@ -95,7 +109,7 @@ const triggerAutoSave = () => {
|
||||
isShowAutoSave.value = true
|
||||
nextTick(async () => {
|
||||
try {
|
||||
const res: any = await saveData()
|
||||
const res: any = await handleSave()
|
||||
if (res.code === 200) {
|
||||
autoSaveStatus.value = 'succeed'
|
||||
} else {
|
||||
@ -120,21 +134,34 @@ const handleSave = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
isShowAutoSave.value = false
|
||||
|
||||
// 保存检测
|
||||
const { checked, msg } = validate()
|
||||
if (!checked) {
|
||||
isSaving.value = false
|
||||
ElMessage.error(msg)
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
const res: any = await saveData()
|
||||
const res: any = await onSave()
|
||||
if(!res) {
|
||||
return
|
||||
}
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('保存成功')
|
||||
return res
|
||||
} else if (res.code === 3006) {
|
||||
ElMessageBox.alert(res.errmsg, '提示', {
|
||||
confirmButtonText: '刷新同步',
|
||||
callback: (action: string) => {
|
||||
if (action === 'confirm') {
|
||||
seize();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessage.error(res.errmsg)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { pick as _pick, get as _get } from 'lodash-es'
|
||||
|
||||
// 生成需要保存到接口的数据
|
||||
export default function (schema) {
|
||||
export default function (schema, sessionId) {
|
||||
const surveyId = _get(schema, 'metaData._id')
|
||||
const configData = _pick(schema, [
|
||||
'bannerConf',
|
||||
@ -19,6 +19,7 @@ export default function (schema) {
|
||||
delete configData.questionDataList
|
||||
return {
|
||||
surveyId,
|
||||
configData
|
||||
configData,
|
||||
sessionId
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ const routes = [
|
||||
background-color: $primary-color;
|
||||
bottom: -16px;
|
||||
left: 20px;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +80,7 @@ import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||
|
||||
export default {
|
||||
name: 'OptionConfig',
|
||||
@ -110,7 +111,7 @@ export default {
|
||||
return mapData
|
||||
},
|
||||
textOptions() {
|
||||
return this.curOptions.map((item) => item.text)
|
||||
return this.curOptions.map((item) => cleanRichTextWithMediaTag(item.text))
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -7,7 +7,7 @@ export default [
|
||||
{
|
||||
title: '提交限制',
|
||||
key: 'limitConfig',
|
||||
formList: ['limit_tLimit']
|
||||
formList: ['limit_tLimit', 'limit_breakAnswer', 'limit_backAnswer']
|
||||
},
|
||||
{
|
||||
title: '作答限制',
|
||||
|
@ -22,6 +22,22 @@ export default {
|
||||
type: 'QuestionTimeHour',
|
||||
placement: 'top'
|
||||
},
|
||||
limit_breakAnswer: {
|
||||
key: 'breakAnswer',
|
||||
label: '允许断点续答',
|
||||
tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
|
||||
placement: 'top',
|
||||
type: 'CustomedSwitch',
|
||||
value: false,
|
||||
},
|
||||
limit_backAnswer: {
|
||||
key: 'backAnswer',
|
||||
label: '自动填充上次提交内容',
|
||||
tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
|
||||
placement: 'top',
|
||||
type: 'CustomedSwitch',
|
||||
value: false,
|
||||
},
|
||||
interview_pwd_switch: {
|
||||
key: 'passwordSwitch',
|
||||
label: '访问密码',
|
||||
@ -96,5 +112,5 @@ export default {
|
||||
relyFunc: (data) => {
|
||||
return data.whitelistType == 'MEMBER'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
<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">问卷列表</el-menu-item>
|
||||
<el-menu-item index="2" @click="handleDownload">下载中心</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="login-info">
|
||||
@ -184,6 +185,10 @@ const handleLogout = () => {
|
||||
userStore.logout()
|
||||
router.replace({ name: 'login' })
|
||||
}
|
||||
// 下载页面
|
||||
const handleDownload = () => {
|
||||
router.push({ name: 'download' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -9,6 +9,9 @@ import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
import 'element-plus/theme-chalk/src/button.scss'
|
||||
import 'element-plus/theme-chalk/src/overlay.scss'
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
|
||||
@ -26,6 +29,14 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '问卷列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/download',
|
||||
name: 'download',
|
||||
component: () => import('../pages/downloadTask/TaskList.vue'),
|
||||
meta: {
|
||||
needLogin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/survey/:id/edit',
|
||||
meta: {
|
||||
|
@ -10,7 +10,7 @@ import { QUESTION_TYPE } from '@/common/typeEnum'
|
||||
import { getQuestionByType } from '@/management/utils/index'
|
||||
import { filterQuestionPreviewData } from '@/management/utils/index'
|
||||
|
||||
import { getSurveyById } from '@/management/api/survey'
|
||||
import { getSurveyById, getSessionId } from '@/management/api/survey'
|
||||
import { getNewField } from '@/management/utils'
|
||||
|
||||
import submitFormConfig from '@/management/pages/edit/setterConfig/submitConfig'
|
||||
@ -93,6 +93,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
||||
})
|
||||
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
|
||||
useLogicEngine(schema)
|
||||
|
||||
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
||||
schema.metaData = metaData
|
||||
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
||||
@ -151,7 +152,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
||||
initSchema,
|
||||
getSchemaFromRemote,
|
||||
showLogicEngine,
|
||||
jumpLogicEngine
|
||||
jumpLogicEngine,
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,21 +478,48 @@ function useLogicEngine(schema: any) {
|
||||
initJumpLogicEngine
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type IBannerItem = {
|
||||
name: string
|
||||
key: string
|
||||
list: Array<Object>
|
||||
}
|
||||
type IBannerList = Record<string, IBannerItem>
|
||||
|
||||
|
||||
|
||||
export const useEditStore = defineStore('edit', () => {
|
||||
const surveyId = ref('')
|
||||
const bannerList: Ref<IBannerList> = ref({})
|
||||
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
||||
const schemaUpdateTime = ref(Date.now())
|
||||
|
||||
function setSurveyId(id: string) {
|
||||
surveyId.value = id
|
||||
}
|
||||
|
||||
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
|
||||
useInitializeSchema(surveyId, () => {
|
||||
editGlobalBaseConf.initCounts()
|
||||
})
|
||||
|
||||
const sessionId = ref('')
|
||||
|
||||
async function initSessionId() {
|
||||
const sessionIdKey = `${surveyId.value}_sessionId`;
|
||||
const localSessionId = sessionStorage.getItem(sessionIdKey)
|
||||
if (localSessionId) {
|
||||
sessionId.value = localSessionId
|
||||
} else {
|
||||
const res: Record<string, any> = await getSessionId({ surveyId: surveyId.value })
|
||||
if (res.code === 200) {
|
||||
sessionId.value = res.data.sessionId
|
||||
sessionStorage.setItem(sessionIdKey, sessionId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const questionDataList = toRef(schema, 'questionDataList')
|
||||
|
||||
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
||||
@ -499,9 +527,6 @@ export const useEditStore = defineStore('edit', () => {
|
||||
schema.questionDataList = data
|
||||
}
|
||||
|
||||
function setSurveyId(id: string) {
|
||||
surveyId.value = id
|
||||
}
|
||||
|
||||
const fetchBannerData = async () => {
|
||||
const res: any = await getBannerData()
|
||||
@ -509,6 +534,7 @@ export const useEditStore = defineStore('edit', () => {
|
||||
bannerList.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCooperPermissions = async (id: string) => {
|
||||
const res: any = await getCollaboratorPermissions(id)
|
||||
if (res.code === CODE_MAP.SUCCESS) {
|
||||
@ -531,6 +557,7 @@ export const useEditStore = defineStore('edit', () => {
|
||||
const { metaData } = schema
|
||||
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
||||
await getSchemaFromRemote()
|
||||
await initSessionId()
|
||||
}
|
||||
currentEditOne.value = null
|
||||
currentEditStatus.value = 'Success'
|
||||
@ -641,7 +668,9 @@ export const useEditStore = defineStore('edit', () => {
|
||||
return {
|
||||
editGlobalBaseConf,
|
||||
surveyId,
|
||||
sessionId,
|
||||
setSurveyId,
|
||||
initSessionId,
|
||||
bannerList,
|
||||
fetchBannerData,
|
||||
cooperPermissions,
|
||||
|
@ -47,6 +47,10 @@ export default defineComponent({
|
||||
voteTotal: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
quotaNoDisplay:{
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
@ -141,8 +145,22 @@ export default defineComponent({
|
||||
<span
|
||||
v-html={filterXSS(item.text)}
|
||||
class="item-title-text"
|
||||
style="display: block; height: auto; padding: 9px 0"
|
||||
></span>
|
||||
style="display: block; height: auto; padding-top: 9px"
|
||||
></span>
|
||||
)}
|
||||
{
|
||||
// 如果设置了配额并且展示配额
|
||||
!this.readonly && (item.quota && item.quota !== "0") && !this.quotaNoDisplay && (
|
||||
<span
|
||||
class="remaining-text"
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 'smaller',
|
||||
color: item.release === 0 ? '#EB505C' : '#92949D'
|
||||
}}
|
||||
>
|
||||
剩余{item.release}
|
||||
</span>
|
||||
)}
|
||||
{slots.vote?.({
|
||||
option: item,
|
||||
|
@ -73,7 +73,7 @@
|
||||
vertical-align: top;
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
margin: 0rem 0.24rem 0 0;
|
||||
margin: 11px 0.24rem 0 0;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
@ -128,7 +128,6 @@
|
||||
.qicon.qicon-gouxuan {
|
||||
display: inline-block;
|
||||
font-size: 0.32rem;
|
||||
line-height: 0.32rem;
|
||||
border-color: $primary-color;
|
||||
background-color: $primary-color;
|
||||
color: #fff;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { computed, defineComponent, shallowRef, defineAsyncComponent } from 'vue'
|
||||
import { computed, defineComponent, shallowRef, defineAsyncComponent, watch } from 'vue'
|
||||
import { includes } from 'lodash-es'
|
||||
|
||||
import BaseChoice from '../BaseChoice'
|
||||
@ -41,10 +41,15 @@ export default defineComponent({
|
||||
maxNum: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
quotaNoDisplay:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const disableState = computed(() => {
|
||||
if (!props.maxNum) {
|
||||
return false
|
||||
@ -53,17 +58,31 @@ export default defineComponent({
|
||||
})
|
||||
const isDisabled = (item) => {
|
||||
const { value } = props
|
||||
return disableState.value && !includes(value, item.value)
|
||||
return disableState.value && !includes(value, item.hash)
|
||||
}
|
||||
const myOptions = computed(() => {
|
||||
const { options } = props
|
||||
return options.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
disabled: isDisabled(item)
|
||||
disabled: (item.release === 0) || isDisabled(item)
|
||||
}
|
||||
})
|
||||
})
|
||||
// 兼容断点续答情况下选项配额为0的情况
|
||||
watch(() => props.value, (value) => {
|
||||
const disabledHash = myOptions.value.filter(i => i.disabled).map(i => i.hash)
|
||||
if (value && disabledHash.length) {
|
||||
disabledHash.forEach(hash => {
|
||||
const index = value.indexOf(hash)
|
||||
if( index> -1) {
|
||||
const newValue = [...value]
|
||||
newValue.splice(index, 1)
|
||||
onChange(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const onChange = (value) => {
|
||||
const key = props.field
|
||||
emit('change', {
|
||||
@ -92,12 +111,13 @@ export default defineComponent({
|
||||
return {
|
||||
onChange,
|
||||
handleSelectMoreChange,
|
||||
disableState,
|
||||
myOptions,
|
||||
selectMoreView
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { readonly, field, myOptions, onChange, maxNum, value, selectMoreView } = this
|
||||
const { readonly, field, myOptions, onChange, maxNum, value, quotaNoDisplay, selectMoreView } = this
|
||||
return (
|
||||
<BaseChoice
|
||||
uiTarget="checkbox"
|
||||
@ -108,6 +128,7 @@ export default defineComponent({
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
layout={this.layout}
|
||||
quotaNoDisplay={quotaNoDisplay}
|
||||
>
|
||||
{{
|
||||
selectMore: (scoped) => {
|
||||
|
@ -117,20 +117,40 @@ const meta = {
|
||||
label: '至少选择数',
|
||||
type: 'InputNumber',
|
||||
key: 'minNum',
|
||||
value: '',
|
||||
value: 0,
|
||||
min: 0,
|
||||
max: 'maxNum',
|
||||
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
|
||||
contentClass: 'input-number-config'
|
||||
},
|
||||
{
|
||||
label: '最多选择数',
|
||||
type: 'InputNumber',
|
||||
key: 'maxNum',
|
||||
value: '',
|
||||
min: 'minNum',
|
||||
value: 0,
|
||||
min: moduleConfig => { return moduleConfig?.minNum || 0 },
|
||||
max: moduleConfig => { return moduleConfig?.options?.length },
|
||||
contentClass: 'input-number-config'
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'optionQuota',
|
||||
label: '选项配额',
|
||||
labelStyle: {
|
||||
'font-weight': 'bold'
|
||||
},
|
||||
type: 'QuotaConfig',
|
||||
// 输出转换
|
||||
valueSetter({ options, quotaNoDisplay}) {
|
||||
return [{
|
||||
key: 'options',
|
||||
value: options
|
||||
},
|
||||
{
|
||||
key: 'quotaNoDisplay',
|
||||
value: quotaNoDisplay
|
||||
}]
|
||||
}
|
||||
}
|
||||
],
|
||||
editConfigure: {
|
||||
|
@ -6,22 +6,13 @@ import GetHash from '@materials/questions/common/utils/getOptionHash'
|
||||
function useOptionBase(options) {
|
||||
const optionList = ref(options)
|
||||
const addOption = (text = '选项', others = false, index = -1, field) => {
|
||||
// const {} = payload
|
||||
let addOne
|
||||
if (optionList.value[0]) {
|
||||
addOne = cloneDeep(optionList.value[0])
|
||||
} else {
|
||||
addOne = {
|
||||
text: '',
|
||||
hash: '',
|
||||
imageUrl: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
score: 0,
|
||||
limit: ''
|
||||
}
|
||||
let addOne = {
|
||||
text: '',
|
||||
hash: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
}
|
||||
if (typeof text !== 'string') {
|
||||
text = '选项'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineComponent, shallowRef, defineAsyncComponent } from 'vue'
|
||||
import { defineComponent, shallowRef, watch, defineAsyncComponent } from 'vue'
|
||||
import BaseChoice from '../BaseChoice'
|
||||
|
||||
/**
|
||||
@ -31,10 +31,28 @@ export default defineComponent({
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
quotaNoDisplay:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit }) {
|
||||
// 兼容断点续答情况下选项配额为0的情况
|
||||
watch(() => props.value, (value) => {
|
||||
const disabledHash = props.options.filter(i => i.disabled).map(i => i.hash)
|
||||
if (value && disabledHash.length) {
|
||||
disabledHash.forEach(hash => {
|
||||
const index = value.indexOf(hash)
|
||||
if( index> -1) {
|
||||
const newValue = [...value]
|
||||
newValue.splice(index, 1)
|
||||
onChange(newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const onChange = (value) => {
|
||||
const key = props.field
|
||||
emit('change', {
|
||||
@ -81,6 +99,7 @@ export default defineComponent({
|
||||
field={this.field}
|
||||
layout={this.layout}
|
||||
onChange={this.onChange}
|
||||
quotaNoDisplay={this.quotaNoDisplay}
|
||||
>
|
||||
{{
|
||||
selectMore: (scoped) => {
|
||||
|
@ -53,22 +53,22 @@ const meta = {
|
||||
description: '这是用于描述选项',
|
||||
defaultValue: [
|
||||
{
|
||||
text: '选项1',
|
||||
imageUrl: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115019'
|
||||
"text": "选项1",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115019"
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
imageUrl: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115020'
|
||||
"text": "选项2",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115020"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -77,6 +77,12 @@ const meta = {
|
||||
propType: String,
|
||||
description: '排列方式',
|
||||
defaultValue: 'vertical'
|
||||
},
|
||||
{
|
||||
name: 'quotaNoDisplay',
|
||||
propType: Boolean,
|
||||
description: '不展示配额剩余数量',
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
formConfig: [basicConfig, {
|
||||
@ -101,6 +107,24 @@ const meta = {
|
||||
]
|
||||
},
|
||||
]
|
||||
},{
|
||||
name: 'optionQuota',
|
||||
label: '选项配额',
|
||||
labelStyle: {
|
||||
'font-weight': 'bold'
|
||||
},
|
||||
type: 'QuotaConfig',
|
||||
// 输出转换
|
||||
valueSetter({ options, quotaNoDisplay}) {
|
||||
return [{
|
||||
key: 'options',
|
||||
value: options
|
||||
},
|
||||
{
|
||||
key: 'quotaNoDisplay',
|
||||
value: quotaNoDisplay
|
||||
}]
|
||||
}
|
||||
}],
|
||||
editConfigure: {
|
||||
optionEdit: {
|
||||
|
@ -120,7 +120,7 @@ const meta = {
|
||||
key: 'minNum',
|
||||
value: '',
|
||||
min: 0,
|
||||
max: 'maxNum',
|
||||
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
|
||||
contentClass: 'input-number-config'
|
||||
},
|
||||
{
|
||||
@ -128,7 +128,8 @@ const meta = {
|
||||
type: 'InputNumber',
|
||||
key: 'maxNum',
|
||||
value: '',
|
||||
min: 'minNum',
|
||||
min: moduleConfig => { return moduleConfig?.minNum || 0 },
|
||||
max: moduleConfig => { return moduleConfig?.options?.length || 0 },
|
||||
contentClass: 'input-number-config'
|
||||
}
|
||||
]
|
||||
|
@ -2,7 +2,7 @@
|
||||
<el-switch v-model="newValue" @change="changeData" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
const props = defineProps({
|
||||
@ -23,4 +23,15 @@ const changeData = (value) => {
|
||||
value
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => props.formConfig.value,
|
||||
(newVal) => {
|
||||
if (newVal !== newValue.value) {
|
||||
newValue.value = newVal
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
@ -13,6 +13,7 @@ import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
|
||||
interface Props {
|
||||
formConfig: any
|
||||
moduleConfig: any
|
||||
@ -24,12 +25,15 @@ interface Emit {
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
const props = defineProps<Props>()
|
||||
const modelValue = ref(Number(props.formConfig.value) || 0)
|
||||
const modelValue = ref(Number(props.formConfig.value))
|
||||
|
||||
const myModuleConfig = ref(props.moduleConfig)
|
||||
|
||||
const minModelValue = computed(() => {
|
||||
const { min } = props.formConfig
|
||||
if (min) {
|
||||
if (min !== undefined) {
|
||||
if (typeof min === 'function') {
|
||||
return min(props.moduleConfig)
|
||||
return min(myModuleConfig.value)
|
||||
} else {
|
||||
return Number(min)
|
||||
}
|
||||
@ -38,16 +42,13 @@ const minModelValue = computed(() => {
|
||||
})
|
||||
|
||||
const maxModelValue = computed(() => {
|
||||
const { max, min } = props.formConfig
|
||||
|
||||
const { max } = props.formConfig
|
||||
if (max) {
|
||||
if (typeof max === 'function') {
|
||||
return max(props.moduleConfig)
|
||||
return max(myModuleConfig.value)
|
||||
} else {
|
||||
return Number(max)
|
||||
}
|
||||
} else if (min !== undefined && Array.isArray(props.moduleConfig?.options)) {
|
||||
return props.moduleConfig.options.length
|
||||
} else {
|
||||
return Infinity
|
||||
}
|
||||
@ -65,6 +66,9 @@ const handleInputChange = (value: number) => {
|
||||
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
||||
}
|
||||
watch(() => props.moduleConfig, (newVal) => {
|
||||
myModuleConfig.value = newVal
|
||||
})
|
||||
watch(
|
||||
() => props.formConfig.value,
|
||||
(newVal) => {
|
||||
|
180
web/src/materials/setters/widgets/QuotaConfig.vue
Normal file
180
web/src/materials/setters/widgets/QuotaConfig.vue
Normal file
@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="quota-wrapper">
|
||||
<span class="quota-config" @click="openQuotaConfig"> 设置> </span>
|
||||
<el-dialog v-model="dialogVisible" @closed="cleanTempQuota" class="dialog">
|
||||
<template #header>
|
||||
<div class="dialog-title">选项配额</div>
|
||||
</template>
|
||||
<el-table
|
||||
:header-cell-style="{ background: '#F6F7F9', color: '#6E707C' }"
|
||||
:data="optionData"
|
||||
border
|
||||
style="width: 100%"
|
||||
@cell-click="handleCellClick"
|
||||
>
|
||||
<el-table-column property="text" label="选项" style="width: 50%">
|
||||
<template v-slot="scope">
|
||||
<div v-html="cleanRichTextWithMediaTag(scope.row.text)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="quota" style="width: 50%">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center">
|
||||
<span>配额设置</span>
|
||||
<el-tooltip
|
||||
class="tooltip"
|
||||
effect="dark"
|
||||
placement="right"
|
||||
content="类似商品库存,表示最多可以被选择多少次;0为无限制;已发布问卷,上限修改时数量不可减小。"
|
||||
>
|
||||
<i-ep-questionFilled class="icon-tip" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="scope">
|
||||
<el-input
|
||||
v-if="scope.row.isEditing"
|
||||
:id="`${scope.row.hash}editInput`"
|
||||
v-model="scope.row.tempQuota"
|
||||
type="number"
|
||||
@blur="handleInput(scope.row)"
|
||||
placeholder="请输入"
|
||||
>
|
||||
</el-input>
|
||||
<div v-else class="item__txt">
|
||||
<span v-if="scope.row.tempQuota !== '0'">{{ scope.row.tempQuota }}</span>
|
||||
<span v-else style="color: #c8c9cd">请输入</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="quota-no-display">
|
||||
<el-checkbox v-model="quotaNoDisplayValue" label="不展示配额剩余数量"> </el-checkbox>
|
||||
<el-tooltip
|
||||
class="tooltip"
|
||||
effect="dark"
|
||||
placement="right"
|
||||
content="勾选后,将不对用户展示剩余配额数量。"
|
||||
>
|
||||
<i-ep-questionFilled class="icon-tip" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="diaglog-footer">
|
||||
<el-button @click="cancel">取消</el-button>
|
||||
<el-button @click="confirm" type="primary">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||
|
||||
const props = defineProps(['formConfig', 'moduleConfig'])
|
||||
const emit = defineEmits(['form-change'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const moduleConfig = ref(props.moduleConfig)
|
||||
const optionData = ref(props.moduleConfig.options)
|
||||
const quotaNoDisplayValue = ref(moduleConfig.value.quotaNoDisplay)
|
||||
|
||||
const openQuotaConfig = () => {
|
||||
optionData.value.forEach((item) => {
|
||||
item.tempQuota = item.quota
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const cancel = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
const confirm = () => {
|
||||
dialogVisible.value = false
|
||||
// 更新选项
|
||||
handleQuotaChange()
|
||||
emit(FORM_CHANGE_EVENT_KEY, {
|
||||
options: optionData.value,
|
||||
quotaNoDisplay: quotaNoDisplayValue.value
|
||||
})
|
||||
}
|
||||
const handleCellClick = (row, column) => {
|
||||
if (column.property === 'quota') {
|
||||
optionData.value.forEach((r) => {
|
||||
if (r !== row) r.isEditing = false
|
||||
})
|
||||
row.tempQuota = row.tempQuota === '0' ? row.quota : row.tempQuota
|
||||
row.isEditing = true
|
||||
nextTick(() => {
|
||||
const input = document.getElementById(`${row.hash}editInput`)
|
||||
input.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleInput = (row) => {
|
||||
if (row.tempQuota !== '0' && +row.tempQuota < +row.quota) {
|
||||
ElMessageBox.alert('配额数不可减少!', '警告', {
|
||||
confirmButtonText: '确定'
|
||||
})
|
||||
row.tempQuota = row.quota
|
||||
}
|
||||
row.isEditing = false
|
||||
}
|
||||
const handleQuotaChange = () => {
|
||||
optionData.value.forEach((item) => {
|
||||
item.quota = item.tempQuota
|
||||
delete item.tempQuota
|
||||
})
|
||||
}
|
||||
const cleanTempQuota = () => {
|
||||
optionData.value.forEach((item) => {
|
||||
delete item.tempQuota
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => props.moduleConfig,
|
||||
(val) => {
|
||||
moduleConfig.value = val
|
||||
optionData.value = val.options
|
||||
quotaNoDisplayValue.value = val.quotaNoDisplay
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quota-wrapper {
|
||||
width: 90%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
:deep(.cell) {
|
||||
line-height: 35px;
|
||||
}
|
||||
.quota-no-display {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
.quota-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
}
|
||||
.quota-config {
|
||||
color: #ffa600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dialog {
|
||||
width: 41vw;
|
||||
.dialog-title {
|
||||
color: #292a36;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
79
web/src/render/components/BackAnswerDialog.vue
Normal file
79
web/src/render/components/BackAnswerDialog.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="mask" v-show="visible">
|
||||
<div class="box">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="btn-box">
|
||||
<div class="btn cancel" @click="handleCancel">{{ cancelBtnText }}</div>
|
||||
<div class="btn confirm" @click="handleConfirm">{{ confirmBtnText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
visible?: boolean
|
||||
cancelBtnText?: string
|
||||
confirmBtnText?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(ev: 'confirm', callback: () => void): void
|
||||
(ev: 'cancel', callback: () => void): void
|
||||
(ev: 'close'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
cancelBtnText: '取消',
|
||||
confirmBtnText: '确定',
|
||||
title: ''
|
||||
})
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', () => {
|
||||
emit('close')
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel', () => {
|
||||
emit('close')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import url('../styles/dialog.scss');
|
||||
|
||||
.btn-box {
|
||||
padding: 20px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
width: 48%;
|
||||
font-size: 0.28rem;
|
||||
border-radius: 0.04rem;
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
line-height: 0.4rem;
|
||||
cursor: pointer;
|
||||
|
||||
&.cancel {
|
||||
background: #fff;
|
||||
color: #92949d;
|
||||
border: 1px solid #e3e4e8;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
background-color: #4a4c5b;
|
||||
border: 1px solid #4a4c5b;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,6 +4,7 @@
|
||||
:moduleConfig="questionConfig"
|
||||
:indexNumber="indexNumber"
|
||||
:showTitle="true"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
></QuestionRuleContainer>
|
||||
</template>
|
||||
@ -14,6 +15,7 @@ import QuestionRuleContainer from '../../materials/questions/QuestionRuleContain
|
||||
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
||||
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
||||
import { useShowInput } from '@/render/hooks/useShowInput'
|
||||
import { useOptionsQuota } from '@/render/hooks/useOptionsQuota'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useQuestionStore } from '../stores/question'
|
||||
import { useSurveyStore } from '../stores/survey'
|
||||
@ -49,16 +51,24 @@ const questionConfig = computed(() => {
|
||||
let alloptions = options
|
||||
|
||||
if (type === QUESTION_TYPE.VOTE) {
|
||||
// 处理投票进度
|
||||
const { options, voteTotal } = useVoteMap(field)
|
||||
const voteOptions = unref(options)
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||
moduleConfig.voteTotal = unref(voteTotal)
|
||||
}
|
||||
|
||||
if(NORMAL_CHOICES.includes(type) &&
|
||||
options.some(option => option.quota > 0)) {
|
||||
// 处理普通选择题的选项配额
|
||||
let { options: optionWithQuota } = useOptionsQuota(field)
|
||||
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, optionWithQuota[index]))
|
||||
}
|
||||
if (
|
||||
NORMAL_CHOICES.includes(type) &&
|
||||
options.filter((optionItem) => optionItem.others).length > 0
|
||||
options.some(option => option.others)
|
||||
) {
|
||||
// 处理普通选择题的填写更多
|
||||
let { options, othersValue } = useShowOthers(field)
|
||||
const othersOptions = unref(options)
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
||||
@ -71,6 +81,7 @@ const questionConfig = computed(() => {
|
||||
Object.keys(rest?.rangeConfig).filter((index) => rest?.rangeConfig[index].isShowInput).length >
|
||||
0
|
||||
) {
|
||||
// 处理评分题的的选项后输入框
|
||||
let { rangeConfig, othersValue } = useShowInput(field)
|
||||
moduleConfig.rangeConfig = unref(rangeConfig)
|
||||
moduleConfig.othersValue = unref(othersValue)
|
||||
@ -126,9 +137,19 @@ const handleChange = (data) => {
|
||||
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
||||
questionStore.updateVoteData(data)
|
||||
}
|
||||
// 处理选项配额
|
||||
if (props.moduleConfig.type === NORMAL_CHOICES) {
|
||||
questionStore.updateQuotaData(data)
|
||||
}
|
||||
// 断点续答的的数据缓存
|
||||
localStorageBack()
|
||||
processJumpSkip()
|
||||
}
|
||||
|
||||
const handleInput = () => {
|
||||
localStorageBack()
|
||||
}
|
||||
|
||||
const processJumpSkip = () => {
|
||||
const targetResult = surveyStore.jumpLogicEngine
|
||||
.getResultsByField(changeField.value, surveyStore.formValues)
|
||||
@ -169,4 +190,12 @@ const processJumpSkip = () => {
|
||||
.map((item) => item.field)
|
||||
questionStore.addNeedHideFields(skipKey)
|
||||
}
|
||||
const localStorageBack = () => {
|
||||
var formData = Object.assign({}, surveyStore.formValues);
|
||||
|
||||
//浏览器存储
|
||||
localStorage.removeItem(surveyStore.surveyPath + "_questionData")
|
||||
localStorage.setItem(surveyStore.surveyPath + "_questionData", JSON.stringify(formData))
|
||||
localStorage.setItem('isSubmit', JSON.stringify(false))
|
||||
}
|
||||
</script>
|
||||
|
23
web/src/render/hooks/useOptionsQuota.js
Normal file
23
web/src/render/hooks/useOptionsQuota.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { useQuestionStore } from '../stores/question'
|
||||
export const useOptionsQuota = (questionKey) => {
|
||||
const questionStore = useQuestionStore()
|
||||
const options = questionStore.questionData[questionKey].options.map((option) => {
|
||||
if(option.quota){
|
||||
const optionHash = option.hash
|
||||
const selectCount = questionStore.quotaMap?.[questionKey]?.[optionHash] || 0
|
||||
const release = Number(option.quota) - Number(selectCount)
|
||||
return {
|
||||
...option,
|
||||
disabled: release === 0,
|
||||
selectCount,
|
||||
release
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...option,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { options }
|
||||
}
|
18
web/src/render/hooks/useQuestionInfo.ts
Normal file
18
web/src/render/hooks/useQuestionInfo.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useQuestionStore } from '@/render/stores/question'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
export const useQuestionInfo = (field: string) => {
|
||||
const questionstore = useQuestionStore()
|
||||
|
||||
const questionTitle = cleanRichText(questionstore.questionData[field]?.title)
|
||||
const getOptionTitle = (value:any) => {
|
||||
const options = questionstore.questionData[field]?.options || []
|
||||
if (value instanceof Array) {
|
||||
return options
|
||||
.filter((item:any) => value.includes(item.hash))
|
||||
.map((item:any) => cleanRichText(item.text))
|
||||
} else {
|
||||
return options.filter((item:any) => item.hash === value).map((item:any) => cleanRichText(item.text))
|
||||
}
|
||||
}
|
||||
return { questionTitle, getOptionTitle }
|
||||
}
|
@ -2,60 +2,10 @@
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
|
||||
import useCommandComponent from '../hooks/useCommandComponent'
|
||||
import { useSurveyStore } from '../stores/survey'
|
||||
|
||||
import AlertDialog from '../components/AlertDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const surveyStore = useSurveyStore()
|
||||
const loadData = (res: any, surveyPath: string) => {
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
const {
|
||||
bannerConf,
|
||||
baseConf,
|
||||
bottomConf,
|
||||
dataConf,
|
||||
skinConf,
|
||||
submitConf,
|
||||
logicConf,
|
||||
pageConf
|
||||
} = data.code
|
||||
const questionData = {
|
||||
bannerConf,
|
||||
baseConf,
|
||||
bottomConf,
|
||||
dataConf,
|
||||
skinConf,
|
||||
submitConf,
|
||||
pageConf
|
||||
}
|
||||
|
||||
if (!pageConf || pageConf?.length == 0) {
|
||||
questionData.pageConf = [dataConf.dataList.length]
|
||||
}
|
||||
|
||||
document.title = data.title
|
||||
|
||||
surveyStore.setSurveyPath(surveyPath)
|
||||
surveyStore.initSurvey(questionData)
|
||||
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
|
||||
surveyStore.initJumpLogicEngine(logicConf.jumpLogicConf)
|
||||
} else {
|
||||
throw new Error(res.errmsg)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
const surveyId = route.params.surveyId
|
||||
console.log({ surveyId })
|
||||
surveyStore.setSurveyPath(surveyId)
|
||||
getDetail(surveyId as string)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.t,
|
||||
@ -63,22 +13,4 @@ watch(
|
||||
location.reload()
|
||||
}
|
||||
)
|
||||
|
||||
const getDetail = async (surveyPath: string) => {
|
||||
const alert = useCommandComponent(AlertDialog)
|
||||
|
||||
try {
|
||||
if (surveyPath.length > 8) {
|
||||
const res: any = await getPreviewSchema({ surveyPath })
|
||||
loadData(res, surveyPath)
|
||||
} else {
|
||||
const res: any = await getPublishedSurveyInfo({ surveyPath })
|
||||
loadData(res, surveyPath)
|
||||
surveyStore.getEncryptInfo()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
alert({ title: error.message || '获取问卷失败' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -21,9 +21,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
// @ts-ignore
|
||||
import communalLoader from '@materials/communals/communalLoader.js'
|
||||
import MainRenderer from '../components/MainRenderer.vue'
|
||||
@ -38,6 +38,8 @@ import { submitForm } from '../api/survey'
|
||||
import encrypt from '../utils/encrypt'
|
||||
|
||||
import useCommandComponent from '../hooks/useCommandComponent'
|
||||
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
|
||||
import { useQuestionInfo } from '../hooks/useQuestionInfo'
|
||||
|
||||
interface Props {
|
||||
questionInfo?: any
|
||||
@ -70,6 +72,68 @@ const pageIndex = computed(() => questionStore.pageIndex)
|
||||
const { bannerConf, submitConf, bottomConf: logoConf, whiteData } = storeToRefs(surveyStore)
|
||||
const surveyPath = computed(() => surveyStore.surveyPath || '')
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
const surveyId = route.params.surveyId
|
||||
console.log({ surveyId })
|
||||
surveyStore.setSurveyPath(surveyId)
|
||||
getDetail(surveyId as string)
|
||||
})
|
||||
const loadData = (res: any, surveyPath: string) => {
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
const {
|
||||
bannerConf,
|
||||
baseConf,
|
||||
bottomConf,
|
||||
dataConf,
|
||||
skinConf,
|
||||
submitConf,
|
||||
logicConf,
|
||||
pageConf
|
||||
} = data.code
|
||||
const questionData = {
|
||||
bannerConf,
|
||||
baseConf,
|
||||
bottomConf,
|
||||
dataConf,
|
||||
skinConf,
|
||||
submitConf,
|
||||
pageConf
|
||||
}
|
||||
|
||||
if (!pageConf || pageConf?.length == 0) {
|
||||
questionData.pageConf = [dataConf.dataList.length]
|
||||
}
|
||||
|
||||
document.title = data.title
|
||||
|
||||
surveyStore.setSurveyPath(surveyPath)
|
||||
surveyStore.initSurvey(questionData)
|
||||
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
|
||||
surveyStore.initJumpLogicEngine(logicConf?.jumpLogicConf)
|
||||
} else {
|
||||
throw new Error(res.errmsg)
|
||||
}
|
||||
}
|
||||
const getDetail = async (surveyPath: string) => {
|
||||
const alert = useCommandComponent(AlertDialog)
|
||||
|
||||
try {
|
||||
if (surveyPath.length > 8) {
|
||||
const res: any = await getPreviewSchema({ surveyPath })
|
||||
loadData(res, surveyPath)
|
||||
} else {
|
||||
const res: any = await getPublishedSurveyInfo({ surveyPath })
|
||||
loadData(res, surveyPath)
|
||||
surveyStore.getEncryptInfo()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
alert({ title: error.message || '获取问卷失败' })
|
||||
}
|
||||
}
|
||||
const validate = (cbk: (v: boolean) => void) => {
|
||||
const index = 0
|
||||
mainRef.value.$refs.formGroup[index].validate(cbk)
|
||||
@ -88,6 +152,15 @@ const normalizationRequestBody = () => {
|
||||
...whiteData.value
|
||||
}
|
||||
|
||||
//浏览器缓存数据
|
||||
localStorage.removeItem(surveyPath.value + "_questionData")
|
||||
localStorage.removeItem("isSubmit")
|
||||
//数据加密
|
||||
var formData : Record<string, any> = Object.assign({}, surveyStore.formValues)
|
||||
|
||||
localStorage.setItem(surveyPath.value + "_questionData", JSON.stringify(formData))
|
||||
localStorage.setItem('isSubmit', JSON.stringify(true))
|
||||
|
||||
if (encryptInfo?.encryptType) {
|
||||
result.encryptType = encryptInfo.encryptType
|
||||
result.data = encrypt[result.encryptType as 'rsa']({
|
||||
@ -111,10 +184,18 @@ const submitSurver = async () => {
|
||||
}
|
||||
try {
|
||||
const params = normalizationRequestBody()
|
||||
console.log(params)
|
||||
const res: any = await submitForm(params)
|
||||
if (res.code === 200) {
|
||||
router.replace({ name: 'successPage' })
|
||||
} else if(res.code === 9003) {
|
||||
// 更新填写的过程中配额减少情况
|
||||
questionStore.initQuotaMap()
|
||||
const titile = useQuestionInfo(res.data.field).questionTitle
|
||||
const optionText = useQuestionInfo(res.data.field).getOptionTitle(res.data.optionHash)
|
||||
const message = `【${titile}】的【${optionText}】配额已满,请重新选择`
|
||||
alert({
|
||||
title: message
|
||||
})
|
||||
} else {
|
||||
alert({
|
||||
title: res.errmsg || '提交失败'
|
||||
|
@ -3,11 +3,195 @@ import { defineStore } from 'pinia'
|
||||
import { set } from 'lodash-es'
|
||||
import { useSurveyStore } from '@/render/stores/survey'
|
||||
import { queryVote } from '@/render/api/survey'
|
||||
import { QUESTION_TYPE, NORMAL_CHOICES } from '@/common/typeEnum'
|
||||
|
||||
const VOTE_INFO_KEY = 'voteinfo'
|
||||
const QUOTA_INFO_KEY = 'limitinfo'
|
||||
|
||||
// 投票进度逻辑聚合
|
||||
const usevVoteMap = (questionData) => {
|
||||
const voteMap = ref({})
|
||||
//初始化投票题的数据
|
||||
const initVoteData = async () => {
|
||||
const surveyStore = useSurveyStore()
|
||||
const surveyPath = surveyStore.surveyPath
|
||||
|
||||
const fieldList = []
|
||||
|
||||
for (const field in questionData.value) {
|
||||
const { type } = questionData.value[field]
|
||||
if (type.includes(QUESTION_TYPE.VOTE)) {
|
||||
fieldList.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldList.length <= 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(VOTE_INFO_KEY)
|
||||
const voteRes = await queryVote({
|
||||
surveyPath,
|
||||
fieldList: fieldList.join(',')
|
||||
})
|
||||
|
||||
if (voteRes.code === 200) {
|
||||
localStorage.setItem(
|
||||
VOTE_INFO_KEY,
|
||||
JSON.stringify({
|
||||
...voteRes.data
|
||||
})
|
||||
)
|
||||
setVoteMap(voteRes.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
const updateVoteMapByKey = (data) => {
|
||||
const { questionKey, voteKey, voteValue } = data
|
||||
// 兼容为空的情况
|
||||
if (!voteMap.value[questionKey]) {
|
||||
voteMap.value[questionKey] = {}
|
||||
}
|
||||
voteMap.value[questionKey][voteKey] = voteValue
|
||||
}
|
||||
const setVoteMap = (data) => {
|
||||
voteMap.value = data
|
||||
}
|
||||
const updateVoteData = (data) => {
|
||||
const { key: questionKey, value: questionVal } = data
|
||||
// 更新前获取接口缓存在localStorage中的数据
|
||||
const localData = localStorage.getItem(VOTE_INFO_KEY)
|
||||
const voteinfo = JSON.parse(localData)
|
||||
const currentQuestion = questionData.value[questionKey]
|
||||
const options = currentQuestion.options
|
||||
const voteTotal = voteinfo?.[questionKey]?.total || 0
|
||||
let totalPayload = {
|
||||
questionKey,
|
||||
voteKey: 'total',
|
||||
voteValue: voteTotal
|
||||
}
|
||||
options.forEach((option) => {
|
||||
const optionhash = option.hash
|
||||
const voteCount = voteinfo?.[questionKey]?.[optionhash] || 0
|
||||
// 如果选中值包含该选项,对应voteCount 和 voteTotal + 1
|
||||
if (
|
||||
Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash
|
||||
) {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
voteKey: optionhash,
|
||||
voteValue: voteCount + 1
|
||||
}
|
||||
totalPayload.voteValue += 1
|
||||
updateVoteMapByKey(countPayload)
|
||||
} else {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
voteKey: optionhash,
|
||||
voteValue: voteCount
|
||||
}
|
||||
updateVoteMapByKey(countPayload)
|
||||
}
|
||||
updateVoteMapByKey(totalPayload)
|
||||
})
|
||||
}
|
||||
return {
|
||||
voteMap,
|
||||
initVoteData,
|
||||
updateVoteData
|
||||
}
|
||||
}
|
||||
|
||||
// 选项配额逻辑聚合
|
||||
const useQuotaMap = (questionData) => {
|
||||
const quotaMap = ref({})
|
||||
const updateQuotaMapByKey = ({ questionKey, optionKey, data }) =>{
|
||||
// 兼容为空的情况
|
||||
if (!quotaMap.value[questionKey]) {
|
||||
quotaMap.value[questionKey] = {}
|
||||
}
|
||||
quotaMap.value[questionKey][optionKey] = data
|
||||
}
|
||||
const initQuotaMap = async () => {
|
||||
const surveyStore = useSurveyStore()
|
||||
const surveyPath = surveyStore.surveyPath
|
||||
const fieldList = Object.keys(questionData.value).filter(field => {
|
||||
if (NORMAL_CHOICES.includes(questionData.value[field].type)) {
|
||||
return questionData.value[field].options.some(option => option.quota > 0)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果不存在则不请求选项上限接口
|
||||
if (fieldList.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(QUOTA_INFO_KEY)
|
||||
const quotaRes = await queryVote({
|
||||
surveyPath,
|
||||
fieldList: fieldList.join(',')
|
||||
})
|
||||
|
||||
if (quotaRes.code === 200) {
|
||||
localStorage.setItem(
|
||||
QUOTA_INFO_KEY,
|
||||
JSON.stringify({
|
||||
...quotaRes.data
|
||||
})
|
||||
)
|
||||
Object.keys(quotaRes.data).forEach(field => {
|
||||
Object.keys(quotaRes.data[field]).forEach((optionHash) => {
|
||||
updateQuotaMapByKey({ questionKey: field, optionKey: optionHash, data: quotaRes.data[field][optionHash] })
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
const updateQuotaData = (data) => {
|
||||
const { key: questionKey, value: questionVal } = data
|
||||
// 更新前获取接口缓存在localStorage中的数据
|
||||
const localData = localStorage.getItem(QUOTA_INFO_KEY)
|
||||
const quotaMap = JSON.parse(localData)
|
||||
// const quotaMap = state.quotaMap
|
||||
const currentQuestion = questionData.value[questionKey]
|
||||
const options = currentQuestion.options
|
||||
options.forEach((option) => {
|
||||
const optionhash = option.hash
|
||||
const selectCount = quotaMap?.[questionKey]?.[optionhash].selectCount || 0
|
||||
// 如果选中值包含该选项,对应 voteCount 和 voteTotal + 1
|
||||
if (
|
||||
Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash
|
||||
) {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
optionKey: optionhash,
|
||||
selectCount: selectCount + 1
|
||||
}
|
||||
updateQuotaMapByKey(countPayload)
|
||||
} else {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
optionKey: optionhash,
|
||||
selectCount: selectCount
|
||||
}
|
||||
updateQuotaMapByKey(countPayload)
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
quotaMap,
|
||||
initQuotaMap,
|
||||
updateQuotaMapByKey,
|
||||
updateQuotaData
|
||||
}
|
||||
}
|
||||
|
||||
export const useQuestionStore = defineStore('question', () => {
|
||||
const voteMap = ref({})
|
||||
const questionData = ref(null)
|
||||
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||
const pageIndex = ref(1) // 当前分页的索引
|
||||
@ -82,6 +266,8 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
const setQuestionData = (data) => {
|
||||
questionData.value = data
|
||||
}
|
||||
const { voteMap, setVoteMap, initVoteData, updateVoteData } = usevVoteMap(questionData)
|
||||
const { quotaMap, initQuotaMap, updateQuotaData } = useQuotaMap(questionData)
|
||||
|
||||
const changeSelectMoreData = (data) => {
|
||||
const { key, value, field } = data
|
||||
@ -91,96 +277,7 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
const setQuestionSeq = (data) => {
|
||||
questionSeq.value = data
|
||||
}
|
||||
|
||||
const setVoteMap = (data) => {
|
||||
voteMap.value = data
|
||||
}
|
||||
|
||||
const updateVoteMapByKey = (data) => {
|
||||
const { questionKey, voteKey, voteValue } = data
|
||||
// 兼容为空的情况
|
||||
if (!voteMap.value[questionKey]) {
|
||||
voteMap.value[questionKey] = {}
|
||||
}
|
||||
voteMap.value[questionKey][voteKey] = voteValue
|
||||
}
|
||||
|
||||
//初始化投票题的数据
|
||||
const initVoteData = async () => {
|
||||
const surveyStore = useSurveyStore()
|
||||
const surveyPath = surveyStore.surveyPath
|
||||
|
||||
const fieldList = []
|
||||
|
||||
for (const field in questionData.value) {
|
||||
const { type } = questionData.value[field]
|
||||
if (/vote/.test(type)) {
|
||||
fieldList.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldList.length <= 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(VOTE_INFO_KEY)
|
||||
const voteRes = await queryVote({
|
||||
surveyPath,
|
||||
fieldList: fieldList.join(',')
|
||||
})
|
||||
|
||||
if (voteRes.code === 200) {
|
||||
localStorage.setItem(
|
||||
VOTE_INFO_KEY,
|
||||
JSON.stringify({
|
||||
...voteRes.data
|
||||
})
|
||||
)
|
||||
setVoteMap(voteRes.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateVoteData = (data) => {
|
||||
const { key: questionKey, value: questionVal } = data
|
||||
// 更新前获取接口缓存在localStorage中的数据
|
||||
const localData = localStorage.getItem(VOTE_INFO_KEY)
|
||||
const voteinfo = JSON.parse(localData)
|
||||
const currentQuestion = questionData.value[questionKey]
|
||||
const options = currentQuestion.options
|
||||
const voteTotal = voteinfo?.[questionKey]?.total || 0
|
||||
let totalPayload = {
|
||||
questionKey,
|
||||
voteKey: 'total',
|
||||
voteValue: voteTotal
|
||||
}
|
||||
options.forEach((option) => {
|
||||
const optionhash = option.hash
|
||||
const voteCount = voteinfo?.[questionKey]?.[optionhash] || 0
|
||||
// 如果选中值包含该选项,对应voteCount 和 voteTotal + 1
|
||||
if (
|
||||
Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash
|
||||
) {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
voteKey: optionhash,
|
||||
voteValue: voteCount + 1
|
||||
}
|
||||
totalPayload.voteValue += 1
|
||||
updateVoteMapByKey(countPayload)
|
||||
} else {
|
||||
const countPayload = {
|
||||
questionKey,
|
||||
voteKey: optionhash,
|
||||
voteValue: voteCount
|
||||
}
|
||||
updateVoteMapByKey(countPayload)
|
||||
}
|
||||
updateVoteMapByKey(totalPayload)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const setChangeField = (field) => {
|
||||
changeField.value = field
|
||||
@ -199,7 +296,7 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
needHideFields.value = needHideFields.value.filter((field) => !fields.includes(field))
|
||||
}
|
||||
return {
|
||||
voteMap,
|
||||
|
||||
questionData,
|
||||
questionSeq,
|
||||
renderData,
|
||||
@ -209,8 +306,8 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
setQuestionData,
|
||||
changeSelectMoreData,
|
||||
setQuestionSeq,
|
||||
voteMap,
|
||||
setVoteMap,
|
||||
updateVoteMapByKey,
|
||||
initVoteData,
|
||||
updateVoteData,
|
||||
changeField,
|
||||
@ -219,6 +316,9 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
needHideFields,
|
||||
addNeedHideFields,
|
||||
removeNeedHideFields,
|
||||
getQuestionIndexByField
|
||||
getQuestionIndexByField,
|
||||
quotaMap,
|
||||
initQuotaMap,
|
||||
updateQuotaData
|
||||
}
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { defineStore } from 'pinia'
|
||||
import { pick } from 'lodash-es'
|
||||
import { cloneDeep, pick } from 'lodash-es'
|
||||
|
||||
import { isMobile as isInMobile } from '@/render/utils/index'
|
||||
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
|
||||
@ -12,12 +12,16 @@ import moment from 'moment'
|
||||
// 引入中文
|
||||
import 'moment/locale/zh-cn'
|
||||
// 设置中文
|
||||
moment.locale('zh-cn')
|
||||
|
||||
|
||||
import adapter from '../adapter'
|
||||
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
||||
// import { jumpLogicRule } from '@/common/logicEngine/jumpLogicRule'
|
||||
import useCommandComponent from '../hooks/useCommandComponent'
|
||||
import BackAnswerDialog from '../components/BackAnswerDialog.vue'
|
||||
|
||||
const confirm = useCommandComponent(BackAnswerDialog)
|
||||
|
||||
moment.locale('zh-cn')
|
||||
/**
|
||||
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
||||
*/
|
||||
@ -26,6 +30,8 @@ const CODE_MAP = {
|
||||
ERROR: 500,
|
||||
NO_AUTH: 403
|
||||
}
|
||||
|
||||
|
||||
export const useSurveyStore = defineStore('survey', () => {
|
||||
const surveyPath = ref('')
|
||||
const isMobile = ref(isInMobile())
|
||||
@ -109,13 +115,11 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
|
||||
return isSuccess
|
||||
}
|
||||
const initSurvey = (option) => {
|
||||
setEnterTime()
|
||||
|
||||
if (!canFillQuestionnaire(option.baseConf, option.submitConf)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 加载空白页面
|
||||
function clearFormData(option) {
|
||||
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
|
||||
const {
|
||||
questionData,
|
||||
@ -134,6 +138,7 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
'pageConf'
|
||||
])
|
||||
)
|
||||
// todo: 建议通过questionStore提供setqueationdata方法修改属性,否则不好跟踪变化
|
||||
questionStore.questionData = questionData
|
||||
questionStore.questionSeq = questionSeq
|
||||
|
||||
@ -148,8 +153,75 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
formValues.value = _formValues
|
||||
whiteData.value = option.whiteData
|
||||
pageConf.value = option.pageConf
|
||||
|
||||
// 获取已投票数据
|
||||
questionStore.initVoteData()
|
||||
questionStore.initQuotaMap()
|
||||
|
||||
}
|
||||
function fillFormData(formData) {
|
||||
const _formValues = cloneDeep(formValues.value)
|
||||
for(const key in formData){
|
||||
_formValues[key] = formData[key]
|
||||
}
|
||||
formValues.value = _formValues
|
||||
}
|
||||
const initSurvey = (option) => {
|
||||
|
||||
setEnterTime()
|
||||
if (!canFillQuestionnaire(option.baseConf, option.submitConf)) {
|
||||
return
|
||||
}
|
||||
// 加载空白问卷
|
||||
clearFormData(option)
|
||||
|
||||
const { breakAnswer, backAnswer } = option.baseConf
|
||||
const localData = JSON.parse(localStorage.getItem(surveyPath.value + "_questionData"))
|
||||
|
||||
const isSubmit = JSON.parse(localStorage.getItem('isSubmit'))
|
||||
|
||||
if(localData) {
|
||||
// 断点续答
|
||||
if(breakAnswer) {
|
||||
confirm({
|
||||
title: "是否继续上次填写的内容?",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
// 回填答题内容
|
||||
fillFormData(localData)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
confirm.close()
|
||||
}
|
||||
},
|
||||
onCancel: async() => {
|
||||
confirm.close()
|
||||
}
|
||||
})
|
||||
} else if (backAnswer) {
|
||||
if(isSubmit){
|
||||
confirm({
|
||||
title: "是否继续上次提交的内容?",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
// 回填答题内容
|
||||
fillFormData(localData)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
confirm.close()
|
||||
}
|
||||
},
|
||||
onCancel: async() => {
|
||||
confirm.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearFormData(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 用户输入或者选择后,更新表单数据
|
||||
@ -163,11 +235,11 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
|
||||
const showLogicEngine = ref()
|
||||
const initShowLogicEngine = (showLogicConf) => {
|
||||
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf)
|
||||
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf || [])
|
||||
}
|
||||
const jumpLogicEngine = ref()
|
||||
const initJumpLogicEngine = (jumpLogicConf) => {
|
||||
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf)
|
||||
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf || [])
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -123,6 +123,10 @@ export default defineConfig({
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/exportfile': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
// 静态文件的默认存储文件夹
|
||||
'/userUpload': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
|
Loading…
Reference in New Issue
Block a user