diff --git a/.gitignore b/.gitignore index 7c79d23c..e64c75db 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,9 @@ pnpm-debug.log* .history -exportfile components.d.ts # 默认的上传文件夹 userUpload +exportfile +yarn.lock \ No newline at end of file diff --git a/README.md b/README.md index 02093549..d475636b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@
-  **XIAOJUSURVEY**是一套轻量、安全的问卷系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。 +  **XIAOJUSURVEY**是一套轻量、安全的调研系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。   内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。 diff --git a/README_EN.md b/README_EN.md index 84902ad4..aa0624ba 100644 --- a/README_EN.md +++ b/README_EN.md @@ -29,7 +29,7 @@
-  XIAOJUSURVEY is an open-source form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online. +  XIAOJUSURVEY is an enterprises 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. diff --git a/server/.env b/server/.env index 8651c097..3e4e9e70 100644 --- a/server/.env +++ b/server/.env @@ -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 diff --git a/server/package.json b/server/package.json index 5222380a..fc06b434 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/enums/exceptionCode.ts b/server/src/enums/exceptionCode.ts index 192abb9d..8ead165e 100644 --- a/server/src/enums/exceptionCode.ts +++ b/server/src/enums/exceptionCode.ts @@ -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, // 问卷类型错误 diff --git a/server/src/modules/auth/__test/auth.controller.spec.ts b/server/src/modules/auth/__test/auth.controller.spec.ts index 9c705944..eb7e0ca7 100644 --- a/server/src/modules/auth/__test/auth.controller.spec.ts +++ b/server/src/modules/auth/__test/auth.controller.spec.ts @@ -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', () => { diff --git a/server/src/modules/auth/controllers/auth.controller.ts b/server/src/modules/auth/controllers/auth.controller.ts index 9d5d6d08..764e4d81 100644 --- a/server/src/modules/auth/controllers/auth.controller.ts +++ b/server/src/modules/auth/controllers/auth.controller.ts @@ -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', + }; + } } diff --git a/server/src/modules/survey/template/surveyTemplate/survey/vote.json b/server/src/modules/survey/template/surveyTemplate/survey/vote.json index f8bbc899..34de3afe 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/vote.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/vote.json @@ -41,8 +41,8 @@ "innerType": "radio", "field": "data606", "title": "标题2", - "minNum": "", - "maxNum": "", + "minNum": 0, + "maxNum": 0, "options": [ { "text": "选项1", diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index fc96e7b1..fb2bcf04 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -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, ); } diff --git a/server/src/utils/xss.ts b/server/src/utils/xss.ts new file mode 100644 index 00000000..7da9f2bf --- /dev/null +++ b/server/src/utils/xss.ts @@ -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) { + // 过滤为空,否则不过滤为空 + 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(//g, '[图片]') + .replace(//g, '[视频]') + const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '') + + return content +} + +export function escapeHtml(html) { + return html.replace(//g, '>') +} +export const transformHtmlTag = (html) => { + if (!html) return '' + if (typeof html !== 'string') return html + '' + return html + .replace(html ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\\\n/g, '\\n') + //.replace(/ /g, "") +} + +const filterXSSClone = myxss.process.bind(myxss) + +export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html)) + +export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html)) diff --git a/web/package.json b/web/package.json index 041059b5..d3275dea 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/management/api/auth.js b/web/src/management/api/auth.js index 8e277637..e9ae55c6 100644 --- a/web/src/management/api/auth.js +++ b/web/src/management/api/auth.js @@ -10,4 +10,12 @@ export const login = (data) => { export const getUserInfo = () => { return axios.get('/user/getUserInfo') -} \ No newline at end of file +} +/** 获取密码强度 */ +export const getPasswordStrength = (password) => { + return axios.get('/auth/register/password/strength', { + params: { + password + } + }) +} diff --git a/web/src/management/hooks/useJumpLogicInfo.js b/web/src/management/hooks/useJumpLogicInfo.js index 2cd906ff..0f02cf0c 100644 --- a/web/src/management/hooks/useJumpLogicInfo.js +++ b/web/src/management/hooks/useJumpLogicInfo.js @@ -3,7 +3,6 @@ import { useQuestionInfo } from './useQuestionInfo' import { useEditStore } from '../stores/edit' import { storeToRefs } from 'pinia' - // 目标题的显示逻辑提示文案 export const useJumpLogicInfo = (field) => { const editStore = useEditStore() diff --git a/web/src/management/pages/edit/components/ModuleNavbar.vue b/web/src/management/pages/edit/components/ModuleNavbar.vue index 96cb7065..bf98988e 100644 --- a/web/src/management/pages/edit/components/ModuleNavbar.vue +++ b/web/src/management/pages/edit/components/ModuleNavbar.vue @@ -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 } diff --git a/web/src/management/pages/edit/index.vue b/web/src/management/pages/edit/index.vue index 6fb1bb64..70808dd3 100644 --- a/web/src/management/pages/edit/index.vue +++ b/web/src/management/pages/edit/index.vue @@ -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() diff --git a/web/src/management/pages/edit/modules/contentModule/SavePanel.vue b/web/src/management/pages/edit/modules/contentModule/SavePanel.vue index a6caf462..334aaa97 100644 --- a/web/src/management/pages/edit/modules/contentModule/SavePanel.vue +++ b/web/src/management/pages/edit/modules/contentModule/SavePanel.vue @@ -86,7 +86,7 @@ const onSave = async () => { } const seize = async () => { - const seizeRes: Record = await seizeSession({ sessionId }) + const seizeRes: Record = 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: '抢占', diff --git a/web/src/management/pages/edit/modules/generalModule/NavPanel.vue b/web/src/management/pages/edit/modules/generalModule/NavPanel.vue index 82f33b90..b50553bc 100644 --- a/web/src/management/pages/edit/modules/generalModule/NavPanel.vue +++ b/web/src/management/pages/edit/modules/generalModule/NavPanel.vue @@ -74,6 +74,7 @@ const routes = [ background-color: $primary-color; bottom: -16px; left: 20px; + z-index: 99; } } diff --git a/web/src/management/pages/edit/modules/questionModule/CatalogPanel.vue b/web/src/management/pages/edit/modules/questionModule/CatalogPanel.vue index 65f794cb..4cd5a3ed 100644 --- a/web/src/management/pages/edit/modules/questionModule/CatalogPanel.vue +++ b/web/src/management/pages/edit/modules/questionModule/CatalogPanel.vue @@ -22,8 +22,6 @@ const tabSelected = ref('0') height: 100%; box-shadow: none; border: none; - display: flex; - flex-direction: column; :deep(.el-tabs__nav) { width: 100%; } diff --git a/web/src/management/pages/edit/modules/questionModule/SetterPanel.vue b/web/src/management/pages/edit/modules/questionModule/SetterPanel.vue index 0b3540b9..03c33fb9 100644 --- a/web/src/management/pages/edit/modules/questionModule/SetterPanel.vue +++ b/web/src/management/pages/edit/modules/questionModule/SetterPanel.vue @@ -102,8 +102,6 @@ watch( width: 360px; height: 100%; border: none; - display: flex; - flex-direction: column; .el-tabs__nav { width: 100%; diff --git a/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue b/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue index 4cbca449..e235f475 100644 --- a/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue +++ b/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue @@ -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: { diff --git a/web/src/management/pages/edit/setterConfig/baseFormConfig.js b/web/src/management/pages/edit/setterConfig/baseFormConfig.js index d6abfa39..0f4239c2 100644 --- a/web/src/management/pages/edit/setterConfig/baseFormConfig.js +++ b/web/src/management/pages/edit/setterConfig/baseFormConfig.js @@ -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', diff --git a/web/src/management/pages/login/LoginPage.vue b/web/src/management/pages/login/LoginPage.vue index 6564c9bf..b4057941 100644 --- a/web/src/management/pages/login/LoginPage.vue +++ b/web/src/management/pages/login/LoginPage.vue @@ -27,10 +27,19 @@ + + + +
- -
+ +
@@ -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({ 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({ const captchaImgData = ref('') const formDataRef = ref(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; + } + } } diff --git a/web/src/management/styles/icon.scss b/web/src/management/styles/icon.scss index f79122d3..aae3e7f6 100644 --- a/web/src/management/styles/icon.scss +++ b/web/src/management/styles/icon.scss @@ -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 { diff --git a/web/src/materials/questions/widgets/BaseChoice/index.jsx b/web/src/materials/questions/widgets/BaseChoice/index.jsx index 063a0cca..aa7b6809 100644 --- a/web/src/materials/questions/widgets/BaseChoice/index.jsx +++ b/web/src/materials/questions/widgets/BaseChoice/index.jsx @@ -101,7 +101,7 @@ export default defineComponent({ return (
-
+
{getOptions.map((item, index) => { return ( !item.hide && ( @@ -110,11 +110,10 @@ export default defineComponent({ style={this.choiceStyle} class={['choice-outer']} > -
+
{!/^\s*$/.test(item.text) && (
{ 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} > {{ diff --git a/web/src/materials/questions/widgets/CheckboxModule/meta.js b/web/src/materials/questions/widgets/CheckboxModule/meta.js index 2af526de..e5561e53 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/meta.js +++ b/web/src/materials/questions/widgets/CheckboxModule/meta.js @@ -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' }, ] diff --git a/web/src/materials/questions/widgets/EditOptions/Options/OptionEditBar.vue b/web/src/materials/questions/widgets/EditOptions/Options/OptionEditBar.vue index e9689c47..1e1405be 100644 --- a/web/src/materials/questions/widgets/EditOptions/Options/OptionEditBar.vue +++ b/web/src/materials/questions/widgets/EditOptions/Options/OptionEditBar.vue @@ -15,7 +15,7 @@ diff --git a/web/src/materials/setters/widgets/ELSwitch.vue b/web/src/materials/setters/widgets/ELSwitch.vue deleted file mode 100644 index 719e8135..00000000 --- a/web/src/materials/setters/widgets/ELSwitch.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/materials/setters/widgets/InputNumber.vue b/web/src/materials/setters/widgets/InputNumber.vue index b34e02f8..b93bb080 100644 --- a/web/src/materials/setters/widgets/InputNumber.vue +++ b/web/src/materials/setters/widgets/InputNumber.vue @@ -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() const props = defineProps() -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) => { diff --git a/web/src/materials/setters/widgets/QuotaConfig.vue b/web/src/materials/setters/widgets/QuotaConfig.vue index 86a54f54..e88cd39c 100644 --- a/web/src/materials/setters/widgets/QuotaConfig.vue +++ b/web/src/materials/setters/widgets/QuotaConfig.vue @@ -12,8 +12,12 @@ style="width: 100%" @cell-click="handleCellClick" > - - + + + +