【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
|
dist
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
@ -25,7 +26,10 @@ pnpm-debug.log*
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.history
|
.history
|
||||||
|
|
||||||
components.d.ts
|
components.d.ts
|
||||||
|
|
||||||
# 默认的上传文件夹
|
# 默认的上传文件夹
|
||||||
userUpload
|
userUpload
|
||||||
|
exportfile
|
||||||
|
yarn.lock
|
@ -52,6 +52,9 @@ http {
|
|||||||
proxy_pass http://127.0.0.1:3000;
|
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
|
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
|
||||||
location /userUpload {
|
location /userUpload {
|
||||||
|
12
server/.env
12
server/.env
@ -1,9 +1,15 @@
|
|||||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
XIAOJU_SURVEY_MONGO_URL= # mongodb://127.0.0.1:27017 # 建议设置强密码
|
||||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
||||||
|
|
||||||
|
XIAOJU_SURVEY_REDIS_HOST=
|
||||||
|
XIAOJU_SURVEY_REDIS_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_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||||
|
|
||||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||||
|
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@ -13,6 +13,7 @@ pnpm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -38,3 +39,5 @@ lerna-debug.log*
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
tmp
|
tmp
|
||||||
|
exportfile
|
||||||
|
userUpload
|
@ -27,10 +27,11 @@
|
|||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/typeorm": "^10.0.1",
|
"@nestjs/typeorm": "^10.0.1",
|
||||||
"ali-oss": "^6.20.0",
|
"ali-oss": "^6.20.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.3.2",
|
"dotenv": "^16.3.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"ioredis": "^5.4.1",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -41,11 +42,14 @@
|
|||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
|
"node-xlsx": "^0.24.0",
|
||||||
"qiniu": "^7.11.1",
|
"qiniu": "^7.11.1",
|
||||||
|
"redlock": "^5.0.0-beta.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0",
|
||||||
"typeorm": "^0.3.19"
|
"typeorm": "^0.3.19",
|
||||||
|
"xss": "^1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
@ -70,6 +74,7 @@
|
|||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"mongodb-memory-server": "^9.1.4",
|
"mongodb-memory-server": "^9.1.4",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
|
"redis-memory-server": "^0.11.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { RedisMemoryServer } from 'redis-memory-server';
|
||||||
|
|
||||||
async function startServerAndRunScript() {
|
async function startServerAndRunScript() {
|
||||||
// 启动 MongoDB 内存服务器
|
// 启动 MongoDB 内存服务器
|
||||||
@ -8,12 +9,19 @@ async function startServerAndRunScript() {
|
|||||||
|
|
||||||
console.log('MongoDB Memory Server started:', mongoUri);
|
console.log('MongoDB Memory Server started:', mongoUri);
|
||||||
|
|
||||||
|
const redisServer = new RedisMemoryServer();
|
||||||
|
const redisHost = await redisServer.getHost();
|
||||||
|
const redisPort = await redisServer.getPort();
|
||||||
|
|
||||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||||
const tsnode = spawn(
|
const tsnode = spawn(
|
||||||
'cross-env',
|
'cross-env',
|
||||||
[
|
[
|
||||||
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||||
|
`XIAOJU_SURVEY_REDIS_HOST=${redisHost}`,
|
||||||
|
`XIAOJU_SURVEY_REDIS_PORT=${redisPort}`,
|
||||||
'NODE_ENV=development',
|
'NODE_ENV=development',
|
||||||
|
'SERVER_ENV=local',
|
||||||
'npm',
|
'npm',
|
||||||
'run',
|
'run',
|
||||||
'start:dev',
|
'start:dev',
|
||||||
@ -31,9 +39,10 @@ async function startServerAndRunScript() {
|
|||||||
console.error(data);
|
console.error(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
tsnode.on('close', (code) => {
|
tsnode.on('close', async (code) => {
|
||||||
console.log(`Nodemon process exited with code ${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 { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||||
import { Logger } from './logger';
|
import { XiaojuSurveyLogger } from './logger';
|
||||||
|
import { DownloadTask } from './models/downloadTask.entity';
|
||||||
|
import { Session } from './models/session.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -81,6 +83,8 @@ import { Logger } from './logger';
|
|||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
Collaborator,
|
Collaborator,
|
||||||
|
DownloadTask,
|
||||||
|
Session,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -128,7 +132,7 @@ export class AppModule {
|
|||||||
),
|
),
|
||||||
new SurveyUtilPlugin(),
|
new SurveyUtilPlugin(),
|
||||||
);
|
);
|
||||||
Logger.init({
|
XiaojuSurveyLogger.init({
|
||||||
filename: this.configService.get<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
|
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_TYPE_ERROR = 3003, // 问卷类型错误
|
||||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||||
|
SURVEY_SAVE_CONFLICT = 3006, // 问卷冲突
|
||||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||||
WHITELIST_ERROR = 4002, // 白名单校验错误
|
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||||
|
|
||||||
|
@ -6,6 +6,9 @@ export enum RECORD_STATUS {
|
|||||||
PUBLISHED = 'published', // 发布
|
PUBLISHED = 'published', // 发布
|
||||||
REMOVED = 'removed', // 删除
|
REMOVED = 'removed', // 删除
|
||||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
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 { get } from 'lodash';
|
||||||
|
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
|
||||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
|
@ -60,6 +60,7 @@ export interface DataItem {
|
|||||||
rangeConfig?: any;
|
rangeConfig?: any;
|
||||||
starStyle?: string;
|
starStyle?: string;
|
||||||
innerType?: string;
|
innerType?: string;
|
||||||
|
quotaNoDisplay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
@ -69,6 +70,7 @@ export interface Option {
|
|||||||
othersKey?: string;
|
othersKey?: string;
|
||||||
placeholderDesc: string;
|
placeholderDesc: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
|
quota?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataConf {
|
export interface DataConf {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import * as log4js from 'log4js';
|
import * as log4js from 'log4js';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Request } from 'express';
|
import { Injectable, Scope } from '@nestjs/common';
|
||||||
const log4jsLogger = log4js.getLogger();
|
const log4jsLogger = log4js.getLogger();
|
||||||
|
|
||||||
export class Logger {
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
|
export class XiaojuSurveyLogger {
|
||||||
private static inited = false;
|
private static inited = false;
|
||||||
|
private traceId: string;
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
static init(config: { filename: string }) {
|
static init(config: { filename: string }) {
|
||||||
if (this.inited) {
|
if (XiaojuSurveyLogger.inited) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log4js.configure({
|
log4js.configure({
|
||||||
@ -30,25 +30,28 @@ export class Logger {
|
|||||||
default: { appenders: ['app'], level: 'trace' },
|
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 datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
const level = options?.level;
|
const level = options?.level;
|
||||||
const dltag = options?.dltag ? `${options.dltag}||` : '';
|
const dltag = options?.dltag ? `${options.dltag}||` : '';
|
||||||
const traceIdStr = options?.req?.['traceId']
|
const traceIdStr = this.traceId ? `traceid=${this.traceId}||` : '';
|
||||||
? `traceid=${options?.req?.['traceId']}||`
|
|
||||||
: '';
|
|
||||||
return log4jsLogger[level](
|
return log4jsLogger[level](
|
||||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
`[${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' });
|
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' });
|
return this._log(message, { ...options, level: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
import { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
import { Logger } from './index';
|
import { XiaojuSurveyLogger } from './index';
|
||||||
|
|
||||||
export const LoggerProvider: Provider = {
|
export const LoggerProvider: Provider = {
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useClass: Logger,
|
useClass: XiaojuSurveyLogger,
|
||||||
};
|
};
|
||||||
|
@ -10,9 +10,9 @@ const getCountStr = () => {
|
|||||||
|
|
||||||
export const genTraceId = ({ ip }) => {
|
export const genTraceId = ({ ip }) => {
|
||||||
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
||||||
ip = ip.replace('::ffff:', '');
|
ip = ip.replace('::ffff:', '').replace('::1', '');
|
||||||
let ipArr;
|
let ipArr;
|
||||||
if (ip.indexOf(':') > 0) {
|
if (ip.indexOf(':') >= 0) {
|
||||||
ipArr = ip.split(':').map((segment) => {
|
ipArr = ip.split(':').map((segment) => {
|
||||||
// 将IPv6每个段转为16位,并补0到长度为4
|
// 将IPv6每个段转为16位,并补0到长度为4
|
||||||
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
// logger.middleware.ts
|
// logger.middleware.ts
|
||||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
import { XiaojuSurveyLogger } from '../logger/index'; // 替换为你实际的logger路径
|
||||||
import { genTraceId } from '../logger/util';
|
import { genTraceId } from '../logger/util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogRequestMiddleware implements NestMiddleware {
|
export class LogRequestMiddleware implements NestMiddleware {
|
||||||
constructor(private readonly logger: Logger) {}
|
constructor(private readonly logger: XiaojuSurveyLogger) {}
|
||||||
|
|
||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
const { method, originalUrl, ip } = req;
|
const { method, originalUrl, ip } = req;
|
||||||
const userAgent = req.get('user-agent') || '';
|
const userAgent = req.get('user-agent') || '';
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const traceId = genTraceId({ ip });
|
const traceId = genTraceId({ ip });
|
||||||
req['traceId'] = traceId;
|
this.logger.setTraceId(traceId);
|
||||||
const query = JSON.stringify(req.query);
|
const query = JSON.stringify(req.query);
|
||||||
const body = JSON.stringify(req.body);
|
const body = JSON.stringify(req.body);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||||
{
|
{
|
||||||
dltag: 'request_in',
|
dltag: 'request_in',
|
||||||
req,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -30,7 +29,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
|||||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||||
{
|
{
|
||||||
dltag: 'request_out',
|
dltag: 'request_out',
|
||||||
req,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,8 +5,7 @@ import { BaseEntity } from './base.entity';
|
|||||||
@Entity({ name: 'captcha' })
|
@Entity({ name: 'captcha' })
|
||||||
export class Captcha extends BaseEntity {
|
export class Captcha extends BaseEntity {
|
||||||
@Index({
|
@Index({
|
||||||
expireAfterSeconds:
|
expireAfterSeconds: 3600,
|
||||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
|
||||||
})
|
})
|
||||||
@ObjectIdColumn()
|
@ObjectIdColumn()
|
||||||
_id: ObjectId;
|
_id: ObjectId;
|
||||||
|
@ -6,8 +6,7 @@ import { BaseEntity } from './base.entity';
|
|||||||
@Entity({ name: 'clientEncrypt' })
|
@Entity({ name: 'clientEncrypt' })
|
||||||
export class ClientEncrypt extends BaseEntity {
|
export class ClientEncrypt extends BaseEntity {
|
||||||
@Index({
|
@Index({
|
||||||
expireAfterSeconds:
|
expireAfterSeconds: 3600,
|
||||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
|
||||||
})
|
})
|
||||||
@ObjectIdColumn()
|
@ObjectIdColumn()
|
||||||
_id: ObjectId;
|
_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;
|
username: string;
|
||||||
_id: 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 { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { Authentication } from 'src/guards/authentication.guard';
|
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;
|
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,
|
configKey,
|
||||||
file,
|
file,
|
||||||
pathPrefix,
|
pathPrefix,
|
||||||
|
keepOriginFilename,
|
||||||
}: {
|
}: {
|
||||||
configKey: string;
|
configKey: string;
|
||||||
file: Express.Multer.File;
|
file: Express.Multer.File;
|
||||||
pathPrefix: string;
|
pathPrefix: string;
|
||||||
|
keepOriginFilename?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const handler = this.getHandler(configKey);
|
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);
|
const url = await handler.getUrl(key);
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
|
@ -12,9 +12,14 @@ export class LocalHandler implements FileUploadHandler {
|
|||||||
|
|
||||||
async upload(
|
async upload(
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
options?: { pathPrefix?: string },
|
options?: { pathPrefix?: string; keepOriginFilename?: boolean },
|
||||||
): Promise<{ key: string }> {
|
): 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(
|
const filePath = join(
|
||||||
options?.pathPrefix ? options?.pathPrefix : '',
|
options?.pathPrefix ? options?.pathPrefix : '',
|
||||||
filename,
|
filename,
|
||||||
@ -35,6 +40,10 @@ export class LocalHandler implements FileUploadHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUrl(key: string): string {
|
getUrl(key: string): string {
|
||||||
|
if (process.env.SERVER_ENV === 'local') {
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
return `http://localhost:${port}/${key}`;
|
||||||
|
}
|
||||||
return `/${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 { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { CollaboratorController } from '../controllers/collaborator.controller';
|
import { CollaboratorController } from '../controllers/collaborator.controller';
|
||||||
import { CollaboratorService } from '../services/collaborator.service';
|
import { CollaboratorService } from '../services/collaborator.service';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
||||||
import { Collaborator } from 'src/models/collaborator.entity';
|
import { Collaborator } from 'src/models/collaborator.entity';
|
||||||
@ -25,7 +25,7 @@ jest.mock('src/guards/workspace.guard');
|
|||||||
describe('CollaboratorController', () => {
|
describe('CollaboratorController', () => {
|
||||||
let controller: CollaboratorController;
|
let controller: CollaboratorController;
|
||||||
let collaboratorService: CollaboratorService;
|
let collaboratorService: CollaboratorService;
|
||||||
let logger: Logger;
|
let logger: XiaojuSurveyLogger;
|
||||||
let userService: UserService;
|
let userService: UserService;
|
||||||
let surveyMetaService: SurveyMetaService;
|
let surveyMetaService: SurveyMetaService;
|
||||||
let workspaceMemberServie: WorkspaceMemberService;
|
let workspaceMemberServie: WorkspaceMemberService;
|
||||||
@ -50,7 +50,7 @@ describe('CollaboratorController', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
@ -84,7 +84,7 @@ describe('CollaboratorController', () => {
|
|||||||
|
|
||||||
controller = module.get<CollaboratorController>(CollaboratorController);
|
controller = module.get<CollaboratorController>(CollaboratorController);
|
||||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||||
logger = module.get<Logger>(Logger);
|
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||||
userService = module.get<UserService>(UserService);
|
userService = module.get<UserService>(UserService);
|
||||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||||
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||||
@ -191,7 +191,6 @@ describe('CollaboratorController', () => {
|
|||||||
describe('getSurveyCollaboratorList', () => {
|
describe('getSurveyCollaboratorList', () => {
|
||||||
it('should return collaborator list', async () => {
|
it('should return collaborator list', async () => {
|
||||||
const query = { surveyId: 'surveyId' };
|
const query = { surveyId: 'surveyId' };
|
||||||
const req = { user: { _id: 'userId' } };
|
|
||||||
const result = [
|
const result = [
|
||||||
{ _id: 'collaboratorId', userId: 'userId', username: '' },
|
{ _id: 'collaboratorId', userId: 'userId', username: '' },
|
||||||
];
|
];
|
||||||
@ -202,7 +201,7 @@ describe('CollaboratorController', () => {
|
|||||||
|
|
||||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
|
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
|
||||||
|
|
||||||
const response = await controller.getSurveyCollaboratorList(query, req);
|
const response = await controller.getSurveyCollaboratorList(query);
|
||||||
|
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -214,11 +213,10 @@ describe('CollaboratorController', () => {
|
|||||||
const query: GetSurveyCollaboratorListDto = {
|
const query: GetSurveyCollaboratorListDto = {
|
||||||
surveyId: '',
|
surveyId: '',
|
||||||
};
|
};
|
||||||
const req = { user: { _id: 'userId' } };
|
|
||||||
|
|
||||||
await expect(
|
await expect(controller.getSurveyCollaboratorList(query)).rejects.toThrow(
|
||||||
controller.getSurveyCollaboratorList(query, req),
|
HttpException,
|
||||||
).rejects.toThrow(HttpException);
|
);
|
||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -230,14 +228,13 @@ describe('CollaboratorController', () => {
|
|||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
permissions: ['read'],
|
permissions: ['read'],
|
||||||
};
|
};
|
||||||
const req = { user: { _id: 'userId' } };
|
|
||||||
const result = { _id: 'userId', permissions: ['read'] };
|
const result = { _id: 'userId', permissions: ['read'] };
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(collaboratorService, 'changeUserPermission')
|
.spyOn(collaboratorService, 'changeUserPermission')
|
||||||
.mockResolvedValue(result);
|
.mockResolvedValue(result);
|
||||||
|
|
||||||
const response = await controller.changeUserPermission(reqBody, req);
|
const response = await controller.changeUserPermission(reqBody);
|
||||||
|
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -251,11 +248,10 @@ describe('CollaboratorController', () => {
|
|||||||
userId: '',
|
userId: '',
|
||||||
permissions: ['surveyManage'],
|
permissions: ['surveyManage'],
|
||||||
};
|
};
|
||||||
const req = { user: { _id: 'userId' } };
|
|
||||||
|
|
||||||
await expect(
|
await expect(controller.changeUserPermission(reqBody)).rejects.toThrow(
|
||||||
controller.changeUserPermission(reqBody, req),
|
HttpException,
|
||||||
).rejects.toThrow(HttpException);
|
);
|
||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -263,14 +259,13 @@ describe('CollaboratorController', () => {
|
|||||||
describe('deleteCollaborator', () => {
|
describe('deleteCollaborator', () => {
|
||||||
it('should delete collaborator successfully', async () => {
|
it('should delete collaborator successfully', async () => {
|
||||||
const query = { surveyId: 'surveyId', userId: 'userId' };
|
const query = { surveyId: 'surveyId', userId: 'userId' };
|
||||||
const req = { user: { _id: 'userId' } };
|
|
||||||
const result = { acknowledged: true, deletedCount: 1 };
|
const result = { acknowledged: true, deletedCount: 1 };
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(collaboratorService, 'deleteCollaborator')
|
.spyOn(collaboratorService, 'deleteCollaborator')
|
||||||
.mockResolvedValue(result);
|
.mockResolvedValue(result);
|
||||||
|
|
||||||
const response = await controller.deleteCollaborator(query, req);
|
const response = await controller.deleteCollaborator(query);
|
||||||
|
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -280,9 +275,8 @@ describe('CollaboratorController', () => {
|
|||||||
|
|
||||||
it('should throw an exception if validation fails', async () => {
|
it('should throw an exception if validation fails', async () => {
|
||||||
const query = { surveyId: '', userId: '' };
|
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,
|
HttpException,
|
||||||
);
|
);
|
||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
|
@ -3,13 +3,13 @@ import { CollaboratorService } from '../services/collaborator.service';
|
|||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { Collaborator } from 'src/models/collaborator.entity';
|
import { Collaborator } from 'src/models/collaborator.entity';
|
||||||
import { MongoRepository } from 'typeorm';
|
import { MongoRepository } from 'typeorm';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { InsertManyResult, ObjectId } from 'mongodb';
|
import { InsertManyResult, ObjectId } from 'mongodb';
|
||||||
|
|
||||||
describe('CollaboratorService', () => {
|
describe('CollaboratorService', () => {
|
||||||
let service: CollaboratorService;
|
let service: CollaboratorService;
|
||||||
let repository: MongoRepository<Collaborator>;
|
let repository: MongoRepository<Collaborator>;
|
||||||
let logger: Logger;
|
let logger: XiaojuSurveyLogger;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -20,7 +20,7 @@ describe('CollaboratorService', () => {
|
|||||||
useClass: MongoRepository,
|
useClass: MongoRepository,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
},
|
},
|
||||||
@ -32,7 +32,7 @@ describe('CollaboratorService', () => {
|
|||||||
repository = module.get<MongoRepository<Collaborator>>(
|
repository = module.get<MongoRepository<Collaborator>>(
|
||||||
getRepositoryToken(Collaborator),
|
getRepositoryToken(Collaborator),
|
||||||
);
|
);
|
||||||
logger = module.get<Logger>(Logger);
|
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
|
@ -9,7 +9,7 @@ import { ResponseSchemaService } from '../../surveyResponse/services/responseSch
|
|||||||
|
|
||||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
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 { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||||
@ -28,7 +28,7 @@ describe('DataStatisticController', () => {
|
|||||||
let dataStatisticService: DataStatisticService;
|
let dataStatisticService: DataStatisticService;
|
||||||
let responseSchemaService: ResponseSchemaService;
|
let responseSchemaService: ResponseSchemaService;
|
||||||
let pluginManager: XiaojuSurveyPluginManager;
|
let pluginManager: XiaojuSurveyPluginManager;
|
||||||
let logger: Logger;
|
let logger: XiaojuSurveyLogger;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -56,7 +56,7 @@ describe('DataStatisticController', () => {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
},
|
},
|
||||||
@ -73,7 +73,7 @@ describe('DataStatisticController', () => {
|
|||||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||||
XiaojuSurveyPluginManager,
|
XiaojuSurveyPluginManager,
|
||||||
);
|
);
|
||||||
logger = module.get<Logger>(Logger);
|
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||||
|
|
||||||
pluginManager.registerPlugin(
|
pluginManager.registerPlugin(
|
||||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||||
@ -123,7 +123,7 @@ describe('DataStatisticController', () => {
|
|||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, mockRequest);
|
const result = await controller.data(mockRequest.query);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -169,7 +169,7 @@ describe('DataStatisticController', () => {
|
|||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, mockRequest);
|
const result = await controller.data(mockRequest.query);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -187,9 +187,9 @@ describe('DataStatisticController', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(
|
await expect(controller.data(mockRequest.query)).rejects.toThrow(
|
||||||
controller.data(mockRequest.query, mockRequest),
|
HttpException,
|
||||||
).rejects.toThrow(HttpException);
|
);
|
||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
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 { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { AuthService } from 'src/modules/auth/services/auth.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/authentication.guard');
|
||||||
jest.mock('src/guards/survey.guard');
|
jest.mock('src/guards/survey.guard');
|
||||||
@ -49,7 +49,7 @@ describe('SurveyHistoryController', () => {
|
|||||||
useClass: jest.fn().mockImplementation(() => ({})),
|
useClass: jest.fn().mockImplementation(() => ({})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
@ -66,7 +66,7 @@ describe('SurveyHistoryController', () => {
|
|||||||
it('should return history list when query is valid', async () => {
|
it('should return history list when query is valid', async () => {
|
||||||
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
||||||
|
|
||||||
await controller.getList(queryInfo, {});
|
await controller.getList(queryInfo);
|
||||||
|
|
||||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
||||||
surveyId: queryInfo.surveyId,
|
surveyId: queryInfo.surveyId,
|
||||||
|
@ -78,7 +78,13 @@ describe('SurveyHistoryService', () => {
|
|||||||
.spyOn(repository, 'save')
|
.spyOn(repository, 'save')
|
||||||
.mockResolvedValueOnce({} as SurveyHistory);
|
.mockResolvedValueOnce({} as SurveyHistory);
|
||||||
|
|
||||||
await service.addHistory({ surveyId, schema, type, user });
|
await service.addHistory({
|
||||||
|
surveyId,
|
||||||
|
schema,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
sessionId: '',
|
||||||
|
});
|
||||||
|
|
||||||
expect(spyCreate).toHaveBeenCalledWith({
|
expect(spyCreate).toHaveBeenCalledWith({
|
||||||
pageId: surveyId,
|
pageId: surveyId,
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
SURVEY_PERMISSION,
|
SURVEY_PERMISSION,
|
||||||
SURVEY_PERMISSION_DESCRIPTION,
|
SURVEY_PERMISSION_DESCRIPTION,
|
||||||
} from 'src/enums/surveyPermission';
|
} from 'src/enums/surveyPermission';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
|
||||||
import { CollaboratorService } from '../services/collaborator.service';
|
import { CollaboratorService } from '../services/collaborator.service';
|
||||||
@ -40,7 +40,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
|||||||
export class CollaboratorController {
|
export class CollaboratorController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly collaboratorService: CollaboratorService,
|
private readonly collaboratorService: CollaboratorService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly surveyMetaService: SurveyMetaService,
|
private readonly surveyMetaService: SurveyMetaService,
|
||||||
private readonly workspaceMemberServie: WorkspaceMemberService,
|
private readonly workspaceMemberServie: WorkspaceMemberService,
|
||||||
@ -69,7 +69,7 @@ export class CollaboratorController {
|
|||||||
) {
|
) {
|
||||||
const { error, value } = CreateCollaboratorDto.validate(reqBody);
|
const { error, value } = CreateCollaboratorDto.validate(reqBody);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'系统错误,请联系管理员',
|
'系统错误,请联系管理员',
|
||||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||||
@ -124,7 +124,7 @@ export class CollaboratorController {
|
|||||||
) {
|
) {
|
||||||
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
|
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'系统错误,请联系管理员',
|
'系统错误,请联系管理员',
|
||||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||||
@ -184,7 +184,7 @@ export class CollaboratorController {
|
|||||||
neIdList: collaboratorIdList,
|
neIdList: collaboratorIdList,
|
||||||
userIdList: newCollaboratorUserIdList,
|
userIdList: newCollaboratorUserIdList,
|
||||||
});
|
});
|
||||||
this.logger.info('batchDelete:' + JSON.stringify(delRes), { req });
|
this.logger.info('batchDelete:' + JSON.stringify(delRes));
|
||||||
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
||||||
const insertRes = await this.collaboratorService.batchCreate({
|
const insertRes = await this.collaboratorService.batchCreate({
|
||||||
surveyId: value.surveyId,
|
surveyId: value.surveyId,
|
||||||
@ -208,7 +208,7 @@ export class CollaboratorController {
|
|||||||
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
|
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
|
||||||
value.surveyId,
|
value.surveyId,
|
||||||
);
|
);
|
||||||
this.logger.info(JSON.stringify(delRes), { req });
|
this.logger.info(JSON.stringify(delRes));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -225,11 +225,10 @@ export class CollaboratorController {
|
|||||||
])
|
])
|
||||||
async getSurveyCollaboratorList(
|
async getSurveyCollaboratorList(
|
||||||
@Query() query: GetSurveyCollaboratorListDto,
|
@Query() query: GetSurveyCollaboratorListDto,
|
||||||
@Request() req,
|
|
||||||
) {
|
) {
|
||||||
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
|
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,17 +262,14 @@ export class CollaboratorController {
|
|||||||
@SetMetadata('surveyPermission', [
|
@SetMetadata('surveyPermission', [
|
||||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||||
])
|
])
|
||||||
async changeUserPermission(
|
async changeUserPermission(@Body() reqBody: ChangeUserPermissionDto) {
|
||||||
@Body() reqBody: ChangeUserPermissionDto,
|
|
||||||
@Request() req,
|
|
||||||
) {
|
|
||||||
const { error, value } = Joi.object({
|
const { error, value } = Joi.object({
|
||||||
surveyId: Joi.string(),
|
surveyId: Joi.string(),
|
||||||
userId: Joi.string(),
|
userId: Joi.string(),
|
||||||
permissions: Joi.array().items(Joi.string().required()),
|
permissions: Joi.array().items(Joi.string().required()),
|
||||||
}).validate(reqBody);
|
}).validate(reqBody);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,13 +288,13 @@ export class CollaboratorController {
|
|||||||
@SetMetadata('surveyPermission', [
|
@SetMetadata('surveyPermission', [
|
||||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||||
])
|
])
|
||||||
async deleteCollaborator(@Query() query, @Request() req) {
|
async deleteCollaborator(@Query() query) {
|
||||||
const { error, value } = Joi.object({
|
const { error, value } = Joi.object({
|
||||||
surveyId: Joi.string(),
|
surveyId: Joi.string(),
|
||||||
userId: Joi.string(),
|
userId: Joi.string(),
|
||||||
}).validate(query);
|
}).validate(query);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +315,7 @@ export class CollaboratorController {
|
|||||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||||
|
|
||||||
if (!surveyMeta) {
|
if (!surveyMeta) {
|
||||||
this.logger.error(`问卷不存在: ${surveyId}`, { req });
|
this.logger.error(`问卷不存在: ${surveyId}`);
|
||||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
SetMetadata,
|
SetMetadata,
|
||||||
Request,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
@ -17,7 +16,7 @@ import { Authentication } from 'src/guards/authentication.guard';
|
|||||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
||||||
@ -32,7 +31,7 @@ export class DataStatisticController {
|
|||||||
private readonly responseSchemaService: ResponseSchemaService,
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
private readonly dataStatisticService: DataStatisticService,
|
private readonly dataStatisticService: DataStatisticService,
|
||||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/dataTable')
|
@Get('/dataTable')
|
||||||
@ -44,7 +43,6 @@ export class DataStatisticController {
|
|||||||
async data(
|
async data(
|
||||||
@Query()
|
@Query()
|
||||||
queryInfo,
|
queryInfo,
|
||||||
@Request() req,
|
|
||||||
) {
|
) {
|
||||||
const { value, error } = await Joi.object({
|
const { value, error } = await Joi.object({
|
||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
@ -53,7 +51,7 @@ export class DataStatisticController {
|
|||||||
pageSize: Joi.number().default(10),
|
pageSize: Joi.number().default(10),
|
||||||
}).validate(queryInfo);
|
}).validate(queryInfo);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const { surveyId, isDesensitive, page, pageSize } = value;
|
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 { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||||
import { ContentSecurityService } from '../services/contentSecurity.service';
|
import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||||
|
import { CounterService } from 'src/modules/surveyResponse/services/counter.service';
|
||||||
|
|
||||||
import BannerData from '../template/banner/index.json';
|
import BannerData from '../template/banner/index.json';
|
||||||
import { CreateSurveyDto } from '../dto/createSurvey.dto';
|
import { CreateSurveyDto } from '../dto/createSurvey.dto';
|
||||||
@ -25,13 +26,15 @@ import { Authentication } from 'src/guards/authentication.guard';
|
|||||||
import { HISTORY_TYPE } from 'src/enums';
|
import { HISTORY_TYPE } from 'src/enums';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
|
||||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||||
|
import { SessionService } from '../services/session.service';
|
||||||
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@Controller('/api/survey')
|
@Controller('/api/survey')
|
||||||
@ -42,7 +45,10 @@ export class SurveyController {
|
|||||||
private readonly responseSchemaService: ResponseSchemaService,
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
private readonly contentSecurityService: ContentSecurityService,
|
private readonly contentSecurityService: ContentSecurityService,
|
||||||
private readonly surveyHistoryService: SurveyHistoryService,
|
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')
|
@Get('/getBannerData')
|
||||||
@ -71,9 +77,7 @@ export class SurveyController {
|
|||||||
) {
|
) {
|
||||||
const { error, value } = CreateSurveyDto.validate(reqBody);
|
const { error, value } = CreateSurveyDto.validate(reqBody);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
|
this.logger.error(`createSurvey_parameter error: ${error.message}`);
|
||||||
req,
|
|
||||||
});
|
|
||||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,13 +133,41 @@ export class SurveyController {
|
|||||||
const { value, error } = Joi.object({
|
const { value, error } = Joi.object({
|
||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
configData: Joi.any().required(),
|
configData: Joi.any().required(),
|
||||||
|
sessionId: Joi.string().required(),
|
||||||
}).validate(surveyInfo);
|
}).validate(surveyInfo);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const username = req.user.username;
|
const sessionId = value.sessionId;
|
||||||
const surveyId = value.surveyId;
|
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;
|
const configData = value.configData;
|
||||||
await this.surveyConfService.saveSurveyConf({
|
await this.surveyConfService.saveSurveyConf({
|
||||||
@ -198,7 +230,7 @@ export class SurveyController {
|
|||||||
}).validate(queryInfo);
|
}).validate(queryInfo);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,15 +273,13 @@ export class SurveyController {
|
|||||||
queryInfo: {
|
queryInfo: {
|
||||||
surveyPath: string;
|
surveyPath: string;
|
||||||
},
|
},
|
||||||
@Request()
|
|
||||||
req,
|
|
||||||
) {
|
) {
|
||||||
const { value, error } = Joi.object({
|
const { value, error } = Joi.object({
|
||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
}).validate({ surveyId: queryInfo.surveyPath });
|
}).validate({ surveyId: queryInfo.surveyPath });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const surveyId = value.surveyId;
|
const surveyId = value.surveyId;
|
||||||
@ -282,7 +312,7 @@ export class SurveyController {
|
|||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
}).validate(surveyInfo);
|
}).validate(surveyInfo);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const username = req.user.username;
|
const username = req.user.username;
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
SetMetadata,
|
SetMetadata,
|
||||||
Request,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
@ -15,16 +14,15 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
|
|||||||
import { Authentication } from 'src/guards/authentication.guard';
|
import { Authentication } from 'src/guards/authentication.guard';
|
||||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@Controller('/api/surveyHisotry')
|
@Controller('/api/surveyHisotry')
|
||||||
export class SurveyHistoryController {
|
export class SurveyHistoryController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly surveyHistoryService: SurveyHistoryService,
|
private readonly surveyHistoryService: SurveyHistoryService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/getList')
|
@Get('/getList')
|
||||||
@ -43,7 +41,6 @@ export class SurveyHistoryController {
|
|||||||
surveyId: string;
|
surveyId: string;
|
||||||
historyType: string;
|
historyType: string;
|
||||||
},
|
},
|
||||||
@Request() req,
|
|
||||||
) {
|
) {
|
||||||
const { value, error } = Joi.object({
|
const { value, error } = Joi.object({
|
||||||
surveyId: Joi.string().required(),
|
surveyId: Joi.string().required(),
|
||||||
@ -51,7 +48,7 @@ export class SurveyHistoryController {
|
|||||||
}).validate(queryInfo);
|
}).validate(queryInfo);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
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 { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
import { Authentication } from 'src/guards/authentication.guard';
|
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 { SurveyGuard } from 'src/guards/survey.guard';
|
||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||||
@ -33,7 +33,7 @@ import { CollaboratorService } from '../services/collaborator.service';
|
|||||||
export class SurveyMetaController {
|
export class SurveyMetaController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly surveyMetaService: SurveyMetaService,
|
private readonly surveyMetaService: SurveyMetaService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
private readonly collaboratorService: CollaboratorService,
|
private readonly collaboratorService: CollaboratorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -51,9 +51,7 @@ export class SurveyMetaController {
|
|||||||
}).validate(reqBody, { allowUnknown: true });
|
}).validate(reqBody, { allowUnknown: true });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||||
req,
|
|
||||||
});
|
|
||||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const survey = req.surveyMeta;
|
const survey = req.surveyMeta;
|
||||||
@ -81,7 +79,7 @@ export class SurveyMetaController {
|
|||||||
) {
|
) {
|
||||||
const { value, error } = GetSurveyListDto.validate(queryInfo);
|
const { value, error } = GetSurveyListDto.validate(queryInfo);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
const { curPage, pageSize, workspaceId } = value;
|
const { curPage, pageSize, workspaceId } = value;
|
||||||
@ -91,14 +89,14 @@ export class SurveyMetaController {
|
|||||||
try {
|
try {
|
||||||
filter = getFilter(JSON.parse(decodeURIComponent(value.filter)));
|
filter = getFilter(JSON.parse(decodeURIComponent(value.filter)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (value.order) {
|
if (value.order) {
|
||||||
try {
|
try {
|
||||||
order = order = getOrder(JSON.parse(decodeURIComponent(value.order)));
|
order = order = getOrder(JSON.parse(decodeURIComponent(value.order)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error.message, { req });
|
this.logger.error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userId = req.user._id.toString();
|
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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { MongoRepository } from 'typeorm';
|
import { MongoRepository } from 'typeorm';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollaboratorService {
|
export class CollaboratorService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Collaborator)
|
@InjectRepository(Collaborator)
|
||||||
private readonly collaboratorRepository: MongoRepository<Collaborator>,
|
private readonly collaboratorRepository: MongoRepository<Collaborator>,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create({ surveyId, userId, permissions }) {
|
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 { SurveyResponseModule } from '../surveyResponse/surveyResponse.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
import { FileModule } from '../file/file.module';
|
||||||
|
|
||||||
import { DataStatisticController } from './controllers/dataStatistic.controller';
|
import { DataStatisticController } from './controllers/dataStatistic.controller';
|
||||||
import { SurveyController } from './controllers/survey.controller';
|
import { SurveyController } from './controllers/survey.controller';
|
||||||
@ -14,6 +15,8 @@ import { SurveyHistoryController } from './controllers/surveyHistory.controller'
|
|||||||
import { SurveyMetaController } from './controllers/surveyMeta.controller';
|
import { SurveyMetaController } from './controllers/surveyMeta.controller';
|
||||||
import { SurveyUIController } from './controllers/surveyUI.controller';
|
import { SurveyUIController } from './controllers/surveyUI.controller';
|
||||||
import { CollaboratorController } from './controllers/collaborator.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 { SurveyConf } from 'src/models/surveyConf.entity';
|
||||||
import { SurveyHistory } from 'src/models/surveyHistory.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 { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||||
import { Word } from 'src/models/word.entity';
|
import { Word } from 'src/models/word.entity';
|
||||||
import { Collaborator } from 'src/models/collaborator.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 { DataStatisticService } from './services/dataStatistic.service';
|
||||||
import { SurveyConfService } from './services/surveyConf.service';
|
import { SurveyConfService } from './services/surveyConf.service';
|
||||||
import { SurveyHistoryService } from './services/surveyHistory.service';
|
import { SurveyHistoryService } from './services/surveyHistory.service';
|
||||||
import { SurveyMetaService } from './services/surveyMeta.service';
|
import { SurveyMetaService } from './services/surveyMeta.service';
|
||||||
import { ContentSecurityService } from './services/contentSecurity.service';
|
import { ContentSecurityService } from './services/contentSecurity.service';
|
||||||
import { CollaboratorService } from './services/collaborator.service';
|
import { CollaboratorService } from './services/collaborator.service';
|
||||||
|
import { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -39,11 +49,15 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
SurveyResponse,
|
SurveyResponse,
|
||||||
Word,
|
Word,
|
||||||
Collaborator,
|
Collaborator,
|
||||||
|
Counter,
|
||||||
|
DownloadTask,
|
||||||
|
Session,
|
||||||
]),
|
]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
SurveyResponseModule,
|
SurveyResponseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
|
FileModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
DataStatisticController,
|
DataStatisticController,
|
||||||
@ -52,6 +66,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
SurveyMetaController,
|
SurveyMetaController,
|
||||||
SurveyUIController,
|
SurveyUIController,
|
||||||
CollaboratorController,
|
CollaboratorController,
|
||||||
|
DownloadTaskController,
|
||||||
|
SessionController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DataStatisticService,
|
DataStatisticService,
|
||||||
@ -62,6 +78,10 @@ import { CollaboratorService } from './services/collaborator.service';
|
|||||||
ContentSecurityService,
|
ContentSecurityService,
|
||||||
CollaboratorService,
|
CollaboratorService,
|
||||||
LoggerProvider,
|
LoggerProvider,
|
||||||
|
CounterService,
|
||||||
|
DownloadTaskService,
|
||||||
|
FileService,
|
||||||
|
SessionService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SurveyModule {}
|
export class SurveyModule {}
|
||||||
|
@ -48,7 +48,8 @@
|
|||||||
"mustOthers": false,
|
"mustOthers": false,
|
||||||
"othersKey": "",
|
"othersKey": "",
|
||||||
"placeholderDesc": "",
|
"placeholderDesc": "",
|
||||||
"hash": "115019"
|
"hash": "115019",
|
||||||
|
"quota": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "选项2",
|
"text": "选项2",
|
||||||
@ -57,9 +58,11 @@
|
|||||||
"mustOthers": false,
|
"mustOthers": false,
|
||||||
"othersKey": "",
|
"othersKey": "",
|
||||||
"placeholderDesc": "",
|
"placeholderDesc": "",
|
||||||
"hash": "115020"
|
"hash": "115020",
|
||||||
|
"quota": "0"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"quotaNoDisplay": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,8 @@
|
|||||||
"innerType": "radio",
|
"innerType": "radio",
|
||||||
"field": "data606",
|
"field": "data606",
|
||||||
"title": "标题2",
|
"title": "标题2",
|
||||||
"minNum": "",
|
"minNum": 0,
|
||||||
"maxNum": "",
|
"maxNum": 0,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"text": "选项1",
|
"text": "选项1",
|
||||||
|
@ -20,7 +20,7 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
|
|||||||
|
|
||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
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 { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
@ -122,7 +122,7 @@ describe('SurveyResponseController', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
@ -220,7 +220,8 @@ describe('SurveyResponseController', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(clientEncryptService, 'deleteEncryptInfo')
|
.spyOn(clientEncryptService, 'deleteEncryptInfo')
|
||||||
.mockResolvedValueOnce(undefined);
|
.mockResolvedValueOnce(undefined);
|
||||||
const result = await controller.createResponse(reqBody, {});
|
|
||||||
|
const result = await controller.createResponse(reqBody);
|
||||||
|
|
||||||
expect(result).toEqual({ code: 200, msg: '提交成功' });
|
expect(result).toEqual({ code: 200, msg: '提交成功' });
|
||||||
expect(
|
expect(
|
||||||
@ -267,7 +268,7 @@ describe('SurveyResponseController', () => {
|
|||||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
.mockResolvedValueOnce(null);
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
SurveyNotFoundException,
|
SurveyNotFoundException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -276,7 +277,7 @@ describe('SurveyResponseController', () => {
|
|||||||
const reqBody = cloneDeep(mockSubmitData);
|
const reqBody = cloneDeep(mockSubmitData);
|
||||||
delete reqBody.sign;
|
delete reqBody.sign;
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -289,7 +290,7 @@ describe('SurveyResponseController', () => {
|
|||||||
const reqBody = cloneDeep(mockDecryptErrorBody);
|
const reqBody = cloneDeep(mockDecryptErrorBody);
|
||||||
reqBody.sign = 'mock sign';
|
reqBody.sign = 'mock sign';
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -305,7 +306,7 @@ describe('SurveyResponseController', () => {
|
|||||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
.mockResolvedValueOnce(mockResponseSchema);
|
.mockResolvedValueOnce(mockResponseSchema);
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -317,7 +318,7 @@ describe('SurveyResponseController', () => {
|
|||||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
.mockResolvedValueOnce(mockResponseSchema);
|
.mockResolvedValueOnce(mockResponseSchema);
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -343,7 +344,7 @@ describe('SurveyResponseController', () => {
|
|||||||
},
|
},
|
||||||
} as ResponseSchema);
|
} as ResponseSchema);
|
||||||
|
|
||||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||||
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
|||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
import { Logger } from 'src/logger';
|
import { XiaojuSurveyLogger } from 'src/logger';
|
||||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
import { WhitelistType } from 'src/interfaces/survey';
|
import { WhitelistType } from 'src/interfaces/survey';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
@ -24,7 +24,7 @@ import { WorkspaceMemberService } from 'src/modules/workspace/services/workspace
|
|||||||
export class ResponseSchemaController {
|
export class ResponseSchemaController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly responseSchemaService: ResponseSchemaService,
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
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 { HttpException } from 'src/exceptions/httpException';
|
||||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
import { checkSign } from 'src/utils/checkSign';
|
import { checkSign } from 'src/utils/checkSign';
|
||||||
@ -7,37 +7,48 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
|||||||
import { getPushingData } from 'src/utils/messagePushing';
|
import { getPushingData } from 'src/utils/messagePushing';
|
||||||
|
|
||||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||||
import { CounterService } from '../services/counter.service';
|
|
||||||
import { SurveyResponseService } from '../services/surveyResponse.service';
|
import { SurveyResponseService } from '../services/surveyResponse.service';
|
||||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||||
import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service';
|
import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service';
|
||||||
|
import { RedisService } from 'src/modules/redis/redis.service';
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import * as forge from 'node-forge';
|
import * as forge from 'node-forge';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
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 { WhitelistType } from 'src/interfaces/survey';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
import { QUESTION_TYPE } from 'src/enums/question';
|
||||||
|
|
||||||
|
const optionQuestionType: Array<string> = [
|
||||||
|
QUESTION_TYPE.RADIO,
|
||||||
|
QUESTION_TYPE.CHECKBOX,
|
||||||
|
QUESTION_TYPE.BINARY_CHOICE,
|
||||||
|
QUESTION_TYPE.VOTE,
|
||||||
|
];
|
||||||
|
|
||||||
@ApiTags('surveyResponse')
|
@ApiTags('surveyResponse')
|
||||||
@Controller('/api/surveyResponse')
|
@Controller('/api/surveyResponse')
|
||||||
export class SurveyResponseController {
|
export class SurveyResponseController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly responseSchemaService: ResponseSchemaService,
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
private readonly counterService: CounterService,
|
|
||||||
private readonly surveyResponseService: SurveyResponseService,
|
private readonly surveyResponseService: SurveyResponseService,
|
||||||
private readonly clientEncryptService: ClientEncryptService,
|
private readonly clientEncryptService: ClientEncryptService,
|
||||||
private readonly messagePushingTaskService: MessagePushingTaskService,
|
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 userService: UserService,
|
||||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/createResponse')
|
@Post('/createResponse')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async createResponse(@Body() reqBody, @Request() req) {
|
async createResponse(@Body() reqBody) {
|
||||||
// 检查签名
|
// 检查签名
|
||||||
checkSign(reqBody);
|
checkSign(reqBody);
|
||||||
// 校验参数
|
// 校验参数
|
||||||
@ -53,9 +64,7 @@ export class SurveyResponseController {
|
|||||||
}).validate(reqBody, { allowUnknown: true });
|
}).validate(reqBody, { allowUnknown: true });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||||
req,
|
|
||||||
});
|
|
||||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +214,7 @@ export class SurveyResponseController {
|
|||||||
const optionTextAndId = dataList
|
const optionTextAndId = dataList
|
||||||
.filter((questionItem) => {
|
.filter((questionItem) => {
|
||||||
return (
|
return (
|
||||||
|
optionQuestionType.includes(questionItem.type) &&
|
||||||
Array.isArray(questionItem.options) &&
|
Array.isArray(questionItem.options) &&
|
||||||
questionItem.options.length > 0 &&
|
questionItem.options.length > 0 &&
|
||||||
decryptedData[questionItem.field]
|
decryptedData[questionItem.field]
|
||||||
@ -214,39 +224,78 @@ export class SurveyResponseController {
|
|||||||
const arr = cur.options.map((optionItem) => ({
|
const arr = cur.options.map((optionItem) => ({
|
||||||
hash: optionItem.hash,
|
hash: optionItem.hash,
|
||||||
text: optionItem.text,
|
text: optionItem.text,
|
||||||
|
quota: optionItem.quota,
|
||||||
}));
|
}));
|
||||||
pre[cur.field] = arr;
|
pre[cur.field] = arr;
|
||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// 对用户提交的数据进行遍历处理
|
// 使用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) {
|
for (const field in decryptedData) {
|
||||||
const val = decryptedData[field];
|
const value = decryptedData[field];
|
||||||
const vals = Array.isArray(val) ? val : [val];
|
const values = Array.isArray(value) ? value : [value];
|
||||||
if (field in optionTextAndId) {
|
if (field in optionTextAndId) {
|
||||||
// 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能
|
const optionCountData =
|
||||||
const optionCountData: Record<string, any> =
|
|
||||||
(await this.counterService.get({
|
(await this.counterService.get({
|
||||||
surveyPath,
|
|
||||||
key: field,
|
key: field,
|
||||||
|
surveyPath,
|
||||||
type: 'option',
|
type: 'option',
|
||||||
})) || { total: 0 };
|
})) || {};
|
||||||
optionCountData.total++;
|
|
||||||
for (const val of vals) {
|
//遍历选项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]) {
|
if (!optionCountData[val]) {
|
||||||
optionCountData[val] = 1;
|
optionCountData[val] = 0;
|
||||||
} else {
|
}
|
||||||
optionCountData[val]++;
|
optionCountData[val]++;
|
||||||
}
|
}
|
||||||
|
if (!optionCountData['total']) {
|
||||||
|
optionCountData['total'] = 1;
|
||||||
|
} else {
|
||||||
|
optionCountData['total']++;
|
||||||
}
|
}
|
||||||
this.counterService.set({
|
successParams.push({
|
||||||
surveyPath,
|
|
||||||
key: field,
|
key: field,
|
||||||
data: optionCountData,
|
surveyPath,
|
||||||
type: 'option',
|
type: 'option',
|
||||||
|
data: optionCountData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 校验通过后统一更新
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 入库
|
// 入库
|
||||||
const surveyResponse =
|
const surveyResponse =
|
||||||
@ -259,7 +308,6 @@ export class SurveyResponseController {
|
|||||||
optionTextAndId,
|
optionTextAndId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const surveyId = responseSchema.pageId;
|
|
||||||
const sendData = getPushingData({
|
const sendData = getPushingData({
|
||||||
surveyResponse,
|
surveyResponse,
|
||||||
questionList: responseSchema?.code?.dataConf?.dataList || [],
|
questionList: responseSchema?.code?.dataConf?.dataList || [],
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
|
|
||||||
import { MessageModule } from '../message/message.module';
|
import { MessageModule } from '../message/message.module';
|
||||||
|
import { RedisModule } from '../redis/redis.module';
|
||||||
|
|
||||||
import { ResponseSchemaService } from './services/responseScheme.service';
|
import { ResponseSchemaService } from './services/responseScheme.service';
|
||||||
import { SurveyResponseService } from './services/surveyResponse.service';
|
import { SurveyResponseService } from './services/surveyResponse.service';
|
||||||
import { CounterService } from './services/counter.service';
|
import { CounterService } from './services/counter.service';
|
||||||
import { ClientEncryptService } from './services/clientEncrypt.service';
|
import { ClientEncryptService } from './services/clientEncrypt.service';
|
||||||
|
import { RedisService } from '../redis/redis.service';
|
||||||
|
|
||||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
import { Counter } from 'src/models/counter.entity';
|
import { Counter } from 'src/models/counter.entity';
|
||||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||||
import { ClientEncrypt } from 'src/models/clientEncrypt.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 { ClientEncryptController } from './controllers/clientEncrpt.controller';
|
||||||
import { CounterController } from './controllers/counter.controller';
|
import { CounterController } from './controllers/counter.controller';
|
||||||
@ -23,6 +22,9 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
|
|||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
@ -33,6 +35,7 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
|||||||
]),
|
]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
MessageModule,
|
MessageModule,
|
||||||
|
RedisModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
],
|
],
|
||||||
@ -48,7 +51,8 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
|||||||
SurveyResponseService,
|
SurveyResponseService,
|
||||||
CounterService,
|
CounterService,
|
||||||
ClientEncryptService,
|
ClientEncryptService,
|
||||||
Logger,
|
LoggerProvider,
|
||||||
|
RedisService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ResponseSchemaService,
|
ResponseSchemaService,
|
||||||
|
@ -10,7 +10,7 @@ import { Workspace } from 'src/models/workspace.entity';
|
|||||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.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';
|
import { User } from 'src/models/user.entity';
|
||||||
|
|
||||||
jest.mock('src/guards/authentication.guard');
|
jest.mock('src/guards/authentication.guard');
|
||||||
@ -65,7 +65,7 @@ describe('WorkspaceController', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: XiaojuSurveyLogger,
|
||||||
useValue: {
|
useValue: {
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
@ -31,7 +31,7 @@ import {
|
|||||||
import { splitMembers } from '../utils/splitMember';
|
import { splitMembers } from '../utils/splitMember';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.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 { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
|
||||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||||
import { Workspace } from 'src/models/workspace.entity';
|
import { Workspace } from 'src/models/workspace.entity';
|
||||||
@ -46,7 +46,7 @@ export class WorkspaceController {
|
|||||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly surveyMetaService: SurveyMetaService,
|
private readonly surveyMetaService: SurveyMetaService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: XiaojuSurveyLogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('getRoleList')
|
@Get('getRoleList')
|
||||||
@ -64,10 +64,7 @@ export class WorkspaceController {
|
|||||||
async create(@Body() workspace: CreateWorkspaceDto, @Request() req) {
|
async create(@Body() workspace: CreateWorkspaceDto, @Request() req) {
|
||||||
const { value, error } = CreateWorkspaceDto.validate(workspace);
|
const { value, error } = CreateWorkspaceDto.validate(workspace);
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(
|
this.logger.error(`CreateWorkspaceDto validate failed: ${error.message}`);
|
||||||
`CreateWorkspaceDto validate failed: ${error.message}`,
|
|
||||||
{ req },
|
|
||||||
);
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`参数错误: 请联系管理员`,
|
`参数错误: 请联系管理员`,
|
||||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||||
@ -137,7 +134,6 @@ export class WorkspaceController {
|
|||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`GetWorkspaceListDto validate failed: ${error.message}`,
|
`GetWorkspaceListDto validate failed: ${error.message}`,
|
||||||
{ req },
|
|
||||||
);
|
);
|
||||||
throw new HttpException(
|
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.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
components.d.ts
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"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": {
|
"dependencies": {
|
||||||
"@logicflow/core": "2.0.0",
|
"@logicflow/core": "2.0.0",
|
||||||
@ -30,6 +30,7 @@
|
|||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"vue": "^3.4.15",
|
"vue": "^3.4.15",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
@ -43,6 +44,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
@ -2,10 +2,71 @@
|
|||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { watch } from 'vue'
|
||||||
name: 'App'
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<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)
|
return axios.post('/auth/login', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUserInfo = () => {
|
||||||
|
return axios.get('/user/getUserInfo')
|
||||||
|
}
|
||||||
/** 获取密码强度 */
|
/** 获取密码强度 */
|
||||||
export const getPasswordStrength = (password) => {
|
export const getPasswordStrength = (password) => {
|
||||||
return axios.get('/auth/register/password/strength', {
|
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 }) => {
|
export const saveSurvey = ({ surveyId, configData, sessionId }) => {
|
||||||
return axios.post('/survey/updateConf', { surveyId, configData })
|
return axios.post('/survey/updateConf', { surveyId, configData, sessionId })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishSurvey = ({ surveyId }) => {
|
export const publishSurvey = ({ surveyId }) => {
|
||||||
@ -52,3 +52,11 @@ export const deleteSurvey = (surveyId) => {
|
|||||||
export const updateSurvey = (data) => {
|
export const updateSurvey = (data) => {
|
||||||
return axios.post('/survey/updateMeta', 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: '未发布',
|
new: '未发布',
|
||||||
editing: '修改中',
|
editing: '修改中',
|
||||||
published: '已发布',
|
published: '已发布',
|
||||||
removed: '',
|
removed: '已删除',
|
||||||
pausing: ''
|
pausing: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,13 +41,15 @@ export const defaultQuestionConfig = {
|
|||||||
text: '选项1',
|
text: '选项1',
|
||||||
others: false,
|
others: false,
|
||||||
othersKey: '',
|
othersKey: '',
|
||||||
placeholderDesc: ''
|
placeholderDesc: '',
|
||||||
|
quota: '0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '选项2',
|
text: '选项2',
|
||||||
others: false,
|
others: false,
|
||||||
othersKey: '',
|
othersKey: '',
|
||||||
placeholderDesc: ''
|
placeholderDesc: '',
|
||||||
|
quota: '0'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
star: 5,
|
star: 5,
|
||||||
@ -74,5 +76,6 @@ export const defaultQuestionConfig = {
|
|||||||
placeholder: '500',
|
placeholder: '500',
|
||||||
value: 500
|
value: 500
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
quotaNoDisplay: false
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import ImagePreview from './ImagePreview.vue'
|
import ImagePreview from './ImagePreview.vue'
|
||||||
|
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tableData: {
|
tableData: {
|
||||||
@ -78,8 +79,16 @@ const popoverVirtualRef = ref()
|
|||||||
const popoverContent = ref('')
|
const popoverContent = ref('')
|
||||||
|
|
||||||
const getContent = (content) => {
|
const getContent = (content) => {
|
||||||
// const content = cleanRichText(value)
|
if (Array.isArray(content)) {
|
||||||
return content === 0 ? 0 : content || '未知'
|
return content.map(item => getContent(item)).join(',');
|
||||||
|
}
|
||||||
|
if (content === null || content === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (typeof content !== 'string') {
|
||||||
|
content = content + ''
|
||||||
|
}
|
||||||
|
return cleanRichTextWithMediaTag(content) || '未知'
|
||||||
}
|
}
|
||||||
const setPopoverContent = (content) => {
|
const setPopoverContent = (content) => {
|
||||||
popoverContent.value = content
|
popoverContent.value = content
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<div class="data-table-page">
|
<div class="data-table-page">
|
||||||
<template v-if="tableData.total">
|
<template v-if="tableData.total">
|
||||||
<div class="menus">
|
<div class="menus">
|
||||||
|
<el-button type="primary" :loading="isDownloading" @click="onDownload">导出全部数据</el-button>
|
||||||
<el-switch
|
<el-switch
|
||||||
|
class="desensitive-switch"
|
||||||
:model-value="isShowOriginData"
|
:model-value="isShowOriginData"
|
||||||
active-text="是否展示原数据"
|
active-text="是否展示原数据"
|
||||||
@input="onIsShowOriginChange"
|
@input="onIsShowOriginChange"
|
||||||
@ -25,11 +27,42 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<EmptyIndex :data="noDataConfig" />
|
<EmptyIndex :data="noDataConfig" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, toRefs } from 'vue'
|
import { reactive, toRefs, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
@ -37,6 +70,7 @@ import EmptyIndex from '@/management/components/EmptyIndex.vue'
|
|||||||
import { getRecycleList } from '@/management/api/analysis'
|
import { getRecycleList } from '@/management/api/analysis'
|
||||||
import { noDataConfig } from '@/management/config/analysisConfig'
|
import { noDataConfig } from '@/management/config/analysisConfig'
|
||||||
import DataTable from '../components/DataTable.vue'
|
import DataTable from '../components/DataTable.vue'
|
||||||
|
import { createDownloadSurveyResponseTask, getDownloadTask } from '@/management/api/downloadTask'
|
||||||
|
|
||||||
const dataTableState = reactive({
|
const dataTableState = reactive({
|
||||||
mainTableLoading: false,
|
mainTableLoading: false,
|
||||||
@ -47,10 +81,16 @@ const dataTableState = reactive({
|
|||||||
},
|
},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
isShowOriginData: false,
|
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()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -115,8 +155,67 @@ const init = async () => {
|
|||||||
ElMessage.error('查询回收数据失败,请重试')
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -126,6 +225,11 @@ init()
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-tips {
|
||||||
|
display: flex;
|
||||||
|
color: #ec4e29;
|
||||||
|
}
|
||||||
|
|
||||||
.menus {
|
.menus {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -139,4 +243,8 @@ init()
|
|||||||
.data-list {
|
.data-list {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desensitive-switch {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
</style>
|
</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()
|
const showLogicConf = showLogicEngine.value.toJson()
|
||||||
|
if(JSON.stringify(schema.logicConf.showLogicConf) !== JSON.stringify(showLogicConf)) {
|
||||||
// 更新逻辑配置
|
// 更新逻辑配置
|
||||||
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||||
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
||||||
|
if(JSON.stringify(schema.logicConf.jumpLogicConf) !== JSON.stringify(jumpLogicConf)){
|
||||||
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||||
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,16 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
|
|||||||
import CommonTemplate from './components/CommonTemplate.vue'
|
import CommonTemplate from './components/CommonTemplate.vue'
|
||||||
import Navbar from './components/ModuleNavbar.vue'
|
import Navbar from './components/ModuleNavbar.vue'
|
||||||
|
|
||||||
|
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const { init, setSurveyId } = editStore
|
const { init, setSurveyId } = editStore
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
setSurveyId(route.params.id as string)
|
const surveyId = route.params.id as string
|
||||||
|
setSurveyId(surveyId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await init()
|
await init()
|
||||||
|
@ -4,14 +4,15 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import { publishSurvey, saveSurvey, seizeSession } from '@/management/api/survey'
|
||||||
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
updateLogicConf: any
|
updateLogicConf: any
|
||||||
@ -22,7 +23,21 @@ const props = defineProps<Props>()
|
|||||||
|
|
||||||
const isPublishing = ref<boolean>(false)
|
const isPublishing = ref<boolean>(false)
|
||||||
const editStore = useEditStore()
|
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 router = useRouter()
|
||||||
|
|
||||||
const validate = () => {
|
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 () => {
|
const handlePublish = async () => {
|
||||||
if (isPublishing.value) {
|
if (isPublishing.value) {
|
||||||
return
|
return
|
||||||
@ -60,22 +115,12 @@ const handlePublish = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveData = buildData(schema)
|
|
||||||
if (!saveData.surveyId) {
|
|
||||||
isPublishing.value = false
|
|
||||||
ElMessage.error('未获取到问卷id')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saveRes: any = await saveSurvey(saveData)
|
const saveRes: any = await onSave()
|
||||||
if (saveRes.code !== 200) {
|
if (!saveRes || saveRes.code !== CODE_MAP.SUCCESS) {
|
||||||
isPublishing.value = false
|
|
||||||
ElMessage.error(saveRes.errmsg || '问卷保存失败')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId })
|
||||||
const publishRes: any = await publishSurvey({ surveyId: saveData.surveyId })
|
|
||||||
if (publishRes.code === 200) {
|
if (publishRes.code === 200) {
|
||||||
ElMessage.success('发布成功')
|
ElMessage.success('发布成功')
|
||||||
getSchemaFromRemote()
|
getSchemaFromRemote()
|
||||||
|
@ -18,10 +18,10 @@ import { ref, computed, nextTick, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
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 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
import { saveSurvey } from '@/management/api/survey'
|
import { saveSurvey, seizeSession } from '@/management/api/survey'
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -44,8 +44,8 @@ const saveText = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const { schemaUpdateTime } = storeToRefs(editStore)
|
const { schemaUpdateTime, schema, sessionId } = storeToRefs(editStore)
|
||||||
const { schema } = editStore
|
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
let checked = true
|
let checked = true
|
||||||
@ -68,18 +68,32 @@ const validate = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveData = async () => {
|
const onSave = async () => {
|
||||||
const saveData = buildData(schema)
|
const saveData = buildData(schema.value, sessionId.value);
|
||||||
|
if (!saveData.sessionId) {
|
||||||
|
ElMessage.error('sessionId有误')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!saveData.surveyId) {
|
if (!saveData.surveyId) {
|
||||||
ElMessage.error('未获取到问卷id')
|
ElMessage.error('未获取到问卷id')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await saveSurvey(saveData)
|
const res: Record<string, any> = await saveSurvey(saveData)
|
||||||
|
|
||||||
return res
|
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 timerHandle = ref<NodeJS.Timeout | number | null>(null)
|
||||||
const triggerAutoSave = () => {
|
const triggerAutoSave = () => {
|
||||||
if (autoSaveStatus.value === 'saving') {
|
if (autoSaveStatus.value === 'saving') {
|
||||||
@ -95,7 +109,7 @@ const triggerAutoSave = () => {
|
|||||||
isShowAutoSave.value = true
|
isShowAutoSave.value = true
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await saveData()
|
const res: any = await handleSave()
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
autoSaveStatus.value = 'succeed'
|
autoSaveStatus.value = 'succeed'
|
||||||
} else {
|
} else {
|
||||||
@ -120,21 +134,34 @@ const handleSave = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSaving.value = true
|
|
||||||
isShowAutoSave.value = false
|
isShowAutoSave.value = false
|
||||||
|
|
||||||
// 保存检测
|
// 保存检测
|
||||||
const { checked, msg } = validate()
|
const { checked, msg } = validate()
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
isSaving.value = false
|
|
||||||
ElMessage.error(msg)
|
ElMessage.error(msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await saveData()
|
const res: any = await onSave()
|
||||||
|
if(!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success('保存成功')
|
||||||
|
return res
|
||||||
|
} else if (res.code === 3006) {
|
||||||
|
ElMessageBox.alert(res.errmsg, '提示', {
|
||||||
|
confirmButtonText: '刷新同步',
|
||||||
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
seize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.errmsg)
|
ElMessage.error(res.errmsg)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { pick as _pick, get as _get } from 'lodash-es'
|
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 surveyId = _get(schema, 'metaData._id')
|
||||||
const configData = _pick(schema, [
|
const configData = _pick(schema, [
|
||||||
'bannerConf',
|
'bannerConf',
|
||||||
@ -19,6 +19,7 @@ export default function (schema) {
|
|||||||
delete configData.questionDataList
|
delete configData.questionDataList
|
||||||
return {
|
return {
|
||||||
surveyId,
|
surveyId,
|
||||||
configData
|
configData,
|
||||||
|
sessionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ const routes = [
|
|||||||
background-color: $primary-color;
|
background-color: $primary-color;
|
||||||
bottom: -16px;
|
bottom: -16px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
|
z-index: 99;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ import 'element-plus/theme-chalk/src/message.scss'
|
|||||||
|
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OptionConfig',
|
name: 'OptionConfig',
|
||||||
@ -110,7 +111,7 @@ export default {
|
|||||||
return mapData
|
return mapData
|
||||||
},
|
},
|
||||||
textOptions() {
|
textOptions() {
|
||||||
return this.curOptions.map((item) => item.text)
|
return this.curOptions.map((item) => cleanRichTextWithMediaTag(item.text))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -7,7 +7,7 @@ export default [
|
|||||||
{
|
{
|
||||||
title: '提交限制',
|
title: '提交限制',
|
||||||
key: 'limitConfig',
|
key: 'limitConfig',
|
||||||
formList: ['limit_tLimit']
|
formList: ['limit_tLimit', 'limit_breakAnswer', 'limit_backAnswer']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '作答限制',
|
title: '作答限制',
|
||||||
|
@ -22,6 +22,22 @@ export default {
|
|||||||
type: 'QuestionTimeHour',
|
type: 'QuestionTimeHour',
|
||||||
placement: 'top'
|
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: {
|
interview_pwd_switch: {
|
||||||
key: 'passwordSwitch',
|
key: 'passwordSwitch',
|
||||||
label: '访问密码',
|
label: '访问密码',
|
||||||
@ -96,5 +112,5 @@ export default {
|
|||||||
relyFunc: (data) => {
|
relyFunc: (data) => {
|
||||||
return data.whitelistType == 'MEMBER'
|
return data.whitelistType == 'MEMBER'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
|
||||||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
|
||||||
<el-menu-item index="1">问卷列表</el-menu-item>
|
<el-menu-item index="1">问卷列表</el-menu-item>
|
||||||
|
<el-menu-item index="2" @click="handleDownload">下载中心</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-info">
|
<div class="login-info">
|
||||||
@ -184,6 +185,10 @@ const handleLogout = () => {
|
|||||||
userStore.logout()
|
userStore.logout()
|
||||||
router.replace({ name: 'login' })
|
router.replace({ name: 'login' })
|
||||||
}
|
}
|
||||||
|
// 下载页面
|
||||||
|
const handleDownload = () => {
|
||||||
|
router.push({ name: 'download' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -9,6 +9,9 @@ import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
|||||||
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
import { analysisTypeMap } from '@/management/config/analysisConfig'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/button.scss'
|
||||||
|
import 'element-plus/theme-chalk/src/overlay.scss'
|
||||||
import { useUserStore } from '@/management/stores/user'
|
import { useUserStore } from '@/management/stores/user'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
|
||||||
@ -26,6 +29,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '问卷列表'
|
title: '问卷列表'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/download',
|
||||||
|
name: 'download',
|
||||||
|
component: () => import('../pages/downloadTask/TaskList.vue'),
|
||||||
|
meta: {
|
||||||
|
needLogin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/survey/:id/edit',
|
path: '/survey/:id/edit',
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -10,7 +10,7 @@ import { QUESTION_TYPE } from '@/common/typeEnum'
|
|||||||
import { getQuestionByType } from '@/management/utils/index'
|
import { getQuestionByType } from '@/management/utils/index'
|
||||||
import { filterQuestionPreviewData } 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 { getNewField } from '@/management/utils'
|
||||||
|
|
||||||
import submitFormConfig from '@/management/pages/edit/setterConfig/submitConfig'
|
import submitFormConfig from '@/management/pages/edit/setterConfig/submitConfig'
|
||||||
@ -93,6 +93,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
|||||||
})
|
})
|
||||||
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
|
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
|
||||||
useLogicEngine(schema)
|
useLogicEngine(schema)
|
||||||
|
|
||||||
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
||||||
schema.metaData = metaData
|
schema.metaData = metaData
|
||||||
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
||||||
@ -151,7 +152,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
|||||||
initSchema,
|
initSchema,
|
||||||
getSchemaFromRemote,
|
getSchemaFromRemote,
|
||||||
showLogicEngine,
|
showLogicEngine,
|
||||||
jumpLogicEngine
|
jumpLogicEngine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,21 +478,48 @@ function useLogicEngine(schema: any) {
|
|||||||
initJumpLogicEngine
|
initJumpLogicEngine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type IBannerItem = {
|
type IBannerItem = {
|
||||||
name: string
|
name: string
|
||||||
key: string
|
key: string
|
||||||
list: Array<Object>
|
list: Array<Object>
|
||||||
}
|
}
|
||||||
type IBannerList = Record<string, IBannerItem>
|
type IBannerList = Record<string, IBannerItem>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useEditStore = defineStore('edit', () => {
|
export const useEditStore = defineStore('edit', () => {
|
||||||
const surveyId = ref('')
|
const surveyId = ref('')
|
||||||
const bannerList: Ref<IBannerList> = ref({})
|
const bannerList: Ref<IBannerList> = ref({})
|
||||||
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
||||||
const schemaUpdateTime = ref(Date.now())
|
const schemaUpdateTime = ref(Date.now())
|
||||||
|
|
||||||
|
function setSurveyId(id: string) {
|
||||||
|
surveyId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
|
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
|
||||||
useInitializeSchema(surveyId, () => {
|
useInitializeSchema(surveyId, () => {
|
||||||
editGlobalBaseConf.initCounts()
|
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 questionDataList = toRef(schema, 'questionDataList')
|
||||||
|
|
||||||
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
||||||
@ -499,9 +527,6 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
schema.questionDataList = data
|
schema.questionDataList = data
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSurveyId(id: string) {
|
|
||||||
surveyId.value = id
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchBannerData = async () => {
|
const fetchBannerData = async () => {
|
||||||
const res: any = await getBannerData()
|
const res: any = await getBannerData()
|
||||||
@ -509,6 +534,7 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
bannerList.value = res.data
|
bannerList.value = res.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchCooperPermissions = async (id: string) => {
|
const fetchCooperPermissions = async (id: string) => {
|
||||||
const res: any = await getCollaboratorPermissions(id)
|
const res: any = await getCollaboratorPermissions(id)
|
||||||
if (res.code === CODE_MAP.SUCCESS) {
|
if (res.code === CODE_MAP.SUCCESS) {
|
||||||
@ -531,6 +557,7 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
const { metaData } = schema
|
const { metaData } = schema
|
||||||
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
||||||
await getSchemaFromRemote()
|
await getSchemaFromRemote()
|
||||||
|
await initSessionId()
|
||||||
}
|
}
|
||||||
currentEditOne.value = null
|
currentEditOne.value = null
|
||||||
currentEditStatus.value = 'Success'
|
currentEditStatus.value = 'Success'
|
||||||
@ -641,7 +668,9 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
return {
|
return {
|
||||||
editGlobalBaseConf,
|
editGlobalBaseConf,
|
||||||
surveyId,
|
surveyId,
|
||||||
|
sessionId,
|
||||||
setSurveyId,
|
setSurveyId,
|
||||||
|
initSessionId,
|
||||||
bannerList,
|
bannerList,
|
||||||
fetchBannerData,
|
fetchBannerData,
|
||||||
cooperPermissions,
|
cooperPermissions,
|
||||||
|
@ -47,6 +47,10 @@ export default defineComponent({
|
|||||||
voteTotal: {
|
voteTotal: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 10
|
default: 10
|
||||||
|
},
|
||||||
|
quotaNoDisplay:{
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change'],
|
emits: ['change'],
|
||||||
@ -141,9 +145,23 @@ export default defineComponent({
|
|||||||
<span
|
<span
|
||||||
v-html={filterXSS(item.text)}
|
v-html={filterXSS(item.text)}
|
||||||
class="item-title-text"
|
class="item-title-text"
|
||||||
style="display: block; height: auto; padding: 9px 0"
|
style="display: block; height: auto; padding-top: 9px"
|
||||||
></span>
|
></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?.({
|
{slots.vote?.({
|
||||||
option: item,
|
option: item,
|
||||||
voteTotal: this.voteTotal
|
voteTotal: this.voteTotal
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 0.32rem;
|
width: 0.32rem;
|
||||||
height: 0.32rem;
|
height: 0.32rem;
|
||||||
margin: 0rem 0.24rem 0 0;
|
margin: 11px 0.24rem 0 0;
|
||||||
border: 1px solid $border-color;
|
border: 1px solid $border-color;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@ -128,7 +128,6 @@
|
|||||||
.qicon.qicon-gouxuan {
|
.qicon.qicon-gouxuan {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.32rem;
|
font-size: 0.32rem;
|
||||||
line-height: 0.32rem;
|
|
||||||
border-color: $primary-color;
|
border-color: $primary-color;
|
||||||
background-color: $primary-color;
|
background-color: $primary-color;
|
||||||
color: #fff;
|
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 { includes } from 'lodash-es'
|
||||||
|
|
||||||
import BaseChoice from '../BaseChoice'
|
import BaseChoice from '../BaseChoice'
|
||||||
@ -41,10 +41,15 @@ export default defineComponent({
|
|||||||
maxNum: {
|
maxNum: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
quotaNoDisplay:{
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change'],
|
emits: ['change'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
|
||||||
const disableState = computed(() => {
|
const disableState = computed(() => {
|
||||||
if (!props.maxNum) {
|
if (!props.maxNum) {
|
||||||
return false
|
return false
|
||||||
@ -53,17 +58,31 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
const isDisabled = (item) => {
|
const isDisabled = (item) => {
|
||||||
const { value } = props
|
const { value } = props
|
||||||
return disableState.value && !includes(value, item.value)
|
return disableState.value && !includes(value, item.hash)
|
||||||
}
|
}
|
||||||
const myOptions = computed(() => {
|
const myOptions = computed(() => {
|
||||||
const { options } = props
|
const { options } = props
|
||||||
return options.map((item) => {
|
return options.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...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 onChange = (value) => {
|
||||||
const key = props.field
|
const key = props.field
|
||||||
emit('change', {
|
emit('change', {
|
||||||
@ -92,12 +111,13 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
onChange,
|
onChange,
|
||||||
handleSelectMoreChange,
|
handleSelectMoreChange,
|
||||||
|
disableState,
|
||||||
myOptions,
|
myOptions,
|
||||||
selectMoreView
|
selectMoreView
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const { readonly, field, myOptions, onChange, maxNum, value, selectMoreView } = this
|
const { readonly, field, myOptions, onChange, maxNum, value, quotaNoDisplay, selectMoreView } = this
|
||||||
return (
|
return (
|
||||||
<BaseChoice
|
<BaseChoice
|
||||||
uiTarget="checkbox"
|
uiTarget="checkbox"
|
||||||
@ -108,6 +128,7 @@ export default defineComponent({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
layout={this.layout}
|
layout={this.layout}
|
||||||
|
quotaNoDisplay={quotaNoDisplay}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
selectMore: (scoped) => {
|
selectMore: (scoped) => {
|
||||||
|
@ -117,20 +117,40 @@ const meta = {
|
|||||||
label: '至少选择数',
|
label: '至少选择数',
|
||||||
type: 'InputNumber',
|
type: 'InputNumber',
|
||||||
key: 'minNum',
|
key: 'minNum',
|
||||||
value: '',
|
value: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 'maxNum',
|
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
|
||||||
contentClass: 'input-number-config'
|
contentClass: 'input-number-config'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '最多选择数',
|
label: '最多选择数',
|
||||||
type: 'InputNumber',
|
type: 'InputNumber',
|
||||||
key: 'maxNum',
|
key: 'maxNum',
|
||||||
value: '',
|
value: 0,
|
||||||
min: 'minNum',
|
min: moduleConfig => { return moduleConfig?.minNum || 0 },
|
||||||
|
max: moduleConfig => { return moduleConfig?.options?.length },
|
||||||
contentClass: 'input-number-config'
|
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: {
|
editConfigure: {
|
||||||
|
@ -6,22 +6,13 @@ import GetHash from '@materials/questions/common/utils/getOptionHash'
|
|||||||
function useOptionBase(options) {
|
function useOptionBase(options) {
|
||||||
const optionList = ref(options)
|
const optionList = ref(options)
|
||||||
const addOption = (text = '选项', others = false, index = -1, field) => {
|
const addOption = (text = '选项', others = false, index = -1, field) => {
|
||||||
// const {} = payload
|
let addOne = {
|
||||||
let addOne
|
|
||||||
if (optionList.value[0]) {
|
|
||||||
addOne = cloneDeep(optionList.value[0])
|
|
||||||
} else {
|
|
||||||
addOne = {
|
|
||||||
text: '',
|
text: '',
|
||||||
hash: '',
|
hash: '',
|
||||||
imageUrl: '',
|
|
||||||
others: false,
|
others: false,
|
||||||
mustOthers: false,
|
mustOthers: false,
|
||||||
othersKey: '',
|
othersKey: '',
|
||||||
placeholderDesc: '',
|
placeholderDesc: '',
|
||||||
score: 0,
|
|
||||||
limit: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
text = '选项'
|
text = '选项'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { defineComponent, shallowRef, defineAsyncComponent } from 'vue'
|
import { defineComponent, shallowRef, watch, defineAsyncComponent } from 'vue'
|
||||||
import BaseChoice from '../BaseChoice'
|
import BaseChoice from '../BaseChoice'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,10 +31,28 @@ export default defineComponent({
|
|||||||
readonly: {
|
readonly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
quotaNoDisplay:{
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change'],
|
emits: ['change'],
|
||||||
setup(props, { emit }) {
|
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 onChange = (value) => {
|
||||||
const key = props.field
|
const key = props.field
|
||||||
emit('change', {
|
emit('change', {
|
||||||
@ -81,6 +99,7 @@ export default defineComponent({
|
|||||||
field={this.field}
|
field={this.field}
|
||||||
layout={this.layout}
|
layout={this.layout}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
quotaNoDisplay={this.quotaNoDisplay}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
selectMore: (scoped) => {
|
selectMore: (scoped) => {
|
||||||
|
@ -53,22 +53,22 @@ const meta = {
|
|||||||
description: '这是用于描述选项',
|
description: '这是用于描述选项',
|
||||||
defaultValue: [
|
defaultValue: [
|
||||||
{
|
{
|
||||||
text: '选项1',
|
"text": "选项1",
|
||||||
imageUrl: '',
|
"imageUrl": "",
|
||||||
others: false,
|
"others": false,
|
||||||
mustOthers: false,
|
"mustOthers": false,
|
||||||
othersKey: '',
|
"othersKey": "",
|
||||||
placeholderDesc: '',
|
"placeholderDesc": "",
|
||||||
hash: '115019'
|
"hash": "115019"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '选项2',
|
"text": "选项2",
|
||||||
imageUrl: '',
|
"imageUrl": "",
|
||||||
others: false,
|
"others": false,
|
||||||
mustOthers: false,
|
"mustOthers": false,
|
||||||
othersKey: '',
|
"othersKey": "",
|
||||||
placeholderDesc: '',
|
"placeholderDesc": "",
|
||||||
hash: '115020'
|
"hash": "115020"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -77,6 +77,12 @@ const meta = {
|
|||||||
propType: String,
|
propType: String,
|
||||||
description: '排列方式',
|
description: '排列方式',
|
||||||
defaultValue: 'vertical'
|
defaultValue: 'vertical'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotaNoDisplay',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '不展示配额剩余数量',
|
||||||
|
defaultValue: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
formConfig: [basicConfig, {
|
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: {
|
editConfigure: {
|
||||||
optionEdit: {
|
optionEdit: {
|
||||||
|
@ -120,7 +120,7 @@ const meta = {
|
|||||||
key: 'minNum',
|
key: 'minNum',
|
||||||
value: '',
|
value: '',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 'maxNum',
|
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
|
||||||
contentClass: 'input-number-config'
|
contentClass: 'input-number-config'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,7 +128,8 @@ const meta = {
|
|||||||
type: 'InputNumber',
|
type: 'InputNumber',
|
||||||
key: 'maxNum',
|
key: 'maxNum',
|
||||||
value: '',
|
value: '',
|
||||||
min: 'minNum',
|
min: moduleConfig => { return moduleConfig?.minNum || 0 },
|
||||||
|
max: moduleConfig => { return moduleConfig?.options?.length || 0 },
|
||||||
contentClass: 'input-number-config'
|
contentClass: 'input-number-config'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<el-switch v-model="newValue" @change="changeData" />
|
<el-switch v-model="newValue" @change="changeData" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -23,4 +23,15 @@ const changeData = (value) => {
|
|||||||
value
|
value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
watch(
|
||||||
|
() => props.formConfig.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal !== newValue.value) {
|
||||||
|
newValue.value = newVal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,6 +13,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
formConfig: any
|
formConfig: any
|
||||||
moduleConfig: any
|
moduleConfig: any
|
||||||
@ -24,12 +25,15 @@ interface Emit {
|
|||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
const emit = defineEmits<Emit>()
|
||||||
const props = defineProps<Props>()
|
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 minModelValue = computed(() => {
|
||||||
const { min } = props.formConfig
|
const { min } = props.formConfig
|
||||||
if (min) {
|
if (min !== undefined) {
|
||||||
if (typeof min === 'function') {
|
if (typeof min === 'function') {
|
||||||
return min(props.moduleConfig)
|
return min(myModuleConfig.value)
|
||||||
} else {
|
} else {
|
||||||
return Number(min)
|
return Number(min)
|
||||||
}
|
}
|
||||||
@ -38,16 +42,13 @@ const minModelValue = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const maxModelValue = computed(() => {
|
const maxModelValue = computed(() => {
|
||||||
const { max, min } = props.formConfig
|
const { max } = props.formConfig
|
||||||
|
|
||||||
if (max) {
|
if (max) {
|
||||||
if (typeof max === 'function') {
|
if (typeof max === 'function') {
|
||||||
return max(props.moduleConfig)
|
return max(myModuleConfig.value)
|
||||||
} else {
|
} else {
|
||||||
return Number(max)
|
return Number(max)
|
||||||
}
|
}
|
||||||
} else if (min !== undefined && Array.isArray(props.moduleConfig?.options)) {
|
|
||||||
return props.moduleConfig.options.length
|
|
||||||
} else {
|
} else {
|
||||||
return Infinity
|
return Infinity
|
||||||
}
|
}
|
||||||
@ -65,6 +66,9 @@ const handleInputChange = (value: number) => {
|
|||||||
|
|
||||||
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
emit(FORM_CHANGE_EVENT_KEY, { key, value })
|
||||||
}
|
}
|
||||||
|
watch(() => props.moduleConfig, (newVal) => {
|
||||||
|
myModuleConfig.value = newVal
|
||||||
|
})
|
||||||
watch(
|
watch(
|
||||||
() => props.formConfig.value,
|
() => props.formConfig.value,
|
||||||
(newVal) => {
|
(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"
|
:moduleConfig="questionConfig"
|
||||||
:indexNumber="indexNumber"
|
:indexNumber="indexNumber"
|
||||||
:showTitle="true"
|
:showTitle="true"
|
||||||
|
@input="handleInput"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
></QuestionRuleContainer>
|
></QuestionRuleContainer>
|
||||||
</template>
|
</template>
|
||||||
@ -14,6 +15,7 @@ import QuestionRuleContainer from '../../materials/questions/QuestionRuleContain
|
|||||||
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
||||||
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
||||||
import { useShowInput } from '@/render/hooks/useShowInput'
|
import { useShowInput } from '@/render/hooks/useShowInput'
|
||||||
|
import { useOptionsQuota } from '@/render/hooks/useOptionsQuota'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { useQuestionStore } from '../stores/question'
|
import { useQuestionStore } from '../stores/question'
|
||||||
import { useSurveyStore } from '../stores/survey'
|
import { useSurveyStore } from '../stores/survey'
|
||||||
@ -49,16 +51,24 @@ const questionConfig = computed(() => {
|
|||||||
let alloptions = options
|
let alloptions = options
|
||||||
|
|
||||||
if (type === QUESTION_TYPE.VOTE) {
|
if (type === QUESTION_TYPE.VOTE) {
|
||||||
|
// 处理投票进度
|
||||||
const { options, voteTotal } = useVoteMap(field)
|
const { options, voteTotal } = useVoteMap(field)
|
||||||
const voteOptions = unref(options)
|
const voteOptions = unref(options)
|
||||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||||
moduleConfig.voteTotal = unref(voteTotal)
|
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 (
|
if (
|
||||||
NORMAL_CHOICES.includes(type) &&
|
NORMAL_CHOICES.includes(type) &&
|
||||||
options.filter((optionItem) => optionItem.others).length > 0
|
options.some(option => option.others)
|
||||||
) {
|
) {
|
||||||
|
// 处理普通选择题的填写更多
|
||||||
let { options, othersValue } = useShowOthers(field)
|
let { options, othersValue } = useShowOthers(field)
|
||||||
const othersOptions = unref(options)
|
const othersOptions = unref(options)
|
||||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
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 >
|
Object.keys(rest?.rangeConfig).filter((index) => rest?.rangeConfig[index].isShowInput).length >
|
||||||
0
|
0
|
||||||
) {
|
) {
|
||||||
|
// 处理评分题的的选项后输入框
|
||||||
let { rangeConfig, othersValue } = useShowInput(field)
|
let { rangeConfig, othersValue } = useShowInput(field)
|
||||||
moduleConfig.rangeConfig = unref(rangeConfig)
|
moduleConfig.rangeConfig = unref(rangeConfig)
|
||||||
moduleConfig.othersValue = unref(othersValue)
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
@ -126,9 +137,19 @@ const handleChange = (data) => {
|
|||||||
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
||||||
questionStore.updateVoteData(data)
|
questionStore.updateVoteData(data)
|
||||||
}
|
}
|
||||||
|
// 处理选项配额
|
||||||
|
if (props.moduleConfig.type === NORMAL_CHOICES) {
|
||||||
|
questionStore.updateQuotaData(data)
|
||||||
|
}
|
||||||
|
// 断点续答的的数据缓存
|
||||||
|
localStorageBack()
|
||||||
processJumpSkip()
|
processJumpSkip()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
localStorageBack()
|
||||||
|
}
|
||||||
|
|
||||||
const processJumpSkip = () => {
|
const processJumpSkip = () => {
|
||||||
const targetResult = surveyStore.jumpLogicEngine
|
const targetResult = surveyStore.jumpLogicEngine
|
||||||
.getResultsByField(changeField.value, surveyStore.formValues)
|
.getResultsByField(changeField.value, surveyStore.formValues)
|
||||||
@ -169,4 +190,12 @@ const processJumpSkip = () => {
|
|||||||
.map((item) => item.field)
|
.map((item) => item.field)
|
||||||
questionStore.addNeedHideFields(skipKey)
|
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>
|
</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>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
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 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(
|
watch(
|
||||||
() => route.query.t,
|
() => route.query.t,
|
||||||
@ -63,22 +13,4 @@ watch(
|
|||||||
location.reload()
|
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>
|
</script>
|
||||||
|
@ -21,9 +21,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, onMounted } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import communalLoader from '@materials/communals/communalLoader.js'
|
import communalLoader from '@materials/communals/communalLoader.js'
|
||||||
import MainRenderer from '../components/MainRenderer.vue'
|
import MainRenderer from '../components/MainRenderer.vue'
|
||||||
@ -38,6 +38,8 @@ import { submitForm } from '../api/survey'
|
|||||||
import encrypt from '../utils/encrypt'
|
import encrypt from '../utils/encrypt'
|
||||||
|
|
||||||
import useCommandComponent from '../hooks/useCommandComponent'
|
import useCommandComponent from '../hooks/useCommandComponent'
|
||||||
|
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
|
||||||
|
import { useQuestionInfo } from '../hooks/useQuestionInfo'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
questionInfo?: any
|
questionInfo?: any
|
||||||
@ -70,6 +72,68 @@ const pageIndex = computed(() => questionStore.pageIndex)
|
|||||||
const { bannerConf, submitConf, bottomConf: logoConf, whiteData } = storeToRefs(surveyStore)
|
const { bannerConf, submitConf, bottomConf: logoConf, whiteData } = storeToRefs(surveyStore)
|
||||||
const surveyPath = computed(() => surveyStore.surveyPath || '')
|
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 validate = (cbk: (v: boolean) => void) => {
|
||||||
const index = 0
|
const index = 0
|
||||||
mainRef.value.$refs.formGroup[index].validate(cbk)
|
mainRef.value.$refs.formGroup[index].validate(cbk)
|
||||||
@ -88,6 +152,15 @@ const normalizationRequestBody = () => {
|
|||||||
...whiteData.value
|
...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) {
|
if (encryptInfo?.encryptType) {
|
||||||
result.encryptType = encryptInfo.encryptType
|
result.encryptType = encryptInfo.encryptType
|
||||||
result.data = encrypt[result.encryptType as 'rsa']({
|
result.data = encrypt[result.encryptType as 'rsa']({
|
||||||
@ -111,10 +184,18 @@ const submitSurver = async () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const params = normalizationRequestBody()
|
const params = normalizationRequestBody()
|
||||||
console.log(params)
|
|
||||||
const res: any = await submitForm(params)
|
const res: any = await submitForm(params)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
router.replace({ name: 'successPage' })
|
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 {
|
} else {
|
||||||
alert({
|
alert({
|
||||||
title: res.errmsg || '提交失败'
|
title: res.errmsg || '提交失败'
|
||||||
|
@ -3,11 +3,195 @@ import { defineStore } from 'pinia'
|
|||||||
import { set } from 'lodash-es'
|
import { set } from 'lodash-es'
|
||||||
import { useSurveyStore } from '@/render/stores/survey'
|
import { useSurveyStore } from '@/render/stores/survey'
|
||||||
import { queryVote } from '@/render/api/survey'
|
import { queryVote } from '@/render/api/survey'
|
||||||
|
import { QUESTION_TYPE, NORMAL_CHOICES } from '@/common/typeEnum'
|
||||||
|
|
||||||
const VOTE_INFO_KEY = 'voteinfo'
|
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', () => {
|
export const useQuestionStore = defineStore('question', () => {
|
||||||
const voteMap = ref({})
|
|
||||||
const questionData = ref(null)
|
const questionData = ref(null)
|
||||||
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||||
const pageIndex = ref(1) // 当前分页的索引
|
const pageIndex = ref(1) // 当前分页的索引
|
||||||
@ -82,6 +266,8 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
const setQuestionData = (data) => {
|
const setQuestionData = (data) => {
|
||||||
questionData.value = data
|
questionData.value = data
|
||||||
}
|
}
|
||||||
|
const { voteMap, setVoteMap, initVoteData, updateVoteData } = usevVoteMap(questionData)
|
||||||
|
const { quotaMap, initQuotaMap, updateQuotaData } = useQuotaMap(questionData)
|
||||||
|
|
||||||
const changeSelectMoreData = (data) => {
|
const changeSelectMoreData = (data) => {
|
||||||
const { key, value, field } = data
|
const { key, value, field } = data
|
||||||
@ -92,95 +278,6 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
questionSeq.value = 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) => {
|
const setChangeField = (field) => {
|
||||||
changeField.value = field
|
changeField.value = field
|
||||||
@ -199,7 +296,7 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
needHideFields.value = needHideFields.value.filter((field) => !fields.includes(field))
|
needHideFields.value = needHideFields.value.filter((field) => !fields.includes(field))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
voteMap,
|
|
||||||
questionData,
|
questionData,
|
||||||
questionSeq,
|
questionSeq,
|
||||||
renderData,
|
renderData,
|
||||||
@ -209,8 +306,8 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
setQuestionData,
|
setQuestionData,
|
||||||
changeSelectMoreData,
|
changeSelectMoreData,
|
||||||
setQuestionSeq,
|
setQuestionSeq,
|
||||||
|
voteMap,
|
||||||
setVoteMap,
|
setVoteMap,
|
||||||
updateVoteMapByKey,
|
|
||||||
initVoteData,
|
initVoteData,
|
||||||
updateVoteData,
|
updateVoteData,
|
||||||
changeField,
|
changeField,
|
||||||
@ -219,6 +316,9 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
needHideFields,
|
needHideFields,
|
||||||
addNeedHideFields,
|
addNeedHideFields,
|
||||||
removeNeedHideFields,
|
removeNeedHideFields,
|
||||||
getQuestionIndexByField
|
getQuestionIndexByField,
|
||||||
|
quotaMap,
|
||||||
|
initQuotaMap,
|
||||||
|
updateQuotaData
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { pick } from 'lodash-es'
|
import { cloneDeep, pick } from 'lodash-es'
|
||||||
|
|
||||||
import { isMobile as isInMobile } from '@/render/utils/index'
|
import { isMobile as isInMobile } from '@/render/utils/index'
|
||||||
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
|
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
|
||||||
@ -12,12 +12,16 @@ import moment from 'moment'
|
|||||||
// 引入中文
|
// 引入中文
|
||||||
import 'moment/locale/zh-cn'
|
import 'moment/locale/zh-cn'
|
||||||
// 设置中文
|
// 设置中文
|
||||||
moment.locale('zh-cn')
|
|
||||||
|
|
||||||
import adapter from '../adapter'
|
import adapter from '../adapter'
|
||||||
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
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
|
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
||||||
*/
|
*/
|
||||||
@ -26,6 +30,8 @@ const CODE_MAP = {
|
|||||||
ERROR: 500,
|
ERROR: 500,
|
||||||
NO_AUTH: 403
|
NO_AUTH: 403
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const useSurveyStore = defineStore('survey', () => {
|
export const useSurveyStore = defineStore('survey', () => {
|
||||||
const surveyPath = ref('')
|
const surveyPath = ref('')
|
||||||
const isMobile = ref(isInMobile())
|
const isMobile = ref(isInMobile())
|
||||||
@ -109,13 +115,11 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
|
|
||||||
return isSuccess
|
return isSuccess
|
||||||
}
|
}
|
||||||
const initSurvey = (option) => {
|
|
||||||
setEnterTime()
|
|
||||||
|
|
||||||
if (!canFillQuestionnaire(option.baseConf, option.submitConf)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
// 加载空白页面
|
||||||
|
function clearFormData(option) {
|
||||||
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
|
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
|
||||||
const {
|
const {
|
||||||
questionData,
|
questionData,
|
||||||
@ -134,6 +138,7 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
'pageConf'
|
'pageConf'
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
// todo: 建议通过questionStore提供setqueationdata方法修改属性,否则不好跟踪变化
|
||||||
questionStore.questionData = questionData
|
questionStore.questionData = questionData
|
||||||
questionStore.questionSeq = questionSeq
|
questionStore.questionSeq = questionSeq
|
||||||
|
|
||||||
@ -148,8 +153,75 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
formValues.value = _formValues
|
formValues.value = _formValues
|
||||||
whiteData.value = option.whiteData
|
whiteData.value = option.whiteData
|
||||||
pageConf.value = option.pageConf
|
pageConf.value = option.pageConf
|
||||||
|
|
||||||
// 获取已投票数据
|
// 获取已投票数据
|
||||||
questionStore.initVoteData()
|
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 showLogicEngine = ref()
|
||||||
const initShowLogicEngine = (showLogicConf) => {
|
const initShowLogicEngine = (showLogicConf) => {
|
||||||
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf)
|
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf || [])
|
||||||
}
|
}
|
||||||
const jumpLogicEngine = ref()
|
const jumpLogicEngine = ref()
|
||||||
const initJumpLogicEngine = (jumpLogicConf) => {
|
const initJumpLogicEngine = (jumpLogicConf) => {
|
||||||
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf)
|
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -123,6 +123,10 @@ export default defineConfig({
|
|||||||
target: 'http://127.0.0.1:3000',
|
target: 'http://127.0.0.1:3000',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
|
'/exportfile': {
|
||||||
|
target: 'http://127.0.0.1:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
// 静态文件的默认存储文件夹
|
// 静态文件的默认存储文件夹
|
||||||
'/userUpload': {
|
'/userUpload': {
|
||||||
target: 'http://127.0.0.1:3000',
|
target: 'http://127.0.0.1:3000',
|
||||||
|
Loading…
Reference in New Issue
Block a user