Merge branch 'develop' into feature/peking
This commit is contained in:
commit
20ef020a19
3
.github/workflows/server-lint.yml
vendored
3
.github/workflows/server-lint.yml
vendored
@ -37,6 +37,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: cd server && npm run lint
|
run: cd server && npm run lint
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: cd server && npm run format
|
|
||||||
|
3
.github/workflows/web-lint.yml
vendored
3
.github/workflows/web-lint.yml
vendored
@ -40,6 +40,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: cd web && npm run lint
|
run: cd web && npm run lint
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: cd web && npm run format
|
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -25,3 +25,7 @@ pnpm-debug.log*
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.history
|
.history
|
||||||
|
components.d.ts
|
||||||
|
|
||||||
|
# 默认的上传文件夹
|
||||||
|
userUpload
|
||||||
|
24
README.md
24
README.md
@ -29,12 +29,10 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
  **XIAOJUSURVEY**是一套轻量、安全的**问卷系统基座**,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
  **XIAOJUSURVEY**是一套轻量、安全的问卷系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
|
||||||
|
|
||||||
  内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
  内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||||
|
|
||||||
  开源项目以打造**调研基座**为核心,围绕**平台能力**、**工程架构**、**研发体系**进行建设,大家可以「快速」打造「专属」问卷系统:[快速了解生态发展理念](https://xiaojusurvey.didi.cn/docs/next/community/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE)
|
|
||||||
|
|
||||||
# 功能简介
|
# 功能简介
|
||||||
|
|
||||||
- 问卷管理:创、编、投、收、数据分析
|
- 问卷管理:创、编、投、收、数据分析
|
||||||
@ -45,7 +43,7 @@
|
|||||||
|
|
||||||
- 数据安全:传输加密、脱敏等
|
- 数据安全:传输加密、脱敏等
|
||||||
|
|
||||||
> 更全的建设请查阅 [官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
> 更全的建设请查阅 [功能介绍](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B)
|
||||||
|
|
||||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
||||||
|
|
||||||
@ -144,13 +142,11 @@ npm run local
|
|||||||
|
|
||||||
### 方案二、(生产推荐)
|
### 方案二、(生产推荐)
|
||||||
|
|
||||||
#### 1、启动数据库
|
#### 1、配置数据库
|
||||||
|
|
||||||
> 项目使用 MongoDB:[MongoDB 安装指导](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
> 项目使用 MongoDB,需要提前准备,请查看[如何拥有 MongoDB 指南](./数据库#安装)
|
||||||
|
|
||||||
- 配置数据库,查看[MongoDB 配置](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93)
|
配置数据库信息,查看[MongoDB 配置](./数据库)。
|
||||||
|
|
||||||
- 启动本地数据库,查看[MongoDB 启动](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E4%BA%94%E5%90%AF%E5%8A%A8)
|
|
||||||
|
|
||||||
#### 2、安装依赖
|
#### 2、安装依赖
|
||||||
|
|
||||||
@ -220,16 +216,10 @@ npm run serve
|
|||||||
|
|
||||||
如果你想成为贡献者或者扩展技术栈,请查看:[贡献者指南](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE),你的加入使我们最大的荣幸。
|
如果你想成为贡献者或者扩展技术栈,请查看:[贡献者指南](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE),你的加入使我们最大的荣幸。
|
||||||
|
|
||||||
## Feature
|
## Future Tasks
|
||||||
|
|
||||||
关注每周推出的建设:[官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
[欢迎共建](https://github.com/didi/xiaoju-survey/issues/85)
|
||||||
|
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
|
|
||||||
关注重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
关注重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||||
|
|
||||||
## 文章分享
|
|
||||||
|
|
||||||
1、[掘金](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish)
|
|
||||||
|
|
||||||
[欢迎投稿](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B)
|
|
||||||
|
24
README_EN.md
24
README_EN.md
@ -29,12 +29,10 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
  XIAOJUSURVEY is a lightweight, secure questionnaire system foundation that provides one-stop product-level solutions for individuals and enterprises, quickly meeting various online survey scenarios.
|
  XIAOJUSURVEY is an open-source form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online.
|
||||||
|
|
||||||
  The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs.
|
  The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs.
|
||||||
|
|
||||||
  The open-source project focuses on building a survey foundation, constructing around platform capabilities, engineering structure, and development systems, allowing everyone to 「quickly」 create their own 「exclusive」 questionnaire system: [quickly understanding the ecological development philosophy](https://xiaojusurvey.didi.cn/docs/next/community/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE).
|
|
||||||
|
|
||||||
# Function Overview
|
# Function Overview
|
||||||
|
|
||||||
- Questionnaire Management: Create, edit, distribute, collect, data analysis.
|
- Questionnaire Management: Create, edit, distribute, collect, data analysis.
|
||||||
@ -45,7 +43,7 @@
|
|||||||
|
|
||||||
- Data Security: Encrypted transmission, data masking, etc.
|
- Data Security: Encrypted transmission, data masking, etc.
|
||||||
|
|
||||||
> For more comprehensive features, please refer to the official Feature documentation.
|
> For more comprehensive features, please refer to the [documentation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B).
|
||||||
|
|
||||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
|
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
|
||||||
|
|
||||||
@ -145,12 +143,11 @@ npm run local
|
|||||||
|
|
||||||
### Option 2: (Recommended for Production)
|
### Option 2: (Recommended for Production)
|
||||||
|
|
||||||
#### 1.Start Database
|
#### 1.Configure Database
|
||||||
|
|
||||||
> The project uses MongoDB: [MongoDB Installation Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
> The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||||
|
|
||||||
- Configure the database, check MongoDB configuration.
|
Configure the database, check MongoDB configuration.
|
||||||
- Start local database, check MongoDB startup.
|
|
||||||
|
|
||||||
#### 2.Install Dependencies
|
#### 2.Install Dependencies
|
||||||
|
|
||||||
@ -218,16 +215,11 @@ If you use this project, please leave feedback:[I'm using](https://github.com/di
|
|||||||
|
|
||||||
If you want to become a contributor or expand your technical stack, please check: [Contributor Guide](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE). Your participation is our greatest honor.
|
If you want to become a contributor or expand your technical stack, please check: [Contributor Guide](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE). Your participation is our greatest honor.
|
||||||
|
|
||||||
## Feature
|
## Future Tasks
|
||||||
|
|
||||||
Pay attention to weekly construction updates: [Official Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
1. [Official Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||||
|
2. [WIP](https://github.com/didi/xiaoju-survey/labels/WIP)
|
||||||
|
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
|
|
||||||
Follow major changes: [MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
Follow major changes: [MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||||
|
|
||||||
## Article Sharing
|
|
||||||
|
|
||||||
1、[JueJin](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish)
|
|
||||||
|
|
||||||
[Welcome to contribute.](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B)
|
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
- xiaoju-survey
|
- xiaoju-survey
|
||||||
|
|
||||||
xiaoju-survey:
|
xiaoju-survey:
|
||||||
image: "xiaojusurvey/xiaoju-survey:1.1.2-slim"
|
image: "xiaojusurvey/xiaoju-survey:1.1.6-slim" # 最新版本:https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
|
||||||
container_name: xiaoju-survey
|
container_name: xiaoju-survey
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
@ -52,6 +52,12 @@ http {
|
|||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 静态文件的默认存储文件夹
|
||||||
|
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
|
||||||
|
location /userUpload {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /500.html;
|
error_page 500 502 503 504 /500.html;
|
||||||
client_max_body_size 20M;
|
client_max_body_size 20M;
|
||||||
}
|
}
|
||||||
|
@ -63,8 +63,8 @@
|
|||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/node-forge": "^1.3.11",
|
"@types/node-forge": "^1.3.11",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^7.16.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
@ -78,7 +78,7 @@
|
|||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
@ -12,6 +12,7 @@ export enum EXCEPTION_CODE {
|
|||||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||||
|
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||||
|
|
||||||
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
||||||
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
||||||
|
37
server/src/enums/question.ts
Normal file
37
server/src/enums/question.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @description 问卷题目类型
|
||||||
|
*/
|
||||||
|
export enum QUESTION_TYPE {
|
||||||
|
/**
|
||||||
|
* 单行输入框
|
||||||
|
*/
|
||||||
|
TEXT = 'text',
|
||||||
|
/**
|
||||||
|
* 多行输入框
|
||||||
|
*/
|
||||||
|
TEXTAREA = 'textarea',
|
||||||
|
/**
|
||||||
|
* 单项选择
|
||||||
|
*/
|
||||||
|
RADIO = 'radio',
|
||||||
|
/**
|
||||||
|
* 多项选择
|
||||||
|
*/
|
||||||
|
CHECKBOX = 'checkbox',
|
||||||
|
/**
|
||||||
|
* 判断题
|
||||||
|
*/
|
||||||
|
BINARY_CHOICE = 'binary-choice',
|
||||||
|
/**
|
||||||
|
* 评分
|
||||||
|
*/
|
||||||
|
RADIO_STAR = 'radio-star',
|
||||||
|
/**
|
||||||
|
* nps评分
|
||||||
|
*/
|
||||||
|
RADIO_NPS = 'radio-nps',
|
||||||
|
/**
|
||||||
|
* 投票
|
||||||
|
*/
|
||||||
|
VOTE = 'vote',
|
||||||
|
}
|
@ -97,6 +97,23 @@ export interface SubmitConf {
|
|||||||
msgContent: MsgContent;
|
msgContent: MsgContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 白名单类型
|
||||||
|
export enum WhitelistType {
|
||||||
|
ALL = 'ALL',
|
||||||
|
// 空间成员
|
||||||
|
MEMBER = 'MEMBER',
|
||||||
|
// 自定义
|
||||||
|
CUSTOM = 'CUSTOM',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单用户类型
|
||||||
|
export enum MemberType {
|
||||||
|
// 手机号
|
||||||
|
MOBILE = 'MOBILE',
|
||||||
|
// 邮箱
|
||||||
|
EMAIL = 'EMAIL',
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseConf {
|
export interface BaseConf {
|
||||||
begTime: string;
|
begTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
@ -104,6 +121,18 @@ export interface BaseConf {
|
|||||||
answerEndTime: string;
|
answerEndTime: string;
|
||||||
tLimit: number;
|
tLimit: number;
|
||||||
language: string;
|
language: string;
|
||||||
|
// 访问密码开关
|
||||||
|
passwordSwitch?: boolean;
|
||||||
|
// 密码
|
||||||
|
password?: string | null;
|
||||||
|
// 白名单类型
|
||||||
|
whitelistType?: WhitelistType;
|
||||||
|
// 白名单用户类型
|
||||||
|
memberType?: MemberType;
|
||||||
|
// 白名单列表
|
||||||
|
whitelist?: string[];
|
||||||
|
// 提示语
|
||||||
|
whitelistTip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkinConf {
|
export interface SkinConf {
|
||||||
|
@ -14,7 +14,7 @@ export class SurveyResponse extends BaseEntity {
|
|||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
difTime: number;
|
diffTime: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
clientTime: number;
|
clientTime: number;
|
||||||
|
@ -106,12 +106,13 @@ describe('DataStatisticController', () => {
|
|||||||
field: 'xxx',
|
field: 'xxx',
|
||||||
title: 'xxx',
|
title: 'xxx',
|
||||||
type: 'xxx',
|
type: 'xxx',
|
||||||
|
diffTime: 'xxx',
|
||||||
othersCode: 'xxx',
|
othersCode: 'xxx',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
listBody: [
|
listBody: [
|
||||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
{ diffTime: '0.5', createDate: '2024-02-11' },
|
||||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
{ diffTime: '0.5', createDate: '2024-02-11' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -151,12 +152,13 @@ describe('DataStatisticController', () => {
|
|||||||
field: 'xxx',
|
field: 'xxx',
|
||||||
title: 'xxx',
|
title: 'xxx',
|
||||||
type: 'xxx',
|
type: 'xxx',
|
||||||
|
diffTime: 'xxx',
|
||||||
othersCode: 'xxx',
|
othersCode: 'xxx',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
listBody: [
|
listBody: [
|
||||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
{ diffTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
||||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
{ diffTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ describe('DataStatisticService', () => {
|
|||||||
data413: 3,
|
data413: 3,
|
||||||
data863: '109239',
|
data863: '109239',
|
||||||
},
|
},
|
||||||
difTime: 21278,
|
diffTime: 21278,
|
||||||
clientTime: 1710340862733.0,
|
clientTime: 1710340862733.0,
|
||||||
secretKeys: [],
|
secretKeys: [],
|
||||||
optionTextAndId: {
|
optionTextAndId: {
|
||||||
@ -197,7 +197,7 @@ describe('DataStatisticService', () => {
|
|||||||
data413_3: expect.any(String),
|
data413_3: expect.any(String),
|
||||||
data413: expect.any(Number),
|
data413: expect.any(Number),
|
||||||
data863: expect.any(String),
|
data863: expect.any(String),
|
||||||
difTime: expect.any(String),
|
diffTime: expect.any(String),
|
||||||
createDate: expect.any(String),
|
createDate: expect.any(String),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -220,7 +220,7 @@ describe('DataStatisticService', () => {
|
|||||||
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
||||||
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
||||||
},
|
},
|
||||||
difTime: 806707,
|
diffTime: 806707,
|
||||||
clientTime: 1710400229573.0,
|
clientTime: 1710400229573.0,
|
||||||
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
||||||
optionTextAndId: {
|
optionTextAndId: {
|
||||||
@ -303,7 +303,7 @@ describe('DataStatisticService', () => {
|
|||||||
data458: expect.any(String),
|
data458: expect.any(String),
|
||||||
data515: expect.any(String),
|
data515: expect.any(String),
|
||||||
data770: expect.any(String),
|
data770: expect.any(String),
|
||||||
difTime: expect.any(String),
|
diffTime: expect.any(String),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,7 @@ 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';
|
||||||
import { handleAggretionData } from '../utils';
|
import { handleAggretionData } from '../utils';
|
||||||
|
import { QUESTION_TYPE } from 'src/enums/question';
|
||||||
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
import { SurveyDownloadService } from '../services/surveyDownload.service';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@ -106,15 +107,15 @@ export class DataStatisticController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const allowQuestionType = [
|
const allowQuestionType = [
|
||||||
'radio',
|
QUESTION_TYPE.RADIO,
|
||||||
'checkbox',
|
QUESTION_TYPE.CHECKBOX,
|
||||||
'binary-choice',
|
QUESTION_TYPE.BINARY_CHOICE,
|
||||||
'radio-star',
|
QUESTION_TYPE.RADIO_STAR,
|
||||||
'radio-nps',
|
QUESTION_TYPE.RADIO_NPS,
|
||||||
'vote',
|
QUESTION_TYPE.VOTE,
|
||||||
];
|
];
|
||||||
const fieldList = responseSchema.code.dataConf.dataList
|
const fieldList = responseSchema.code.dataConf.dataList
|
||||||
.filter((item) => allowQuestionType.includes(item.type))
|
.filter((item) => allowQuestionType.includes(item.type as QUESTION_TYPE))
|
||||||
.map((item) => item.field);
|
.map((item) => item.field);
|
||||||
const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => {
|
const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => {
|
||||||
pre[cur.field] = cur;
|
pre[cur.field] = cur;
|
||||||
|
@ -32,6 +32,7 @@ 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 { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@Controller('/api/survey')
|
@Controller('/api/survey')
|
||||||
@ -218,6 +219,16 @@ export class SurveyController {
|
|||||||
surveyMeta.isCollaborated = false;
|
surveyMeta.isCollaborated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 白名单相关字段的默认值
|
||||||
|
const baseConf = surveyConf.code?.baseConf;
|
||||||
|
if (baseConf) {
|
||||||
|
baseConf.passwordSwitch = baseConf.passwordSwitch ?? false;
|
||||||
|
baseConf.password = baseConf.password ?? '';
|
||||||
|
baseConf.whitelistType = baseConf.whitelistType ?? WhitelistType.ALL;
|
||||||
|
baseConf.whitelist = baseConf.whitelist ?? [];
|
||||||
|
baseConf.memberType = baseConf.memberType ?? MemberType.MOBILE;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: {
|
data: {
|
||||||
|
@ -8,9 +8,10 @@ import { keyBy } from 'lodash';
|
|||||||
import { DataItem } from 'src/interfaces/survey';
|
import { DataItem } from 'src/interfaces/survey';
|
||||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
||||||
|
import { QUESTION_TYPE } from 'src/enums/question';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataStatisticService {
|
export class DataStatisticService {
|
||||||
private radioType = ['radio-star', 'radio-nps'];
|
private radioType = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SurveyResponse)
|
@InjectRepository(SurveyResponse)
|
||||||
@ -68,7 +69,7 @@ export class DataStatisticService {
|
|||||||
}
|
}
|
||||||
// 处理选项的更多输入框
|
// 处理选项的更多输入框
|
||||||
if (
|
if (
|
||||||
this.radioType.includes(itemConfig.type) &&
|
this.radioType.includes(itemConfig.type as QUESTION_TYPE) &&
|
||||||
!data[`${itemConfigKey}_custom`]
|
!data[`${itemConfigKey}_custom`]
|
||||||
) {
|
) {
|
||||||
data[`${itemConfigKey}_custom`] =
|
data[`${itemConfigKey}_custom`] =
|
||||||
@ -89,7 +90,7 @@ export class DataStatisticService {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
difTime: (submitedData.difTime / 1000).toFixed(2),
|
diffTime: (submitedData.diffTime / 1000).toFixed(2),
|
||||||
createDate: moment(submitedData.createDate).format(
|
createDate: moment(submitedData.createDate).format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
),
|
),
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
"skinColor": "#4a4c5b",
|
"skinColor": "#4a4c5b",
|
||||||
"inputBgColor": "#ffffff",
|
"inputBgColor": "#ffffff",
|
||||||
"backgroundConf": {
|
"backgroundConf": {
|
||||||
"color": "#fff"
|
"color": "#ffffff"
|
||||||
},
|
},
|
||||||
"themeConf": {
|
"themeConf": {
|
||||||
"color": "#ffa600"
|
"color": "#ffa600"
|
||||||
@ -49,6 +49,7 @@
|
|||||||
"opacity": 100
|
"opacity": 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pageConf": [],
|
||||||
"logicConf": {
|
"logicConf": {
|
||||||
"showLogicConf": []
|
"showLogicConf": []
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import normalCode from '../template/surveyTemplate/survey/normal.json';
|
|||||||
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
||||||
import registerCode from '../template/surveyTemplate/survey/register.json';
|
import registerCode from '../template/surveyTemplate/survey/register.json';
|
||||||
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
||||||
|
import { QUESTION_TYPE } from 'src/enums/question';
|
||||||
|
|
||||||
const schemaDataMap = {
|
const schemaDataMap = {
|
||||||
normal: normalCode,
|
normal: normalCode,
|
||||||
@ -31,9 +32,11 @@ export async function getSchemaBySurveyType(surveyType: string) {
|
|||||||
export function getListHeadByDataList(dataList) {
|
export function getListHeadByDataList(dataList) {
|
||||||
const listHead = dataList.map((question) => {
|
const listHead = dataList.map((question) => {
|
||||||
let othersCode;
|
let othersCode;
|
||||||
const radioType = ['radio-star', 'radio-nps'];
|
const radioType = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS];
|
||||||
if (radioType.includes(question.type)) {
|
if (radioType.includes(question.type)) {
|
||||||
const rangeConfigKeys = question.rangeConfig ? Object.keys(question.rangeConfig) : [];
|
const rangeConfigKeys = question.rangeConfig
|
||||||
|
? Object.keys(question.rangeConfig)
|
||||||
|
: [];
|
||||||
if (rangeConfigKeys.length > 0) {
|
if (rangeConfigKeys.length > 0) {
|
||||||
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
||||||
}
|
}
|
||||||
@ -55,14 +58,14 @@ export function getListHeadByDataList(dataList) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
listHead.push({
|
listHead.push({
|
||||||
field: 'difTime',
|
field: 'diffTime',
|
||||||
title: '答题耗时(秒)',
|
title: '答题耗时(秒)',
|
||||||
type: 'text',
|
type: QUESTION_TYPE.TEXT,
|
||||||
});
|
});
|
||||||
listHead.push({
|
listHead.push({
|
||||||
field: 'createDate',
|
field: 'createDate',
|
||||||
title: '提交时间',
|
title: '提交时间',
|
||||||
type: 'text',
|
type: QUESTION_TYPE.TEXT,
|
||||||
});
|
});
|
||||||
return listHead;
|
return listHead;
|
||||||
}
|
}
|
||||||
@ -109,7 +112,14 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
pre[cur.id] = cur;
|
pre[cur.id] = cur;
|
||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
if (['radio', 'checkbox', 'vote', 'binary-choice'].includes(type)) {
|
if (
|
||||||
|
[
|
||||||
|
QUESTION_TYPE.RADIO,
|
||||||
|
QUESTION_TYPE.CHECKBOX,
|
||||||
|
QUESTION_TYPE.VOTE,
|
||||||
|
QUESTION_TYPE.BINARY_CHOICE,
|
||||||
|
].includes(type)
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
title: dataMap[item.field].title,
|
title: dataMap[item.field].title,
|
||||||
@ -125,7 +135,9 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else if (['radio-star', 'radio-nps'].includes(type)) {
|
} else if (
|
||||||
|
[QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS].includes(type)
|
||||||
|
) {
|
||||||
const summary: Record<string, any> = {};
|
const summary: Record<string, any> = {};
|
||||||
const average = getAverage({ aggregation: item.data.aggregation });
|
const average = getAverage({ aggregation: item.data.aggregation });
|
||||||
const median = getMedian({ aggregation: item.data.aggregation });
|
const median = getMedian({ aggregation: item.data.aggregation });
|
||||||
@ -136,10 +148,10 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
summary['average'] = average;
|
summary['average'] = average;
|
||||||
summary['median'] = median;
|
summary['median'] = median;
|
||||||
summary['variance'] = variance;
|
summary['variance'] = variance;
|
||||||
if (type === 'radio-nps') {
|
if (type === QUESTION_TYPE.RADIO_NPS) {
|
||||||
summary['nps'] = getNps({ aggregation: item.data.aggregation });
|
summary['nps'] = getNps({ aggregation: item.data.aggregation });
|
||||||
}
|
}
|
||||||
const range = type === 'radio-nps' ? [0, 10] : [1, 5];
|
const range = type === QUESTION_TYPE.RADIO_NPS ? [0, 10] : [1, 5];
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (let i = range[0]; i <= range[1]; i++) {
|
for (let i = range[0]; i <= range[1]; i++) {
|
||||||
arr.push(i);
|
arr.push(i);
|
||||||
|
@ -6,6 +6,11 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
|||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
|
||||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
|
import { Logger } from 'src/logger';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
|
|
||||||
jest.mock('../services/responseScheme.service');
|
jest.mock('../services/responseScheme.service');
|
||||||
|
|
||||||
@ -16,7 +21,40 @@ describe('ResponseSchemaController', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [ResponseSchemaController],
|
controllers: [ResponseSchemaController],
|
||||||
providers: [ResponseSchemaService],
|
providers: [
|
||||||
|
ResponseSchemaService,
|
||||||
|
AuthService,
|
||||||
|
{
|
||||||
|
provide: Logger,
|
||||||
|
useValue: {
|
||||||
|
info: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {
|
||||||
|
getUserByUsername: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: WorkspaceMemberService,
|
||||||
|
useValue: {
|
||||||
|
findAllByUserId: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: Logger,
|
||||||
|
useValue: {
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<ResponseSchemaController>(ResponseSchemaController);
|
controller = module.get<ResponseSchemaController>(ResponseSchemaController);
|
||||||
@ -66,5 +104,146 @@ describe('ResponseSchemaController', () => {
|
|||||||
new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED),
|
new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => {
|
||||||
|
const surveyPath = '';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123456',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => {
|
||||||
|
const surveyPath = '';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue({
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123457',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should be successfully', async () => {
|
||||||
|
const surveyPath = 'test';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue({
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123456',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ code: 200, data: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
|
||||||
|
const surveyPath = '';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue({
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
whitelistType: 'CUSTOM',
|
||||||
|
memberType: 'MOBILE',
|
||||||
|
whitelist: ['13500000000'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123456',
|
||||||
|
whitelist: '13500000001',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => {
|
||||||
|
const surveyPath = '';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue({
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
whitelistType: 'MEMBER',
|
||||||
|
whitelist: ['Jack'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123456',
|
||||||
|
whitelist: 'James',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('whitelistValidate should return verifyId successfully', async () => {
|
||||||
|
const surveyPath = '';
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValue({
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
whitelistType: 'CUSTOM',
|
||||||
|
memberType: 'MOBILE',
|
||||||
|
whitelist: ['13500000000'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.whitelistValidate(surveyPath, {
|
||||||
|
password: '123456',
|
||||||
|
whitelist: '13500000000',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ code: 200, data: null });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -21,6 +21,10 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
|
|||||||
import { RECORD_STATUS } from 'src/enums';
|
import { 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 { Logger } from 'src/logger';
|
||||||
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
|
||||||
const mockDecryptErrorBody = {
|
const mockDecryptErrorBody = {
|
||||||
surveyPath: 'EBzdmnSp',
|
surveyPath: 'EBzdmnSp',
|
||||||
@ -28,11 +32,11 @@ const mockDecryptErrorBody = {
|
|||||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||||
],
|
],
|
||||||
difTime: 806707,
|
diffTime: 806707,
|
||||||
clientTime: 1710400229573,
|
clientTime: 1710400229573,
|
||||||
encryptType: 'rsa',
|
encryptType: 'rsa',
|
||||||
sessionId: '65f2664c92862d6a9067ad18',
|
sessionId: '65f2664c92862d6a9067ad18',
|
||||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
sign: '95d6ff5dd3d9ddc205cbab88defe40ebe889952961f1d60e760fa411e2cb39fe.1710400229589',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSubmitData = {
|
const mockSubmitData = {
|
||||||
@ -41,11 +45,11 @@ const mockSubmitData = {
|
|||||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||||
],
|
],
|
||||||
difTime: 806707,
|
diffTime: 806707,
|
||||||
clientTime: 1710400229573,
|
clientTime: 1710400229573,
|
||||||
encryptType: 'rsa',
|
encryptType: 'rsa',
|
||||||
sessionId: '65f29fc192862d6a9067ad28',
|
sessionId: '65f29fc192862d6a9067ad28',
|
||||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
sign: '95d6ff5dd3d9ddc205cbab88defe40ebe889952961f1d60e760fa411e2cb39fe.1710400229589',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockClientEncryptInfo = {
|
const mockClientEncryptInfo = {
|
||||||
@ -124,6 +128,18 @@ describe('SurveyResponseController', () => {
|
|||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {
|
||||||
|
getUserByUsername: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: WorkspaceMemberService,
|
||||||
|
useValue: {
|
||||||
|
findAllByUserId: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -169,7 +185,7 @@ describe('SurveyResponseController', () => {
|
|||||||
status: RECORD_STATUS.NEW,
|
status: RECORD_STATUS.NEW,
|
||||||
date: 1711025113146,
|
date: 1711025113146,
|
||||||
},
|
},
|
||||||
difTime: 30518,
|
diffTime: 30518,
|
||||||
data: {
|
data: {
|
||||||
data458: '15000000000',
|
data458: '15000000000',
|
||||||
data515: '115019',
|
data515: '115019',
|
||||||
@ -204,7 +220,6 @@ 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: '提交成功' });
|
||||||
@ -224,7 +239,7 @@ describe('SurveyResponseController', () => {
|
|||||||
data770: '123456@qq.com',
|
data770: '123456@qq.com',
|
||||||
},
|
},
|
||||||
clientTime: reqBody.clientTime,
|
clientTime: reqBody.clientTime,
|
||||||
difTime: reqBody.difTime,
|
diffTime: reqBody.diffTime,
|
||||||
surveyId: mockResponseSchema.pageId,
|
surveyId: mockResponseSchema.pageId,
|
||||||
optionTextAndId: {
|
optionTextAndId: {
|
||||||
data515: [
|
data515: [
|
||||||
@ -306,5 +321,31 @@ describe('SurveyResponseController', () => {
|
|||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw HttpException if password does not match', async () => {
|
||||||
|
const reqBody = {
|
||||||
|
...mockSubmitData,
|
||||||
|
password: '123457',
|
||||||
|
sign: '145595d85079af3b1fb30784177c348555f442837c051d90f57a01ce1ff53c32.1710400229589',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
curStatus: {
|
||||||
|
status: RECORD_STATUS.PUBLISHED,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
baseConf: {
|
||||||
|
passwordSwitch: true,
|
||||||
|
password: '123456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ResponseSchema);
|
||||||
|
|
||||||
|
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||||
|
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ describe('SurveyResponseService', () => {
|
|||||||
const surveyData = {
|
const surveyData = {
|
||||||
data: {},
|
data: {},
|
||||||
clientTime: new Date(),
|
clientTime: new Date(),
|
||||||
difTime: 0,
|
diffTime: 0,
|
||||||
surveyId: 'testId',
|
surveyId: 'testId',
|
||||||
surveyPath: 'testPath',
|
surveyPath: 'testPath',
|
||||||
optionTextAndId: {},
|
optionTextAndId: {},
|
||||||
@ -59,7 +59,7 @@ describe('SurveyResponseService', () => {
|
|||||||
surveyPath: surveyData.surveyPath,
|
surveyPath: surveyData.surveyPath,
|
||||||
data: surveyData.data,
|
data: surveyData.data,
|
||||||
clientTime: surveyData.clientTime,
|
clientTime: surveyData.clientTime,
|
||||||
difTime: surveyData.difTime,
|
diffTime: surveyData.diffTime,
|
||||||
pageId: surveyData.surveyId,
|
pageId: surveyData.surveyId,
|
||||||
secretKeys: [],
|
secretKeys: [],
|
||||||
optionTextAndId: surveyData.optionTextAndId,
|
optionTextAndId: surveyData.optionTextAndId,
|
||||||
|
@ -1,14 +1,33 @@
|
|||||||
import { Controller, Get, HttpCode, Query } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||||
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 { 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 { Logger } from 'src/logger';
|
||||||
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
|
import { WhitelistType } from 'src/interfaces/survey';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
|
||||||
@ApiTags('surveyResponse')
|
@ApiTags('surveyResponse')
|
||||||
@Controller('/api/responseSchema')
|
@Controller('/api/responseSchema')
|
||||||
export class ResponseSchemaController {
|
export class ResponseSchemaController {
|
||||||
constructor(private readonly responseSchemaService: ResponseSchemaService) {}
|
constructor(
|
||||||
|
private readonly responseSchemaService: ResponseSchemaService,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('/getSchema')
|
@Get('/getSchema')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ -34,9 +53,79 @@ export class ResponseSchemaController {
|
|||||||
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
|
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 去掉C端的敏感字段
|
||||||
|
if (responseSchema.code?.baseConf) {
|
||||||
|
responseSchema.code.baseConf.password = null;
|
||||||
|
responseSchema.code.baseConf.whitelist = [];
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: responseSchema,
|
data: responseSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 白名单验证
|
||||||
|
@Post('/:surveyPath/validate')
|
||||||
|
@HttpCode(200)
|
||||||
|
async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) {
|
||||||
|
const { value, error } = Joi.object({
|
||||||
|
password: Joi.string().allow(null, ''),
|
||||||
|
whitelist: Joi.string().allow(null, ''),
|
||||||
|
}).validate(body, { allowUnknown: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(`whitelistValidate error: ${error.message}`, {});
|
||||||
|
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 问卷信息
|
||||||
|
const schema =
|
||||||
|
await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
|
||||||
|
if (!schema || schema.curStatus.status === 'removed') {
|
||||||
|
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password, whitelist: whitelistValue } = value;
|
||||||
|
const {
|
||||||
|
passwordSwitch,
|
||||||
|
password: settingPassword,
|
||||||
|
whitelistType,
|
||||||
|
whitelist,
|
||||||
|
} = schema.code.baseConf;
|
||||||
|
|
||||||
|
// 密码校验
|
||||||
|
if (passwordSwitch) {
|
||||||
|
if (settingPassword !== password) {
|
||||||
|
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 名单校验(手机号/邮箱)
|
||||||
|
if (whitelistType === WhitelistType.CUSTOM) {
|
||||||
|
if (!whitelist.includes(whitelistValue)) {
|
||||||
|
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 团队成员昵称校验
|
||||||
|
if (whitelistType === WhitelistType.MEMBER) {
|
||||||
|
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||||
|
{ userId: user._id.toString() },
|
||||||
|
);
|
||||||
|
if (!workspaceMember.length) {
|
||||||
|
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,9 @@ import { ApiTags } from '@nestjs/swagger';
|
|||||||
import { MutexService } from 'src/modules/mutex/services/mutexService.service';
|
import { MutexService } from 'src/modules/mutex/services/mutexService.service';
|
||||||
import { CounterService } from '../services/counter.service';
|
import { CounterService } from '../services/counter.service';
|
||||||
import { Logger } from 'src/logger';
|
import { Logger } from 'src/logger';
|
||||||
|
import { WhitelistType } from 'src/interfaces/survey';
|
||||||
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
|
|
||||||
@ApiTags('surveyResponse')
|
@ApiTags('surveyResponse')
|
||||||
@Controller('/api/surveyResponse')
|
@Controller('/api/surveyResponse')
|
||||||
@ -31,6 +34,8 @@ export class SurveyResponseController {
|
|||||||
private readonly mutexService: MutexService,
|
private readonly mutexService: MutexService,
|
||||||
private readonly counterService: CounterService,
|
private readonly counterService: CounterService,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/createResponse')
|
@Post('/createResponse')
|
||||||
@ -45,7 +50,9 @@ export class SurveyResponseController {
|
|||||||
encryptType: Joi.string(),
|
encryptType: Joi.string(),
|
||||||
sessionId: Joi.string(),
|
sessionId: Joi.string(),
|
||||||
clientTime: Joi.number().required(),
|
clientTime: Joi.number().required(),
|
||||||
difTime: Joi.number(),
|
diffTime: Joi.number(),
|
||||||
|
password: Joi.string().allow(null, ''),
|
||||||
|
whitelist: Joi.string().allow(null, ''),
|
||||||
}).validate(reqBody, { allowUnknown: true });
|
}).validate(reqBody, { allowUnknown: true });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -55,8 +62,16 @@ export class SurveyResponseController {
|
|||||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { surveyPath, encryptType, data, sessionId, clientTime, difTime } =
|
const {
|
||||||
value;
|
surveyPath,
|
||||||
|
encryptType,
|
||||||
|
data,
|
||||||
|
sessionId,
|
||||||
|
clientTime,
|
||||||
|
diffTime,
|
||||||
|
password,
|
||||||
|
whitelist: whitelistValue,
|
||||||
|
} = value;
|
||||||
|
|
||||||
// 查询schema
|
// 查询schema
|
||||||
const responseSchema =
|
const responseSchema =
|
||||||
@ -65,6 +80,50 @@ export class SurveyResponseController {
|
|||||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 白名单的verifyId校验
|
||||||
|
const baseConf = responseSchema.code.baseConf;
|
||||||
|
|
||||||
|
// 密码校验
|
||||||
|
if (baseConf?.passwordSwitch && baseConf.password) {
|
||||||
|
if (baseConf.password !== password) {
|
||||||
|
throw new HttpException(
|
||||||
|
'白名单验证失败',
|
||||||
|
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 名单校验(手机号/邮箱)
|
||||||
|
if (baseConf?.whitelistType === WhitelistType.CUSTOM) {
|
||||||
|
if (!baseConf.whitelist.includes(whitelistValue)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'白名单验证失败',
|
||||||
|
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 团队成员昵称校验
|
||||||
|
if (baseConf?.whitelistType === WhitelistType.MEMBER) {
|
||||||
|
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException(
|
||||||
|
'白名单验证失败',
|
||||||
|
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||||
|
{ userId: user._id.toString() },
|
||||||
|
);
|
||||||
|
if (!workspaceMember.length) {
|
||||||
|
throw new HttpException(
|
||||||
|
'白名单验证失败',
|
||||||
|
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 提交时间限制
|
// 提交时间限制
|
||||||
const begTime = responseSchema.code?.baseConf?.begTime || 0;
|
const begTime = responseSchema.code?.baseConf?.begTime || 0;
|
||||||
@ -224,7 +283,7 @@ export class SurveyResponseController {
|
|||||||
surveyPath: value.surveyPath,
|
surveyPath: value.surveyPath,
|
||||||
data: decryptedData,
|
data: decryptedData,
|
||||||
clientTime,
|
clientTime,
|
||||||
difTime,
|
diffTime,
|
||||||
surveyId: responseSchema.pageId,
|
surveyId: responseSchema.pageId,
|
||||||
optionTextAndId,
|
optionTextAndId,
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ export class SurveyResponseService {
|
|||||||
async createSurveyResponse({
|
async createSurveyResponse({
|
||||||
data,
|
data,
|
||||||
clientTime,
|
clientTime,
|
||||||
difTime,
|
diffTime,
|
||||||
surveyId,
|
surveyId,
|
||||||
surveyPath,
|
surveyPath,
|
||||||
optionTextAndId,
|
optionTextAndId,
|
||||||
@ -22,7 +22,7 @@ export class SurveyResponseService {
|
|||||||
data,
|
data,
|
||||||
secretKeys: [],
|
secretKeys: [],
|
||||||
clientTime,
|
clientTime,
|
||||||
difTime,
|
diffTime,
|
||||||
pageId: surveyId,
|
pageId: surveyId,
|
||||||
optionTextAndId,
|
optionTextAndId,
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,8 @@ import { CounterController } from './controllers/counter.controller';
|
|||||||
import { ResponseSchemaController } from './controllers/responseSchema.controller';
|
import { ResponseSchemaController } from './controllers/responseSchema.controller';
|
||||||
import { SurveyResponseController } from './controllers/surveyResponse.controller';
|
import { SurveyResponseController } from './controllers/surveyResponse.controller';
|
||||||
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
|
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
@ -32,6 +34,8 @@ import { MutexModule } from '../mutex/mutex.module';
|
|||||||
]),
|
]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
MessageModule,
|
MessageModule,
|
||||||
|
AuthModule,
|
||||||
|
WorkspaceModule,
|
||||||
MutexModule,
|
MutexModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
|
@ -32,8 +32,10 @@ describe('WorkspaceController', () => {
|
|||||||
useValue: {
|
useValue: {
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
findAllById: jest.fn(),
|
findAllById: jest.fn(),
|
||||||
|
findAllByIdWithPagination: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
findAllByUserId: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -45,6 +47,7 @@ describe('WorkspaceController', () => {
|
|||||||
batchUpdate: jest.fn(),
|
batchUpdate: jest.fn(),
|
||||||
batchDelete: jest.fn(),
|
batchDelete: jest.fn(),
|
||||||
countByWorkspaceId: jest.fn(),
|
countByWorkspaceId: jest.fn(),
|
||||||
|
batchSearchByWorkspace: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -145,20 +148,30 @@ describe('WorkspaceController', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(workspaceMemberService, 'findAllByUserId')
|
.spyOn(workspaceMemberService, 'findAllByUserId')
|
||||||
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
|
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(workspaceService, 'findAllById')
|
.spyOn(workspaceService, 'findAllByIdWithPagination')
|
||||||
.mockResolvedValue(workspaces as Array<Workspace>);
|
.mockResolvedValue({
|
||||||
|
list: workspaces as Array<Workspace>,
|
||||||
|
count: workspaces.length,
|
||||||
|
});
|
||||||
|
|
||||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
|
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
|
||||||
|
|
||||||
const result = await controller.findAll(req);
|
const result = await controller.findAll(req, {
|
||||||
|
curPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.code).toEqual(200);
|
expect(result.code).toEqual(200);
|
||||||
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
|
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
});
|
});
|
||||||
expect(workspaceService.findAllById).toHaveBeenCalledWith({
|
expect(workspaceService.findAllByIdWithPagination).toHaveBeenCalledWith({
|
||||||
workspaceIdList: memberList.map((item) => item.workspaceId),
|
workspaceIdList: memberList.map((item) => item.workspaceId),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
name: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -226,4 +239,31 @@ describe('WorkspaceController', () => {
|
|||||||
expect(workspaceService.delete).toHaveBeenCalledWith(id);
|
expect(workspaceService.delete).toHaveBeenCalledWith(id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getWorkspaceAndMember', () => {
|
||||||
|
it('should return a list of workspaces and members for the user', async () => {
|
||||||
|
const req = { user: { _id: new ObjectId() } };
|
||||||
|
|
||||||
|
const workspaceId = new ObjectId();
|
||||||
|
const memberList = [{ workspaceId, userId: new ObjectId() }];
|
||||||
|
const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceService, 'findAllByUserId')
|
||||||
|
.mockResolvedValue(workspaces as Array<Workspace>);
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceMemberService, 'batchSearchByWorkspace')
|
||||||
|
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
|
||||||
|
|
||||||
|
const result = await controller.getWorkspaceAndMember(req);
|
||||||
|
|
||||||
|
expect(result.code).toEqual(200);
|
||||||
|
expect(workspaceService.findAllByUserId).toHaveBeenCalledWith(
|
||||||
|
req.user._id.toString(),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
workspaceMemberService.batchSearchByWorkspace,
|
||||||
|
).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString()));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -123,4 +123,25 @@ describe('WorkspaceService', () => {
|
|||||||
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
|
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findAllByUserId', () => {
|
||||||
|
it('should return all workspaces under a user', async () => {
|
||||||
|
const workspaceIdList = [
|
||||||
|
new ObjectId().toString(),
|
||||||
|
new ObjectId().toString(),
|
||||||
|
];
|
||||||
|
const workspaces = [
|
||||||
|
{ _id: workspaceIdList[0], name: 'Workspace 1' },
|
||||||
|
{ _id: workspaceIdList[1], name: 'Workspace 2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceRepository, 'find')
|
||||||
|
.mockResolvedValue(workspaces as any);
|
||||||
|
|
||||||
|
const result = await service.findAllByUserId('');
|
||||||
|
expect(result).toEqual(workspaces);
|
||||||
|
expect(workspaceRepository.find).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -193,4 +193,21 @@ describe('WorkspaceMemberService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('batchSearchByWorkspace', () => {
|
||||||
|
it('should return all workspace members by workspace id list', async () => {
|
||||||
|
const workspaceList = ['workspaceId1', 'workspaceId2'];
|
||||||
|
const members = [
|
||||||
|
{ userId: 'userId1', workspaceId: workspaceList[0] },
|
||||||
|
{ userId: 'userId2', workspaceId: workspaceList[1] },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(repository, 'find').mockResolvedValue(members as any);
|
||||||
|
|
||||||
|
const result = await service.batchSearchByWorkspace(workspaceList);
|
||||||
|
|
||||||
|
expect(result).toEqual(members);
|
||||||
|
expect(repository.find).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
Request,
|
Request,
|
||||||
SetMetadata,
|
SetMetadata,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
@ -31,6 +32,9 @@ 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 { Logger } from 'src/logger';
|
||||||
|
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
|
||||||
|
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||||
|
import { Workspace } from 'src/models/workspace.entity';
|
||||||
|
|
||||||
@ApiTags('workspace')
|
@ApiTags('workspace')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ -128,8 +132,21 @@ export class WorkspaceController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async findAll(@Request() req) {
|
async findAll(@Request() req, @Query() queryInfo: GetWorkspaceListDto) {
|
||||||
|
const { value, error } = GetWorkspaceListDto.validate(queryInfo);
|
||||||
|
if (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`GetWorkspaceListDto validate failed: ${error.message}`,
|
||||||
|
{ req },
|
||||||
|
);
|
||||||
|
throw new HttpException(
|
||||||
|
`参数错误: 请联系管理员`,
|
||||||
|
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
const userId = req.user._id.toString();
|
const userId = req.user._id.toString();
|
||||||
|
const curPage = Number(value.curPage);
|
||||||
|
const pageSize = Number(value.pageSize);
|
||||||
// 查询当前用户参与的空间
|
// 查询当前用户参与的空间
|
||||||
const workspaceInfoList = await this.workspaceMemberService.findAllByUserId(
|
const workspaceInfoList = await this.workspaceMemberService.findAllByUserId(
|
||||||
{ userId },
|
{ userId },
|
||||||
@ -139,9 +156,16 @@ export class WorkspaceController {
|
|||||||
pre[cur.workspaceId] = cur;
|
pre[cur.workspaceId] = cur;
|
||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// 查询当前用户的空间列表
|
// 查询当前用户的空间列表
|
||||||
const list = await this.workspaceService.findAllById({ workspaceIdList });
|
const { list, count } =
|
||||||
const ownerIdList = list.map((item) => item.ownerId);
|
await this.workspaceService.findAllByIdWithPagination({
|
||||||
|
workspaceIdList,
|
||||||
|
page: curPage,
|
||||||
|
limit: pageSize,
|
||||||
|
name: queryInfo.name,
|
||||||
|
});
|
||||||
|
const ownerIdList = list.map((item: { ownerId: any }) => item.ownerId);
|
||||||
const userList = await this.userService.getUserListByIds({
|
const userList = await this.userService.getUserListByIds({
|
||||||
idList: ownerIdList,
|
idList: ownerIdList,
|
||||||
});
|
});
|
||||||
@ -150,6 +174,7 @@ export class WorkspaceController {
|
|||||||
pre[id] = cur;
|
pre[id] = cur;
|
||||||
return pre;
|
return pre;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const surveyTotalList = await Promise.all(
|
const surveyTotalList = await Promise.all(
|
||||||
workspaceIdList.map((item) => {
|
workspaceIdList.map((item) => {
|
||||||
return this.surveyMetaService.countSurveyMetaByWorkspaceId({
|
return this.surveyMetaService.countSurveyMetaByWorkspaceId({
|
||||||
@ -193,6 +218,7 @@ export class WorkspaceController {
|
|||||||
memberTotal: memberTotalMap[workspaceId] || 0,
|
memberTotal: memberTotalMap[workspaceId] || 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
count,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -326,4 +352,53 @@ export class WorkspaceController {
|
|||||||
code: 200,
|
code: 200,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/member/list')
|
||||||
|
@HttpCode(200)
|
||||||
|
async getWorkspaceAndMember(@Request() req) {
|
||||||
|
const userId = req.user._id.toString();
|
||||||
|
|
||||||
|
// 所在所有空间
|
||||||
|
const workspaceList = await this.workspaceService.findAllByUserId(userId);
|
||||||
|
if (!workspaceList.length) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有空间下的所有成员
|
||||||
|
const workspaceMemberList =
|
||||||
|
await this.workspaceMemberService.batchSearchByWorkspace(
|
||||||
|
workspaceList.map((item) => item._id.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 查询成员姓名
|
||||||
|
const userList = await this.userService.getUserListByIds({
|
||||||
|
idList: workspaceMemberList.map((member) => member.userId),
|
||||||
|
});
|
||||||
|
const userInfoMap = userList.reduce((pre, cur) => {
|
||||||
|
const id = cur._id.toString();
|
||||||
|
pre[id] = cur;
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const temp: Record<string, WorkspaceMember[]> = {};
|
||||||
|
const list = workspaceList.map(
|
||||||
|
(item: Workspace & { members: WorkspaceMember[] }) => {
|
||||||
|
temp[item._id.toString()] = item.members = [];
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceMemberList.forEach((member: WorkspaceMember) => {
|
||||||
|
(member as any).username = userInfoMap[member.userId.toString()].username;
|
||||||
|
temp[member.workspaceId.toString()].push(member);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: list,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
21
server/src/modules/workspace/dto/getWorkspaceList.dto.ts
Normal file
21
server/src/modules/workspace/dto/getWorkspaceList.dto.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import Joi from 'joi';
|
||||||
|
|
||||||
|
export class GetWorkspaceListDto {
|
||||||
|
@ApiProperty({ description: '当前页码', required: true })
|
||||||
|
curPage: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分页', required: false })
|
||||||
|
pageSize: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '空间名称', required: false })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
static validate(data: Partial<GetWorkspaceListDto>): Joi.ValidationResult {
|
||||||
|
return Joi.object({
|
||||||
|
curPage: Joi.number().required(),
|
||||||
|
pageSize: Joi.number().allow(null).default(10),
|
||||||
|
name: Joi.string().allow(null, '').optional(),
|
||||||
|
}).validate(data);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,17 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
|||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
|
||||||
|
interface FindAllByIdWithPaginationParams {
|
||||||
|
workspaceIdList: string[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
interface FindAllByIdWithPaginationResult {
|
||||||
|
list: Workspace[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -41,15 +52,17 @@ export class WorkspaceService {
|
|||||||
}: {
|
}: {
|
||||||
workspaceIdList: string[];
|
workspaceIdList: string[];
|
||||||
}): Promise<Workspace[]> {
|
}): Promise<Workspace[]> {
|
||||||
return this.workspaceRepository.find({
|
const query = {
|
||||||
where: {
|
|
||||||
_id: {
|
_id: {
|
||||||
$in: workspaceIdList.map((item) => new ObjectId(item)),
|
$in: workspaceIdList.map((item) => new ObjectId(item)),
|
||||||
},
|
},
|
||||||
'curStatus.status': {
|
'curStatus.status': {
|
||||||
$ne: RECORD_STATUS.REMOVED,
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
return this.workspaceRepository.find({
|
||||||
|
where: query,
|
||||||
order: {
|
order: {
|
||||||
_id: -1,
|
_id: -1,
|
||||||
},
|
},
|
||||||
@ -64,6 +77,38 @@ export class WorkspaceService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAllByIdWithPagination({
|
||||||
|
workspaceIdList,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
name,
|
||||||
|
}: FindAllByIdWithPaginationParams): Promise<FindAllByIdWithPaginationResult> {
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
if (!Array.isArray(workspaceIdList) || workspaceIdList.length === 0) {
|
||||||
|
return { list: [], count: 0 };
|
||||||
|
}
|
||||||
|
const query = {
|
||||||
|
_id: {
|
||||||
|
$in: workspaceIdList.map((m) => new ObjectId(m)),
|
||||||
|
},
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (name) {
|
||||||
|
query['name'] = { $regex: name, $options: 'i' };
|
||||||
|
}
|
||||||
|
const [data, count] = await this.workspaceRepository.findAndCount({
|
||||||
|
where: query,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
order: {
|
||||||
|
createDate: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { list: data, count };
|
||||||
|
}
|
||||||
|
|
||||||
update(id: string, workspace: Partial<Workspace>) {
|
update(id: string, workspace: Partial<Workspace>) {
|
||||||
return this.workspaceRepository.update(id, workspace);
|
return this.workspaceRepository.update(id, workspace);
|
||||||
}
|
}
|
||||||
@ -104,4 +149,27 @@ export class WorkspaceService {
|
|||||||
surveyRes,
|
surveyRes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户下的所有空间
|
||||||
|
async findAllByUserId(userId: string) {
|
||||||
|
return await this.workspaceRepository.find({
|
||||||
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: RECORD_STATUS.REMOVED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
_id: -1,
|
||||||
|
},
|
||||||
|
select: [
|
||||||
|
'_id',
|
||||||
|
'curStatus',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'ownerId',
|
||||||
|
'createDate',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,4 +140,19 @@ export class WorkspaceMemberService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据空间id批量查询成员
|
||||||
|
async batchSearchByWorkspace(workspaceList: string[]) {
|
||||||
|
return await this.workspaceMemberRepository.find({
|
||||||
|
where: {
|
||||||
|
workspaceId: {
|
||||||
|
$in: workspaceList,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
_id: -1,
|
||||||
|
},
|
||||||
|
select: ['_id', 'userId', 'role', 'workspaceId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ describe('getPushingData', () => {
|
|||||||
status: RECORD_STATUS.NEW,
|
status: RECORD_STATUS.NEW,
|
||||||
date: 1711025113146,
|
date: 1711025113146,
|
||||||
},
|
},
|
||||||
difTime: 30518,
|
diffTime: 30518,
|
||||||
data: {
|
data: {
|
||||||
data458: '15000000000',
|
data458: '15000000000',
|
||||||
data515: '115019',
|
data515: '115019',
|
||||||
|
73
web/components.d.ts
vendored
73
web/components.d.ts
vendored
@ -1,73 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module 'vue' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
||||||
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
|
||||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
|
||||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
|
||||||
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
|
||||||
IEpClose: typeof import('~icons/ep/close')['default']
|
|
||||||
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
|
||||||
IEpDelete: typeof import('~icons/ep/delete')['default']
|
|
||||||
IEpIphone: typeof import('~icons/ep/iphone')['default']
|
|
||||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
|
||||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
|
||||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
|
||||||
IEpMore: typeof import('~icons/ep/more')['default']
|
|
||||||
IEpPlus: typeof import('~icons/ep/plus')['default']
|
|
||||||
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
|
||||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
|
||||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
|
||||||
IEpSearch: typeof import('~icons/ep/search')['default']
|
|
||||||
IEpSort: typeof import('~icons/ep/sort')['default']
|
|
||||||
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
|
|
||||||
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
|
|
||||||
IEpTop: typeof import('~icons/ep/top')['default']
|
|
||||||
IEpView: typeof import('~icons/ep/view')['default']
|
|
||||||
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
}
|
|
||||||
export interface ComponentCustomProperties {
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
||||||
}
|
|
||||||
}
|
|
7
web/env.d.ts
vendored
7
web/env.d.ts
vendored
@ -1,8 +1 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
declare module "vuex" {
|
|
||||||
export * from "vuex/types/index.d.ts";
|
|
||||||
export * from "vuex/types/helpers.d.ts";
|
|
||||||
export * from "vuex/types/logger.d.ts";
|
|
||||||
export * from "vuex/types/vue.d.ts";
|
|
||||||
}
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
|||||||
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logicflow/core": "2.0.0",
|
||||||
|
"@logicflow/extension": "2.0.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
@ -26,12 +28,12 @@
|
|||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"uuid": "^10.0.0",
|
"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",
|
||||||
"vuex": "^4.0.2",
|
|
||||||
"xss": "^1.0.14",
|
"xss": "^1.0.14",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
@ -53,7 +55,7 @@
|
|||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"sass": "^1.72.0",
|
"sass": "1.77.6",
|
||||||
"typescript": "~5.3.0",
|
"typescript": "~5.3.0",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-icons": "^0.18.5",
|
"unplugin-icons": "^0.18.5",
|
||||||
|
@ -23,22 +23,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, shallowRef, onBeforeMount, watch, computed } from 'vue'
|
||||||
|
import { get as _get } from 'lodash-es'
|
||||||
|
|
||||||
import '@wangeditor/editor/dist/css/style.css'
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
import './styles/reset-wangeditor.scss'
|
import './styles/reset-wangeditor.scss'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
import { ref, shallowRef, onBeforeMount, watch } from 'vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['input', 'onFocus', 'change', 'blur'])
|
import { useUserStore } from '@/management/stores/user'
|
||||||
|
import { replacePxWithRem } from './utils'
|
||||||
|
|
||||||
|
const emit = defineEmits(['input', 'onFocus', 'change', 'blur', 'created'])
|
||||||
const model = defineModel()
|
const model = defineModel()
|
||||||
const props = defineProps(['staticToolBar'])
|
const props = defineProps({
|
||||||
|
staticToolBar: { default: false, required: false },
|
||||||
|
needUploadImage: { default: false, required: false }
|
||||||
|
})
|
||||||
|
|
||||||
const curValue = ref('')
|
const curValue = ref('')
|
||||||
const editorRef = shallowRef()
|
const editorRef = shallowRef()
|
||||||
const showToolbar = ref(props.staticToolBar || false)
|
const showToolbar = ref(props.staticToolBar)
|
||||||
|
|
||||||
const mode = 'simple'
|
const mode = 'simple'
|
||||||
|
|
||||||
const toolbarConfig = {
|
const toolbarConfig = computed(() => {
|
||||||
|
const config = {
|
||||||
toolbarKeys: [
|
toolbarKeys: [
|
||||||
'color', // 字体色
|
'color', // 字体色
|
||||||
'bgColor', // 背景色
|
'bgColor', // 背景色
|
||||||
@ -46,8 +55,37 @@ const toolbarConfig = {
|
|||||||
'insertLink' // 链接
|
'insertLink' // 链接
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
if (props.needUploadImage) {
|
||||||
|
config.toolbarKeys.push('uploadImage')
|
||||||
|
}
|
||||||
|
|
||||||
const editorConfig = {}
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
MENU_CONF: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const token = _get(userStore, 'userInfo.token')
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
editorConfig.MENU_CONF['uploadImage'] = {
|
||||||
|
allowedFileTypes: ['image/jpeg', 'image/png'],
|
||||||
|
server: '/api/file/upload',
|
||||||
|
fieldName: 'file',
|
||||||
|
meta: {
|
||||||
|
//! 此处的channel需要跟上传接口内配置的channel一致
|
||||||
|
channel: 'upload'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
customInsert(res, insertFn) {
|
||||||
|
const url = res.data.url
|
||||||
|
insertFn(url, '', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setHtml = (newHtml) => {
|
const setHtml = (newHtml) => {
|
||||||
const editor = editorRef.value
|
const editor = editorRef.value
|
||||||
@ -60,9 +98,11 @@ const onCreated = (editor) => {
|
|||||||
if (model.value) {
|
if (model.value) {
|
||||||
setHtml(model.value)
|
setHtml(model.value)
|
||||||
}
|
}
|
||||||
|
emit('created', editor)
|
||||||
}
|
}
|
||||||
const onChange = (editor) => {
|
const onChange = (editor) => {
|
||||||
const editorHtml = editor.getHtml()
|
const editorHtml = replacePxWithRem(editor.getHtml())
|
||||||
|
|
||||||
curValue.value = editorHtml // 记录当前 html 内容
|
curValue.value = editorHtml // 记录当前 html 内容
|
||||||
emit('input', editorHtml) // 用于自定义 v-model
|
emit('input', editorHtml) // 用于自定义 v-model
|
||||||
}
|
}
|
||||||
@ -113,6 +153,7 @@ onBeforeMount(() => {
|
|||||||
.static-toolbar {
|
.static-toolbar {
|
||||||
border-bottom: 1px solid #dedede;
|
border-bottom: 1px solid #dedede;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamic-toolbar {
|
.dynamic-toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
28
web/src/common/Editor/utils.js
Normal file
28
web/src/common/Editor/utils.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// px 转换为 rem
|
||||||
|
const pxToRem = (px) => {
|
||||||
|
return `${(parseFloat(px) / 50).toFixed(2)}rem`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片style的宽高改成rem
|
||||||
|
export const replacePxWithRem = (html) => {
|
||||||
|
const imgRegex = /<img[^>]*style=["'][^"']*\b(?:width|height):\s*\d+(\.\d+)?px[^"']*["'][^>]*>/gi
|
||||||
|
const styleRegex = /style="([^"]*)"/g
|
||||||
|
if (!imgRegex.test(html)) {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = html.replaceAll(imgRegex, (imgHtml) => {
|
||||||
|
return imgHtml.replace(styleRegex, (match, content) => {
|
||||||
|
let styleContent = content
|
||||||
|
const pxRegex = /(width|height):\s*(\d+(\.\d+)?)px/g
|
||||||
|
|
||||||
|
styleContent = styleContent.replace(pxRegex, (pxMatch, prop, value) => {
|
||||||
|
return `${prop}: ${pxToRem(value)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `style="${styleContent}"`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
@ -33,8 +33,8 @@ export class RuleNode {
|
|||||||
conditions: ConditionNode[] = []
|
conditions: ConditionNode[] = []
|
||||||
scope: string = Scope.Question
|
scope: string = Scope.Question
|
||||||
target: string = ''
|
target: string = ''
|
||||||
constructor(scope: string = Scope.Question, target: string = '') {
|
constructor(target: string = '', scope: string = Scope.Question, id?: string) {
|
||||||
this.id = generateID(PrefixID.Rule)
|
this.id = id || generateID(PrefixID.Rule)
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
this.target = target
|
this.target = target
|
||||||
}
|
}
|
||||||
@ -54,14 +54,8 @@ export class RuleNode {
|
|||||||
|
|
||||||
export class RuleBuild {
|
export class RuleBuild {
|
||||||
rules: RuleNode[] = []
|
rules: RuleNode[] = []
|
||||||
static instance: RuleBuild
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.rules = []
|
this.rules = []
|
||||||
if (!RuleBuild.instance) {
|
|
||||||
RuleBuild.instance = this
|
|
||||||
}
|
|
||||||
|
|
||||||
return RuleBuild.instance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加条件规则到规则引擎中
|
// 添加条件规则到规则引擎中
|
||||||
@ -71,6 +65,9 @@ export class RuleBuild {
|
|||||||
removeRule(ruleId: string) {
|
removeRule(ruleId: string) {
|
||||||
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
||||||
}
|
}
|
||||||
|
clear() {
|
||||||
|
this.rules = []
|
||||||
|
}
|
||||||
findRule(ruleId: string) {
|
findRule(ruleId: string) {
|
||||||
return this.rules.find((rule) => rule.id === ruleId)
|
return this.rules.find((rule) => rule.id === ruleId)
|
||||||
}
|
}
|
||||||
@ -94,7 +91,7 @@ export class RuleBuild {
|
|||||||
if (ruleConf instanceof Array) {
|
if (ruleConf instanceof Array) {
|
||||||
ruleConf.forEach((rule: any) => {
|
ruleConf.forEach((rule: any) => {
|
||||||
const { scope, target } = rule
|
const { scope, target } = rule
|
||||||
const ruleNode = new RuleNode(scope, target)
|
const ruleNode = new RuleNode(target, scope)
|
||||||
rule.conditions.forEach((condition: any) => {
|
rule.conditions.forEach((condition: any) => {
|
||||||
const { field, operator, value } = condition
|
const { field, operator, value } = condition
|
||||||
const conditionNode = new ConditionNode(field, operator, value)
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
@ -112,19 +109,19 @@ export class RuleBuild {
|
|||||||
findTargetsByScope(scope: string) {
|
findTargetsByScope(scope: string) {
|
||||||
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
||||||
}
|
}
|
||||||
|
findRulesByField(field: string) {
|
||||||
|
return this.rules.filter((rule) => {
|
||||||
|
return rule.conditions.filter((condition) => condition.field === field).length
|
||||||
|
})
|
||||||
|
}
|
||||||
// 实现前置题删除校验
|
// 实现前置题删除校验
|
||||||
findTargetsByFields(field: string) {
|
findTargetsByField(field: string) {
|
||||||
const nodes = this.rules.filter((rule: RuleNode) => {
|
const nodes = this.findRulesByField(field)
|
||||||
const conditions = rule.conditions.filter((item: any) => {
|
|
||||||
return item.field === field
|
|
||||||
})
|
|
||||||
return conditions.length > 0
|
|
||||||
})
|
|
||||||
return nodes.map((item: any) => {
|
return nodes.map((item: any) => {
|
||||||
return item.target
|
return item.target
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 根据目标题获取显示逻辑
|
// 根据目标题获取关联的逻辑条件
|
||||||
findConditionByTarget(target: string) {
|
findConditionByTarget(target: string) {
|
||||||
return this.rules.filter((rule) => rule.target === target).map((item) => item.conditions)
|
return this.rules.filter((rule) => rule.target === target).map((item) => item.conditions)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Operator, type FieldTypes, type Fact } from './BasicType'
|
|||||||
// 定义条件规则类
|
// 定义条件规则类
|
||||||
export class ConditionNode<F extends string, O extends Operator> {
|
export class ConditionNode<F extends string, O extends Operator> {
|
||||||
// 默认显示
|
// 默认显示
|
||||||
public result: boolean = false
|
public result: boolean | undefined = undefined
|
||||||
constructor(
|
constructor(
|
||||||
public field: F,
|
public field: F,
|
||||||
public operator: O,
|
public operator: O,
|
||||||
@ -16,7 +16,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
return this.field + this.operator + this.value
|
return this.field + this.operator + this.value
|
||||||
}
|
}
|
||||||
|
|
||||||
match(facts: Fact): boolean {
|
match(facts: Fact): boolean | undefined {
|
||||||
// console.log(this.calculateHash())
|
// console.log(this.calculateHash())
|
||||||
// 如果该特征在事实对象中不存在,则直接返回false
|
// 如果该特征在事实对象中不存在,则直接返回false
|
||||||
if (!facts[this.field]) {
|
if (!facts[this.field]) {
|
||||||
@ -45,7 +45,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
this.result = this.value.some((v) => !facts[this.field].includes(v))
|
this.result = this.value.some((v) => !facts[this.field].includes(v))
|
||||||
return this.result
|
return this.result
|
||||||
} else {
|
} else {
|
||||||
this.result = facts[this.field].includes(this.value)
|
this.result = !facts[this.field].includes(this.value)
|
||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
case Operator.NotEqual:
|
case Operator.NotEqual:
|
||||||
@ -53,7 +53,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
this.result = this.value.every((v) => !facts[this.field].includes(v))
|
this.result = this.value.every((v) => !facts[this.field].includes(v))
|
||||||
return this.result
|
return this.result
|
||||||
} else {
|
} else {
|
||||||
this.result = facts[this.field].includes(this.value)
|
this.result = facts[this.field].toString() !== this.value
|
||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
// 其他比较操作符的判断逻辑
|
// 其他比较操作符的判断逻辑
|
||||||
@ -69,7 +69,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
|
|
||||||
export class RuleNode {
|
export class RuleNode {
|
||||||
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
||||||
public result: boolean = false
|
public result: boolean | undefined
|
||||||
constructor(
|
constructor(
|
||||||
public target: string,
|
public target: string,
|
||||||
public scope: string
|
public scope: string
|
||||||
@ -83,8 +83,10 @@ export class RuleNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 匹配条件规则
|
// 匹配条件规则
|
||||||
match(fact: Fact) {
|
match(fact: Fact, comparor?: any) {
|
||||||
const res = Array.from(this.conditions.entries()).every(([, value]) => {
|
let res: boolean | undefined = undefined
|
||||||
|
if (comparor === 'or') {
|
||||||
|
res = Array.from(this.conditions.entries()).some(([, value]) => {
|
||||||
const res = value.match(fact)
|
const res = value.match(fact)
|
||||||
if (res) {
|
if (res) {
|
||||||
return true
|
return true
|
||||||
@ -92,6 +94,17 @@ export class RuleNode {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
res = Array.from(this.conditions.entries()).every(([, value]) => {
|
||||||
|
const res = value.match(fact)
|
||||||
|
if (res) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.result = res
|
this.result = res
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -121,14 +134,14 @@ export class RuleNode {
|
|||||||
|
|
||||||
export class RuleMatch {
|
export class RuleMatch {
|
||||||
rules: Map<string, RuleNode>
|
rules: Map<string, RuleNode>
|
||||||
static instance: any
|
// static instance: any
|
||||||
constructor() {
|
constructor() {
|
||||||
this.rules = new Map()
|
this.rules = new Map()
|
||||||
if (!RuleMatch.instance) {
|
// if (!RuleMatch.instance) {
|
||||||
RuleMatch.instance = this
|
// RuleMatch.instance = this
|
||||||
}
|
// }
|
||||||
|
|
||||||
return RuleMatch.instance
|
// return RuleMatch.instance
|
||||||
}
|
}
|
||||||
fromJson(ruleConf: any) {
|
fromJson(ruleConf: any) {
|
||||||
if (ruleConf instanceof Array) {
|
if (ruleConf instanceof Array) {
|
||||||
@ -145,6 +158,7 @@ export class RuleMatch {
|
|||||||
this.addRule(ruleNode)
|
this.addRule(ruleNode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加条件规则到规则引擎中
|
// 添加条件规则到规则引擎中
|
||||||
@ -160,22 +174,31 @@ export class RuleMatch {
|
|||||||
this.rules.set(hash, rule)
|
this.rules.set(hash, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匹配条件规则
|
// 特定目标题规则匹配
|
||||||
match(target: string, scope: string, fact: Fact) {
|
match(target: string, scope: string, fact: Fact, comparor?: any) {
|
||||||
const hash = this.calculateHash(target, scope)
|
const hash = this.calculateHash(target, scope)
|
||||||
|
|
||||||
const rule = this.rules.get(hash)
|
const rule = this.rules.get(hash)
|
||||||
if (rule) {
|
if (rule) {
|
||||||
const result = rule.match(fact)
|
const result = rule.match(fact, comparor)
|
||||||
// this.matchCache.set(hash, result);
|
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
// 默认显示
|
// 默认显示
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 获取条件题关联的多个目标题匹配情况 */
|
||||||
getResult(target: string, scope: string) {
|
getResultsByField(field: string, fact: Fact) {
|
||||||
|
const rules = this.findRulesByField(field)
|
||||||
|
return rules.map(([, rule]) => {
|
||||||
|
return {
|
||||||
|
target: rule.target,
|
||||||
|
result: this.match(rule.target, 'question', fact, 'or')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/* 获取目标题的规则是否匹配 */
|
||||||
|
getResultByTarget(target: string, scope: string) {
|
||||||
const hash = this.calculateHash(target, scope)
|
const hash = this.calculateHash(target, scope)
|
||||||
const rule = this.rules.get(hash)
|
const rule = this.rules.get(hash)
|
||||||
if (rule) {
|
if (rule) {
|
||||||
@ -191,15 +214,18 @@ export class RuleMatch {
|
|||||||
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
||||||
return target + scope
|
return target + scope
|
||||||
}
|
}
|
||||||
findTargetsByField(field: string) {
|
// 查找条件题的规则
|
||||||
const rules = new Map(
|
findRulesByField(field: string) {
|
||||||
[...this.rules.entries()].filter(([, value]) => {
|
const list = [...this.rules.entries()]
|
||||||
return [...value.conditions.entries()].filter(([, value]) => {
|
const match = list.filter(([, ruleValue]) => {
|
||||||
return value.field === field
|
const list = [...ruleValue.conditions.entries()]
|
||||||
|
const res = list.filter(([, conditionValue]) => {
|
||||||
|
const hit = conditionValue.field === field
|
||||||
|
return hit
|
||||||
})
|
})
|
||||||
|
return res.length
|
||||||
})
|
})
|
||||||
)
|
return match
|
||||||
return [...rules.values()].map((obj) => obj.target)
|
|
||||||
}
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
return Array.from(this.rules.entries()).map(([, value]) => {
|
return Array.from(this.rules.entries()).map(([, value]) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// 静态数据
|
// test:静态数据,实际业务里无用
|
||||||
export const ruleConf = [
|
export const ruleConf = [
|
||||||
{
|
{
|
||||||
conditions: [
|
conditions: [
|
||||||
|
11
web/src/common/regexpMap.ts
Normal file
11
web/src/common/regexpMap.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const regexpMap = {
|
||||||
|
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
||||||
|
m: /^[1]([3-9])[0-9]{9}$/,
|
||||||
|
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
|
||||||
|
strictIdcard:
|
||||||
|
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
|
||||||
|
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
|
||||||
|
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
|
||||||
|
licensePlate:
|
||||||
|
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
|
||||||
|
}
|
@ -7,7 +7,7 @@ export enum QUESTION_TYPE {
|
|||||||
BINARY_CHOICE = 'binary-choice',
|
BINARY_CHOICE = 'binary-choice',
|
||||||
RADIO_STAR = 'radio-star',
|
RADIO_STAR = 'radio-star',
|
||||||
RADIO_NPS = 'radio-nps',
|
RADIO_NPS = 'radio-nps',
|
||||||
VOTE = 'vote',
|
VOTE = 'vote'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题目类型标签映射对象
|
// 题目类型标签映射对象
|
||||||
@ -23,16 +23,10 @@ export const typeTagLabels: Record<QUESTION_TYPE, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 输入类题型
|
// 输入类题型
|
||||||
export const INPUT = [
|
export const INPUT = [QUESTION_TYPE.TEXT, QUESTION_TYPE.TEXTAREA]
|
||||||
QUESTION_TYPE.TEXT,
|
|
||||||
QUESTION_TYPE.TEXTAREA
|
|
||||||
]
|
|
||||||
|
|
||||||
// 选择类题型分类
|
// 选择类题型分类
|
||||||
export const NORMAL_CHOICES = [
|
export const NORMAL_CHOICES = [QUESTION_TYPE.RADIO, QUESTION_TYPE.CHECKBOX]
|
||||||
QUESTION_TYPE.RADIO,
|
|
||||||
QUESTION_TYPE.CHECKBOX
|
|
||||||
]
|
|
||||||
|
|
||||||
// 选择类题型分类
|
// 选择类题型分类
|
||||||
export const CHOICES = [
|
export const CHOICES = [
|
||||||
@ -43,8 +37,4 @@ export const CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// 评分题题型分类
|
// 评分题题型分类
|
||||||
export const RATES = [
|
export const RATES = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS]
|
||||||
QUESTION_TYPE.RADIO_STAR,
|
|
||||||
QUESTION_TYPE.RADIO_NPS
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@ -27,6 +27,18 @@ const isVideo = (html) => {
|
|||||||
return html.indexOf('<video') > -1
|
return html.indexOf('<video') > -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 const cleanRichText = (text) => {
|
export const cleanRichText = (text) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return text === 0 ? 0 : ''
|
return text === 0 ? 0 : ''
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -14,6 +12,7 @@ export default {
|
|||||||
@import url('./styles/icon.scss');
|
@import url('./styles/icon.scss');
|
||||||
@import url('../materials/questions/common/css/icon.scss');
|
@import url('../materials/questions/common/css/icon.scss');
|
||||||
@import url('./styles/reset.scss');
|
@import url('./styles/reset.scss');
|
||||||
|
@import url('./styles/common.scss');
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import store from '@/management/store/index'
|
|
||||||
import router from '@/management/router/index'
|
import router from '@/management/router/index'
|
||||||
import { get as _get } from 'lodash-es'
|
import { get as _get } from 'lodash-es'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
export const CODE_MAP = {
|
export const CODE_MAP = {
|
||||||
SUCCESS: 200,
|
SUCCESS: 200,
|
||||||
@ -36,8 +36,9 @@ instance.interceptors.response.use(
|
|||||||
)
|
)
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
const hasLogined = _get(store, 'state.user.hasLogined')
|
const userStore = useUserStore()
|
||||||
const token = _get(store, 'state.user.userInfo.token')
|
const hasLogined = _get(userStore, 'hasLogined')
|
||||||
|
const token = _get(userStore, 'userInfo.token')
|
||||||
if (hasLogined && token) {
|
if (hasLogined && token) {
|
||||||
if (!config.headers) {
|
if (!config.headers) {
|
||||||
config.headers = {}
|
config.headers = {}
|
||||||
|
@ -8,14 +8,20 @@ export const updateSpace = ({ workspaceId, name, description, members }: any) =>
|
|||||||
return axios.post(`/workspace/${workspaceId}`, { name, description, members })
|
return axios.post(`/workspace/${workspaceId}`, { name, description, members })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSpaceList = () => {
|
export const getSpaceList = (params: any) => {
|
||||||
return axios.get('/workspace')
|
return axios.get('/workspace', {
|
||||||
|
params
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSpaceDetail = (workspaceId: string) => {
|
export const getSpaceDetail = (workspaceId: string) => {
|
||||||
return axios.get(`/workspace/${workspaceId}`)
|
return axios.get(`/workspace/${workspaceId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getMemberList = () => {
|
||||||
|
return axios.get('/workspace/member/list')
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteSpace = (workspaceId: string) => {
|
export const deleteSpace = (workspaceId: string) => {
|
||||||
return axios.delete(`/workspace/${workspaceId}`)
|
return axios.delete(`/workspace/${workspaceId}`)
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import { type IMember, type ListItem } from '@/management/utils/types/workSpace'
|
import { type IMember, type ListItem } from '@/management/utils/types/workSpace'
|
||||||
import OperationSelect from './OperationSelect.vue'
|
import OperationSelect from './OperationSelect.vue'
|
||||||
|
import { useWorkSpaceStore } from '@/management/stores/workSpace'
|
||||||
|
|
||||||
const store = useStore()
|
const workSpaceStore = useWorkSpaceStore()
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
members: IMember[]
|
members: IMember[]
|
||||||
@ -43,7 +43,7 @@ const list = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
const currentUserId = computed(() => {
|
const currentUserId = computed(() => {
|
||||||
return store.state.list.spaceDetail?.currentUserId
|
return workSpaceStore.spaceDetail?.currentUserId
|
||||||
})
|
})
|
||||||
const handleRemove = (index: number) => {
|
const handleRemove = (index: number) => {
|
||||||
list.value.splice(index, 1)
|
list.value.splice(index, 1)
|
@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import MemberList from './MemberList.vue'
|
import MemberList from './MemberList.vue'
|
||||||
import { getUserList } from '@/management/api/space'
|
import { getUserList } from '@/management/api/space'
|
||||||
import {
|
import {
|
||||||
@ -33,8 +32,9 @@ import {
|
|||||||
roleLabels
|
roleLabels
|
||||||
} from '@/management/utils/types/workSpace'
|
} from '@/management/utils/types/workSpace'
|
||||||
import { CODE_MAP } from '@/management/api/base'
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
|
import { useUserStore } from '@/management/stores/user'
|
||||||
|
|
||||||
const store = useStore()
|
const userStore = useUserStore()
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
members?: IMember[]
|
members?: IMember[]
|
||||||
@ -65,7 +65,7 @@ const remoteMethod = async (query: string) => {
|
|||||||
if (res.code === CODE_MAP.SUCCESS) {
|
if (res.code === CODE_MAP.SUCCESS) {
|
||||||
selectOptions.value = res.data.map((item: any) => {
|
selectOptions.value = res.data.map((item: any) => {
|
||||||
// 不可以选中自己
|
// 不可以选中自己
|
||||||
const currentUser = item.username === store.state.user.userInfo.username
|
const currentUser = item.username === userStore.userInfo?.username
|
||||||
return {
|
return {
|
||||||
value: item.userId,
|
value: item.userId,
|
||||||
label: item.username,
|
label: item.username,
|
@ -43,10 +43,13 @@
|
|||||||
import { computed, ref, shallowRef, onMounted, watch } from 'vue'
|
import { computed, ref, shallowRef, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
import MemberSelect from './MemberSelect.vue'
|
|
||||||
import { getPermissionList, getCollaborator, saveCollaborator } from '@/management/api/space'
|
import { getPermissionList, getCollaborator, saveCollaborator } from '@/management/api/space'
|
||||||
import { type IMember, SurveyPermissions } from '@/management/utils/types/workSpace'
|
import { type IMember, SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||||
import { CODE_MAP } from '@/management/api/base'
|
import { CODE_MAP } from '@/management/api/base'
|
||||||
|
|
||||||
|
import MemberSelect from './MemberSelect.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur'])
|
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur'])
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -110,9 +113,6 @@ const rules = {
|
|||||||
{
|
{
|
||||||
trigger: 'change',
|
trigger: 'change',
|
||||||
validator: (rule: any, value: IMember[], callback: Function) => {
|
validator: (rule: any, value: IMember[], callback: Function) => {
|
||||||
if (value.length === 0) {
|
|
||||||
callback('请至少添加一名协作者')
|
|
||||||
}
|
|
||||||
if (value.filter((item: IMember) => !item.role.length).length) {
|
if (value.filter((item: IMember) => !item.role.length).length) {
|
||||||
callback('请设置协作者对应权限')
|
callback('请设置协作者对应权限')
|
||||||
}
|
}
|
||||||
@ -132,7 +132,6 @@ const rules = {
|
|||||||
const formDisabled = computed(() => {
|
const formDisabled = computed(() => {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
emit('on-close-codify')
|
emit('on-close-codify')
|
||||||
}
|
}
|
||||||
@ -140,7 +139,7 @@ const onConfirm = async () => {
|
|||||||
ruleForm.value?.validate(async (valid: boolean) => {
|
ruleForm.value?.validate(async (valid: boolean) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
const collaborators = formModel.value.members.map((i: any) => {
|
const collaborators = formModel.value.members.map((i) => {
|
||||||
const collaborator = {
|
const collaborator = {
|
||||||
userId: i.userId,
|
userId: i.userId,
|
||||||
permissions: i.role
|
permissions: i.role
|
@ -6,9 +6,12 @@
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'tab-btn',
|
'tab-btn',
|
||||||
(['QuestionEditIndex', 'QuestionEditSetting', 'QuestionSkinSetting'].includes(
|
([
|
||||||
route.name
|
'QuestionEditIndex',
|
||||||
) &&
|
'QuestionEditSetting',
|
||||||
|
'QuestionSkinSetting',
|
||||||
|
'QuestionEditResultConfig'
|
||||||
|
].includes(route.name) &&
|
||||||
tab.to.name === 'QuestionEditIndex') ||
|
tab.to.name === 'QuestionEditIndex') ||
|
||||||
isActive
|
isActive
|
||||||
? 'router-link-active'
|
? 'router-link-active'
|
||||||
@ -27,12 +30,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
import LogoIcon from './LogoIcon.vue'
|
import LogoIcon from './LogoIcon.vue'
|
||||||
import { SurveyPermissions } from '@/management/utils/types/workSpace.ts'
|
import { SurveyPermissions } from '@/management/utils/types/workSpace.ts'
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
|
||||||
const tabArr = [
|
const tabArr = [
|
||||||
{
|
{
|
||||||
@ -58,7 +61,9 @@ const tabArr = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
const tabs = ref([])
|
const tabs = ref([])
|
||||||
watch(() => store.state.cooperPermissions, (newVal) => {
|
watch(
|
||||||
|
() => editStore.cooperPermissions,
|
||||||
|
(newVal) => {
|
||||||
tabs.value = []
|
tabs.value = []
|
||||||
// 如果有问卷管理权限,则加入问卷编辑和投放菜单
|
// 如果有问卷管理权限,则加入问卷编辑和投放菜单
|
||||||
if (newVal.includes(SurveyPermissions.SurveyManage)) {
|
if (newVal.includes(SurveyPermissions.SurveyManage)) {
|
||||||
@ -69,7 +74,9 @@ watch(() => store.state.cooperPermissions, (newVal) => {
|
|||||||
if (newVal.includes(SurveyPermissions.DataManage)) {
|
if (newVal.includes(SurveyPermissions.DataManage)) {
|
||||||
tabs.value.push(tabArr[2])
|
tabs.value.push(tabArr[2])
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nav {
|
.nav {
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export const DND_GROUP = 'question'
|
export const DND_GROUP = 'question'
|
||||||
|
export const QUESTION_CATALOG = 'questionCatalog'
|
||||||
|
@ -9,7 +9,7 @@ export const spaceListConfig = {
|
|||||||
name: {
|
name: {
|
||||||
title: '空间名称',
|
title: '空间名称',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 300
|
width: 200
|
||||||
},
|
},
|
||||||
surveyTotal: {
|
surveyTotal: {
|
||||||
title: '问卷数',
|
title: '问卷数',
|
||||||
@ -82,6 +82,16 @@ export const noListDataConfig = {
|
|||||||
img: '/imgs/icons/list-empty.webp'
|
img: '/imgs/icons/list-empty.webp'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const noSpaceDataConfig = {
|
||||||
|
title: '您还没有创建团队空间',
|
||||||
|
desc: '赶快点击右上角立即创建团队空间吧!',
|
||||||
|
img: '/imgs/icons/list-empty.webp'
|
||||||
|
}
|
||||||
|
export const noSpaceSearchDataConfig = {
|
||||||
|
title: '没有满足该查询条件的团队空间哦',
|
||||||
|
desc: '可以更换条件查询试试',
|
||||||
|
img: '/imgs/icons/list-empty.webp'
|
||||||
|
}
|
||||||
export const noSearchDataConfig = {
|
export const noSearchDataConfig = {
|
||||||
title: '没有满足该查询条件的问卷哦',
|
title: '没有满足该查询条件的问卷哦',
|
||||||
desc: '可以更换条件查询试试',
|
desc: '可以更换条件查询试试',
|
||||||
|
@ -55,6 +55,7 @@ export const defaultQuestionConfig = {
|
|||||||
star: 5,
|
star: 5,
|
||||||
optionOrigin: '',
|
optionOrigin: '',
|
||||||
originType: 'selected',
|
originType: 'selected',
|
||||||
|
innerType: '',
|
||||||
matrixOptionsRely: '',
|
matrixOptionsRely: '',
|
||||||
numberRange: {
|
numberRange: {
|
||||||
min: {
|
min: {
|
||||||
|
@ -1,6 +1,25 @@
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts/core'
|
||||||
|
import { BarChart, PieChart, GaugeChart } from 'echarts/charts'
|
||||||
|
import {
|
||||||
|
TooltipComponent,
|
||||||
|
TitleComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent
|
||||||
|
} from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { getOption } from '@/management/config/chartConfig'
|
import { getOption } from '@/management/config/chartConfig'
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
GaugeChart,
|
||||||
|
CanvasRenderer
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绘制图表
|
* 绘制图表
|
||||||
* @param {Object} el
|
* @param {Object} el
|
||||||
|
171
web/src/management/hooks/useJumpLogicFlow.ts
Normal file
171
web/src/management/hooks/useJumpLogicFlow.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { Operator } from '@/common/logicEngine/BasicType'
|
||||||
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
import { CHOICES } from '@/common/typeEnum'
|
||||||
|
|
||||||
|
export const generateNodes = (questionDataList: [any]) => {
|
||||||
|
let x = 50
|
||||||
|
const y = 300
|
||||||
|
const startNode = [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
type: 'start-node',
|
||||||
|
x: 50,
|
||||||
|
y,
|
||||||
|
text: '开始'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const nodes: any[] = questionDataList.map((item) => {
|
||||||
|
x = x + 300
|
||||||
|
let options = []
|
||||||
|
if (CHOICES.includes(item.type)) {
|
||||||
|
options = item?.options.map((option: any) => {
|
||||||
|
return {
|
||||||
|
key: option?.hash,
|
||||||
|
type: cleanRichText(option?.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: item?.field,
|
||||||
|
type: 'q-node',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
properties: {
|
||||||
|
questionType: item?.type,
|
||||||
|
field: item.field,
|
||||||
|
title: cleanRichText(item?.title),
|
||||||
|
options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const endNode = [
|
||||||
|
{
|
||||||
|
id: 'end',
|
||||||
|
type: 'end-node',
|
||||||
|
x: x + 200,
|
||||||
|
y,
|
||||||
|
text: '结束'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return startNode.concat(nodes).concat(endNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跳转逻辑的初始化 */
|
||||||
|
export const generateLine = (models: Array<any>) => {
|
||||||
|
const acc: Array<any> = []
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const jumpLogicRule = editStore.jumpLogicEngine?.toJson()
|
||||||
|
|
||||||
|
const edges = models.reduce((prev: any, point: any, index: number, array: any[]) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
const previousPoint: any = array[index - 1]
|
||||||
|
if (!previousPoint) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
let edge
|
||||||
|
if (previousPoint?.type === 'start-node') {
|
||||||
|
// 开始节点连接线
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[0].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`,
|
||||||
|
// properties: {
|
||||||
|
draggable: false
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
} else if (previousPoint?.type === 'q-node') {
|
||||||
|
// 生成题目节点连接线
|
||||||
|
// 方案1:以条件节点为主体
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const rules = editStore.jumpLogicEngine.findRulesByField(previousPoint.id)
|
||||||
|
if (!jumpLogicRule.length || !rules.length) {
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
} else {
|
||||||
|
const hasDefault = rules.filter((i: any) => {
|
||||||
|
return i.conditions.filter((item: any) => item.operator === Operator.NotEqual).length
|
||||||
|
})
|
||||||
|
if (!hasDefault.length) {
|
||||||
|
// 如果规则中没有默认答题跳转则生成一条默认的题目答完链接线
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
}
|
||||||
|
rules.forEach((rule: any) => {
|
||||||
|
const condition = rule.conditions[0]
|
||||||
|
let sourceAnchorId = `${condition.field}_right`
|
||||||
|
if (condition.operator === 'in') {
|
||||||
|
sourceAnchorId = `${condition.value}_right`
|
||||||
|
}
|
||||||
|
const targetAnchorId = `${rule.target}_left`
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: rule.target,
|
||||||
|
sourceAnchorId: `${sourceAnchorId}`,
|
||||||
|
targetAnchorId: `${targetAnchorId}`,
|
||||||
|
properties: {
|
||||||
|
ruleId: rule.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`,
|
||||||
|
draggable: false
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
})
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNodesStep = (source: string, target: string, questionDataList: any[]) => {
|
||||||
|
const sourceIndex = questionDataList.findIndex((item: any) => item.field === source)
|
||||||
|
const targetIndex = questionDataList.findIndex((item: any) => item.field === target)
|
||||||
|
return targetIndex - sourceIndex
|
||||||
|
}
|
||||||
|
export const getCondition = (sourceInfo: any): any => {
|
||||||
|
const { nodeId, anchorId } = sourceInfo
|
||||||
|
const anchorKey = anchorId.split('_right')[0]
|
||||||
|
if (nodeId === anchorKey) {
|
||||||
|
// 答完跳转
|
||||||
|
return {
|
||||||
|
field: nodeId,
|
||||||
|
operator: Operator.NotEqual,
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 选中optionhash跳转
|
||||||
|
return {
|
||||||
|
field: nodeId,
|
||||||
|
operator: Operator.Include,
|
||||||
|
value: anchorKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
web/src/management/hooks/useJumpLogicInfo.js
Normal file
43
web/src/management/hooks/useJumpLogicInfo.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { computed, unref } from 'vue'
|
||||||
|
import { useQuestionInfo } from './useQuestionInfo'
|
||||||
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
|
||||||
|
// 目标题的显示逻辑提示文案
|
||||||
|
export const useJumpLogicInfo = (field) => {
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { jumpLogicEngine } = storeToRefs(editStore)
|
||||||
|
const hasJumpLogic = computed(() => {
|
||||||
|
const logicEngine = jumpLogicEngine.value
|
||||||
|
// 判断该题是否作为了跳转逻辑条件
|
||||||
|
const isField = logicEngine?.findTargetsByField(field)?.length > 0
|
||||||
|
// 判断该题是否作为了跳转目标
|
||||||
|
const isTarget = logicEngine?.findConditionByTarget(field)?.length > 0
|
||||||
|
return isField || isTarget
|
||||||
|
})
|
||||||
|
const getJumpLogicText = computed(() => {
|
||||||
|
const logicEngine = jumpLogicEngine.value
|
||||||
|
if (!logicEngine) return
|
||||||
|
// 获取跳转
|
||||||
|
const rules = logicEngine?.findRulesByField(field) || []
|
||||||
|
if (!rules) return
|
||||||
|
const ruleText = rules.map((rule) => {
|
||||||
|
const conditions = rule.conditions.map((condition) => {
|
||||||
|
const { getOptionTitle } = useQuestionInfo(condition.field)
|
||||||
|
if (condition.operator === 'in') {
|
||||||
|
return `<span> 选择了 【${getOptionTitle.value(unref(condition.value)).join('')}】</span>`
|
||||||
|
} else if (condition.operator === 'neq') {
|
||||||
|
return `<span> 答完题目 </span>`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
const { getQuestionTitle } = useQuestionInfo(rule.target)
|
||||||
|
return (
|
||||||
|
conditions.join('') + `<span> 则跳转到 【${getQuestionTitle.value()}】</span> </br>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return ruleText.join('')
|
||||||
|
})
|
||||||
|
return { hasJumpLogic, getJumpLogicText }
|
||||||
|
}
|
@ -1,17 +1,20 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import store from '@/management/store'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
export const useQuestionInfo = (field) => {
|
export const useQuestionInfo = (field) => {
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { questionDataList } = storeToRefs(editStore)
|
||||||
|
|
||||||
const getQuestionTitle = computed(() => {
|
const getQuestionTitle = computed(() => {
|
||||||
const questionDataList = store.state.edit.schema.questionDataList
|
|
||||||
return () => {
|
return () => {
|
||||||
return questionDataList.find((item) => item.field === field)?.title
|
if (field === 'end') return '问卷末尾'
|
||||||
|
return cleanRichText(questionDataList.value.find((item) => item.field === field)?.title)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const getOptionTitle = computed(() => {
|
const getOptionTitle = computed(() => {
|
||||||
const questionDataList = store.state.edit.schema.questionDataList
|
|
||||||
return (value) => {
|
return (value) => {
|
||||||
const options = questionDataList.find((item) => item.field === field)?.options || []
|
const options = questionDataList.value.find((item) => item.field === field)?.options || []
|
||||||
if (value instanceof Array) {
|
if (value instanceof Array) {
|
||||||
return options
|
return options
|
||||||
.filter((item) => value.includes(item.hash))
|
.filter((item) => value.includes(item.hash))
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
|
||||||
|
|
||||||
export const showLogicEngine = ref()
|
|
||||||
export const initShowLogicEngine = (ruleConf) => {
|
|
||||||
showLogicEngine.value = new RuleBuild().fromJson(ruleConf)
|
|
||||||
}
|
|
@ -2,16 +2,20 @@ import { computed, unref } from 'vue'
|
|||||||
import { useQuestionInfo } from './useQuestionInfo'
|
import { useQuestionInfo } from './useQuestionInfo'
|
||||||
import { flatten } from 'lodash-es'
|
import { flatten } from 'lodash-es'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
// 目标题的显示逻辑提示文案
|
// 目标题的显示逻辑提示文案
|
||||||
export const useShowLogicInfo = (field) => {
|
export const useShowLogicInfo = (field) => {
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
|
|
||||||
const hasShowLogic = computed(() => {
|
const hasShowLogic = computed(() => {
|
||||||
const logicEngine = showLogicEngine.value
|
const logicEngine = showLogicEngine.value
|
||||||
// 判断该题是否作为了显示逻辑前置题
|
// 判断该题是否作为了显示逻辑前置题
|
||||||
const isField = logicEngine?.findTargetsByFields(field)?.length > 0
|
const isField = logicEngine?.findTargetsByField(field)?.length > 0
|
||||||
// 判断该题是否作为了显示逻辑目标题
|
// 判断该题是否作为了显示逻辑目标题
|
||||||
const isTarget = logicEngine?.findTargetsByScope(field)?.length > 0
|
const isTarget = logicEngine?.findConditionByTarget(field)?.length > 0
|
||||||
return isField || isTarget
|
return isField || isTarget
|
||||||
})
|
})
|
||||||
const getShowLogicText = computed(() => {
|
const getShowLogicText = computed(() => {
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import store from './store'
|
import { createPinia } from 'pinia'
|
||||||
import plainText from './directive/plainText'
|
import plainText from './directive/plainText'
|
||||||
import safeHtml from './directive/safeHtml'
|
import safeHtml from './directive/safeHtml'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(store)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.use(plainText)
|
app.use(plainText)
|
||||||
|
@ -41,7 +41,7 @@ import { analysisType } from '@/management/config/analysisConfig'
|
|||||||
.right {
|
.right {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 1160px;
|
min-width: 1300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -13,28 +13,30 @@
|
|||||||
v-for="item in props.tableData.listHead"
|
v-for="item in props.tableData.listHead"
|
||||||
:key="item.field"
|
:key="item.field"
|
||||||
:prop="item.field"
|
:prop="item.field"
|
||||||
:label="cleanRichText(item.title)"
|
:label="item.title"
|
||||||
minWidth="200"
|
minWidth="200"
|
||||||
>
|
>
|
||||||
<template #header="scope">
|
<template #header="scope">
|
||||||
<div
|
<div class="table-row-cell">
|
||||||
class="table-row-cell"
|
<span
|
||||||
|
class="table-row-head"
|
||||||
|
@click="onPreviewImage"
|
||||||
@mouseover="onPopoverRefOver(scope, 'head')"
|
@mouseover="onPopoverRefOver(scope, 'head')"
|
||||||
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
|
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
|
||||||
|
v-html="item.title"
|
||||||
>
|
>
|
||||||
<span>
|
|
||||||
{{ scope.column.label.replace(/ /g, '') }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div
|
<div>
|
||||||
|
<span
|
||||||
class="table-row-cell"
|
class="table-row-cell"
|
||||||
@mouseover="onPopoverRefOver(scope, 'content')"
|
@mouseover="onPopoverRefOver(scope, 'content')"
|
||||||
|
@click="onPreviewImage"
|
||||||
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
|
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
|
||||||
|
v-html="getContent(scope.row[scope.column.property])"
|
||||||
>
|
>
|
||||||
<span>
|
|
||||||
{{ getContent(scope.row[scope.column.property]) }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -42,21 +44,22 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
<el-popover
|
<el-popover
|
||||||
ref="popover"
|
ref="popover"
|
||||||
popper-style="text-align: center;"
|
popper-style="text-align: center;font-size: 13px;"
|
||||||
:virtual-ref="popoverVirtualRef"
|
:virtual-ref="popoverVirtualRef"
|
||||||
placement="top"
|
placement="bottom"
|
||||||
width="400"
|
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
virtual-triggering
|
virtual-triggering
|
||||||
:content="popoverContent"
|
|
||||||
>
|
>
|
||||||
|
<div v-html="popoverContent"></div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
|
<ImagePreview :url="previewImageUrl" v-model:visible="showPreviewImage" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import ImagePreview from './ImagePreview.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tableData: {
|
tableData: {
|
||||||
@ -74,8 +77,8 @@ const popoverRefMap = ref({})
|
|||||||
const popoverVirtualRef = ref()
|
const popoverVirtualRef = ref()
|
||||||
const popoverContent = ref('')
|
const popoverContent = ref('')
|
||||||
|
|
||||||
const getContent = (value) => {
|
const getContent = (content) => {
|
||||||
const content = cleanRichText(value)
|
// const content = cleanRichText(value)
|
||||||
return content === 0 ? 0 : content || '未知'
|
return content === 0 ? 0 : content || '未知'
|
||||||
}
|
}
|
||||||
const setPopoverContent = (content) => {
|
const setPopoverContent = (content) => {
|
||||||
@ -83,23 +86,31 @@ const setPopoverContent = (content) => {
|
|||||||
}
|
}
|
||||||
const onPopoverRefOver = (scope, type) => {
|
const onPopoverRefOver = (scope, type) => {
|
||||||
let popoverContent
|
let popoverContent
|
||||||
if (type == 'head') {
|
if (type === 'head') {
|
||||||
popoverVirtualRef.value = popoverRefMap.value[scope.column.id]
|
popoverVirtualRef.value = popoverRefMap.value[scope.column.id]
|
||||||
popoverContent = scope.column.label.replace(/ /g, '')
|
popoverContent = scope.column.label.replace(/ /g, '')
|
||||||
}
|
}
|
||||||
if (type == 'content') {
|
if (type === 'content') {
|
||||||
popoverVirtualRef.value = popoverRefMap.value[scope.$index + scope.column.property]
|
popoverVirtualRef.value = popoverRefMap.value[scope.$index + scope.column.property]
|
||||||
popoverContent = getContent(scope.row[scope.column.property])
|
popoverContent = getContent(scope.row[scope.column.property])
|
||||||
}
|
}
|
||||||
setPopoverContent(popoverContent)
|
setPopoverContent(popoverContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewImageUrl = ref('')
|
||||||
|
const showPreviewImage = ref(false)
|
||||||
|
const onPreviewImage = (e) => {
|
||||||
|
if (e.target.tagName === 'IMG') {
|
||||||
|
previewImageUrl.value = e.target.src
|
||||||
|
showPreviewImage.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.data-table-wrapper {
|
.data-table-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 20px;
|
|
||||||
min-height: v-bind('tableMinHeight');
|
min-height: v-bind('tableMinHeight');
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
@ -122,14 +133,19 @@ const onPopoverRefOver = (scope, type) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-row-cell {
|
.table-row-cell {
|
||||||
max-width: 100%;
|
:deep(img) {
|
||||||
display: inline-block;
|
height: 23px !important;
|
||||||
white-space: nowrap;
|
width: auto !important;
|
||||||
/* 禁止自动换行 */
|
object-fit: cover;
|
||||||
overflow: hidden;
|
margin-left: 5px;
|
||||||
/* 超出部分隐藏 */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* 显示省略号 */
|
|
||||||
}
|
}
|
||||||
|
:deep(p) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-table td.el-table__cell div) {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition>
|
||||||
|
<div class="image-preview" v-show="visible">
|
||||||
|
<div class="close-btn" @click="visible = false"><i-ep-close /></div>
|
||||||
|
<div class="image-con">
|
||||||
|
<img :src="props.url" class="image-item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = defineModel('visible')
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.4);
|
||||||
|
z-index: 2024;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.image-con {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -38,13 +38,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, toRefs } from 'vue'
|
import { ref, reactive, computed, toRefs } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
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 { createSurvey } from '@/management/api/survey'
|
import { createSurvey } from '@/management/api/survey'
|
||||||
|
|
||||||
import { SURVEY_TYPE_LIST } from '../types'
|
import { SURVEY_TYPE_LIST } from '../types'
|
||||||
|
import { useWorkSpaceStore } from '@/management/stores/workSpace'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectType?: string
|
selectType?: string
|
||||||
@ -54,6 +52,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
selectType: 'normal'
|
selectType: 'normal'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const workSpaceStore = useWorkSpaceStore()
|
||||||
const ruleForm = ref<any>(null)
|
const ruleForm = ref<any>(null)
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@ -79,7 +78,6 @@ const checkForm = (fn: Function) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useStore()
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!state.canSubmit) {
|
if (!state.canSubmit) {
|
||||||
return
|
return
|
||||||
@ -94,8 +92,8 @@ const submit = () => {
|
|||||||
surveyType: selectType,
|
surveyType: selectType,
|
||||||
...state.form
|
...state.form
|
||||||
}
|
}
|
||||||
if (store.state.list.workSpaceId) {
|
if (workSpaceStore.workSpaceId) {
|
||||||
payload.workspaceId = store.state.list.workSpaceId
|
payload.workspaceId = workSpaceStore.workSpaceId
|
||||||
}
|
}
|
||||||
const res: any = await createSurvey(payload)
|
const res: any = await createSurvey(payload)
|
||||||
if (res?.code === 200 && res?.data?.id) {
|
if (res?.code === 200 && res?.data?.id) {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="nav" v-if="slots.hasOwnProperty('nav')">
|
<div class="nav" v-if="slots.nav">
|
||||||
<slot name="nav"></slot>
|
<slot name="nav"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<slot v-if="slots.hasOwnProperty('body')" name="body"></slot>
|
<slot v-if="slots.body" name="body"></slot>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="left" v-if="slots.hasOwnProperty('left')">
|
<div class="left" v-if="slots.left">
|
||||||
<slot name="left"></slot>
|
<slot name="left"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="center" v-if="slots.hasOwnProperty('center')">
|
<div class="center" v-if="slots.center">
|
||||||
<slot name="center"></slot>
|
<slot name="center"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="right" v-if="slots.hasOwnProperty('right')">
|
<div class="right" v-if="slots.right">
|
||||||
<slot name="right"></slot>
|
<slot name="right"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="renderData"
|
v-model="renderData"
|
||||||
handle=".question-wrapper.isSelected"
|
handle=".question-wrapper.is-move"
|
||||||
filter=".question-wrapper.isSelected .question.isSelected"
|
filter=".question-wrapper.is-move .question.isSelected"
|
||||||
|
:preventOnFilter="false"
|
||||||
:group="DND_GROUP"
|
:group="DND_GROUP"
|
||||||
:onEnd="checkEnd"
|
:onEnd="checkEnd"
|
||||||
:move="checkMove"
|
:move="checkMove"
|
||||||
@ -13,8 +14,9 @@
|
|||||||
:ref="`questionWrapper-${element.field}`"
|
:ref="`questionWrapper-${element.field}`"
|
||||||
:moduleConfig="element"
|
:moduleConfig="element"
|
||||||
:qIndex="element.qIndex"
|
:qIndex="element.qIndex"
|
||||||
|
:isFirst="index == 0"
|
||||||
:indexNumber="element.indexNumber"
|
:indexNumber="element.indexNumber"
|
||||||
:isSelected="currentEditOne === index"
|
:isSelected="currentEditOne === element.qIndex"
|
||||||
:isLast="index + 1 === questionDataList.length"
|
:isLast="index + 1 === questionDataList.length"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
@changeSeq="handleChangeSeq"
|
@changeSeq="handleChangeSeq"
|
||||||
@ -23,10 +25,14 @@
|
|||||||
:type="element.type"
|
:type="element.type"
|
||||||
:moduleConfig="element"
|
:moduleConfig="element"
|
||||||
:indexNumber="element.indexNumber"
|
:indexNumber="element.indexNumber"
|
||||||
:isSelected="currentEditOne === index"
|
:isSelected="currentEditOne === element.qIndex"
|
||||||
:readonly="true"
|
:readonly="true"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
></QuestionContainerB>
|
>
|
||||||
|
<template #advancedEdit>
|
||||||
|
<slot name="advancedEdit" :moduleConfig="element"></slot>
|
||||||
|
</template>
|
||||||
|
</QuestionContainerB>
|
||||||
</QuestionWrapper>
|
</QuestionWrapper>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@ -34,11 +40,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { computed, defineComponent, ref, getCurrentInstance } from 'vue'
|
import { computed, defineComponent, ref, getCurrentInstance } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import QuestionContainerB from '@/materials/questions/QuestionContainerB'
|
import QuestionContainerB from '@/materials/questions/QuestionContainerB'
|
||||||
import QuestionWrapper from '@/management/pages/edit/components/QuestionWrapper.vue'
|
import QuestionWrapper from '@/management/pages/edit/components/QuestionWrapper.vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { filterQuestionPreviewData } from '@/management/utils/index'
|
|
||||||
import { DND_GROUP } from '@/management/config/dnd'
|
import { DND_GROUP } from '@/management/config/dnd'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -59,15 +64,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change', 'select', 'changeSeq'],
|
emits: ['change', 'select', 'changeSeq', 'change'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
const renderData = computed({
|
const renderData = computed({
|
||||||
get() {
|
get() {
|
||||||
return filterQuestionPreviewData(props.questionDataList)
|
return props.questionDataList //filterQuestionPreviewData(props.questionDataList)
|
||||||
},
|
},
|
||||||
set(questionDataList) {
|
set(value) {
|
||||||
store.commit('edit/setQuestionDataList', questionDataList)
|
editStore.moveQuestionDataList(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const handleSelect = (index) => {
|
const handleSelect = (index) => {
|
||||||
|
@ -8,17 +8,28 @@
|
|||||||
<NavPanel></NavPanel>
|
<NavPanel></NavPanel>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-group">
|
<div class="right-group">
|
||||||
|
<CooperationPanel>
|
||||||
|
<template #content="{ onCooper }">
|
||||||
|
<div class="btn" @click="onCooper">
|
||||||
|
<i-ep-connection class="view-icon" :size="20" />
|
||||||
|
<span class="btn-txt">协作</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CooperationPanel>
|
||||||
<PreviewPanel></PreviewPanel>
|
<PreviewPanel></PreviewPanel>
|
||||||
<HistoryPanel></HistoryPanel>
|
<HistoryPanel></HistoryPanel>
|
||||||
<SavePanel></SavePanel>
|
<SavePanel :updateLogicConf="updateLogicConf" :updateWhiteConf="updateWhiteConf"></SavePanel>
|
||||||
<PublishPanel></PublishPanel>
|
<PublishPanel
|
||||||
|
:updateLogicConf="updateLogicConf"
|
||||||
|
:updateWhiteConf="updateWhiteConf"
|
||||||
|
></PublishPanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { get as _get } from 'lodash-es'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
import BackPanel from '../modules/generalModule/BackPanel.vue'
|
import BackPanel from '../modules/generalModule/BackPanel.vue'
|
||||||
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
||||||
@ -27,11 +38,80 @@ import HistoryPanel from '../modules/contentModule/HistoryPanel.vue'
|
|||||||
import PreviewPanel from '../modules/contentModule/PreviewPanel.vue'
|
import PreviewPanel from '../modules/contentModule/PreviewPanel.vue'
|
||||||
import SavePanel from '../modules/contentModule/SavePanel.vue'
|
import SavePanel from '../modules/contentModule/SavePanel.vue'
|
||||||
import PublishPanel from '../modules/contentModule/PublishPanel.vue'
|
import PublishPanel from '../modules/contentModule/PublishPanel.vue'
|
||||||
|
import CooperationPanel from '../modules/contentModule/CooperationPanel.vue'
|
||||||
|
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
const title = computed(() => _get(store.state, 'edit.schema.metaData.title'))
|
const { schema, changeSchema } = editStore
|
||||||
|
const title = computed(() => (editStore.schema?.metaData as any)?.title || '')
|
||||||
|
|
||||||
|
const { showLogicEngine, jumpLogicEngine } = storeToRefs(editStore)
|
||||||
|
// 校验 - 逻辑
|
||||||
|
const updateLogicConf = () => {
|
||||||
|
let res = {
|
||||||
|
validated: true,
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
showLogicEngine.value &&
|
||||||
|
showLogicEngine.value.rules &&
|
||||||
|
showLogicEngine.value.rules.length !== 0
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
showLogicEngine.value.validateSchema()
|
||||||
|
} catch (error) {
|
||||||
|
res = {
|
||||||
|
validated: false,
|
||||||
|
message: '逻辑配置不能为空'
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const showLogicConf = showLogicEngine.value.toJson()
|
||||||
|
|
||||||
|
// 更新逻辑配置
|
||||||
|
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
||||||
|
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 - 白名单
|
||||||
|
const updateWhiteConf = () => {
|
||||||
|
let res = {
|
||||||
|
validated: true,
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
const baseConf = (schema?.baseConf as any) || {}
|
||||||
|
if (baseConf.passwordSwitch && !baseConf.password) {
|
||||||
|
res = {
|
||||||
|
validated: false,
|
||||||
|
message: '访问密码不能为空'
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if (baseConf.whitelistType != 'ALL' && !baseConf.whitelist?.length) {
|
||||||
|
res = {
|
||||||
|
validated: false,
|
||||||
|
message: '白名单不能为空'
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import url('@/management/styles/edit-btn.scss');
|
||||||
|
.view-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
height: 29px;
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
.nav {
|
.nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
@ -0,0 +1,306 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="props.totalPage > 0" class="com-pagination">
|
||||||
|
<span :class="['com-pagination-item', prev_class]" @click="changePage(prev_page)">
|
||||||
|
<i-ep-ArrowLeft />
|
||||||
|
</span>
|
||||||
|
<template v-if="!is_more_filled">
|
||||||
|
<div
|
||||||
|
v-for="i in firstPagination"
|
||||||
|
:key="i"
|
||||||
|
:class="['com-pagination-item', `page-${i}`, now_page == i ? 'current' : '']"
|
||||||
|
@click="changePage(i)"
|
||||||
|
>
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
<div v-if="!props.readonly" :class="['moreControls']" @click.stop="showTooltipVisible(i)">
|
||||||
|
<i-ep-MoreFilled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="i in more_filled_arr.startArr"
|
||||||
|
@click="changePage(i)"
|
||||||
|
:key="i"
|
||||||
|
:class="['com-pagination-item', ` page-${i}`, now_page == i ? 'current' : '']"
|
||||||
|
>
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
<div v-if="!props.readonly" :class="['moreControls']" @click.stop="showTooltipVisible(i)">
|
||||||
|
<i-ep-MoreFilled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-tooltip class="controls-wrap" effect="light" placement="bottom" :visible="moreVisible">
|
||||||
|
<span class="com-pagination-item" @click.stop="moreVisible = true">
|
||||||
|
<i-ep-MoreFilled />
|
||||||
|
</span>
|
||||||
|
<template #content>
|
||||||
|
<div class="bubble-wrap">
|
||||||
|
<div
|
||||||
|
class="bubble-item"
|
||||||
|
v-for="i in more_filled_arr.bubbleArr"
|
||||||
|
:key="i"
|
||||||
|
@click="changePage(i)"
|
||||||
|
>
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tooltip>
|
||||||
|
<div
|
||||||
|
v-for="i in more_filled_arr.endArr"
|
||||||
|
:key="i"
|
||||||
|
:class="['com-pagination-item', `page-${i}`, now_page == i ? 'current' : '']"
|
||||||
|
@click="changePage(i)"
|
||||||
|
>
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
<div v-if="!props.readonly" :class="['moreControls']" @click.stop="showTooltipVisible(i)">
|
||||||
|
<i-ep-MoreFilled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span :class="['com-pagination-item', next_class]" @click="changePage(next_page)">
|
||||||
|
<i-ep-ArrowRight />
|
||||||
|
</span>
|
||||||
|
<el-tooltip
|
||||||
|
v-if="slot.tooltip && props.readonly == false"
|
||||||
|
:visible="tooltipVisible"
|
||||||
|
:popper-options="{
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'computeStyles',
|
||||||
|
options: {
|
||||||
|
adaptive: false,
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
:virtual-ref="triggerBtn"
|
||||||
|
virtual-triggering
|
||||||
|
effect="light"
|
||||||
|
popper-class="singleton-tooltip"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<slot name="tooltip" :index="tooltipIndex"></slot>
|
||||||
|
</template>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { clone } from 'lodash-es'
|
||||||
|
import { reactive, computed, watch, ref, onMounted, onUnmounted, nextTick, useSlots } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: number // 页码
|
||||||
|
totalPage?: number
|
||||||
|
intervalCount?: number
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: 1,
|
||||||
|
totalPage: 1,
|
||||||
|
intervalCount: 8,
|
||||||
|
readonly: false
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['change-page', 'update:modelValue'])
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
now: props.modelValue,
|
||||||
|
jump: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const slot = useSlots()
|
||||||
|
|
||||||
|
const moreVisible = ref(false)
|
||||||
|
const tooltipVisible = ref(false)
|
||||||
|
const triggerBtn = ref<EventTarget | null>(null)
|
||||||
|
const tooltipIndex = ref(0)
|
||||||
|
|
||||||
|
const now_page = computed(() => {
|
||||||
|
return state.now * 1
|
||||||
|
})
|
||||||
|
const prev_class = computed(() => {
|
||||||
|
return now_page.value == 1 ? 'disabled' : ''
|
||||||
|
})
|
||||||
|
const next_class = computed(() => {
|
||||||
|
return now_page.value == props.totalPage ? 'disabled' : ''
|
||||||
|
})
|
||||||
|
const prev_page = computed(() => {
|
||||||
|
return now_page.value > 1 ? now_page.value - 1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const next_page = computed(() => {
|
||||||
|
return now_page.value < props.totalPage ? now_page.value + 1 : props.totalPage
|
||||||
|
})
|
||||||
|
|
||||||
|
const is_more_filled = computed(() => {
|
||||||
|
const intervalNum = props.totalPage - now_page.value + 1
|
||||||
|
if (intervalNum >= props.intervalCount + 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalArr = computed(() => {
|
||||||
|
const arr = []
|
||||||
|
for (let i = 0; i < props.totalPage; i++) {
|
||||||
|
arr.push(i + 1)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
const more_filled_arr = computed(() => {
|
||||||
|
let startArr = []
|
||||||
|
let bubbleArr = []
|
||||||
|
let endArr = []
|
||||||
|
const arr = clone(totalArr.value)
|
||||||
|
const intervalNum = Math.round(props.intervalCount / 2)
|
||||||
|
startArr = arr.slice(now_page.value - 1, intervalNum + now_page.value - 1)
|
||||||
|
endArr = arr.slice(intervalNum * -1)
|
||||||
|
bubbleArr = arr.slice(startArr[startArr.length - 1], endArr[0] - 1)
|
||||||
|
return {
|
||||||
|
startArr,
|
||||||
|
bubbleArr,
|
||||||
|
endArr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const firstPagination = computed(() => {
|
||||||
|
const arr = clone(totalArr.value)
|
||||||
|
return arr.splice(props.intervalCount * -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const changePage = (page: number) => {
|
||||||
|
state.now = page
|
||||||
|
emit('update:modelValue', state.now)
|
||||||
|
emit('change-page', state.now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTooltipVisible = (index: number) => {
|
||||||
|
if (slot.tooltip) {
|
||||||
|
nextTick(() => {
|
||||||
|
tooltipIndex.value = index
|
||||||
|
triggerBtn.value = document.getElementsByClassName(`page-${index}`)[0] || null
|
||||||
|
tooltipVisible.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideMoreVisible = () => {
|
||||||
|
moreVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltipVisible = () => {
|
||||||
|
tooltipVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', hideMoreVisible)
|
||||||
|
if (slot.tooltip) {
|
||||||
|
document.addEventListener('click', hideTooltipVisible)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', hideMoreVisible)
|
||||||
|
if (slot.tooltip) {
|
||||||
|
document.removeEventListener('click', hideTooltipVisible)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
state.now = props.modelValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bubble-wrap {
|
||||||
|
.bubble-item {
|
||||||
|
max-width: 100px;
|
||||||
|
min-width: 10px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #cccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.com-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0;
|
||||||
|
user-select: none;
|
||||||
|
.moreControls {
|
||||||
|
color: #6e707c;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 22px;
|
||||||
|
svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-item {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
// padding: 0 4px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: #303133;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
.moreControls {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $primary-color;
|
||||||
|
.moreControls {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled,
|
||||||
|
.disabled:hover {
|
||||||
|
svg {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #303133 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current,
|
||||||
|
.current:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<PaginationPanel
|
||||||
|
v-model="schema.pageEditOne"
|
||||||
|
:readonly="props.readonly"
|
||||||
|
:totalPage="pageCount"
|
||||||
|
@changePage="updatePage"
|
||||||
|
:intervalCount="10"
|
||||||
|
>
|
||||||
|
<template #tooltip="{ index }">
|
||||||
|
<div>
|
||||||
|
<div v-if="index != 1" class="controls-wrap-item" @click="movePage(index, 'up')">
|
||||||
|
前移一页
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="index != pageCount"
|
||||||
|
class="mt8 controls-wrap-item"
|
||||||
|
@click="movePage(index, 'down')"
|
||||||
|
>
|
||||||
|
后移一页
|
||||||
|
</div>
|
||||||
|
<div class="mt8 controls-wrap-item" @click="copyPage(index)">复制</div>
|
||||||
|
<div class="mt8 controls-wrap-item" @click="deletePage(index)">删除</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PaginationPanel>
|
||||||
|
<i-ep-plus
|
||||||
|
v-if="!props.readonly"
|
||||||
|
style="font-size: 12px"
|
||||||
|
@click="addPageControls"
|
||||||
|
class="plus-add"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
|
|
||||||
|
import PaginationPanel from './PaginationPanel.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { pageCount, schema, newQuestionIndex } = storeToRefs(editStore)
|
||||||
|
|
||||||
|
const {
|
||||||
|
updatePageEditOne,
|
||||||
|
addPage,
|
||||||
|
createNewQuestion,
|
||||||
|
addQuestion,
|
||||||
|
setCurrentEditOne,
|
||||||
|
deletePage,
|
||||||
|
swapArrayRanges,
|
||||||
|
copyPage
|
||||||
|
} = editStore
|
||||||
|
|
||||||
|
const updatePage = (index) => {
|
||||||
|
setCurrentEditOne(null)
|
||||||
|
updatePageEditOne(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const movePage = (position, type) => {
|
||||||
|
setCurrentEditOne(null)
|
||||||
|
const pageIndex = type === 'up' ? position - 1 : position + 1
|
||||||
|
updatePageEditOne(pageIndex)
|
||||||
|
if (type === 'up') {
|
||||||
|
swapArrayRanges(position, position - 1)
|
||||||
|
}
|
||||||
|
if (type === 'down') {
|
||||||
|
swapArrayRanges(position + 1, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPageControls = () => {
|
||||||
|
const newQuestion = createNewQuestion({ type: QUESTION_TYPE.TEXT })
|
||||||
|
updatePageEditOne(pageCount.value + 1)
|
||||||
|
setCurrentEditOne(null)
|
||||||
|
addQuestion({ question: newQuestion, index: newQuestionIndex.value })
|
||||||
|
setCurrentEditOne(newQuestionIndex.value)
|
||||||
|
addPage()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mt8 {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-wrap {
|
||||||
|
&-item {
|
||||||
|
color: #4a4c5b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0px 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.plus-add {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="itemClass"
|
:class="[itemClass, { 'is-move': isSelected || isMove }]"
|
||||||
@mouseenter="onMouseenter"
|
@mouseenter="onMouseenter"
|
||||||
@mouseleave="onMouseleave"
|
@mouseleave="onMouseleave"
|
||||||
@click="clickFormItem"
|
@click="clickFormItem"
|
||||||
@ -8,7 +8,12 @@
|
|||||||
<div><slot v-if="moduleConfig.type !== 'section'"></slot></div>
|
<div><slot v-if="moduleConfig.type !== 'section'"></slot></div>
|
||||||
|
|
||||||
<div :class="[showHover ? 'visibily' : 'hidden', 'hoverItem']">
|
<div :class="[showHover ? 'visibily' : 'hidden', 'hoverItem']">
|
||||||
<div class="item el-icon-rank" @click.stop.prevent="onMove">
|
<div
|
||||||
|
class="item el-icon-rank"
|
||||||
|
@click.stop.prevent
|
||||||
|
@mouseenter="setMoveState(true)"
|
||||||
|
@mouseleave="setMoveState(false)"
|
||||||
|
>
|
||||||
<i-ep-rank />
|
<i-ep-rank />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showUp" class="item" @click.stop.prevent="onMoveUp">
|
<div v-if="showUp" class="item" @click.stop.prevent="onMoveUp">
|
||||||
@ -24,7 +29,8 @@
|
|||||||
<i-ep-close />
|
<i-ep-close />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="logic-text" v-html="getShowLogicText"></div>
|
<div class="logic-text showText" v-html="getShowLogicText"></div>
|
||||||
|
<div class="logic-text jumpText" v-html="getJumpLogicText"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -32,6 +38,7 @@ import { ref, computed, unref } from 'vue'
|
|||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
||||||
|
import { useJumpLogicInfo } from '@/management/hooks/useJumpLogicInfo'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
qIndex: {
|
qIndex: {
|
||||||
@ -46,6 +53,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isLast: {
|
isLast: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@ -60,8 +71,10 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['changeSeq', 'select'])
|
const emit = defineEmits(['changeSeq', 'select'])
|
||||||
|
|
||||||
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
||||||
|
const { getJumpLogicText, hasJumpLogic } = useJumpLogicInfo(props.moduleConfig.field)
|
||||||
|
|
||||||
const isHover = ref(false)
|
const isHover = ref(false)
|
||||||
|
const isMove = ref(false)
|
||||||
|
|
||||||
const itemClass = computed(() => {
|
const itemClass = computed(() => {
|
||||||
return {
|
return {
|
||||||
@ -75,7 +88,7 @@ const showHover = computed(() => {
|
|||||||
return isHover.value || props.isSelected
|
return isHover.value || props.isSelected
|
||||||
})
|
})
|
||||||
const showUp = computed(() => {
|
const showUp = computed(() => {
|
||||||
return props.qIndex !== 0
|
return !props.isFirst
|
||||||
})
|
})
|
||||||
const showDown = computed(() => {
|
const showDown = computed(() => {
|
||||||
return !props.isLast
|
return !props.isLast
|
||||||
@ -128,8 +141,15 @@ const onMoveDown = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
if (unref(hasShowLogic)) {
|
if (unref(hasShowLogic) || getShowLogicText.value) {
|
||||||
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
ElMessageBox.alert('该题目被显示逻辑关联,请先清除逻辑依赖', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (unref(hasJumpLogic)) {
|
||||||
|
ElMessageBox.alert('该题目被跳转逻辑关联,请先清除逻辑依赖', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
@ -154,7 +174,9 @@ const onDelete = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMove = () => {}
|
const setMoveState = (state: boolean) => {
|
||||||
|
isMove.value = state
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -163,7 +185,7 @@ const onMove = () => {}
|
|||||||
padding: 0.36rem 0 0.36rem;
|
padding: 0.36rem 0 0.36rem;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
&.spliter {
|
&.spliter {
|
||||||
border-bottom: 0.12rem solid $spliter-color;
|
border-bottom: 0.1rem solid $spliter-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mouse-hover {
|
&.mouse-hover {
|
||||||
|
@ -37,8 +37,8 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch, ref, shallowRef } from 'vue'
|
import { watch, ref, shallowRef, type Component } from 'vue'
|
||||||
import { get as _get, pick as _pick, isFunction as _isFunction } from 'lodash-es'
|
import { get as _get, pick as _pick, isFunction as _isFunction, values as _values } from 'lodash-es'
|
||||||
|
|
||||||
import FormItem from '@/materials/setters/widgets/FormItem.vue'
|
import FormItem from '@/materials/setters/widgets/FormItem.vue'
|
||||||
import setterLoader from '@/materials/setters/setterLoader'
|
import setterLoader from '@/materials/setters/setterLoader'
|
||||||
@ -46,8 +46,9 @@ import setterLoader from '@/materials/setters/setterLoader'
|
|||||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
formConfigList: Array<any>
|
formConfigList: Array<any> // 设置器的配置
|
||||||
moduleConfig: any
|
moduleConfig: any // 当前问卷schema
|
||||||
|
customComponents?: Record<string, Component>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emit {
|
interface Emit {
|
||||||
@ -70,7 +71,7 @@ const formatValue = ({ item, moduleConfig }: any) => {
|
|||||||
result = _get(moduleConfig, key, item.value)
|
result = _get(moduleConfig, key, item.value)
|
||||||
}
|
}
|
||||||
if (keys) {
|
if (keys) {
|
||||||
result = _pick(moduleConfig, keys)
|
result = _values(_pick(moduleConfig, keys))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -79,12 +80,14 @@ const formatValue = ({ item, moduleConfig }: any) => {
|
|||||||
|
|
||||||
const formFieldData = ref<Array<any>>([])
|
const formFieldData = ref<Array<any>>([])
|
||||||
const init = ref<boolean>(true)
|
const init = ref<boolean>(true)
|
||||||
const components = shallowRef<any>({})
|
const components = shallowRef<any>(props.customComponents || {})
|
||||||
|
|
||||||
const handleFormChange = (data: any, formConfig: any) => {
|
const handleFormChange = (data: any, formConfig: any) => {
|
||||||
|
// 处理用户操作的设置器的值
|
||||||
if (_isFunction(formConfig?.valueSetter)) {
|
if (_isFunction(formConfig?.valueSetter)) {
|
||||||
const resultData = formConfig.valueSetter(data)
|
const resultData = formConfig.valueSetter(data)
|
||||||
|
|
||||||
|
// 批量触发设置值的变化
|
||||||
if (Array.isArray(resultData)) {
|
if (Array.isArray(resultData)) {
|
||||||
resultData.forEach((item) => {
|
resultData.forEach((item) => {
|
||||||
emit(FORM_CHANGE_EVENT_KEY, item)
|
emit(FORM_CHANGE_EVENT_KEY, item)
|
||||||
@ -124,7 +127,7 @@ const normalizationValues = (configList: Array<any> = []) => {
|
|||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
value: formatValue({ item, moduleConfig: props.moduleConfig }) // 动态复值
|
value: formatValue({ item, moduleConfig: props.moduleConfig }) // 动态赋值
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -132,7 +135,9 @@ const normalizationValues = (configList: Array<any> = []) => {
|
|||||||
const registerComponents = async (formFieldData: any) => {
|
const registerComponents = async (formFieldData: any) => {
|
||||||
let innerSetters: Array<any> = []
|
let innerSetters: Array<any> = []
|
||||||
|
|
||||||
const setters = formFieldData.map((item: any) => {
|
const setters = formFieldData
|
||||||
|
.filter((item: any) => !item.custom)
|
||||||
|
.map((item: any) => {
|
||||||
if (item.type === 'Customed') {
|
if (item.type === 'Customed') {
|
||||||
innerSetters.push(...(item.content || []).map((content: any) => content.type))
|
innerSetters.push(...(item.content || []).map((content: any) => content.type))
|
||||||
}
|
}
|
||||||
@ -167,14 +172,13 @@ const registerComponents = async (formFieldData: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.formConfigList,
|
() => props.formConfigList, // 设置器的配置
|
||||||
async (newVal: Array<any>) => {
|
async (newVal: Array<any>) => {
|
||||||
init.value = true
|
init.value = true
|
||||||
|
|
||||||
if (!newVal || !newVal.length) {
|
if (!newVal || !newVal.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await registerComponents(newVal)
|
await registerComponents(newVal)
|
||||||
init.value = false
|
init.value = false
|
||||||
formFieldData.value = normalizationValues(newVal)
|
formFieldData.value = normalizationValues(newVal)
|
||||||
@ -186,7 +190,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.moduleConfig,
|
() => props.moduleConfig, // 当前问卷schema
|
||||||
() => {
|
() => {
|
||||||
// 配置变化后初次不监听value变化(如题型切换场景避免多次计算)
|
// 配置变化后初次不监听value变化(如题型切换场景避免多次计算)
|
||||||
if (init.value) {
|
if (init.value) {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, onUnmounted } from 'vue'
|
import { onMounted, ref, onUnmounted } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { Action } from 'element-plus'
|
import type { Action } from 'element-plus'
|
||||||
@ -27,7 +27,10 @@ import Navbar from './components/ModuleNavbar.vue'
|
|||||||
import axios from '../../api/base'
|
import axios from '../../api/base'
|
||||||
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { init, setSurveyId } = editStore
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authCheckInterval = ref<any>(null)
|
const authCheckInterval = ref<any>(null)
|
||||||
@ -69,12 +72,10 @@ const checkAuth = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
store.commit('edit/setSurveyId', route.params.id)
|
setSurveyId(route.params.id as string)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.dispatch('edit/init')
|
await init()
|
||||||
await initShowLogicEngine(store.state.edit.schema.logicConf.showLogicConf || {})
|
|
||||||
|
|
||||||
// 启动定时器,每30分钟调用一次
|
// 启动定时器,每30分钟调用一次
|
||||||
authCheckInterval.value = setInterval(() => checkAuth(), 1000);
|
authCheckInterval.value = setInterval(() => checkAuth(), 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -104,7 +105,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
min-width: 1160px;
|
min-width: 1300px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-left: 80px;
|
padding-left: 80px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="btn" @click="onCooper">
|
||||||
|
<i-ep-connection class="view-icon" :size="20" />
|
||||||
|
<span class="btn-txt">协作</span>
|
||||||
|
</div>
|
||||||
|
<CooperModify :modifyId="cooperId" :visible="cooperModify" @on-close-codify="onCooperClose" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
|
import CooperModify from '@/management/components/CooperModify/ModifyDialog.vue'
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
content: (scope: { onCooper: () => void }) => any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const surveyId = route.params?.id as string
|
||||||
|
const cooperModify = ref<boolean>(false)
|
||||||
|
const cooperId = ref<string>('')
|
||||||
|
|
||||||
|
const onCooper = (): void => {
|
||||||
|
cooperId.value = surveyId
|
||||||
|
cooperModify.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCooperClose = (): void => {
|
||||||
|
cooperModify.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import url('@/management/styles/edit-btn.scss');
|
||||||
|
</style>
|
@ -25,9 +25,9 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { storeToRefs } from 'pinia'
|
||||||
import { get as _get } from 'lodash-es'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import 'moment/locale/zh-cn'
|
import 'moment/locale/zh-cn'
|
||||||
moment.locale('zh-cn')
|
moment.locale('zh-cn')
|
||||||
@ -44,21 +44,21 @@ const publishList = ref<Array<any>>([])
|
|||||||
const currentTab = ref<'daily' | 'publish'>('daily')
|
const currentTab = ref<'daily' | 'publish'>('daily')
|
||||||
const visible = ref<boolean>(false)
|
const visible = ref<boolean>(false)
|
||||||
|
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
const { surveyId, schemaUpdateTime } = storeToRefs(editStore)
|
||||||
|
|
||||||
const queryHistories = async () => {
|
const queryHistories = async () => {
|
||||||
if (dirtyMonitor.value) {
|
if (dirtyMonitor.value) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
dirtyMonitor.value = false
|
dirtyMonitor.value = false
|
||||||
|
|
||||||
const surveyId = _get(store.state, 'edit.surveyId')
|
|
||||||
const [dHis, pHis] = await Promise.all([
|
const [dHis, pHis] = await Promise.all([
|
||||||
getSurveyHistory({
|
getSurveyHistory({
|
||||||
surveyId,
|
surveyId: surveyId.value,
|
||||||
historyType: 'dailyHis'
|
historyType: 'dailyHis'
|
||||||
}),
|
}),
|
||||||
getSurveyHistory({
|
getSurveyHistory({
|
||||||
surveyId,
|
surveyId: surveyId.value,
|
||||||
historyType: 'publishHis'
|
historyType: 'publishHis'
|
||||||
})
|
})
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
@ -81,7 +81,6 @@ const handlePopoverShow = async () => {
|
|||||||
}
|
}
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const dirtyMonitor = ref<boolean>(true)
|
const dirtyMonitor = ref<boolean>(true)
|
||||||
const schemaUpdateTime = computed(() => _get(store.state, 'edit.schemaUpdateTime'))
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
schemaUpdateTime,
|
schemaUpdateTime,
|
||||||
|
@ -87,12 +87,6 @@ const closedDialog = () => {
|
|||||||
margin-left: 75px;
|
margin-left: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
height: 29px;
|
|
||||||
line-height: 29px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-tab {
|
.preview-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -131,7 +125,7 @@ const closedDialog = () => {
|
|||||||
&.pc {
|
&.pc {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #ffffff;
|
background: #f7f7f7;
|
||||||
box-shadow: 0px 2px 10px -2px rgba(82, 82, 102, 0.2);
|
box-shadow: 0px 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||||
height: 726px;
|
height: 726px;
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
@ -5,33 +5,101 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox, type Action } from 'element-plus'
|
import { ElMessage, ElMessageBox, type Action } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
import { publishSurvey, saveSurvey, getConflictHistory } from '@/management/api/survey'
|
import { publishSurvey, saveSurvey, getConflictHistory } from '@/management/api/survey'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
updateLogicConf: any
|
||||||
|
updateWhiteConf: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const isPublishing = ref<boolean>(false)
|
const isPublishing = ref<boolean>(false)
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
const { schema, getSchemaFromRemote } = editStore
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const saveData = computed(() => {
|
const saveData = computed(() => {
|
||||||
return buildData(store.state.edit.schema, sessionStorage.getItem('sessionUUID'))
|
return buildData(store.state.edit.schema, sessionStorage.getItem('sessionUUID'))
|
||||||
})
|
})
|
||||||
const updateLogicConf = () => {
|
|
||||||
if (
|
const validate = () => {
|
||||||
showLogicEngine.value &&
|
let checked = true
|
||||||
showLogicEngine.value.rules &&
|
let msg = ''
|
||||||
showLogicEngine.value.rules.length !== 0
|
const { validated, message } = props.updateLogicConf()
|
||||||
) {
|
if (!validated) {
|
||||||
showLogicEngine.value.validateSchema()
|
checked = validated
|
||||||
const showLogicConf = showLogicEngine.value.toJson()
|
msg = `检查页面"问卷编辑>显示逻辑":${message}`
|
||||||
// 更新逻辑配置
|
}
|
||||||
store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
const { validated: whiteValidated, message: whiteMsg } = props.updateWhiteConf()
|
||||||
|
if (!whiteValidated) {
|
||||||
|
checked = whiteValidated
|
||||||
|
msg = `检查页面"问卷设置>作答限制":${whiteMsg}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkConflict = async (surveyid:string) => {
|
||||||
|
try {
|
||||||
|
const dailyHis = await getConflictHistory({surveyId: surveyid, historyType: 'dailyHis', sessionId: sessionStorage.getItem('sessionUUID')})
|
||||||
|
if (dailyHis.data.length > 0) {
|
||||||
|
const lastHis = dailyHis.data.at(0)
|
||||||
|
if (Date.now() - lastHis.createDate > 2 * 60 * 1000) {
|
||||||
|
return [false, '']
|
||||||
|
}
|
||||||
|
return [true, lastHis.operator.username]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
return [false, '']
|
||||||
|
}
|
||||||
|
const onSave = async () => {
|
||||||
|
let res
|
||||||
|
|
||||||
|
if (!saveData.value.surveyId) {
|
||||||
|
ElMessage.error('未获取到问卷id')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// 增加冲突检测
|
||||||
|
const [isconflict, conflictName] = await checkConflict(saveData.value.surveyId)
|
||||||
|
if(isconflict) {
|
||||||
|
if (conflictName == store.state.user.userInfo.username) {
|
||||||
|
ElMessageBox.alert('当前问卷已在其它页面开启编辑,刷新以获取最新内容。', '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
callback: (action: Action) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
store.dispatch('edit/getSchemaFromRemote')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ElMessageBox.alert(`当前问卷2分钟内由${conflictName}编辑,刷新以获取最新内容。`, '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
callback: (action: Action) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
store.dispatch('edit/getSchemaFromRemote')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
// 保存数据
|
||||||
|
res = await saveSurvey(saveData.value)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
const checkConflict = async (surveyid:string) => {
|
const checkConflict = async (surveyid:string) => {
|
||||||
try {
|
try {
|
||||||
const dailyHis = await getConflictHistory({surveyId: surveyid, historyType: 'dailyHis', sessionId: sessionStorage.getItem('sessionUUID')})
|
const dailyHis = await getConflictHistory({surveyId: surveyid, historyType: 'dailyHis', sessionId: sessionStorage.getItem('sessionUUID')})
|
||||||
@ -90,11 +158,11 @@ const handlePublish = async () => {
|
|||||||
|
|
||||||
isPublishing.value = true
|
isPublishing.value = true
|
||||||
|
|
||||||
try {
|
// 发布检测
|
||||||
updateLogicConf()
|
const { checked, msg } = validate()
|
||||||
} catch (err) {
|
if (!checked) {
|
||||||
isPublishing.value = false
|
isPublishing.value = false
|
||||||
ElMessage.error('请检查逻辑配置是否有误')
|
ElMessage.error(msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +177,7 @@ const handlePublish = async () => {
|
|||||||
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId, sessionId: sessionStorage.getItem('sessionUUID') })
|
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId, sessionId: sessionStorage.getItem('sessionUUID') })
|
||||||
if (publishRes.code === 200) {
|
if (publishRes.code === 200) {
|
||||||
ElMessage.success('发布成功')
|
ElMessage.success('发布成功')
|
||||||
store.dispatch('edit/getSchemaFromRemote')
|
getSchemaFromRemote()
|
||||||
router.push({ name: 'publish' })
|
router.push({ name: 'publish' })
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(`发布失败 ${publishRes.errmsg}`)
|
ElMessage.error(`发布失败 ${publishRes.errmsg}`)
|
||||||
|
@ -15,18 +15,25 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, nextTick, watch, onMounted } from 'vue'
|
import { ref, computed, nextTick, watch, onMounted } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useRoute } from 'vue-router'
|
||||||
import { get as _get } from 'lodash-es'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
import { get as _get } from 'lodash-es'
|
||||||
import { ElMessage, ElMessageBox, type Action } from 'element-plus'
|
import { ElMessage, ElMessageBox, type Action } 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 } from '@/management/api/survey'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
import { getConflictHistory } from '@/management/api/survey'
|
import { getConflictHistory } from '@/management/api/survey'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
updateLogicConf: any
|
||||||
|
updateWhiteConf: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const props = defineProps<Props>()
|
||||||
const isSaving = ref<boolean>(false)
|
const isSaving = ref<boolean>(false)
|
||||||
const isShowAutoSave = ref<boolean>(false)
|
const isShowAutoSave = ref<boolean>(false)
|
||||||
const autoSaveStatus = ref<'succeed' | 'saving' | 'failed'>('succeed')
|
const autoSaveStatus = ref<'succeed' | 'saving' | 'failed'>('succeed')
|
||||||
@ -39,8 +46,30 @@ const saveText = computed(
|
|||||||
})[autoSaveStatus.value]
|
})[autoSaveStatus.value]
|
||||||
)
|
)
|
||||||
|
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
const { schemaUpdateTime } = storeToRefs(editStore)
|
||||||
|
const { schema } = editStore
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
let checked = true
|
||||||
|
let msg = ''
|
||||||
|
if (route.path.includes('edit/logic')) {
|
||||||
|
const { validated, message } = props.updateLogicConf()
|
||||||
|
checked = validated
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.path.includes('edit/setting')) {
|
||||||
|
const { validated, message } = props.updateWhiteConf()
|
||||||
|
checked = validated
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!sessionStorage.getItem('sessionUUID')) {
|
if (!sessionStorage.getItem('sessionUUID')) {
|
||||||
sessionStorage.setItem('sessionUUID', nanoid());
|
sessionStorage.setItem('sessionUUID', nanoid());
|
||||||
@ -102,19 +131,6 @@ const onSave = async () => {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLogicConf = () => {
|
|
||||||
if (
|
|
||||||
showLogicEngine.value &&
|
|
||||||
showLogicEngine.value.rules &&
|
|
||||||
showLogicEngine.value.rules.length !== 0
|
|
||||||
) {
|
|
||||||
showLogicEngine.value.validateSchema()
|
|
||||||
const showLogicConf = showLogicEngine.value.toJson()
|
|
||||||
// 更新逻辑配置
|
|
||||||
store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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') {
|
||||||
@ -158,11 +174,11 @@ const handleSave = async () => {
|
|||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
isShowAutoSave.value = false
|
isShowAutoSave.value = false
|
||||||
|
|
||||||
try {
|
// 保存检测
|
||||||
updateLogicConf()
|
const { checked, msg } = validate()
|
||||||
} catch (error) {
|
if (!checked) {
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
ElMessage.error('请检查逻辑配置是否有误')
|
ElMessage.error(msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +200,6 @@ const handleSave = async () => {
|
|||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaUpdateTime = computed(() => _get(store.state, 'edit.schemaUpdateTime'))
|
const schemaUpdateTime = computed(() => _get(store.state, 'edit.schemaUpdateTime'))
|
||||||
watch(schemaUpdateTime, () => {
|
watch(schemaUpdateTime, () => {
|
||||||
triggerAutoSave()
|
triggerAutoSave()
|
||||||
|
@ -10,6 +10,7 @@ export default function (schema, sessionId) {
|
|||||||
'skinConf',
|
'skinConf',
|
||||||
'submitConf',
|
'submitConf',
|
||||||
'questionDataList',
|
'questionDataList',
|
||||||
|
'pageConf',
|
||||||
'logicConf'
|
'logicConf'
|
||||||
])
|
])
|
||||||
configData.dataConf = {
|
configData.dataConf = {
|
||||||
|
@ -49,14 +49,13 @@ const hideFullTitle = () => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.title-container {
|
.title-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 280px;
|
max-width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
449
web/src/management/pages/edit/modules/logicModule/JumpLogic.vue
Normal file
449
web/src/management/pages/edit/modules/logicModule/JumpLogic.vue
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch, toRaw, computed } from 'vue'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import LogicFlow from '@logicflow/core'
|
||||||
|
import { MiniMap, Control } from '@logicflow/extension'
|
||||||
|
import '@logicflow/extension/lib/style/index.css'
|
||||||
|
import '@logicflow/core/es/index.css'
|
||||||
|
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateNodes,
|
||||||
|
generateLine,
|
||||||
|
getNodesStep,
|
||||||
|
getCondition
|
||||||
|
} from '@/management/hooks/useJumpLogicFlow'
|
||||||
|
|
||||||
|
import NodeExtension from './components/nodeExtension/index'
|
||||||
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
|
||||||
|
const jumpLogicEngine = computed(() => {
|
||||||
|
return editStore.jumpLogicEngine
|
||||||
|
})
|
||||||
|
const questionDataList = computed(() => {
|
||||||
|
return editStore.schema.questionDataList
|
||||||
|
})
|
||||||
|
const config: Partial<LogicFlow.Options> = {
|
||||||
|
snapline: false,
|
||||||
|
isSilentMode: false,
|
||||||
|
stopScrollGraph: true,
|
||||||
|
stopZoomGraph: true,
|
||||||
|
style: {
|
||||||
|
rect: {
|
||||||
|
rx: 5,
|
||||||
|
ry: 5,
|
||||||
|
strokeWidth: 2
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
fill: '#f5f5f5',
|
||||||
|
stroke: '#666'
|
||||||
|
},
|
||||||
|
ellipse: {
|
||||||
|
fill: '#dae8fc',
|
||||||
|
stroke: '#6c8ebf'
|
||||||
|
},
|
||||||
|
polygon: {
|
||||||
|
fill: '#d5e8d4',
|
||||||
|
stroke: '#82b366'
|
||||||
|
},
|
||||||
|
diamond: {
|
||||||
|
fill: '#ffe6cc',
|
||||||
|
stroke: '#d79b00'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: '#b85450',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adjustEdgeStartAndEnd: true,
|
||||||
|
adjustEdgeStart: false,
|
||||||
|
adjustEdgeEnd: true
|
||||||
|
// grid: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTheme: Partial<LogicFlow.Theme> = {
|
||||||
|
baseNode: {
|
||||||
|
stroke: '#FBC559'
|
||||||
|
},
|
||||||
|
nodeText: {
|
||||||
|
overflowMode: 'ellipsis',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: 13
|
||||||
|
},
|
||||||
|
edgeText: {
|
||||||
|
overflowMode: 'ellipsis',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: 13,
|
||||||
|
textWidth: 100
|
||||||
|
}, // 确认 textWidth 是否必传
|
||||||
|
polyline: {
|
||||||
|
stroke: 'red'
|
||||||
|
},
|
||||||
|
rect: {
|
||||||
|
width: 200,
|
||||||
|
height: 40
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
offset: 4, // 箭头长度
|
||||||
|
verticalLength: 2 // 箭头垂直于边的距离
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
nodes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const lfRef = ref<LogicFlow | null>(null)
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const initGraph = (questionDataList: any) => {
|
||||||
|
const list = toRaw(questionDataList)
|
||||||
|
if (list.length) {
|
||||||
|
const nodes = generateNodes(list)
|
||||||
|
let models: any[] = []
|
||||||
|
nodes.forEach((item: any) => {
|
||||||
|
const nodeModel = lfRef.value?.addNode(item)
|
||||||
|
models.push(nodeModel)
|
||||||
|
})
|
||||||
|
const edges = generateLine(models)
|
||||||
|
edges.forEach((item: any) => {
|
||||||
|
const edgeModel = lfRef.value?.addEdge(item)
|
||||||
|
if (edgeModel && ('start_node' === item.sourceNodeId || 'end_node' === item.targetNodeId)) {
|
||||||
|
edgeModel.draggable = false
|
||||||
|
edgeModel.isSelected = false
|
||||||
|
edgeModel.isShowAdjustPoint = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerEvents = (lf: LogicFlow) => {
|
||||||
|
// 更新从选项拉出的逻辑
|
||||||
|
lf.on('anchor:drop', ({ edgeModel }) => {
|
||||||
|
/* 添加规则 **/
|
||||||
|
const { sourceNodeId, sourceAnchorId, targetNodeId } = edgeModel
|
||||||
|
const target = targetNodeId
|
||||||
|
const { field, operator, value } = getCondition({
|
||||||
|
anchorId: sourceAnchorId,
|
||||||
|
nodeId: sourceNodeId
|
||||||
|
})
|
||||||
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
|
|
||||||
|
const ruleNode = new RuleNode(target)
|
||||||
|
ruleNode.addCondition(conditionNode)
|
||||||
|
edgeModel.setProperties({
|
||||||
|
ruleId: ruleNode.id,
|
||||||
|
conditionId: conditionNode.id
|
||||||
|
})
|
||||||
|
jumpLogicEngine.value.addRule(ruleNode)
|
||||||
|
})
|
||||||
|
// 调整边的起点和终点,更新题目默认的连接线
|
||||||
|
lf.on('edge:exchange-node', ({ data }: any) => {
|
||||||
|
console.log('edge:exchange-node', { data })
|
||||||
|
/* 更新规则目标 **/
|
||||||
|
|
||||||
|
const { newEdge, oldEdge } = data
|
||||||
|
|
||||||
|
const ruleId = oldEdge.properties.ruleId
|
||||||
|
// 如果新的连接线,默认的步长 == 1
|
||||||
|
if (
|
||||||
|
getNodesStep(newEdge.sourceNodeId, newEdge.targetNodeId, questionDataList.value) === 1 &&
|
||||||
|
ruleId
|
||||||
|
) {
|
||||||
|
/** 删除逻辑。step n --> 1 */
|
||||||
|
console.log('删除逻辑。step n --> 1')
|
||||||
|
jumpLogicEngine.value.removeRule(ruleId)
|
||||||
|
} else {
|
||||||
|
if (ruleId) {
|
||||||
|
/** 更新逻辑. step n --> m */
|
||||||
|
console.log('更新逻辑. step n --> m ')
|
||||||
|
// const ruleId = oldEdge.properties.ruleId
|
||||||
|
const ruleNode = jumpLogicEngine.value.findRule(ruleId)
|
||||||
|
ruleNode.setTarget(newEdge.targetNodeId)
|
||||||
|
} else {
|
||||||
|
/** 添加逻辑。step 1 --> n */
|
||||||
|
console.log('添加逻辑。step 1 --> n')
|
||||||
|
const newEdgeModel = lf.graphModel.getEdgeModelById(newEdge.id)
|
||||||
|
|
||||||
|
const { sourceNodeId, sourceAnchorId } = newEdge
|
||||||
|
const { field, operator, value } = getCondition({
|
||||||
|
anchorId: sourceAnchorId,
|
||||||
|
nodeId: sourceNodeId
|
||||||
|
})
|
||||||
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
|
|
||||||
|
const ruleNode = new RuleNode(newEdge.targetNodeId)
|
||||||
|
ruleNode.addCondition(conditionNode)
|
||||||
|
newEdgeModel?.setProperties({
|
||||||
|
ruleId: ruleNode.id,
|
||||||
|
conditionId: conditionNode.id
|
||||||
|
})
|
||||||
|
jumpLogicEngine.value.addRule(ruleNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
if (containerRef.value) {
|
||||||
|
const lf = new LogicFlow({
|
||||||
|
...config,
|
||||||
|
container: containerRef.value,
|
||||||
|
// height: 700,
|
||||||
|
translateCenter: true,
|
||||||
|
multipleSelectKey: 'shift',
|
||||||
|
disabledTools: ['multipleSelect'],
|
||||||
|
autoExpand: true,
|
||||||
|
adjustEdgeStartAndEnd: true,
|
||||||
|
allowRotate: false,
|
||||||
|
edgeTextEdit: false,
|
||||||
|
nodeTextEdit: false,
|
||||||
|
keyboard: {
|
||||||
|
enabled: true,
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: ['backspace'],
|
||||||
|
callback: () => {
|
||||||
|
ElMessageBox.confirm('确定要删除吗?', '删除提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const elements = lf.getSelectElements(true)
|
||||||
|
lf.clearSelectElements()
|
||||||
|
elements.edges.forEach((edge) => {
|
||||||
|
console.log({ edge })
|
||||||
|
const { sourceNodeId, sourceAnchorId } = edge
|
||||||
|
if (sourceAnchorId?.split('_right')[0] === sourceNodeId) {
|
||||||
|
ElMessage({
|
||||||
|
message: '题目答完跳转的连接线不可以删除',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const { properties } = edge
|
||||||
|
jumpLogicEngine.value.removeRule(properties?.ruleId)
|
||||||
|
lf.deleteEdge(edge.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(42, elements)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
partial: false,
|
||||||
|
background: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
},
|
||||||
|
edgeTextDraggable: false,
|
||||||
|
edgeType: 'bezier',
|
||||||
|
style: {
|
||||||
|
inputText: {
|
||||||
|
background: 'black',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
idGenerator(type) {
|
||||||
|
return type + '_' + Math.random()
|
||||||
|
},
|
||||||
|
plugins: [NodeExtension, MiniMap, Control],
|
||||||
|
pluginsOptions: {
|
||||||
|
miniMap: {
|
||||||
|
width: 284,
|
||||||
|
height: 84,
|
||||||
|
isShowHeader: false,
|
||||||
|
isShowCloseIcon: false,
|
||||||
|
leftPosition: 0,
|
||||||
|
rightPosition: 0,
|
||||||
|
bottomPosition: 50,
|
||||||
|
showEdge: false,
|
||||||
|
isShow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lf.setTheme(customTheme)
|
||||||
|
registerEvents(lf)
|
||||||
|
const control = lf.extension.control as Control
|
||||||
|
control.removeItem('zoom-out')
|
||||||
|
control.removeItem('zoom-in')
|
||||||
|
control.removeItem('reset')
|
||||||
|
control.removeItem('reset')
|
||||||
|
control.removeItem('undo')
|
||||||
|
control.removeItem('redo')
|
||||||
|
control.addItem({
|
||||||
|
key: 'zoom-out',
|
||||||
|
iconClass: 'iconfont icon-suoxiao',
|
||||||
|
title: '缩小流程图',
|
||||||
|
text: '缩小',
|
||||||
|
onClick: () => {
|
||||||
|
lf.zoom(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
control.addItem({
|
||||||
|
key: 'zoom-in',
|
||||||
|
iconClass: 'iconfont icon-fangda',
|
||||||
|
title: '放大流程图',
|
||||||
|
text: '放大',
|
||||||
|
onClick: () => {
|
||||||
|
lf.zoom(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
control.addItem({
|
||||||
|
key: 'reset',
|
||||||
|
iconClass: 'iconfont icon-shiying',
|
||||||
|
title: '恢复流程原有尺寸',
|
||||||
|
text: '适应',
|
||||||
|
onClick: () => {
|
||||||
|
lf.resetZoom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lf.render(data)
|
||||||
|
const miniMap = lf.extension.miniMap as MiniMap
|
||||||
|
miniMap?.show()
|
||||||
|
|
||||||
|
lfRef.value = lf
|
||||||
|
if (questionDataList.value.length) {
|
||||||
|
initGraph(questionDataList.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => questionDataList.value,
|
||||||
|
(value) => {
|
||||||
|
const list = toRaw(value)
|
||||||
|
if (list.length) {
|
||||||
|
initGraph(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" id="graph" class="viewport"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.viewport {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* q-node */
|
||||||
|
.table-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node::before {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #fbc559;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-1::before {
|
||||||
|
background: #9673a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-2::before {
|
||||||
|
background: #dae8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-3::before {
|
||||||
|
background: #82b366;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-4::before {
|
||||||
|
background: #f8cecc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-name {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: left;
|
||||||
|
color: #4a4c5b;
|
||||||
|
width: 180px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-feild {
|
||||||
|
height: 28x;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #4a4c5b;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feild-type {
|
||||||
|
color: #9f9c9f;
|
||||||
|
}
|
||||||
|
/* 自定义锚点样式 */
|
||||||
|
.custom-anchor {
|
||||||
|
cursor: crosshair;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
rx: 3;
|
||||||
|
ry: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-anchor:hover {
|
||||||
|
fill: #ff7f0e;
|
||||||
|
stroke: #ff7f0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-node-not-allow .custom-anchor:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incomming-anchor {
|
||||||
|
stroke: #d79b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing-anchor {
|
||||||
|
stroke: #82b366;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-mini-map {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
background: #f6f7f9;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.lf-control {
|
||||||
|
.iconfont {
|
||||||
|
color: #6e707c;
|
||||||
|
}
|
||||||
|
color: #6e707c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,17 +5,15 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, provide } from 'vue'
|
import { computed, provide } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
import RulePanel from '../../modules/logicModule/RulePanel.vue'
|
import RulePanel from './components/RulePanel.vue'
|
||||||
import { filterQuestionPreviewData } from '@/management/utils/index'
|
import { filterQuestionPreviewData } from '@/management/utils/index'
|
||||||
|
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
const { questionDataList } = storeToRefs(editStore)
|
||||||
const questionDataList = computed(() => {
|
|
||||||
return store.state.edit.schema.questionDataList
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderData = computed(() => {
|
const renderData = computed(() => {
|
||||||
return filterQuestionPreviewData(cloneDeep(questionDataList.value))
|
return filterQuestionPreviewData(cloneDeep(questionDataList.value))
|
||||||
@ -25,11 +23,11 @@ provide('renderData', renderData)
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logic-wrapper {
|
.logic-wrapper {
|
||||||
height: calc(100% - 120px);
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 12px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
// position: fixed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -57,7 +57,7 @@
|
|||||||
import { computed, inject, ref, type ComputedRef } from 'vue'
|
import { computed, inject, ref, type ComputedRef } from 'vue'
|
||||||
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
|
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { CHOICES } from '@/common/typeEnum'
|
import { CHOICES } from '@/common/typeEnum'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichTextWithMediaTag } from '@/common/xss'
|
||||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
index: {
|
index: {
|
||||||
@ -88,7 +88,7 @@ const fieldList = computed(() => {
|
|||||||
.filter((question: any) => CHOICES.includes(question.type))
|
.filter((question: any) => CHOICES.includes(question.type))
|
||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
|
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichTextWithMediaTag(item.title)}`,
|
||||||
value: item.field
|
value: item.field
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -102,7 +102,7 @@ const getRelyOptions = computed(() => {
|
|||||||
return (
|
return (
|
||||||
currentQuestion?.options.map((item: any) => {
|
currentQuestion?.options.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
label: cleanRichText(item.text),
|
label: cleanRichTextWithMediaTag(item.text),
|
||||||
value: item.hash
|
value: item.hash
|
||||||
}
|
}
|
||||||
}) || []
|
}) || []
|
||||||
|
@ -52,7 +52,10 @@ import { ElMessageBox } from 'element-plus'
|
|||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
import ConditionView from './ConditionView.vue'
|
import ConditionView from './ConditionView.vue'
|
||||||
|
|
||||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
|
@ -21,8 +21,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { shallowRef, computed } from 'vue'
|
import { shallowRef, computed } from 'vue'
|
||||||
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import RuleNodeView from './components/RuleNodeView.vue'
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
|
|
||||||
|
import RuleNodeView from './RuleNodeView.vue'
|
||||||
|
|
||||||
const list = computed(() => {
|
const list = computed(() => {
|
||||||
return showLogicEngine.value?.rules || []
|
return showLogicEngine.value?.rules || []
|
@ -0,0 +1,55 @@
|
|||||||
|
import { BezierEdge, BezierEdgeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class CustomEdge2 extends BezierEdge {}
|
||||||
|
|
||||||
|
class CustomEdgeModel2 extends BezierEdgeModel {
|
||||||
|
getEdgeStyle() {
|
||||||
|
const style = super.getEdgeStyle()
|
||||||
|
// svg属性
|
||||||
|
style.strokeWidth = 1
|
||||||
|
style.stroke = '#ababac'
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重写此方法,使保存数据是能带上锚点数据。
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
const data = super.getData()
|
||||||
|
data.sourceAnchorId = this.sourceAnchorId
|
||||||
|
data.targetAnchorId = this.targetAnchorId
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
|
||||||
|
*/
|
||||||
|
updatePathByAnchor() {
|
||||||
|
// TODO
|
||||||
|
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
|
||||||
|
const sourceAnchor = sourceNodeModel
|
||||||
|
.getDefaultAnchor()
|
||||||
|
.find((anchor) => anchor.id === this.sourceAnchorId)
|
||||||
|
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
|
||||||
|
const targetAnchor = targetNodeModel
|
||||||
|
.getDefaultAnchor()
|
||||||
|
.find((anchor) => anchor.id === this.targetAnchorId)
|
||||||
|
const startPoint = {
|
||||||
|
x: sourceAnchor.x,
|
||||||
|
y: sourceAnchor.y
|
||||||
|
}
|
||||||
|
this.updateStartPoint(startPoint)
|
||||||
|
const endPoint = {
|
||||||
|
x: targetAnchor.x,
|
||||||
|
y: targetAnchor.y
|
||||||
|
}
|
||||||
|
this.updateEndPoint(endPoint)
|
||||||
|
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
|
||||||
|
this.pointsList = []
|
||||||
|
this.initPoints()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'q-edge',
|
||||||
|
view: CustomEdge2,
|
||||||
|
model: CustomEdgeModel2
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import StartNode from './nodes/StartNode'
|
||||||
|
import EndNode from './nodes/EndNode'
|
||||||
|
import QNode from './nodes/QNode'
|
||||||
|
import QEdge from './edges/QEdge'
|
||||||
|
|
||||||
|
class NodeExtension {
|
||||||
|
static pluginName = 'NodeExtension'
|
||||||
|
constructor({ lf }) {
|
||||||
|
lf.register(StartNode)
|
||||||
|
lf.register(EndNode)
|
||||||
|
lf.register(QNode)
|
||||||
|
lf.register(QEdge)
|
||||||
|
lf.setDefaultEdgeType('q-edge')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeExtension
|
@ -0,0 +1,41 @@
|
|||||||
|
import { CircleNode, CircleNodeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class EndNodeModel extends CircleNodeModel {
|
||||||
|
constructor(data, graphModel) {
|
||||||
|
data.text = {
|
||||||
|
value: data.text,
|
||||||
|
x: data.x,
|
||||||
|
y: data.y
|
||||||
|
}
|
||||||
|
super(data, graphModel)
|
||||||
|
|
||||||
|
this.r = 30
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重写定义锚点
|
||||||
|
*/
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const { x, y, id, width } = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x - width / 2,
|
||||||
|
y: y,
|
||||||
|
id: `${id}_left`,
|
||||||
|
type: 'left'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isAllowConnectedAsSource() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'end-node',
|
||||||
|
model: EndNodeModel,
|
||||||
|
view: CircleNode
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
import { HtmlNode, HtmlNodeModel, h } from '@logicflow/core'
|
||||||
|
|
||||||
|
class QNode extends HtmlNode {
|
||||||
|
/**
|
||||||
|
* 1.1.7版本后支持在view中重写锚点形状。
|
||||||
|
* 重写锚点新增
|
||||||
|
*/
|
||||||
|
getAnchorShape(anchorData) {
|
||||||
|
const { x, y, type } = anchorData
|
||||||
|
return h('rect', {
|
||||||
|
x: x - 5,
|
||||||
|
y: y - 5,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
className: `custom-anchor ${type === 'left' ? 'incomming-anchor' : 'outgoing-anchor'}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setHtml(rootEl) {
|
||||||
|
rootEl.innerHTML = ''
|
||||||
|
const {
|
||||||
|
properties: { options = [], title }
|
||||||
|
} = this.props.model
|
||||||
|
rootEl.setAttribute('class', 'table-container')
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.className = `table-node`
|
||||||
|
const tableNameElement = document.createElement('div')
|
||||||
|
tableNameElement.innerHTML = title
|
||||||
|
tableNameElement.className = 'table-name'
|
||||||
|
container.appendChild(tableNameElement)
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const item = options[i]
|
||||||
|
const itemElement = document.createElement('div')
|
||||||
|
itemElement.className = 'table-feild'
|
||||||
|
const itemKey = document.createElement('span')
|
||||||
|
itemKey.innerHTML = item.type
|
||||||
|
itemElement.appendChild(itemKey)
|
||||||
|
// itemKey.innerText = item.key;
|
||||||
|
// const itemType = document.createElement('span');
|
||||||
|
// itemType.innerHTML = item.type;
|
||||||
|
// itemType.className = 'feild-type';
|
||||||
|
// itemElement.appendChild(itemType);
|
||||||
|
fragment.appendChild(itemElement)
|
||||||
|
}
|
||||||
|
container.appendChild(fragment)
|
||||||
|
rootEl.appendChild(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QNodeModel extends HtmlNodeModel {
|
||||||
|
getOutlineStyle() {
|
||||||
|
const style = super.getOutlineStyle()
|
||||||
|
style.stroke = 'none'
|
||||||
|
style.hover.stroke = 'none'
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
// 如果不用修改锚地形状,可以重写颜色相关样式
|
||||||
|
getAnchorStyle(anchorInfo) {
|
||||||
|
const style = super.getAnchorStyle()
|
||||||
|
if (anchorInfo.type === 'left') {
|
||||||
|
style.fill = 'red'
|
||||||
|
style.hover.fill = 'transparent'
|
||||||
|
style.hover.stroke = 'transpanrent'
|
||||||
|
style.className = 'lf-hide-default'
|
||||||
|
} else {
|
||||||
|
style.fill = 'green'
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
setAttributes() {
|
||||||
|
this.width = 200
|
||||||
|
const {
|
||||||
|
properties: { options = [] }
|
||||||
|
} = this
|
||||||
|
this.height = 60 + options.length * 28
|
||||||
|
const circleOnlyAsTarget = {
|
||||||
|
message: '只允许从右边的锚点连出',
|
||||||
|
validate: (sourceNode, targetNode, sourceAnchor) => {
|
||||||
|
return sourceAnchor.type === 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sourceRules.push(circleOnlyAsTarget)
|
||||||
|
this.targetRules.push({
|
||||||
|
message: '只允许连接左边的锚点',
|
||||||
|
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
|
||||||
|
return targetAnchor.type === 'left'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
properties: { options }
|
||||||
|
} = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x - width / 2 + 10,
|
||||||
|
y: y - height / 2 + 60 - 28,
|
||||||
|
id: `${id}_left`,
|
||||||
|
edgeAddable: false,
|
||||||
|
type: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: x + width / 2 - 10,
|
||||||
|
y: y - height / 2 + 60 - 28,
|
||||||
|
id: `${id}_right`,
|
||||||
|
edgeAddable: false,
|
||||||
|
type: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
options.forEach((feild, index) => {
|
||||||
|
const anchorId = `${feild.key}_right`
|
||||||
|
const { edges } = this.outgoing
|
||||||
|
let edgeAddable = true
|
||||||
|
if (edges.length) {
|
||||||
|
const sourceAnchorIds = edges.map((edge) => edge.sourceAnchorId)
|
||||||
|
edgeAddable = !sourceAnchorIds.includes(anchorId)
|
||||||
|
}
|
||||||
|
anchors.push({
|
||||||
|
x: x + width / 2 - 10,
|
||||||
|
y: y - height / 2 + 60 - 28 + (index + 1) * 30,
|
||||||
|
id: anchorId,
|
||||||
|
type: 'right',
|
||||||
|
key: feild.key,
|
||||||
|
edgeAddable
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 获取当前节点作为边的起始节点规则。
|
||||||
|
// getConnectedSourceRules() {
|
||||||
|
// const rules = super.getConnectedSourceRules();
|
||||||
|
// // 开始节点的锚点只能拉出一条连接线
|
||||||
|
// const geteWayOnlyAsTarget = {
|
||||||
|
// message: "开始节点的锚点只能拉出一条连接线",
|
||||||
|
// validate: (
|
||||||
|
// source,
|
||||||
|
// target,
|
||||||
|
// sourceAnchor,
|
||||||
|
// targetAnchor
|
||||||
|
// ) => {
|
||||||
|
// // 获取该节点下目标连接线
|
||||||
|
// const edges = this.graphModel.getNodeOutgoingEdge(source.id);
|
||||||
|
// console.log({edges});
|
||||||
|
// // 如果连接线的锚点id存在则不允许链接
|
||||||
|
// const sourceAnchorIds = edges.map(edge => edge.sourceAnchorId);
|
||||||
|
// // 判断该新拉出的边是否已经有了目标链接,有的话disable
|
||||||
|
// let isValid = true;
|
||||||
|
// if (sourceAnchorIds.includes(sourceAnchor.id)) {
|
||||||
|
// isValid = false;
|
||||||
|
// }
|
||||||
|
// console.log(edges[0].targetNodeId,target.id, sourceAnchorIds, sourceAnchor.id, isValid)
|
||||||
|
// return isValid;
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// // 如果该题存在无条件跳转则禁用选项跳转
|
||||||
|
|
||||||
|
// // 如果该题存在选项跳转则禁用无条件跳转
|
||||||
|
|
||||||
|
// rules.push(geteWayOnlyAsTarget);
|
||||||
|
// return rules;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'q-node',
|
||||||
|
model: QNodeModel,
|
||||||
|
view: QNode
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { CircleNode, CircleNodeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class StartNodeModel extends CircleNodeModel {
|
||||||
|
constructor(data, graphModel) {
|
||||||
|
data.text = {
|
||||||
|
value: data.text,
|
||||||
|
x: data.x,
|
||||||
|
y: data.y
|
||||||
|
}
|
||||||
|
super(data, graphModel)
|
||||||
|
|
||||||
|
this.r = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写定义锚点
|
||||||
|
*/
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const { x, y, id, width } = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x + width / 2,
|
||||||
|
y: y,
|
||||||
|
id: `${id}_right`,
|
||||||
|
type: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isAllowConnectedAsTarget() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'start-node',
|
||||||
|
model: StartNodeModel,
|
||||||
|
view: CircleNode
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
# 说明
|
||||||
|
|
||||||
|
参考node-red样式,以logicflow插件的方式实现。
|
@ -0,0 +1,23 @@
|
|||||||
|
.custom-anchor {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
cursor: crosshair;
|
||||||
|
rx: 3;
|
||||||
|
ry: 3;
|
||||||
|
}
|
||||||
|
.custom-anchor:hover {
|
||||||
|
fill: #ff7f0e;
|
||||||
|
stroke: #ff7f0e;
|
||||||
|
}
|
||||||
|
.node-red-palette {
|
||||||
|
width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
.node-red-start {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
/* 求字符串的字节长度 */
|
||||||
|
export const getBytesLength = (word) => {
|
||||||
|
if (!word) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let totalLength = 0
|
||||||
|
for (let i = 0; i < word.length; i++) {
|
||||||
|
const c = word.charCodeAt(i)
|
||||||
|
if (word.match(/[A-Z]/)) {
|
||||||
|
totalLength += 1.5
|
||||||
|
} else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) {
|
||||||
|
totalLength += 1
|
||||||
|
} else {
|
||||||
|
totalLength += 1.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalLength
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tabs type="border-card" v-model="tabSelected" class="tab-box">
|
<el-tabs type="border-card" v-model="tabSelected" stretch class="tab-box">
|
||||||
<el-tab-pane label="题型选择">
|
<el-tab-pane label="题型选择">
|
||||||
<TypeList />
|
<TypeList />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@ -18,17 +18,25 @@ const tabSelected = ref<string>('0')
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tab-box {
|
.tab-box {
|
||||||
width: 300px;
|
width: 360px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: none;
|
border: none;
|
||||||
overflow-y: auto;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
:deep(.el-tabs__nav) {
|
:deep(.el-tabs__nav) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
:deep(.el-tabs__item) {
|
:deep(.el-tabs__item) {
|
||||||
width: 50%;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.el-tabs--border-card :deep(.el-tabs__item:last-child.is-active) {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,28 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-operation" @click="onMainClick" ref="mainOperation">
|
<div class="main-operation" @click="onMainClick" ref="mainOperation">
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<PageWrapper :readonly="false" />
|
||||||
|
</div>
|
||||||
<div class="operation-wrapper" ref="operationWrapper">
|
<div class="operation-wrapper" ref="operationWrapper">
|
||||||
<div class="box content" ref="box">
|
<div class="box content" ref="box">
|
||||||
<MainTitle
|
<MainTitle
|
||||||
|
v-if="pageEditOne == 1"
|
||||||
:bannerConf="bannerConf"
|
:bannerConf="bannerConf"
|
||||||
:readonly="false"
|
:readonly="false"
|
||||||
:is-selected="currentEditOne === 'mainTitle'"
|
:is-selected="currentEditOne === 'mainTitle'"
|
||||||
@select="onSelectEditOne('mainTitle')"
|
@select="setCurrentEditOne('mainTitle')"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
/>
|
/>
|
||||||
<MaterialGroup
|
<MaterialGroup
|
||||||
:current-edit-one="parseInt(currentEditOne)"
|
:current-edit-one="parseInt(currentEditOne)"
|
||||||
:questionDataList="questionDataList"
|
:questionDataList="pageQuestionData"
|
||||||
@select="onSelectEditOne"
|
@select="setCurrentEditOne"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@changeSeq="onQuestionOperation"
|
@changeSeq="onQuestionOperation"
|
||||||
ref="materialGroup"
|
ref="materialGroup"
|
||||||
/>
|
>
|
||||||
|
<template #advancedEdit="{ moduleConfig }">
|
||||||
|
<AdvancedComponent :moduleConfig="moduleConfig" @handleChange="handleChange" />
|
||||||
|
</template>
|
||||||
|
</MaterialGroup>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
:submit-conf="submitConf"
|
:submit-conf="submitConf"
|
||||||
:readonly="false"
|
:readonly="false"
|
||||||
:skin-conf="skinConf"
|
:skin-conf="skinConf"
|
||||||
|
:is-finally-page="isFinallyPage"
|
||||||
:is-selected="currentEditOne === 'submit'"
|
:is-selected="currentEditOne === 'submit'"
|
||||||
@select="onSelectEditOne('submit')"
|
@select="setCurrentEditOne('submit')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,34 +39,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { ref, watch, toRefs } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
import communalLoader from '@materials/communals/communalLoader.js'
|
import communalLoader from '@materials/communals/communalLoader.js'
|
||||||
|
|
||||||
|
import PageWrapper from '@/management/pages/edit/components/Pagination/PaginationWrapper.vue'
|
||||||
import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
|
import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
|
||||||
import { useStore } from 'vuex'
|
import AdvancedComponent from './components/AdvancedConfig/index.vue'
|
||||||
|
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
|
||||||
const MainTitle = communalLoader.loadComponent('MainTitle')
|
const MainTitle = communalLoader.loadComponent('MainTitle')
|
||||||
const SubmitButton = communalLoader.loadComponent('SubmitButton')
|
const SubmitButton = communalLoader.loadComponent('SubmitButton')
|
||||||
|
|
||||||
const store = useStore()
|
const editStore = useEditStore()
|
||||||
|
const { currentEditOne, currentEditKey, pageQuestionData, isFinallyPage, pageEditOne } =
|
||||||
|
storeToRefs(editStore)
|
||||||
|
const { schema, changeSchema, moveQuestion, copyQuestion, deleteQuestion, setCurrentEditOne } =
|
||||||
|
editStore
|
||||||
const mainOperation = ref(null)
|
const mainOperation = ref(null)
|
||||||
const materialGroup = ref(null)
|
const materialGroup = ref(null)
|
||||||
|
|
||||||
const bannerConf = computed(() => store.state.edit.schema.bannerConf)
|
const { bannerConf, submitConf, skinConf } = toRefs(schema)
|
||||||
const submitConf = computed(() => store.state.edit.schema.submitConf)
|
|
||||||
const skinConf = computed(() => store.state.edit.schema.skinConf)
|
|
||||||
const questionDataList = computed(() => store.state.edit.schema.questionDataList)
|
|
||||||
const currentEditOne = computed(() => store.state.edit.currentEditOne)
|
|
||||||
const currentEditKey = computed(() => store.getters['edit/currentEditKey'])
|
|
||||||
const autoScrollData = computed(() => {
|
|
||||||
return {
|
|
||||||
currentEditOne: currentEditOne.value,
|
|
||||||
len: questionDataList.value.length
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSelectEditOne = async (currentEditOne) => {
|
// const autoScrollData = computed(() => {
|
||||||
store.commit('edit/setCurrentEditOne', currentEditOne)
|
// return {
|
||||||
}
|
// currentEditOne: currentEditOne.value,
|
||||||
|
// len: questionDataList.value.length
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
const handleChange = (data) => {
|
const handleChange = (data) => {
|
||||||
if (currentEditOne.value === null) {
|
if (currentEditOne.value === null) {
|
||||||
@ -65,28 +76,28 @@ const handleChange = (data) => {
|
|||||||
}
|
}
|
||||||
const { key, value } = data
|
const { key, value } = data
|
||||||
const resultKey = `${currentEditKey.value}.${key}`
|
const resultKey = `${currentEditKey.value}.${key}`
|
||||||
store.dispatch('edit/changeSchema', { key: resultKey, value })
|
changeSchema({ key: resultKey, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMainClick = (e) => {
|
const onMainClick = (e) => {
|
||||||
if (e.target === mainOperation.value) {
|
if (e.target === mainOperation.value) {
|
||||||
store.commit('edit/setCurrentEditOne', null)
|
setCurrentEditOne(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onQuestionOperation = (data) => {
|
const onQuestionOperation = (data) => {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'move':
|
case 'move':
|
||||||
store.dispatch('edit/moveQuestion', {
|
moveQuestion({
|
||||||
index: data.index,
|
index: data.index,
|
||||||
range: data.range
|
range: data.range
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
store.dispatch('edit/deleteQuestion', { index: data.index })
|
deleteQuestion({ index: data.index })
|
||||||
break
|
break
|
||||||
case 'copy':
|
case 'copy':
|
||||||
store.dispatch('edit/copyQuestion', { index: data.index })
|
copyQuestion({ index: data.index })
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -114,22 +125,24 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(autoScrollData, (newVal) => {
|
// 实际编辑题目不会只是从上到下而需要上下题目对比。
|
||||||
const { currentEditOne } = newVal
|
// 一直跳动到顶部影响编辑操作,若有场景需要可自行放开
|
||||||
if (typeof currentEditOne === 'number') {
|
// watch(autoScrollData, (newVal) => {
|
||||||
setTimeout(() => {
|
// const { currentEditOne } = newVal
|
||||||
const field = questionDataList.value?.[currentEditOne]?.field
|
// if (typeof currentEditOne === 'number') {
|
||||||
if (field) {
|
// setTimeout(() => {
|
||||||
const questionModule = materialGroup.value?.getQuestionRefByField(field)
|
// const field = questionDataList.value?.[currentEditOne]?.field
|
||||||
if (questionModule && questionModule.$el) {
|
// if (field) {
|
||||||
questionModule.$el.scrollIntoView({
|
// const questionModule = materialGroup.value?.getQuestionRefByField(field)
|
||||||
behavior: 'smooth'
|
// if (questionModule && questionModule.$el) {
|
||||||
})
|
// questionModule.$el.scrollIntoView({
|
||||||
}
|
// behavior: 'smooth'
|
||||||
}
|
// })
|
||||||
}, 0)
|
// }
|
||||||
}
|
// }
|
||||||
})
|
// }, 0)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -142,6 +155,13 @@ watch(autoScrollData, (newVal) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #f6f7f9;
|
background-color: #f6f7f9;
|
||||||
}
|
}
|
||||||
|
.pagination-wrapper {
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 30px;
|
||||||
|
margin-right: -30px;
|
||||||
|
position: relative;
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,41 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setter-wrapper">
|
<div class="setter-wrapper">
|
||||||
<div class="no-select-question" v-if="currentEditOne === null">
|
<el-tabs v-model="confType" type="border-card" stretch>
|
||||||
|
<el-tab-pane name="baseConf" label="单题设置">
|
||||||
|
<div v-if="currentEditMeta?.title" class="setter-title">
|
||||||
|
{{ currentEditMeta?.title || '' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="no-select-question"
|
||||||
|
v-if="editStore.currentEditOne === 'mainTitle' || editStore.currentEditOne === null"
|
||||||
|
>
|
||||||
<img src="/imgs/icons/unselected.webp" />
|
<img src="/imgs/icons/unselected.webp" />
|
||||||
<h4 class="tipFont">选中题型可以编辑</h4>
|
<h4 class="tipFont">选中题型可以编辑</h4>
|
||||||
<span class="tip">来!试试看~</span>
|
<span class="tip">来!试试看~</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
|
||||||
<div class="setter-title">{{ currentEditMeta?.title || '' }}</div>
|
|
||||||
<SetterField
|
<SetterField
|
||||||
class="question-config-form"
|
v-else
|
||||||
:form-config-list="formConfigList"
|
:form-config-list="formConfigList"
|
||||||
:module-config="moduleConfig"
|
:module-config="moduleConfig"
|
||||||
@form-change="handleFormChange"
|
@form-change="handleFormChange"
|
||||||
/>
|
/>
|
||||||
</template>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane name="globalBaseConf" label="整卷设置">
|
||||||
|
<SetterField
|
||||||
|
:form-config-list="[basicConfig]"
|
||||||
|
:module-config="editStore.editGlobalBaseConf.globalBaseConfig"
|
||||||
|
@form-change="editStore.editGlobalBaseConf.updateGlobalConfOption"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import basicConfig from '@/materials/questions/common/config/basicConfig'
|
||||||
import SetterField from '@/management/pages/edit/components/SetterField.vue'
|
import SetterField from '@/management/pages/edit/components/SetterField.vue'
|
||||||
|
|
||||||
const store = useStore()
|
const confType = ref('baseConf')
|
||||||
|
const editStore = useEditStore()
|
||||||
const currentEditOne = computed(() => store.state?.edit?.currentEditOne)
|
|
||||||
const formConfigList = computed(() => store.getters['edit/formConfigList'])
|
|
||||||
const moduleConfig = computed(() => store.getters['edit/moduleConfig'])
|
|
||||||
const currentEditKey = computed(() => store.getters['edit/currentEditKey'])
|
|
||||||
const currentEditMeta = computed(() => store.getters['edit/currentEditMeta'])
|
|
||||||
|
|
||||||
|
const { currentEditKey, currentEditMeta, formConfigList, moduleConfig } = storeToRefs(editStore)
|
||||||
|
const { changeSchema } = editStore
|
||||||
const handleFormChange = (data: any) => {
|
const handleFormChange = (data: any) => {
|
||||||
const { key, value } = data
|
const { key, value } = data
|
||||||
const resultKey = `${currentEditKey.value}.${key}`
|
const resultKey = `${currentEditKey.value}.${key}`
|
||||||
store.dispatch('edit/changeSchema', { key: resultKey, value })
|
changeSchema({ key: resultKey, value })
|
||||||
|
if (key in editStore.editGlobalBaseConf.globalBaseConfig)
|
||||||
|
editStore.editGlobalBaseConf.updateCounts('MODIFY', { key, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => editStore.currentEditOne,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === 0 || (!!newVal && newVal !== 'mainTitle')) {
|
||||||
|
confType.value = 'baseConf'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.setter-wrapper {
|
.setter-wrapper {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
@ -43,17 +71,6 @@ const handleFormChange = (data: any) => {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
|
||||||
|
|
||||||
.setter-title {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: $primary-color;
|
|
||||||
padding-left: 20px;
|
|
||||||
border-bottom: 1px solid #edeffc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-select-question {
|
.no-select-question {
|
||||||
padding-top: 125px;
|
padding-top: 125px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -72,7 +89,35 @@ const handleFormChange = (data: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-config-form {
|
.setter-title {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary-color;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-bottom: 1px solid #edeffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs) {
|
||||||
|
width: 360px;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.el-tabs__nav {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.config-form {
|
||||||
padding: 30px 20px 50px 20px;
|
padding: 30px 20px 50px 20px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="primary-color" @click="openOptionConfig"> 高级设置 > </span>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="选项高级设置"
|
title="选项高级设置"
|
||||||
class="option-config-wrapper"
|
class="option-config-wrapper"
|
||||||
v-model="configVisible"
|
v-model="configVisible"
|
||||||
:append-to-body="true"
|
:append-to-body="true"
|
||||||
:width="dialogWidth"
|
width="60%"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<div class="option-handwrite">
|
<div class="option-handwrite">
|
||||||
<div class="option-header">
|
<div class="option-header">
|
||||||
<div class="header-item flex-1" v-if="showText">选项内容</div>
|
<div class="header-item flex-1">选项内容</div>
|
||||||
<div class="header-item w285" v-if="showOthers">选项后增添输入框</div>
|
<div class="header-item w285">选项后增添输入框</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<draggable :list="curOptions" handle=".drag-handle" itemKey="hash">
|
<draggable :list="curOptions" handle=".drag-handle" itemKey="hash">
|
||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<div class="option-item">
|
<div class="option-item">
|
||||||
<span class="drag-handle qicon qicon-tuodong"></span>
|
<span class="drag-handle qicon qicon-tuodong"></span>
|
||||||
<div class="flex-1 oitem" v-if="showText">
|
<div class="flex-1 oitem">
|
||||||
<div
|
<div
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
class="render-html"
|
class="render-html"
|
||||||
@ -25,7 +28,7 @@
|
|||||||
@blur="onBlur($event, index)"
|
@blur="onBlur($event, index)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="oitem moreInfo lh36" v-if="showOthers">
|
<div class="oitem moreInfo lh36">
|
||||||
<el-switch
|
<el-switch
|
||||||
:modelValue="element.others"
|
:modelValue="element.others"
|
||||||
@change="(val) => changeOptionOthers(val, element)"
|
@change="(val) => changeOptionOthers(val, element)"
|
||||||
@ -36,12 +39,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="operate-area" v-if="showOperateOption">
|
<div class="operate-area">
|
||||||
<i-ep-circlePlus
|
<i-ep-circlePlus class="area-btn-icon" @click="addOption('选项', false, index)" />
|
||||||
v-if="showOperateOption"
|
|
||||||
class="area-btn-icon"
|
|
||||||
@click="addOption('选项', false, index)"
|
|
||||||
/>
|
|
||||||
<i-ep-remove
|
<i-ep-remove
|
||||||
v-show="curOptions.length"
|
v-show="curOptions.length"
|
||||||
class="area-btn-icon"
|
class="area-btn-icon"
|
||||||
@ -53,15 +52,12 @@
|
|||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-btn-row">
|
<div class="add-btn-row">
|
||||||
<div class="add-option" v-if="showOperateOption" @click="addOption()">
|
<div class="add-option" @click="addOption()">
|
||||||
<span class="add-option-item"> <i-ep-circlePlus class="icon" /> 添加新选项 </span>
|
<span class="add-option-item"> <i-ep-circlePlus class="icon" /> 添加新选项 </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showOperateOption && showOthers" class="add-option" @click="addOtherOption">
|
<div class="add-option" @click="addOtherOption">
|
||||||
<span>
|
<span class="add-option-item"> <i-ep-circlePlus class="icon" /> 其他____ </span>
|
||||||
<extra-icon type="add-square"></extra-icon>
|
|
||||||
其他____
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,6 +68,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -81,54 +78,27 @@ import { forEach as _forEach, cloneDeep as _cloneDeep } from 'lodash-es'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
import ExtraIcon from '../ExtraIcon/index.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OptionConfig',
|
name: 'OptionConfig',
|
||||||
inject: ['moduleConfig'],
|
props: {
|
||||||
|
fieldId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
curOptions: _cloneDeep(this.options),
|
configVisible: false,
|
||||||
popoverVisible: false
|
curOptions: []
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showOptionDialog: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
dialogWidth: {
|
|
||||||
type: String,
|
|
||||||
default: '60%'
|
|
||||||
},
|
|
||||||
showOperateOption: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showText: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showOthers: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
configVisible: {
|
options() {
|
||||||
get() {
|
const editStore = useEditStore()
|
||||||
return this.showOptionDialog
|
return editStore.moduleConfig.options
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
this.$emit('update:modelValue', newVal)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hashMap() {
|
hashMap() {
|
||||||
const mapData = {}
|
const mapData = {}
|
||||||
@ -144,20 +114,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
draggable,
|
draggable
|
||||||
ExtraIcon
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initCurOption()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
options(val) {
|
options: {
|
||||||
|
handler(val) {
|
||||||
this.curOptions = _cloneDeep(val)
|
this.curOptions = _cloneDeep(val)
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addOtherOption() {
|
initCurOption() {
|
||||||
const { field } = this.moduleConfig
|
const editStore = useEditStore()
|
||||||
this.addOption('其他', true, -1, field)
|
this.curOptions = _cloneDeep(editStore.moduleConfig.options)
|
||||||
},
|
},
|
||||||
addOption(text = '选项', others = false, index = -1, field) {
|
addOtherOption() {
|
||||||
|
this.addOption('其他', true, -1, this.fieldId)
|
||||||
|
},
|
||||||
|
openOptionConfig() {
|
||||||
|
this.configVisible = true
|
||||||
|
this.initCurOption()
|
||||||
|
},
|
||||||
|
addOption(text = '选项', others = false, index = -1, fieldId) {
|
||||||
let addOne
|
let addOne
|
||||||
if (this.curOptions[0]) {
|
if (this.curOptions[0]) {
|
||||||
addOne = _cloneDeep(this.curOptions[0])
|
addOne = _cloneDeep(this.curOptions[0])
|
||||||
@ -176,7 +158,7 @@ export default {
|
|||||||
for (const i in addOne) {
|
for (const i in addOne) {
|
||||||
if (i === 'others') {
|
if (i === 'others') {
|
||||||
addOne[i] = others
|
addOne[i] = others
|
||||||
if (others) addOne.othersKey = `${field}_${addOne.hash}`
|
if (others) addOne.othersKey = `${fieldId}_${addOne.hash}`
|
||||||
} else if (i === 'mustOthers') {
|
} else if (i === 'mustOthers') {
|
||||||
addOne[i] = false
|
addOne[i] = false
|
||||||
} else if (i === 'text') {
|
} else if (i === 'text') {
|
||||||
@ -194,14 +176,13 @@ export default {
|
|||||||
|
|
||||||
return addOne
|
return addOne
|
||||||
},
|
},
|
||||||
async deleteOption(index) {
|
deleteOption(index) {
|
||||||
this.curOptions.splice(index, 1)
|
this.curOptions.splice(index, 1)
|
||||||
},
|
},
|
||||||
parseImport(newOptions) {
|
parseImport(newOptions) {
|
||||||
if (typeof newOptions !== 'undefined' && newOptions.length > 0) {
|
if (typeof newOptions !== 'undefined' && newOptions.length > 0) {
|
||||||
this.curOptions = newOptions
|
this.curOptions = newOptions
|
||||||
this.importKey = 'single'
|
this.importKey = 'single'
|
||||||
this.popoverVisible = false
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('最少保留一项')
|
ElMessage.warning('最少保留一项')
|
||||||
}
|
}
|
||||||
@ -217,10 +198,9 @@ export default {
|
|||||||
return Math.random().toString().slice(-6)
|
return Math.random().toString().slice(-6)
|
||||||
},
|
},
|
||||||
changeOptionOthers(val, option) {
|
changeOptionOthers(val, option) {
|
||||||
const { field } = this.moduleConfig
|
|
||||||
option.others = val
|
option.others = val
|
||||||
if (val) {
|
if (val) {
|
||||||
option.othersKey = `${field}_${option.hash}`
|
option.othersKey = `${this.fieldId}_${option.hash}`
|
||||||
} else {
|
} else {
|
||||||
option.othersKey = ''
|
option.othersKey = ''
|
||||||
}
|
}
|
||||||
@ -238,7 +218,7 @@ export default {
|
|||||||
ElMessage.warning('已存在相同的标签内容,请重新输入')
|
ElMessage.warning('已存在相同的标签内容,请重新输入')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit('optionChange', this.curOptions)
|
this.$emit('handleChange', { key: 'options', value: this.curOptions })
|
||||||
this.configVisible = false
|
this.configVisible = false
|
||||||
},
|
},
|
||||||
onBlur(e, index) {
|
onBlur(e, index) {
|
||||||
@ -250,6 +230,10 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.primary-color {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
.option-config-wrapper {
|
.option-config-wrapper {
|
||||||
.option-handwrite {
|
.option-handwrite {
|
||||||
.option-header {
|
.option-header {
|
@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="primary-color" @click="openOptionConfig"> 评分高级设置 > </span>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="评分高级设置"
|
||||||
|
custom-class="option-config-wrapper"
|
||||||
|
v-model="configVisible"
|
||||||
|
:append-to-body="true"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<div class="head">
|
||||||
|
<div class="row">
|
||||||
|
<div class="score">评分数值</div>
|
||||||
|
<div class="explain" v-if="isStar">评分释义</div>
|
||||||
|
<div class="other">评分后增添输入框</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="row" v-for="item in range" :key="item.index">
|
||||||
|
<div class="score">{{ item.index }}</div>
|
||||||
|
<div class="explain" v-if="isStar">
|
||||||
|
<el-input class="text" v-model="item.explain" maxlength="200" placeholder="最多200字" />
|
||||||
|
</div>
|
||||||
|
<div class="other">
|
||||||
|
<el-switch class="is-show" v-model="item.isShowInput"></el-switch>
|
||||||
|
<el-input
|
||||||
|
class="text"
|
||||||
|
v-show="item.isShowInput"
|
||||||
|
v-model="item.text"
|
||||||
|
placeholder="提示文案"
|
||||||
|
/>
|
||||||
|
<el-checkbox class="required" v-show="item.isShowInput" v-model="item.required"
|
||||||
|
>必填</el-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="configVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="onConfirm">确认</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { get as _get } from 'lodash-es'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { QUESTION_TYPE } from '@/common/typeEnum'
|
||||||
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
range: [],
|
||||||
|
configVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initRange()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isStar() {
|
||||||
|
return editStore.moduleConfig.type === QUESTION_TYPE.RADIO_STAR
|
||||||
|
},
|
||||||
|
isNps() {
|
||||||
|
return editStore.moduleConfig.type === QUESTION_TYPE.RADIO_NPS
|
||||||
|
},
|
||||||
|
min() {
|
||||||
|
const { min = 1, starMin = 1 } = editStore.moduleConfig
|
||||||
|
return this.isNps ? min : starMin
|
||||||
|
},
|
||||||
|
max() {
|
||||||
|
const { max = 5, starMax = 5 } = editStore.moduleConfig
|
||||||
|
return this.isNps ? max : starMax
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
min(newValue, oldValue) {
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
this.initRange()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
max(newValue, oldValue) {
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
this.initRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openOptionConfig() {
|
||||||
|
this.configVisible = true
|
||||||
|
},
|
||||||
|
initRange() {
|
||||||
|
if (this.min >= this.max) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = []
|
||||||
|
const rangeConfig = editStore.moduleConfig.rangeConfig
|
||||||
|
|
||||||
|
for (let i = this.min; i <= this.max; i++) {
|
||||||
|
res.push({
|
||||||
|
index: i,
|
||||||
|
isShowInput: _get(rangeConfig, `${i}.isShowInput`) || false,
|
||||||
|
text: _get(rangeConfig, `${i}.text`) || '',
|
||||||
|
required: _get(rangeConfig, `${i}.required`) || false,
|
||||||
|
explain: _get(rangeConfig, `${i}.explain`) || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.range = res
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
const res = {}
|
||||||
|
for (const item of this.range) {
|
||||||
|
res[item.index] = {
|
||||||
|
isShowInput: item.isShowInput,
|
||||||
|
text: item.text,
|
||||||
|
required: item.required,
|
||||||
|
explain: item.explain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('handleChange', {
|
||||||
|
key: `rangeConfig`,
|
||||||
|
value: res
|
||||||
|
})
|
||||||
|
this.configVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.primary-color {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
.score {
|
||||||
|
flex-basis: 110px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.other {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
.is-show {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
width: 240px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.explain {
|
||||||
|
width: 216px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.head .row {
|
||||||
|
border: 1px solid #edeffc;
|
||||||
|
background-color: #f9fafc;
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user