From 8dd539e06ab3914b0a31519f81e942cdd1de69bb Mon Sep 17 00:00:00 2001 From: dayou <853094838@qq.com> Date: Mon, 26 Aug 2024 17:45:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=96=AD=E7=82=B9?= =?UTF-8?q?=E7=BB=AD=E7=AD=94=E4=BB=A5=E5=8F=8A=E6=A0=B7=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 更新填写的过程中配额减少情况 --- .gitignore | 3 +- server/.env | 2 +- server/package.json | 3 +- .../template/surveyTemplate/survey/vote.json | 4 +- .../controllers/surveyResponse.controller.ts | 3 +- server/src/utils/xss.ts | 53 ++++++++ .../pages/edit/components/ModuleNavbar.vue | 14 ++- web/src/management/pages/edit/index.vue | 2 +- .../edit/modules/contentModule/SavePanel.vue | 3 +- .../edit/modules/generalModule/NavPanel.vue | 1 + .../modules/questionModule/CatalogPanel.vue | 2 - .../modules/questionModule/SetterPanel.vue | 2 - .../AdvancedConfig/OptionConfig.vue | 3 +- .../pages/edit/setterConfig/baseFormConfig.js | 16 +-- .../questions/widgets/BaseChoice/style.scss | 1 - .../widgets/CheckboxModule/index.jsx | 20 ++- .../questions/widgets/CheckboxModule/meta.js | 9 +- .../EditOptions/Options/UseOptionBase.jsx | 23 ++-- .../questions/widgets/RadioModule/index.jsx | 16 ++- .../questions/widgets/VoteModule/meta.js | 5 +- .../setters/widgets/CustomedSwitch.vue | 13 +- .../materials/setters/widgets/ELSwitch.vue | 37 ------ .../materials/setters/widgets/InputNumber.vue | 20 +-- .../materials/setters/widgets/QuotaConfig.vue | 27 +++- web/src/render/components/QuestionWrapper.vue | 9 +- web/src/render/hooks/useQuestionInfo.ts | 18 +++ web/src/render/pages/IndexPage.vue | 70 +---------- web/src/render/pages/RenderPage.vue | 84 +++++++++++-- web/src/render/stores/survey.js | 117 +++++------------- 29 files changed, 312 insertions(+), 268 deletions(-) create mode 100644 server/src/utils/xss.ts delete mode 100644 web/src/materials/setters/widgets/ELSwitch.vue create mode 100644 web/src/render/hooks/useQuestionInfo.ts 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/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/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/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/materials/questions/widgets/BaseChoice/style.scss b/web/src/materials/questions/widgets/BaseChoice/style.scss index d54a0339..91b30193 100644 --- a/web/src/materials/questions/widgets/BaseChoice/style.scss +++ b/web/src/materials/questions/widgets/BaseChoice/style.scss @@ -98,7 +98,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; diff --git a/web/src/materials/questions/widgets/CheckboxModule/index.jsx b/web/src/materials/questions/widgets/CheckboxModule/index.jsx index b197a3f5..35cdfbb5 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/index.jsx +++ b/web/src/materials/questions/widgets/CheckboxModule/index.jsx @@ -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 } diff --git a/web/src/materials/questions/widgets/CheckboxModule/meta.js b/web/src/materials/questions/widgets/CheckboxModule/meta.js index 2af526de..90792a91 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/meta.js +++ b/web/src/materials/questions/widgets/CheckboxModule/meta.js @@ -96,17 +96,18 @@ const meta = { label: '至少选择数', type: 'InputNumber', key: 'minNum', - value: '', + value: 0, min: 0, - max: 'maxNum', + max: moduleConfig => { return moduleConfig?.maxNum || 0 }, contentClass: 'input-number-config' }, { label: '最多选择数', type: 'InputNumber', key: 'maxNum', - value: '', - min: 'minNum', + value: 0, + min: moduleConfig => { return moduleConfig?.minNum || 0 }, + max: moduleConfig => { return moduleConfig?.options?.length }, contentClass: 'input-number-config' }, ] diff --git a/web/src/materials/questions/widgets/EditOptions/Options/UseOptionBase.jsx b/web/src/materials/questions/widgets/EditOptions/Options/UseOptionBase.jsx index 02a644ef..3c3d7890 100644 --- a/web/src/materials/questions/widgets/EditOptions/Options/UseOptionBase.jsx +++ b/web/src/materials/questions/widgets/EditOptions/Options/UseOptionBase.jsx @@ -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 = '选项' diff --git a/web/src/materials/questions/widgets/RadioModule/index.jsx b/web/src/materials/questions/widgets/RadioModule/index.jsx index 008dc0c2..d47bf416 100644 --- a/web/src/materials/questions/widgets/RadioModule/index.jsx +++ b/web/src/materials/questions/widgets/RadioModule/index.jsx @@ -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', { diff --git a/web/src/materials/questions/widgets/VoteModule/meta.js b/web/src/materials/questions/widgets/VoteModule/meta.js index a4cf4df8..f649e490 100644 --- a/web/src/materials/questions/widgets/VoteModule/meta.js +++ b/web/src/materials/questions/widgets/VoteModule/meta.js @@ -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' } ] diff --git a/web/src/materials/setters/widgets/CustomedSwitch.vue b/web/src/materials/setters/widgets/CustomedSwitch.vue index bb4ad191..3a00b7d6 100644 --- a/web/src/materials/setters/widgets/CustomedSwitch.vue +++ b/web/src/materials/setters/widgets/CustomedSwitch.vue @@ -2,7 +2,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" > - - + + + +