From f16aeb4791bc89c07f26def69d6a9f551df55eb8 Mon Sep 17 00:00:00 2001 From: XiaoYuan <2521510174@qq.com> Date: Sun, 9 Jun 2024 14:14:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=89=E9=A1=B9=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.env | 2 +- server/package.json | 1 + server/src/interfaces/survey.ts | 3 + server/src/modules/mutex/mutex.module.ts | 9 + .../mutex/services/mutexService.service.ts | 25 +++ .../survey/controllers/survey.controller.ts | 7 + server/src/modules/survey/survey.module.ts | 4 + .../surveyTemplate/survey/normal.json | 10 +- .../controllers/counter.controller.ts | 2 + .../controllers/surveyResponse.controller.ts | 80 +++++--- .../services/counter.service.ts | 22 ++ .../surveyResponse/surveyResponse.module.ts | 2 + web/components.d.ts | 12 +- web/src/management/config/questionConfig.js | 10 +- .../questions/widgets/BaseChoice/index.jsx | 23 ++- .../questions/widgets/BaseChoice/style.scss | 4 +- .../widgets/CheckboxModule/index.jsx | 7 +- .../questions/widgets/CheckboxModule/meta.js | 7 +- .../questions/widgets/RadioModule/index.jsx | 5 + .../questions/widgets/RadioModule/meta.js | 5 + .../setters/widgets/CheckboxGroup.vue | 2 +- .../materials/setters/widgets/QuotaConfig.vue | 189 ++++++++++++++++++ web/src/render/components/QuestionWrapper.vue | 2 +- web/src/render/store/actions.js | 2 +- 24 files changed, 379 insertions(+), 56 deletions(-) create mode 100644 server/src/modules/mutex/mutex.module.ts create mode 100644 server/src/modules/mutex/services/mutexService.service.ts create mode 100644 web/src/materials/setters/widgets/QuotaConfig.vue diff --git a/server/.env b/server/.env index 3a0589b4..c039a348 100644 --- a/server/.env +++ b/server/.env @@ -9,4 +9,4 @@ XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret XIAOJU_SURVEY_JWT_EXPIRES_IN=8h -XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log +XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log \ No newline at end of file diff --git a/server/package.json b/server/package.json index 44164d74..ca225d79 100644 --- a/server/package.json +++ b/server/package.json @@ -27,6 +27,7 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.1", "ali-oss": "^6.20.0", + "async-mutex": "^0.5.0", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dotenv": "^16.3.2", diff --git a/server/src/interfaces/survey.ts b/server/src/interfaces/survey.ts index 30403597..86ddf2c1 100644 --- a/server/src/interfaces/survey.ts +++ b/server/src/interfaces/survey.ts @@ -60,6 +60,8 @@ export interface DataItem { rangeConfig?: any; starStyle?: string; innerType?: string; + deleteRecover?: boolean; + noDisplay?: boolean; } export interface Option { @@ -69,6 +71,7 @@ export interface Option { othersKey?: string; placeholderDesc: string; hash: string; + quota?: number; } export interface DataConf { diff --git a/server/src/modules/mutex/mutex.module.ts b/server/src/modules/mutex/mutex.module.ts new file mode 100644 index 00000000..d74c7da9 --- /dev/null +++ b/server/src/modules/mutex/mutex.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { MutexService } from './services/mutexService.service' + +@Global() +@Module({ + providers: [MutexService], + exports: [MutexService], +}) +export class MutexModule {} \ No newline at end of file diff --git a/server/src/modules/mutex/services/mutexService.service.ts b/server/src/modules/mutex/services/mutexService.service.ts new file mode 100644 index 00000000..4d809ebd --- /dev/null +++ b/server/src/modules/mutex/services/mutexService.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { Mutex } from 'async-mutex'; +import { HttpException } from 'src/exceptions/httpException'; +import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; + +@Injectable() +export class MutexService { + private mutex = new Mutex(); + + async runLocked(callback: () => Promise): Promise { + // acquire lock + const release = await this.mutex.acquire(); + try { + return await callback(); + } catch (error) { + if (error instanceof HttpException) { + throw new HttpException(error.message, EXCEPTION_CODE.RESPONSE_OVER_LIMIT); + } else { + throw error; + } + } finally { + release(); + } + } +} \ No newline at end of file diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index 7516cf5e..74d2ab0d 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -14,6 +14,7 @@ import { SurveyConfService } from '../services/surveyConf.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import { ContentSecurityService } from '../services/contentSecurity.service'; import { SurveyHistoryService } from '../services/surveyHistory.service'; +import { CounterService } from 'src/modules/surveyResponse/services/counter.service'; import BannerData from '../template/banner/index.json'; @@ -35,6 +36,7 @@ export class SurveyController { private readonly contentSecurityService: ContentSecurityService, private readonly surveyHistoryService: SurveyHistoryService, private readonly logger: Logger, + private readonly counterService: CounterService ) {} @Get('/getBannerData') @@ -253,6 +255,11 @@ export class SurveyController { pageId: surveyId, }); + await this.counterService.createCounters({ + surveyPath: surveyMeta.surveyPath, + dataList: surveyConf.code.dataConf.dataList + }) + await this.surveyHistoryService.addHistory({ surveyId, schema: surveyConf.code, diff --git a/server/src/modules/survey/survey.module.ts b/server/src/modules/survey/survey.module.ts index 58528219..a35eb6a3 100644 --- a/server/src/modules/survey/survey.module.ts +++ b/server/src/modules/survey/survey.module.ts @@ -25,6 +25,8 @@ import { SurveyConfService } from './services/surveyConf.service'; import { SurveyHistoryService } from './services/surveyHistory.service'; import { SurveyMetaService } from './services/surveyMeta.service'; import { ContentSecurityService } from './services/contentSecurity.service'; +import { Counter } from 'src/models/counter.entity'; +import { CounterService } from '../surveyResponse/services/counter.service'; @Module({ imports: [ @@ -34,6 +36,7 @@ import { ContentSecurityService } from './services/contentSecurity.service'; SurveyHistory, SurveyResponse, Word, + Counter ]), ConfigModule, SurveyResponseModule, @@ -54,6 +57,7 @@ import { ContentSecurityService } from './services/contentSecurity.service'; PluginManagerProvider, ContentSecurityService, LoggerProvider, + CounterService ], }) export class SurveyModule {} diff --git a/server/src/modules/survey/template/surveyTemplate/survey/normal.json b/server/src/modules/survey/template/surveyTemplate/survey/normal.json index 3808e775..c6e66906 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/normal.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/normal.json @@ -51,7 +51,8 @@ "mustOthers": false, "othersKey": "", "placeholderDesc": "", - "hash": "115019" + "hash": "115019", + "quota": "0" }, { "text": "选项2", @@ -60,7 +61,8 @@ "mustOthers": false, "othersKey": "", "placeholderDesc": "", - "hash": "115020" + "hash": "115020", + "quota": "0" } ], "importKey": "single", @@ -78,7 +80,9 @@ "placeholder": "500", "value": 500 } - } + }, + "deleteRecover": false, + "noDisplay": false } ] } diff --git a/server/src/modules/surveyResponse/controllers/counter.controller.ts b/server/src/modules/surveyResponse/controllers/counter.controller.ts index a795e62a..f0e1d9e9 100644 --- a/server/src/modules/surveyResponse/controllers/counter.controller.ts +++ b/server/src/modules/surveyResponse/controllers/counter.controller.ts @@ -3,6 +3,8 @@ import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { CounterService } from '../services/counter.service'; import { ApiTags } from '@nestjs/swagger'; +import { Authtication } from 'src/guards/authtication'; +import { UseGuards } from '@nestjs/common'; @ApiTags('surveyResponse') @Controller('/api/counter') diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 8c77584e..f4e70472 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -7,7 +7,6 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { getPushingData } from 'src/utils/messagePushing'; import { ResponseSchemaService } from '../services/responseScheme.service'; -import { CounterService } from '../services/counter.service'; import { SurveyResponseService } from '../services/surveyResponse.service'; import { ClientEncryptService } from '../services/clientEncrypt.service'; import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service'; @@ -16,16 +15,19 @@ import moment from 'moment'; import * as Joi from 'joi'; import * as forge from 'node-forge'; import { ApiTags } from '@nestjs/swagger'; +import { MutexService } from 'src/modules/mutex/services/mutexService.service'; +import { CounterService } from '../services/counter.service'; @ApiTags('surveyResponse') @Controller('/api/surveyResponse') export class SurveyResponseController { constructor( private readonly responseSchemaService: ResponseSchemaService, - private readonly counterService: CounterService, private readonly surveyResponseService: SurveyResponseService, private readonly clientEncryptService: ClientEncryptService, private readonly messagePushingTaskService: MessagePushingTaskService, + private readonly mutexService: MutexService, + private readonly counterService: CounterService, ) {} @Post('/createResponse') @@ -146,39 +148,59 @@ export class SurveyResponseController { const arr = cur.options.map((optionItem) => ({ hash: optionItem.hash, text: optionItem.text, + quota: optionItem.quota })); pre[cur.field] = arr; return pre; }, {}); - - // 对用户提交的数据进行遍历处理 - for (const field in decryptedData) { - const value = decryptedData[field]; - const values = Array.isArray(value) ? value : [value]; - if (field in optionTextAndId) { - // 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能 - const optionCountData: Record = - (await this.counterService.get({ - surveyPath, - key: field, - type: 'option', - })) || { total: 0 }; - optionCountData.total++; - for (const val of values) { - if (!optionCountData[val]) { - optionCountData[val] = 1; - } else { + + //选项配额校验 + await this.mutexService.runLocked(async () => { + for (const field in decryptedData) { + const value = decryptedData[field]; + const values = Array.isArray(value) ? value : [value]; + if (field in optionTextAndId) { + const optionCountData = await this.counterService.get({ + key: field, + surveyPath, + type: 'option' + }); + + //遍历选项hash值 + for (const val of values) { + const option = optionTextAndId[field].find(opt => opt["hash"] === val); + if (option["quota"] != 0 && option["quota"] <= optionCountData[val]) { + const item = dataList.find(item => item["field"] === field); + throw new HttpException(`${item['title']}中的${option['text']}所选人数已达到上限,请重新选择`, EXCEPTION_CODE.RESPONSE_OVER_LIMIT); + } + } + } + }; + + for (const field in decryptedData) { + const value = decryptedData[field]; + const values = Array.isArray(value) ? value : [value]; + if (field in optionTextAndId) { + const optionCountData = await this.counterService.get({ + key: field, + surveyPath, + type: 'option' + }); + for (const val of values) { optionCountData[val]++; + this.counterService.set({ + key: field, + surveyPath, + type: 'option', + data: optionCountData + }); } + optionCountData['total']++; } - this.counterService.set({ - surveyPath, - key: field, - data: optionCountData, - type: 'option', - }); - } - } + }; + + }) + // 入库 const surveyResponse = @@ -213,4 +235,4 @@ export class SurveyResponseController { msg: '提交成功', }; } -} +} \ No newline at end of file diff --git a/server/src/modules/surveyResponse/services/counter.service.ts b/server/src/modules/surveyResponse/services/counter.service.ts index 0b72ab82..b9e8eda1 100644 --- a/server/src/modules/surveyResponse/services/counter.service.ts +++ b/server/src/modules/surveyResponse/services/counter.service.ts @@ -65,4 +65,26 @@ export class CounterService { return pre; }, {}); } + + async createCounters({ surveyPath, dataList}) { + const optionList = dataList.filter((questionItem) => { + return ( + Array.isArray(questionItem.options) && + questionItem.options.length > 0 + ); + }); + optionList.forEach(option => { + let data = {}; + option.options.forEach(option => { + data[option.hash] = 0; + }); + data["total"] = 0; + this.set({ + surveyPath, + key: option.field, + type: 'option', + data: data + }); + }); + } } diff --git a/server/src/modules/surveyResponse/surveyResponse.module.ts b/server/src/modules/surveyResponse/surveyResponse.module.ts index 357644c6..4652c4f6 100644 --- a/server/src/modules/surveyResponse/surveyResponse.module.ts +++ b/server/src/modules/surveyResponse/surveyResponse.module.ts @@ -20,6 +20,7 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; +import { MutexModule } from '../mutex/mutex.module'; @Module({ imports: [ @@ -31,6 +32,7 @@ import { ConfigModule } from '@nestjs/config'; ]), ConfigModule, MessageModule, + MutexModule ], controllers: [ ClientEncryptController, diff --git a/web/components.d.ts b/web/components.d.ts index 8a826169..05a27791 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -11,10 +11,8 @@ declare module 'vue' { 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'] ElInput: typeof import('element-plus/es')['ElInput'] @@ -22,29 +20,21 @@ declare module 'vue' { ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] - ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] - ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] - 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'] IEpLoading: typeof import('~icons/ep/loading')['default'] - IEpMinus: typeof import('~icons/ep/minus')['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'] diff --git a/web/src/management/config/questionConfig.js b/web/src/management/config/questionConfig.js index b6964730..7aed7f64 100644 --- a/web/src/management/config/questionConfig.js +++ b/web/src/management/config/questionConfig.js @@ -41,13 +41,15 @@ export const defaultQuestionConfig = { text: '选项1', others: false, othersKey: '', - placeholderDesc: '' + placeholderDesc: '', + quota: '0' }, { text: '选项2', others: false, othersKey: '', - placeholderDesc: '' + placeholderDesc: '', + quota: '0' } ], star: 5, @@ -73,5 +75,7 @@ export const defaultQuestionConfig = { placeholder: '500', value: 500 } - } + }, + deleteRecover: false, + noDisplay: false } diff --git a/web/src/materials/questions/widgets/BaseChoice/index.jsx b/web/src/materials/questions/widgets/BaseChoice/index.jsx index efc6d270..107d2a7c 100644 --- a/web/src/materials/questions/widgets/BaseChoice/index.jsx +++ b/web/src/materials/questions/widgets/BaseChoice/index.jsx @@ -47,6 +47,10 @@ export default defineComponent({ voteTotal: { type: Number, default: 10 + }, + noDisplay:{ + type: Boolean, + default: true } }, emits: ['change'], @@ -96,6 +100,7 @@ export default defineComponent({
{getOptions.map((item, index) => { + item.disabled = !this.readonly && item.quota !== "0" && (item.quota - item.voteCount) === 0 return ( !item.hide && (
+ style="display: block; height: auto; padding-top: 9px" + > + )} + { + // 如果设置了配额并且展示配额 + !this.readonly && item.quota !== "0" && !this.noDisplay && ( + + 剩余{item.quota - item.voteCount} + )} {slots.vote?.({ option: item, diff --git a/web/src/materials/questions/widgets/BaseChoice/style.scss b/web/src/materials/questions/widgets/BaseChoice/style.scss index 291fb67a..6cf54a67 100644 --- a/web/src/materials/questions/widgets/BaseChoice/style.scss +++ b/web/src/materials/questions/widgets/BaseChoice/style.scss @@ -28,7 +28,7 @@ .choice-item { position: relative; display: inline-flex; - align-items: center; + // align-items: center; width: 50%; box-sizing: border-box; vertical-align: top; @@ -43,7 +43,7 @@ vertical-align: top; width: 0.32rem; height: 0.32rem; - margin: 0rem 0.24rem 0 0; + margin: 11px 0.24rem 0 0; border: 1px solid $border-color; border-radius: 2px; background-color: #fff; diff --git a/web/src/materials/questions/widgets/CheckboxModule/index.jsx b/web/src/materials/questions/widgets/CheckboxModule/index.jsx index b48c649c..90141539 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/index.jsx +++ b/web/src/materials/questions/widgets/CheckboxModule/index.jsx @@ -41,6 +41,10 @@ export default defineComponent({ maxNum: { type: [Number, String], default: 1 + }, + noDisplay:{ + type: Boolean, + default: false } }, emits: ['change'], @@ -97,7 +101,7 @@ export default defineComponent({ } }, render() { - const { readonly, field, myOptions, onChange, maxNum, value, selectMoreView } = this + const { readonly, field, myOptions, onChange, maxNum, value, noDisplay, selectMoreView } = this return ( {{ selectMore: (scoped) => { diff --git a/web/src/materials/questions/widgets/CheckboxModule/meta.js b/web/src/materials/questions/widgets/CheckboxModule/meta.js index d9148884..50357104 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/meta.js +++ b/web/src/materials/questions/widgets/CheckboxModule/meta.js @@ -28,8 +28,13 @@ const meta = { value: '', min: 'minNum', contentClass: 'input-number-config' - } + }, ] + }, + { + key: "quotaConfig", + name: "quotaConfig", + type: "QuotaConfig", } ], editConfigure: { diff --git a/web/src/materials/questions/widgets/RadioModule/index.jsx b/web/src/materials/questions/widgets/RadioModule/index.jsx index 6985478c..120fb2e0 100644 --- a/web/src/materials/questions/widgets/RadioModule/index.jsx +++ b/web/src/materials/questions/widgets/RadioModule/index.jsx @@ -31,6 +31,10 @@ export default defineComponent({ readonly: { type: Boolean, default: false + }, + noDisplay:{ + type: Boolean, + default: false } }, emits: ['change'], @@ -81,6 +85,7 @@ export default defineComponent({ field={this.field} layout={this.layout} onChange={this.onChange} + noDisplay={this.noDisplay} > {{ selectMore: (scoped) => { diff --git a/web/src/materials/questions/widgets/RadioModule/meta.js b/web/src/materials/questions/widgets/RadioModule/meta.js index 20e8c6a9..623f30a0 100644 --- a/web/src/materials/questions/widgets/RadioModule/meta.js +++ b/web/src/materials/questions/widgets/RadioModule/meta.js @@ -36,6 +36,11 @@ const meta = { options: [], keys: 'extraOptions', hidden: true + }, + { + key: "quotaConfig", + name: "quotaConfig", + type: "QuotaConfig", } ], editConfigure: { diff --git a/web/src/materials/setters/widgets/CheckboxGroup.vue b/web/src/materials/setters/widgets/CheckboxGroup.vue index d041efa7..3356ca34 100644 --- a/web/src/materials/setters/widgets/CheckboxGroup.vue +++ b/web/src/materials/setters/widgets/CheckboxGroup.vue @@ -41,7 +41,7 @@ const handleCheckboxChange = (key: string, value: boolean) => { const optionsValue = reactive(props.formConfig?.value) watch( - props.formConfig.value, + () => props.formConfig?.value, (val) => { const keys = Object.keys(val) diff --git a/web/src/materials/setters/widgets/QuotaConfig.vue b/web/src/materials/setters/widgets/QuotaConfig.vue new file mode 100644 index 00000000..916f6f42 --- /dev/null +++ b/web/src/materials/setters/widgets/QuotaConfig.vue @@ -0,0 +1,189 @@ + + + + + \ No newline at end of file diff --git a/web/src/render/components/QuestionWrapper.vue b/web/src/render/components/QuestionWrapper.vue index 97f568b0..f6dd295e 100644 --- a/web/src/render/components/QuestionWrapper.vue +++ b/web/src/render/components/QuestionWrapper.vue @@ -41,7 +41,7 @@ const questionConfig = computed(() => { const { type, field, options, ...rest } = cloneDeep(moduleConfig) // console.log(field,'这里依赖的formValue,所以change时会触发重新计算') let alloptions = options - if (type === QUESTION_TYPE.VOTE) { + if (type === QUESTION_TYPE.VOTE || QUESTION_TYPE.CHOICES.includes(type)) { const { options, voteTotal } = useVoteMap(field) const voteOptions = unref(options) alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index])) diff --git a/web/src/render/store/actions.js b/web/src/render/store/actions.js index 3438ebea..f8516b8d 100644 --- a/web/src/render/store/actions.js +++ b/web/src/render/store/actions.js @@ -94,7 +94,7 @@ export default { for (const field in questionData) { const { type } = questionData[field] - if (/vote/.test(type)) { + if (/vote/.test(type) || /radio/.test(type) || /checkbox/.test(type)) { fieldList.push(field) } }