fix: 修复断点续答以及样式问题 (#420)

* feat: 修改readme

* [Feature]: 密码复杂度检测 (#407)

* feat: 密码复杂度检测

* chore: 改为服务端校验

* feat: 优化展示

* fix:修复编辑页在不同element版本下表现不一致问题 (#406)

* fix: 通过声明element最低版本来确定tab样式表现

* fix lint

* feat(选项设置扩展):选择类题型增加选项排列配置 (#403)

* build: add optimizeDeps packages

* feat(选项设置扩展):选择类题型增加选项排列配置

* feat(选项设置扩展): 验收问题修复

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>

* fix: 删除多余内容

* feat: 优化登录窗口

* fix: 修复断点续答以及样式问题

fix: 修复选项引用验收bug

fix: 修复断点续答问题

fix: 修复断点续答

fix: ignore

fix: 修复投票题默认值

fix: 优化断点续答逻辑

fix: 选中图标适应高度

fix: 回退最大最小选择

fix: 修复断点续答

fix: 修复elswitch不更新问题

fix: 修复访问密码更新不生效问题

fix: 修复样式

fix: 修复多选题最大最小限制

fix: 优化断点续答问题

修复多选题命中最多选择后无法取消问题

fix: 修复服务端的富文本解析

fix:  lint

fix: min error

fix: 修复最少最多选择

fix: 修复投票问卷的最少最多选择

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容单选题的断点续答下的选项配额

fix: 修复添加选项问题

fix: 前端提示服务的配额已满

fix: 更新填写的过程中配额减少情况

---------

Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com>
Co-authored-by: Jiangchunfu <mrj_kevin@163.com>
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
This commit is contained in:
dayou 2024-09-12 17:51:29 +08:00 committed by GitHub
parent d08f1c71e5
commit 6cbfe20be1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 654 additions and 377 deletions

3
.gitignore vendored
View File

@ -27,8 +27,9 @@ pnpm-debug.log*
.history
exportfile
components.d.ts
# 默认的上传文件夹
userUpload
exportfile
yarn.lock

View File

@ -29,7 +29,7 @@
<br />
&ensp;&ensp;**XIAOJUSURVEY**是一套轻量、安全的问卷系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
&ensp;&ensp;**XIAOJUSURVEY**是一套轻量、安全的调研系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
&ensp;&ensp;内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。

View File

@ -29,7 +29,7 @@
<br />
&ensp;&ensp;XIAOJUSURVEY is an open-source form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online.
&ensp;&ensp;XIAOJUSURVEY is an enterprises form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online.
&ensp;&ensp;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.

View File

@ -1,4 +1,4 @@
XIAOJU_SURVEY_MONGO_DB_NAME= # xiaojuSurvey
XIAOJU_SURVEY_MONGO_DB_NAME= xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL= # mongodb://localhost:27017 # 建议设置强密码
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin

View File

@ -48,7 +48,8 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"svg-captcha": "^1.4.0",
"typeorm": "^0.3.19"
"typeorm": "^0.3.19",
"xss": "^1.0.15"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",

View File

@ -6,6 +6,7 @@ export enum EXCEPTION_CODE {
USER_EXISTS = 2001, // 用户已存在
USER_NOT_EXISTS = 2002, // 用户不存在
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误
PASSWORD_INVALID = 2004, // 密码无效
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误

View File

@ -82,6 +82,19 @@ describe('AuthController', () => {
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
);
});
it('should throw HttpException with PASSWORD_INVALID code when password is invalid', async () => {
const mockUserInfo = {
username: 'testUser',
password: '无效的密码abc123',
captchaId: 'testCaptchaId',
captcha: 'testCaptcha',
};
await expect(controller.register(mockUserInfo)).rejects.toThrow(
new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID),
);
});
});
describe('login', () => {

View File

@ -1,4 +1,4 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { Controller, Post, Body, HttpCode, Get, Query } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UserService } from '../services/user.service';
import { CaptchaService } from '../services/captcha.service';
@ -7,6 +7,9 @@ import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { create } from 'svg-captcha';
import { ApiTags } from '@nestjs/swagger';
const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/;
@ApiTags('auth')
@Controller('/api/auth')
export class AuthController {
@ -28,6 +31,24 @@ export class AuthController {
captcha: string;
},
) {
if (!userInfo.password) {
throw new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID);
}
if (userInfo.password.length < 6 || userInfo.password.length > 16) {
throw new HttpException(
'密码长度在 6 到 16 个字符',
EXCEPTION_CODE.PASSWORD_INVALID,
);
}
if (!passwordReg.test(userInfo.password)) {
throw new HttpException(
'密码只能输入数字、字母、特殊字符',
EXCEPTION_CODE.PASSWORD_INVALID,
);
}
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
captcha: userInfo.captcha,
id: userInfo.captchaId,
@ -162,4 +183,35 @@ export class AuthController {
},
};
}
/**
*
*/
@Get('register/password/strength')
@HttpCode(200)
async getPasswordStrength(@Query('password') password: string) {
const numberReg = /[0-9]/.test(password);
const letterReg = /[a-zA-Z]/.test(password);
const symbolReg = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
// 包含三种、且长度大于8
if (numberReg && letterReg && symbolReg && password.length >= 8) {
return {
code: 200,
data: 'Strong',
};
}
// 满足任意两种
if ([numberReg, letterReg, symbolReg].filter(Boolean).length >= 2) {
return {
code: 200,
data: 'Medium',
};
}
return {
code: 200,
data: 'Weak',
};
}
}

View File

@ -41,8 +41,8 @@
"innerType": "radio",
"field": "data606",
"title": "标题2",
"minNum": "",
"maxNum": "",
"minNum": 0,
"maxNum": 0,
"options": [
{
"text": "选项1",

View File

@ -2,6 +2,7 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { checkSign } from 'src/utils/checkSign';
import { cleanRichTextWithMediaTag } from 'src/utils/xss'
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { getPushingData } from 'src/utils/messagePushing';
@ -245,7 +246,7 @@ export class SurveyResponseController {
if (quota !== 0 && quota <= optionCountData[val]) {
const item = dataList.find((item) => item['field'] === field);
throw new HttpException(
`${item['title']}】中的【${option['text']}】所选人数已达到上限,请重新选择`,
`${cleanRichTextWithMediaTag(item['title'])}】中的【${cleanRichTextWithMediaTag(option['text'])}】所选人数已达到上限,请重新选择`,
EXCEPTION_CODE.RESPONSE_OVER_LIMIT,
);
}

53
server/src/utils/xss.ts Normal file
View File

@ -0,0 +1,53 @@
import xss from 'xss'
const myxss = new (xss as any).FilterXSS({
onIgnoreTagAttr(tag, name, value) {
if (name === 'style' || name === 'class') {
return `${name}="${value}"`
}
return undefined
},
onIgnoreTag(tag, html) {
// <xxx>过滤为空,否则不过滤为空
var re1 = new RegExp('<.+?>', 'g')
if (re1.test(html)) {
return ''
} else {
return html
}
}
})
export const cleanRichTextWithMediaTag = (text) => {
if (!text) {
return text === 0 ? 0 : ''
}
const html = transformHtmlTag(text)
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
.replace(/<video.*\/video>/g, '[视频]')
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '')
return content
}
export function escapeHtml(html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export const transformHtmlTag = (html) => {
if (!html) return ''
if (typeof html !== 'string') return html + ''
return html
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\\\n/g, '\\n')
//.replace(/&nbsp;/g, "")
}
const filterXSSClone = myxss.process.bind(myxss)
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html))
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html))

View File

@ -23,7 +23,7 @@
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"echarts": "^5.5.0",
"element-plus": "^2.7.0",
"element-plus": "^2.8.1",
"lodash-es": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "^5.0.7",

View File

@ -10,4 +10,12 @@ export const login = (data) => {
export const getUserInfo = () => {
return axios.get('/user/getUserInfo')
}
}
/** 获取密码强度 */
export const getPasswordStrength = (password) => {
return axios.get('/auth/register/password/strength', {
params: {
password
}
})
}

View File

@ -3,7 +3,6 @@ import { useQuestionInfo } from './useQuestionInfo'
import { useEditStore } from '../stores/edit'
import { storeToRefs } from 'pinia'
// 目标题的显示逻辑提示文案
export const useJumpLogicInfo = (field) => {
const editStore = useEditStore()

View File

@ -68,15 +68,17 @@ const updateLogicConf = () => {
}
const showLogicConf = showLogicEngine.value.toJson()
//
changeSchema({ key: 'logicConf', value: { showLogicConf } })
if(JSON.stringify(schema.logicConf.showLogicConf) !== JSON.stringify(showLogicConf)) {
//
changeSchema({ key: 'logicConf', value: { showLogicConf } })
}
return res
}
const jumpLogicConf = jumpLogicEngine.value.toJson()
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
if(JSON.stringify(schema.logicConf.jumpLogicConf) !== JSON.stringify(jumpLogicConf)){
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
}
return res
}

View File

@ -26,7 +26,7 @@ import Navbar from './components/ModuleNavbar.vue'
const editStore = useEditStore()
const { init, setSurveyId, initSessionId } = editStore
const { init, setSurveyId } = editStore
const router = useRouter()
const route = useRoute()

View File

@ -86,7 +86,7 @@ const onSave = async () => {
}
const seize = async () => {
const seizeRes: Record<string, any> = await seizeSession({ sessionId })
const seizeRes: Record<string, any> = await seizeSession({ sessionId:sessionId.value })
if (seizeRes.code === 200) {
location.reload();
} else {
@ -152,6 +152,7 @@ const handleSave = async () => {
}
if (res.code === 200) {
ElMessage.success('保存成功')
return res
} else if (res.code === 3006) {
ElMessageBox.alert('当前问卷已在其它页面开启编辑,点击“抢占”以获取保存权限。', '提示', {
confirmButtonText: '抢占',

View File

@ -74,6 +74,7 @@ const routes = [
background-color: $primary-color;
bottom: -16px;
left: 20px;
z-index: 99;
}
}

View File

@ -22,8 +22,6 @@ const tabSelected = ref<string>('0')
height: 100%;
box-shadow: none;
border: none;
display: flex;
flex-direction: column;
:deep(.el-tabs__nav) {
width: 100%;
}

View File

@ -102,8 +102,6 @@ watch(
width: 360px;
height: 100%;
border: none;
display: flex;
flex-direction: column;
.el-tabs__nav {
width: 100%;

View File

@ -80,6 +80,7 @@ import 'element-plus/theme-chalk/src/message.scss'
import { useEditStore } from '@/management/stores/edit'
import { cleanRichText } from '@/common/xss'
import { cleanRichTextWithMediaTag } from '@/common/xss'
export default {
name: 'OptionConfig',
@ -110,7 +111,7 @@ export default {
return mapData
},
textOptions() {
return this.curOptions.map((item) => item.text)
return this.curOptions.map((item) => cleanRichTextWithMediaTag(item.text))
}
},
components: {

View File

@ -23,18 +23,20 @@ export default {
placement: 'top'
},
limit_breakAnswer: {
key: 'baseConf.breakAnswer',
key: 'breakAnswer',
label: '允许断点续答',
tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
type: 'ELSwitch',
value: false
placement: 'top',
type: 'CustomedSwitch',
value: false,
},
limit_backAnswer: {
key: 'baseConf.backAnswer',
label: '自动填充上次填写内容',
key: 'backAnswer',
label: '自动填充上次提交内容',
tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
type: 'ELSwitch',
value: false
placement: 'top',
type: 'CustomedSwitch',
value: false,
},
interview_pwd_switch: {
key: 'passwordSwitch',

View File

@ -27,10 +27,19 @@
<el-input type="password" v-model="formData.password" size="large"></el-input>
</el-form-item>
<el-form-item label="" v-if="passwordStrength">
<span
class="strength"
v-for="item in 3"
:key="item"
:style="{ backgroundColor: strengthColor[item - 1][passwordStrength] }"
></span>
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<div class="captcha-wrapper">
<el-input style="width: 150px" v-model="formData.captcha" size="large"></el-input>
<div class="captcha-img" @click="refreshCaptcha" v-html="captchaImgData"></div>
<el-input style="width: 280px" v-model="formData.captcha" size="large"></el-input>
<div class="captcha-img" click="refreshCaptcha" v-html="captchaImgData"></div>
</div>
</el-form-item>
@ -62,7 +71,9 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { login, register } from '@/management/api/auth'
import { debounce as _debounce } from 'lodash-es'
import { getPasswordStrength, login, register } from '@/management/api/auth'
import { refreshCaptcha as refreshCaptchaApi } from '@/management/api/captcha'
import { CODE_MAP } from '@/management/api/base'
import { useUserStore } from '@/management/stores/user'
@ -89,6 +100,55 @@ const formData = reactive<FormData>({
captchaId: ''
})
// 0
const strengthColor = reactive([
{
Strong: '#67C23A',
Medium: '#ebb563',
Weak: '#f78989'
},
{
Strong: '#67C23A',
Medium: '#ebb563',
Weak: '#2a598a'
},
{
Strong: '#67C23A',
Medium: '#2a598a',
Weak: '#2a598a'
}
])
//
const passwordValidator = (_: any, value: any, callback: any) => {
if (!value) {
callback(new Error('请输入密码'))
passwordStrength.value = undefined
return
}
if (value.length < 6 || value.length > 16) {
callback(new Error('长度在 6 到 16 个字符'))
passwordStrength.value = undefined
return
}
if (!/^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/.test(value)) {
callback(new Error('只能输入数字、字母、特殊字符'))
passwordStrength.value = undefined
return
}
passwordStrengthHandle(value)
callback()
}
const passwordStrengthHandle = async (value: string) => {
const res: any = await getPasswordStrength(value)
if (res.code === CODE_MAP.SUCCESS) {
passwordStrength.value = res.data
}
}
const rules = {
name: [
{ required: true, message: '请输入账号', trigger: 'blur' },
@ -100,12 +160,9 @@ const rules = {
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
min: 8,
max: 16,
message: '长度在 8 到 16 个字符',
trigger: 'blur'
validator: _debounce(passwordValidator, 500),
trigger: 'change'
}
],
captcha: [
@ -128,6 +185,7 @@ const pending = reactive<Pending>({
const captchaImgData = ref<string>('')
const formDataRef = ref<any>(null)
const passwordStrength = ref<'Strong' | 'Medium' | 'Weak'>()
const submitForm = (type: 'login' | 'register') => {
formDataRef.value.validate(async (valid: boolean) => {
@ -220,13 +278,14 @@ const refreshCaptcha = async () => {
background: #fff;
box-shadow: 4px 0 20px 0 rgba(82, 82, 102, 0.15);
margin-top: -150px;
width: 580px;
.button-group {
margin-top: 40px;
}
.button {
width: 160px;
width: 200px;
height: 40px;
font-size: 14px;
}
@ -255,8 +314,21 @@ const refreshCaptcha = async () => {
cursor: pointer;
:deep(> svg) {
max-height: 40px;
width: 120px;
margin-left: 20px;
}
}
}
.strength {
display: inline-block;
width: 30%;
height: 6px;
border-radius: 8px;
background: red;
&:not(:first-child) {
margin-left: 8px;
}
}
}
</style>

View File

@ -1,8 +1,9 @@
@font-face {
font-family: 'iconfont'; /* Project id 4263849 */
src: url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff2?t=1723600417360') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff?t=1723600417360') format('woff'),
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.ttf?t=1723600417360') format('truetype');
font-family: 'iconfont'; /* Project id 4263849 */
src:
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff2?t=1723600417360') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff?t=1723600417360') format('woff'),
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.ttf?t=1723600417360') format('truetype');
}
.iconfont {

View File

@ -101,7 +101,7 @@ export default defineComponent({
return (
<div class="choice-wrapper">
<div class={[isMatrix ? 'nest-box' : '', 'choice-box']}>
<div class={[isMatrix ? 'nest-box' : '', 'choice-box', this.layout || 'vertical']}>
{getOptions.map((item, index) => {
return (
!item.hide && (
@ -110,11 +110,10 @@ export default defineComponent({
style={this.choiceStyle}
class={['choice-outer']}
>
<div style="position: relative">
<div style="position: relative" class="choice-content">
{!/^\s*$/.test(item.text) && (
<div
class={[
this.layout === 'vertical' ? 'vertical' : '',
isChecked(item) ? 'is-checked' : '',
index === getOptions.length - 1 ? 'lastchild' : '',
index === getOptions.length - 2 ? 'last2child' : '',

View File

@ -25,11 +25,41 @@
padding: 0 0.2rem !important;
box-sizing: border-box;
// .choice-content, .choice-item {
// width: 100%;
// }
&.vertical {
display: flex;
flex-direction: column;
.choice-outer {
width: 100%;
}
}
&.horizontal {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
.choice-outer {
width: 50%;
.question {
padding-top: 0;
}
}
}
.choice-item {
position: relative;
display: inline-flex;
// align-items: center;
width: 50%;
align-items: center;
width: 100%;
box-sizing: border-box;
vertical-align: top;
// height: .88rem;
@ -98,7 +128,6 @@
.qicon.qicon-gouxuan {
display: inline-block;
font-size: 0.32rem;
line-height: 0.32rem;
border-color: $primary-color;
background-color: $primary-color;
color: #fff;

View File

@ -71,9 +71,37 @@ const meta = {
hash: '115020'
}
]
},
{
name: 'layout',
propType: String,
description: '排列方式',
defaultValue: 'vertical'
}
],
formConfig: [basicConfig],
formConfig: [basicConfig, {
name: 'optionConfig',
title: '选项配置',
type: 'Customed',
content: [
{
label: '排列方式',
type: 'RadioGroup',
key: 'layout',
value: 'vertical',
options: [
{
label: '竖排',
value: 'vertical'
},
{
label: '横排',
value: 'horizontal'
},
]
},
]
}],
editConfigure: {
optionEdit: {
show: false

View File

@ -1,4 +1,4 @@
import { computed, defineComponent, shallowRef, defineAsyncComponent } from 'vue'
import { computed, defineComponent, shallowRef, defineAsyncComponent, watch } from 'vue'
import { includes } from 'lodash-es'
import BaseChoice from '../BaseChoice'
@ -49,6 +49,7 @@ export default defineComponent({
},
emits: ['change'],
setup(props, { emit }) {
const disableState = computed(() => {
if (!props.maxNum) {
return false
@ -57,7 +58,7 @@ export default defineComponent({
})
const isDisabled = (item) => {
const { value } = props
return disableState.value && !includes(value, item.value)
return disableState.value && !includes(value, item.hash)
}
const myOptions = computed(() => {
const { options } = props
@ -68,6 +69,20 @@ export default defineComponent({
}
})
})
// 0
watch(() => props.value, (value) => {
const disabledHash = myOptions.value.filter(i => i.disabled).map(i => i.hash)
if (value && disabledHash.length) {
disabledHash.forEach(hash => {
const index = value.indexOf(hash)
if( index> -1) {
const newValue = [...value]
newValue.splice(index, 1)
onChange(newValue)
}
})
}
})
const onChange = (value) => {
const key = props.field
emit('change', {
@ -96,6 +111,7 @@ export default defineComponent({
return {
onChange,
handleSelectMoreChange,
disableState,
myOptions,
selectMoreView
}
@ -111,6 +127,7 @@ export default defineComponent({
options={myOptions}
onChange={onChange}
value={value}
layout={this.layout}
quotaNoDisplay={quotaNoDisplay}
>
{{

View File

@ -1,5 +1,4 @@
import basicConfig from '@materials/questions/common/config/basicConfig'
const meta = {
title: '多选',
type: 'checkbox',
@ -83,6 +82,12 @@ const meta = {
propType: Number,
description: '最多选择数',
defaultValue: 0
},
{
name: 'layout',
propType: String,
description: '排列方式',
defaultValue: 'vertical'
}
],
formConfig: [
@ -92,21 +97,38 @@ const meta = {
title: '选项配置',
type: 'Customed',
content: [
{
label: '排列方式',
type: 'RadioGroup',
key: 'layout',
value: 'vertical',
options: [
{
label: '竖排',
value: 'vertical'
},
{
label: '横排',
value: 'horizontal'
},
]
},
{
label: '至少选择数',
type: 'InputNumber',
key: 'minNum',
value: '',
value: 0,
min: 0,
max: 'maxNum',
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
contentClass: 'input-number-config'
},
{
label: '最多选择数',
type: 'InputNumber',
key: 'maxNum',
value: '',
min: 'minNum',
value: 0,
min: moduleConfig => { return moduleConfig?.minNum || 0 },
max: moduleConfig => { return moduleConfig?.options?.length },
contentClass: 'input-number-config'
},
]

View File

@ -15,7 +15,7 @@
</template>
<script setup>
import { ref, inject } from 'vue'
import { inject } from 'vue'
import ExtraIcon from '../ExtraIcon/index.vue'
defineProps({
@ -34,14 +34,8 @@ defineProps({
})
const emit = defineEmits(['addOther', 'optionChange', 'change'])
const moduleConfig = inject('moduleConfig')
const slots = inject('slots')
const optionConfigVisible = ref(false)
const openOptionConfig = () => {
optionConfigVisible.value = true
}
const addOther = () => {
emit('addOther')
}

View File

@ -6,22 +6,13 @@ import GetHash from '@materials/questions/common/utils/getOptionHash'
function useOptionBase(options) {
const optionList = ref(options)
const addOption = (text = '选项', others = false, index = -1, field) => {
// const {} = payload
let addOne
if (optionList.value[0]) {
addOne = cloneDeep(optionList.value[0])
} else {
addOne = {
text: '',
hash: '',
imageUrl: '',
others: false,
mustOthers: false,
othersKey: '',
placeholderDesc: '',
score: 0,
limit: ''
}
let addOne = {
text: '',
hash: '',
others: false,
mustOthers: false,
othersKey: '',
placeholderDesc: '',
}
if (typeof text !== 'string') {
text = '选项'

View File

@ -1,4 +1,4 @@
import { defineComponent, shallowRef, defineAsyncComponent } from 'vue'
import { defineComponent, shallowRef, watch, defineAsyncComponent } from 'vue'
import BaseChoice from '../BaseChoice'
/**
@ -39,6 +39,20 @@ export default defineComponent({
},
emits: ['change'],
setup(props, { emit }) {
// 0
watch(() => props.value, (value) => {
const disabledHash = props.options.filter(i => i.disabled).map(i => i.hash)
if (value && disabledHash.length) {
disabledHash.forEach(hash => {
const index = value.indexOf(hash)
if( index> -1) {
const newValue = [...value]
newValue.splice(index, 1)
onChange(newValue)
}
})
}
})
const onChange = (value) => {
const key = props.field
emit('change', {

View File

@ -72,6 +72,12 @@ const meta = {
}
]
},
{
name: 'layout',
propType: String,
description: '排列方式',
defaultValue: 'vertical'
},
{
name: 'quotaNoDisplay',
propType: Boolean,
@ -79,39 +85,47 @@ const meta = {
defaultValue: false
}
],
formConfig: [
basicConfig,
{
name: 'optionsExtra',
label: '固定选项配置',
labelStyle: {
'font-weight': 'bold'
formConfig: [basicConfig, {
name: 'optionConfig',
title: '选项配置',
type: 'Customed',
content: [
{
label: '排列方式',
type: 'RadioGroup',
key: 'layout',
value: 'vertical',
options: [
{
label: '竖排',
value: 'vertical'
},
{
label: '横排',
value: 'horizontal'
},
]
},
type: 'Options',
options: [],
keys: 'extraOptions',
hidden: true
]
},{
name: 'optionQuota',
label: '选项配额',
labelStyle: {
'font-weight': 'bold'
},
{
name: 'optionQuota',
label: '选项配额',
labelStyle: {
'font-weight': 'bold'
type: 'QuotaConfig',
// 输出转换
valueSetter({ options, quotaNoDisplay}) {
return [{
key: 'options',
value: options
},
type: 'QuotaConfig',
// 输出转换
valueSetter({ options, quotaNoDisplay}) {
return [{
key: 'options',
value: options
},
{
key: 'quotaNoDisplay',
value: quotaNoDisplay
}]
}
{
key: 'quotaNoDisplay',
value: quotaNoDisplay
}]
}
],
}],
editConfigure: {
optionEdit: {
show: true

View File

@ -103,7 +103,6 @@ export default defineComponent({
return (
<BaseChoice
uiTarget={innerType}
layout={'vertical'}
name={this.field}
innerType={this.innerType}
value={this.value}

View File

@ -120,7 +120,7 @@ const meta = {
key: 'minNum',
value: '',
min: 0,
max: 'maxNum',
max: moduleConfig => { return moduleConfig?.maxNum || 0 },
contentClass: 'input-number-config'
},
{
@ -128,7 +128,8 @@ const meta = {
type: 'InputNumber',
key: 'maxNum',
value: '',
min: 'minNum',
min: moduleConfig => { return moduleConfig?.minNum || 0 },
max: moduleConfig => { return moduleConfig?.options?.length || 0 },
contentClass: 'input-number-config'
}
]

View File

@ -2,7 +2,7 @@
<el-switch v-model="newValue" @change="changeData" />
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
const props = defineProps({
@ -23,4 +23,15 @@ const changeData = (value) => {
value
})
}
watch(
() => props.formConfig.value,
(newVal) => {
if (newVal !== newValue.value) {
newValue.value = newVal
}
},
{
immediate: true
}
)
</script>

View File

@ -1,37 +0,0 @@
<template>
<el-switch
v-model="modelValue"
class="ml-2"
active-text="是"
inactive-text="否"
@change="handleInputChange"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
interface Props {
formConfig: any
moduleConfig: any
}
interface Emit {
(ev: typeof FORM_CHANGE_EVENT_KEY, arg: { key: string; value: boolean }): void
}
const emit = defineEmits<Emit>()
const props = defineProps<Props>()
const modelValue = ref(props.formConfig.value)
const handleInputChange = (value: boolean) => {
const key = props.formConfig.key
modelValue.value = value
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
</script>

View File

@ -13,6 +13,7 @@ import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
interface Props {
formConfig: any
moduleConfig: any
@ -24,12 +25,15 @@ interface Emit {
const emit = defineEmits<Emit>()
const props = defineProps<Props>()
const modelValue = ref(Number(props.formConfig.value) || 0)
const modelValue = ref(Number(props.formConfig.value))
const myModuleConfig = ref(props.moduleConfig)
const minModelValue = computed(() => {
const { min } = props.formConfig
if (min) {
if (min !== undefined) {
if (typeof min === 'function') {
return min(props.moduleConfig)
return min(myModuleConfig.value)
} else {
return Number(min)
}
@ -38,16 +42,13 @@ const minModelValue = computed(() => {
})
const maxModelValue = computed(() => {
const { max, min } = props.formConfig
const { max } = props.formConfig
if (max) {
if (typeof max === 'function') {
return max(props.moduleConfig)
return max(myModuleConfig.value)
} else {
return Number(max)
}
} else if (min !== undefined && Array.isArray(props.moduleConfig?.options)) {
return props.moduleConfig.options.length
} else {
return Infinity
}
@ -65,6 +66,9 @@ const handleInputChange = (value: number) => {
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
watch(() => props.moduleConfig, (newVal) => {
myModuleConfig.value = newVal
})
watch(
() => props.formConfig.value,
(newVal) => {

View File

@ -12,8 +12,12 @@
style="width: 100%"
@cell-click="handleCellClick"
>
<el-table-column property="text" label="选项" style="width: 50%"></el-table-column>
<el-table-column property="quota" style="width: 50%">
<el-table-column property="text" label="选项" style="width: 50%">
<template v-slot="scope">
<div v-html="cleanRichTextWithMediaTag(scope.row.text)"></div>
</template>
</el-table-column>
<el-table-column property="quota" style="width: 50%;">
<template #header>
<div style="display: flex; align-items: center">
<span>配额设置</span>
@ -30,6 +34,7 @@
<template v-slot="scope">
<el-input
v-if="scope.row.isEditing"
:id="`${scope.row.hash}editInput`"
v-model="scope.row.tempQuota"
type="number"
@blur="handleInput(scope.row)"
@ -43,8 +48,7 @@
</template>
</el-table-column>
</el-table>
<div></div>
<div>
<div class="quota-no-display">
<el-checkbox v-model="quotaNoDisplayValue" label="不展示配额剩余数量"> </el-checkbox>
<el-tooltip
class="tooltip"
@ -56,7 +60,6 @@
</el-tooltip>
</div>
<el-divider />
<template #footer>
<div class="diaglog-footer">
<el-button @click="cancel">取消</el-button>
@ -68,9 +71,10 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, nextTick } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
import { ElMessageBox } from 'element-plus'
import { cleanRichTextWithMediaTag } from '@/common/xss'
const props = defineProps(['formConfig', 'moduleConfig'])
const emit = defineEmits(['form-change'])
@ -105,6 +109,10 @@ const handleCellClick = (row, column) => {
})
row.tempQuota = row.tempQuota === '0' ? row.quota : row.tempQuota
row.isEditing = true
nextTick(() => {
const input = document.getElementById(`${row.hash}editInput`)
input.focus()
})
}
}
const handleInput = (row) => {
@ -143,6 +151,12 @@ watch(
width: 90%;
display: flex;
justify-content: flex-end;
:deep(.cell){
line-height: 35px;
}
.quota-no-display{
padding-top: 8px
}
}
.quota-title {
font-size: 14px;
@ -155,6 +169,7 @@ watch(
color: #ffa600;
cursor: pointer;
font-size: 14px;
}
.dialog {
width: 41vw;

View File

@ -4,6 +4,7 @@
:moduleConfig="questionConfig"
:indexNumber="indexNumber"
:showTitle="true"
@input="handleInput"
@change="handleChange"
></QuestionRuleContainer>
</template>
@ -31,7 +32,7 @@ const props = defineProps({
default: () => {
return {}
}
},
}
})
const emit = defineEmits(['change'])
const questionStore = useQuestionStore()
@ -41,11 +42,7 @@ const formValues = computed(() => {
return surveyStore.formValues
})
const { showLogicEngine } = storeToRefs(surveyStore)
const {
changeField,
changeIndex,
needHideFields,
} = storeToRefs(questionStore)
const { changeField, changeIndex, needHideFields } = storeToRefs(questionStore)
//
const questionConfig = computed(() => {
let moduleConfig = props.moduleConfig
@ -66,7 +63,6 @@ const questionConfig = computed(() => {
let { options: optionWithQuota } = useOptionsQuota(field)
alloptions = alloptions.map((obj, index) => Object.assign(obj, optionWithQuota[index]))
console.log({alloptions})
}
if (
NORMAL_CHOICES.includes(type) &&
@ -104,7 +100,6 @@ const logicshow = computed(() => {
return result === undefined ? true : result
})
const logicskip = computed(() => {
return needHideFields.value.includes(props.moduleConfig.field)
})
@ -112,7 +107,6 @@ const visibily = computed(() => {
return logicshow.value && !logicskip.value
})
// abbcbc
watch(
() => visibily.value,
@ -120,7 +114,7 @@ watch(
const { field, type, innerType } = props.moduleConfig
if (!newVal && oldVal) {
//
if(formValues.value[field].toString()) {
if (formValues.value[field].toString()) {
let value = ''
// innerType
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
@ -152,45 +146,52 @@ const handleChange = (data) => {
processJumpSkip()
}
const handleInput = () => {
localStorageBack()
}
const processJumpSkip = () => {
const targetResult = surveyStore.jumpLogicEngine
.getResultsByField(changeField.value, surveyStore.formValues)
.map(item => {
//
const index = item.target === 'end' ? surveyStore.dataConf.dataList.length : questionStore.getQuestionIndexByField(item.target)
return {
index,
...item
}
})
const notMatchedFields = targetResult.filter(item => !item.result)
const matchedFields = targetResult.filter(item => item.result)
//
if (notMatchedFields.length) {
notMatchedFields.forEach(element => {
const endIndex = element.index
const fields = surveyStore.dataConf.dataList.slice(changeIndex.value + 1, endIndex).map(item => item.field)
// hideMapremove
questionStore.removeNeedHideFields(fields)
});
}
if (!matchedFields.length) return
//
const maxIndexQuestion =
matchedFields.filter(item => item.result).sort((a, b) => b.index - a.index)[0].index
//
const skipKey = surveyStore.dataConf.dataList
.slice(changeIndex.value + 1, maxIndexQuestion).map(item => item.field)
questionStore.addNeedHideFields(skipKey)
const targetResult = surveyStore.jumpLogicEngine
.getResultsByField(changeField.value, surveyStore.formValues)
.map((item) => {
//
const index =
item.target === 'end'
? surveyStore.dataConf.dataList.length
: questionStore.getQuestionIndexByField(item.target)
return {
index,
...item
}
})
const notMatchedFields = targetResult.filter((item) => !item.result)
const matchedFields = targetResult.filter((item) => item.result)
//
if (notMatchedFields.length) {
notMatchedFields.forEach((element) => {
const endIndex = element.index
const fields = surveyStore.dataConf.dataList
.slice(changeIndex.value + 1, endIndex)
.map((item) => item.field)
// hideMapremove
questionStore.removeNeedHideFields(fields)
})
}
if (!matchedFields.length) return
//
const maxIndexQuestion = matchedFields
.filter((item) => item.result)
.sort((a, b) => b.index - a.index)[0].index
//
const skipKey = surveyStore.dataConf.dataList
.slice(changeIndex.value + 1, maxIndexQuestion)
.map((item) => item.field)
questionStore.addNeedHideFields(skipKey)
}
const localStorageBack = () => {
var formData = Object.assign({}, surveyStore.formValues);
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
//
localStorage.removeItem(surveyStore.surveyPath + "_questionData")

View File

@ -0,0 +1,18 @@
import { useQuestionStore } from '@/render/stores/question'
import { cleanRichText } from '@/common/xss'
export const useQuestionInfo = (field: string) => {
const questionstore = useQuestionStore()
const questionTitle = cleanRichText(questionstore.questionData[field]?.title)
const getOptionTitle = (value:any) => {
const options = questionstore.questionData[field]?.options || []
if (value instanceof Array) {
return options
.filter((item:any) => value.includes(item.hash))
.map((item:any) => cleanRichText(item.text))
} else {
return options.filter((item:any) => item.hash === value).map((item:any) => cleanRichText(item.text))
}
}
return { questionTitle, getOptionTitle }
}

View File

@ -2,60 +2,10 @@
<router-view></router-view>
</template>
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { watch } from 'vue'
import { useRoute } from 'vue-router'
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
import useCommandComponent from '../hooks/useCommandComponent'
import { useSurveyStore } from '../stores/survey'
import AlertDialog from '../components/AlertDialog.vue'
const route = useRoute()
const surveyStore = useSurveyStore()
const loadData = (res: any, surveyPath: string) => {
if (res.code === 200) {
const data = res.data
const {
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
logicConf,
pageConf
} = data.code
const questionData = {
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
pageConf
}
if (!pageConf || pageConf?.length == 0) {
questionData.pageConf = [dataConf.dataList.length]
}
document.title = data.title
surveyStore.setSurveyPath(surveyPath)
surveyStore.initSurvey(questionData)
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
surveyStore.initJumpLogicEngine(logicConf.jumpLogicConf)
} else {
throw new Error(res.errmsg)
}
}
onMounted(() => {
const surveyId = route.params.surveyId
console.log({ surveyId })
surveyStore.setSurveyPath(surveyId)
getDetail(surveyId as string)
})
watch(
() => route.query.t,
@ -63,22 +13,4 @@ watch(
location.reload()
}
)
const getDetail = async (surveyPath: string) => {
const alert = useCommandComponent(AlertDialog)
try {
if (surveyPath.length > 8) {
const res: any = await getPreviewSchema({ surveyPath })
loadData(res, surveyPath)
} else {
const res: any = await getPublishedSurveyInfo({ surveyPath })
loadData(res, surveyPath)
surveyStore.getEncryptInfo()
}
} catch (error: any) {
console.log(error)
alert({ title: error.message || '获取问卷失败' })
}
}
</script>

View File

@ -21,9 +21,9 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
// @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js'
import MainRenderer from '../components/MainRenderer.vue'
@ -38,6 +38,8 @@ import { submitForm } from '../api/survey'
import encrypt from '../utils/encrypt'
import useCommandComponent from '../hooks/useCommandComponent'
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
import { useQuestionInfo } from '../hooks/useQuestionInfo'
interface Props {
questionInfo?: any
@ -70,6 +72,68 @@ const pageIndex = computed(() => questionStore.pageIndex)
const { bannerConf, submitConf, bottomConf: logoConf, whiteData } = storeToRefs(surveyStore)
const surveyPath = computed(() => surveyStore.surveyPath || '')
const route = useRoute()
onMounted(() => {
const surveyId = route.params.surveyId
console.log({ surveyId })
surveyStore.setSurveyPath(surveyId)
getDetail(surveyId as string)
})
const loadData = (res: any, surveyPath: string) => {
if (res.code === 200) {
const data = res.data
const {
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
logicConf,
pageConf
} = data.code
const questionData = {
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
pageConf
}
if (!pageConf || pageConf?.length == 0) {
questionData.pageConf = [dataConf.dataList.length]
}
document.title = data.title
surveyStore.setSurveyPath(surveyPath)
surveyStore.initSurvey(questionData)
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
surveyStore.initJumpLogicEngine(logicConf?.jumpLogicConf)
} else {
throw new Error(res.errmsg)
}
}
const getDetail = async (surveyPath: string) => {
const alert = useCommandComponent(AlertDialog)
try {
if (surveyPath.length > 8) {
const res: any = await getPreviewSchema({ surveyPath })
loadData(res, surveyPath)
} else {
const res: any = await getPublishedSurveyInfo({ surveyPath })
loadData(res, surveyPath)
surveyStore.getEncryptInfo()
}
} catch (error: any) {
console.log(error)
alert({ title: error.message || '获取问卷失败' })
}
}
const validate = (cbk: (v: boolean) => void) => {
const index = 0
mainRef.value.$refs.formGroup[index].validate(cbk)
@ -93,9 +157,7 @@ const normalizationRequestBody = () => {
localStorage.removeItem("isSubmit")
//
var formData : Record<string, any> = Object.assign({}, surveyStore.formValues)
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
localStorage.setItem(surveyPath.value + "_questionData", JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(true))
@ -125,13 +187,19 @@ const submitSurver = async () => {
const res: any = await submitForm(params)
if (res.code === 200) {
router.replace({ name: 'successPage' })
} else if(res.code === 9003) {
//
questionStore.initQuotaMap()
const titile = useQuestionInfo(res.data.field).questionTitle
const optionText = useQuestionInfo(res.data.field).getOptionTitle(res.data.optionHash)
const message = `${titile}】的【${optionText}】配额已满,请重新选择`
alert({
title: message
})
} else {
alert({
title: res.errmsg || '提交失败'
})
if (res.code === 9003) {
questionStore.initQuotaMap()
}
}
} catch (error) {
console.log(error)

View File

@ -286,14 +286,14 @@ export const useQuestionStore = defineStore('question', () => {
return questionData.value[field].index
}
const addNeedHideFields = (fields) => {
fields.forEach(field => {
if(!needHideFields.value.includes(field)) {
fields.forEach((field) => {
if (!needHideFields.value.includes(field)) {
needHideFields.value.push(field)
}
})
}
const removeNeedHideFields = (fields) => {
needHideFields.value = needHideFields.value.filter(field => !fields.includes(field))
needHideFields.value = needHideFields.value.filter((field) => !fields.includes(field))
}
return {

View File

@ -1,7 +1,7 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { defineStore } from 'pinia'
import { pick } from 'lodash-es'
import { cloneDeep, pick } from 'lodash-es'
import { isMobile as isInMobile } from '@/render/utils/index'
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
@ -47,7 +47,6 @@ export const useSurveyStore = defineStore('survey', () => {
const formValues = ref({})
const whiteData = ref({})
const pageConf = ref([])
const router = useRouter()
const questionStore = useQuestionStore()
@ -160,97 +159,54 @@ export const useSurveyStore = defineStore('survey', () => {
questionStore.initQuotaMap()
}
// 加载上次填写过的数据到问卷页
function loadFormData(params, formData) {
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf: params.bannerConf,
baseConf: params.baseConf,
bottomConf: params.bottomConf,
dataConf: params.dataConf,
skinConf: params.skinConf,
submitConf: params.submitConf,
})
function fillFormData(formData) {
const _formValues = cloneDeep(formValues.value)
for(const key in formData){
formValues[key] = formData[key]
_formValues[key] = formData[key]
}
// todo: 建议通过questionStore提供setqueationdata方法修改属性否则不好跟踪变化
questionStore.questionData = questionData
questionStore.questionSeq = questionSeq
// 将数据设置到state上
rules.value = rules
bannerConf.value = params.bannerConf
baseConf.value = params.baseConf
bottomConf.value = params.bottomConf
dataConf.value = params.dataConf
skinConf.value = params.skinConf
submitConf.value = params.submitConf
formValues.value = formValues
whiteData.value = params.whiteData
pageConf.value = params.pageConf
// 获取已投票数据
questionStore.initVoteData()
questionStore.initQuotaMap()
formValues.value = _formValues
}
const initSurvey = (option) => {
setEnterTime()
if (!canFillQuestionnaire(option.baseConf, option.submitConf)) {
return
}
// 加载空白问卷
clearFormData(option)
const { breakAnswer } = option.baseConf
const { breakAnswer, backAnswer } = option.baseConf
const localData = JSON.parse(localStorage.getItem(surveyPath.value + "_questionData"))
for(const key in localData){
localData[key] = decodeURIComponent(localData[key])
}
const isSubmit = JSON.parse(localStorage.getItem('isSubmit'))
if(localData) {
if(isSubmit){
if(!option.baseConf.backAnswer) {
clearFormData(option)
} else {
confirm({
title: "您之前已提交过问卷,是否要回填?",
onConfirm: async () => {
try {
loadFormData(option, localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async() => {
try {
clearFormData({ bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
// 断点续答
if(breakAnswer) {
confirm({
title: "是否继续上次填写的内容?",
onConfirm: async () => {
try {
// 回填答题内容
fillFormData(localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
})
}
} else {
if(!breakAnswer) {
clearFormData(option)
} else {
},
onCancel: async() => {
confirm.close()
}
})
} else if (backAnswer) {
if(isSubmit){
confirm({
title: "您之前已填写部分内容, 是否要继续填写?",
title: "是否继续上次提交的内容?",
onConfirm: async () => {
try {
loadFormData(option, localData)
// 回填答题内容
fillFormData(localData)
} catch (error) {
console.log(error)
} finally {
@ -258,13 +214,7 @@ export const useSurveyStore = defineStore('survey', () => {
}
},
onCancel: async() => {
try {
clearFormData(option)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
confirm.close()
}
})
}
@ -285,11 +235,11 @@ export const useSurveyStore = defineStore('survey', () => {
const showLogicEngine = ref()
const initShowLogicEngine = (showLogicConf) => {
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf)
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf || [])
}
const jumpLogicEngine = ref()
const initJumpLogicEngine = (jumpLogicConf) => {
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf)
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf || [])
}
return {

View File

@ -29,4 +29,4 @@ export const formatLink = (url) => {
return url
}
return `http://${url}`
}
}

View File

@ -67,7 +67,9 @@ export default defineConfig({
'yup',
'crypto-js/sha256',
'element-plus/es/locale/lang/zh-cn',
'node-forge'
'node-forge',
'@logicflow/core',
'@logicflow/extension'
]
},
plugins: [