【北大开源实践】-选项限制 (#284)

* format: 代码格式化 (#160)

* feat: 选项限制

* fix: 同步代码并解决冲突

* fix conflict

* fix conflict

* fix lint

* fix server lint

---------

Co-authored-by: dayou <853094838@qq.com>
Co-authored-by: XiaoYuan <2521510174@qq.com>
This commit is contained in:
yiyeah 2024-07-05 16:51:37 +08:00 committed by GitHub
parent 400aee9fda
commit b5bcb7ff7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 471 additions and 45 deletions

View File

@ -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

View File

@ -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",

View File

@ -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 {

View File

@ -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 {}

View File

@ -0,0 +1,28 @@
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<T>(callback: () => Promise<T>): Promise<T> {
// 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();
}
}
}

View File

@ -17,6 +17,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';
import { CreateSurveyDto } from '../dto/createSurvey.dto';
@ -42,6 +43,7 @@ export class SurveyController {
private readonly contentSecurityService: ContentSecurityService,
private readonly surveyHistoryService: SurveyHistoryService,
private readonly logger: Logger,
private readonly counterService: CounterService,
) {}
@Get('/getBannerData')
@ -302,6 +304,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,

View File

@ -22,13 +22,14 @@ import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Word } from 'src/models/word.entity';
import { Collaborator } from 'src/models/collaborator.entity';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { DataStatisticService } from './services/dataStatistic.service';
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 { CollaboratorService } from './services/collaborator.service';
import { Counter } from 'src/models/counter.entity';
import { CounterService } from '../surveyResponse/services/counter.service';
//后添加
import { SurveyDownload } from 'src/models/surveyDownload.entity';
import { SurveyDownloadService } from './services/surveyDownload.service';
@ -44,6 +45,7 @@ import { MessageService } from './services/message.service';
SurveyResponse,
Word,
Collaborator,
Counter,
//后添加
SurveyDownload,
]),
@ -71,6 +73,7 @@ import { MessageService } from './services/message.service';
ContentSecurityService,
CollaboratorService,
LoggerProvider,
CounterService,
//后添加
SurveyDownloadService,
MessageService,

View File

@ -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
}
]
}

View File

@ -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,6 +15,9 @@ 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';
import { Logger } from 'src/logger';
@ApiTags('surveyResponse')
@ -23,10 +25,11 @@ import { Logger } from 'src/logger';
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,
private readonly logger: Logger,
) {}
@ -155,39 +158,65 @@ 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 val = decryptedData[field];
const vals = Array.isArray(val) ? val : [val];
if (field in optionTextAndId) {
// 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能
const optionCountData: Record<string, any> =
(await this.counterService.get({
surveyPath,
//选项配额校验
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',
})) || { total: 0 };
optionCountData.total++;
for (const val of vals) {
if (!optionCountData[val]) {
optionCountData[val] = 1;
} else {
optionCountData[val]++;
});
//遍历选项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,
);
}
}
}
this.counterService.set({
surveyPath,
key: field,
data: optionCountData,
type: 'option',
});
}
}
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']++;
}
}
});
// 入库
const surveyResponse =

View File

@ -65,4 +65,25 @@ 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) => {
const data = {};
option.options.forEach((option) => {
data[option.hash] = 0;
});
data['total'] = 0;
this.set({
surveyPath,
key: option.field,
type: 'option',
data: data,
});
});
}
}

View File

@ -1,7 +1,4 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { MessageModule } from '../message/message.module';
import { ResponseSchemaService } from './services/responseScheme.service';
@ -21,6 +18,10 @@ import { ResponseSchemaController } from './controllers/responseSchema.controlle
import { SurveyResponseController } from './controllers/surveyResponse.controller';
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { MutexModule } from '../mutex/mutex.module';
@Module({
imports: [
TypeOrmModule.forFeature([
@ -31,6 +32,7 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
]),
ConfigModule,
MessageModule,
MutexModule,
],
controllers: [
ClientEncryptController,

86
web/components.d.ts vendored Normal file
View File

@ -0,0 +1,86 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
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']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
IEpBottom: typeof import('~icons/ep/bottom')['default']
IEpCheck: typeof import('~icons/ep/check')['default']
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
IEpDelete: typeof import('~icons/ep/delete')['default']
IEpIphone: typeof import('~icons/ep/iphone')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpMinus: typeof import('~icons/ep/minus')['default']
IEpMonitor: typeof import('~icons/ep/monitor')['default']
IEpMore: typeof import('~icons/ep/more')['default']
IEpMonitor: typeof import('~icons/ep/monitor')['default']
IEpMore: typeof import('~icons/ep/more')['default']
IEpPlus: typeof import('~icons/ep/plus')['default']
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
IEpRank: typeof import('~icons/ep/rank')['default']
IEpRemove: typeof import('~icons/ep/remove')['default']
IEpSearch: typeof import('~icons/ep/search')['default']
IEpSort: typeof import('~icons/ep/sort')['default']
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
IEpTop: typeof import('~icons/ep/top')['default']
IEpView: typeof import('~icons/ep/view')['default']
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
IEpView: typeof import('~icons/ep/view')['default']
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -11,7 +11,7 @@
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
},
"dependencies": {
"@types/lodash-es": "^4.17.12",

View File

@ -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
}

View File

@ -47,6 +47,10 @@ export default defineComponent({
voteTotal: {
type: Number,
default: 10
},
noDisplay:{
type: Boolean,
default: true
}
},
emits: ['change'],
@ -99,6 +103,7 @@ export default defineComponent({
<div class="choice-wrapper">
<div class={[isMatrix ? 'nest-box' : '', 'choice-box']}>
{getOptions.map((item, index) => {
item.disabled = !this.readonly && item.quota !== "0" && (item.quota - item.voteCount) === 0
return (
!item.hide && (
<div
@ -142,8 +147,22 @@ export default defineComponent({
<span
v-html={filterXSS(item.text)}
class="item-title-text"
style="display: block; height: auto; padding: 9px 0"
></span>
style="display: block; height: auto; padding-top: 9px"
></span>
)}
{
//
!this.readonly && item.quota !== "0" && !this.noDisplay && (
<span
class="remaining-text"
style={{
display: 'block',
fontSize: 'smaller',
color: item.quota - item.voteCount === 0 ? '#EB505C' : '#92949D'
}}
>
剩余{item.quota - item.voteCount}
</span>
)}
{slots.vote?.({
option: item,

View File

@ -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;

View File

@ -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 (
<BaseChoice
uiTarget="checkbox"
@ -107,6 +111,7 @@ export default defineComponent({
options={myOptions}
onChange={onChange}
value={value}
noDisplay={noDisplay}
>
{{
selectMore: (scoped) => {

View File

@ -28,8 +28,13 @@ const meta = {
value: '',
min: 'minNum',
contentClass: 'input-number-config'
}
},
]
},
{
key: "quotaConfig",
name: "quotaConfig",
type: "QuotaConfig",
}
],
editConfigure: {

View File

@ -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) => {

View File

@ -36,6 +36,11 @@ const meta = {
options: [],
keys: 'extraOptions',
hidden: true
},
{
key: "quotaConfig",
name: "quotaConfig",
type: "QuotaConfig",
}
],
editConfigure: {

View File

@ -0,0 +1,190 @@
<template>
<div class="quota-wrapper">
<span class="quota-title">选项配额</span>
<span class="quota-config" @click="openQuotaConfig"> 设置> </span>
<el-dialog v-model="dialogTableVisible" @closed="cleanTempQuota" class="dialog">
<template #header>
<div class="dialog-title">选项配额</div>
</template>
<el-table
:header-cell-style="{ background: '#F6F7F9', color: '#6E707C' }"
:data="optionData"
border
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%">
<template #header>
<div style="display: flex; align-items: center">
<span>配额设置</span>
<el-tooltip
class="tooltip"
effect="dark"
placement="right"
content="类似商品库存表示最多可以被选择多少次0为无限制已发布问卷上限修改时数量不可减小。"
>
<i-ep-questionFilled class="icon-tip" />
</el-tooltip>
</div>
</template>
<template v-slot="scope">
<el-input
v-if="scope.row.isEditing"
v-model="scope.row.tempQuota"
type="number"
@blur="handleInput(scope.row)"
placeholder="请输入"
>
</el-input>
<div v-else class="item__txt">
<span v-if="scope.row.tempQuota !== '0'">{{ scope.row.tempQuota }}</span>
<span v-else style="color: #c8c9cd">请输入</span>
</div>
</template>
</el-table-column>
</el-table>
<div></div>
<div>
<el-checkbox v-model="deleteRecoverValue" label="删除后恢复选项配额"> </el-checkbox>
<el-tooltip
class="tooltip"
effect="dark"
placement="right"
content="勾选后,把收集到的数据项删除或者设置为无效回收即可恢复选项配额。"
>
<i-ep-questionFilled class="icon-tip" />
</el-tooltip>
</div>
<div>
<el-checkbox v-model="noDisplayValue" label="不展示配额剩余数量"> </el-checkbox>
<el-tooltip
class="tooltip"
effect="dark"
placement="right"
content="勾选后,将不对用户展示剩余配额数量。"
>
<i-ep-questionFilled class="icon-tip" />
</el-tooltip>
</div>
<el-divider />
<template #footer>
<div class="diaglog-footer">
<el-button @click="cancel">取消</el-button>
<el-button @click="confirm" type="primary">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
import { ElMessageBox } from 'element-plus'
const props = defineProps(['formConfig', 'moduleConfig'])
const emit = defineEmits(['form-change'])
const dialogTableVisible = ref(false)
const moduleConfig = ref(props.moduleConfig)
const optionData = ref(props.moduleConfig.options)
const deleteRecoverValue = ref(moduleConfig.value.deleteRecover)
const noDisplayValue = ref(moduleConfig.value.noDisplay)
const openQuotaConfig = () => {
optionData.value.forEach((item) => {
item.tempQuota = item.quota
})
dialogTableVisible.value = true
}
const cancel = () => {
dialogTableVisible.value = false
}
const confirm = () => {
handleDeleteRecoverChange()
handleNoDisplayChange()
handleQuotaChange()
dialogTableVisible.value = false
}
const handleCellClick = (row, column) => {
if (column.property === 'quota') {
optionData.value.forEach((r) => {
if (r !== row) r.isEditing = false
})
row.tempQuota = row.tempQuota === '0' ? row.quota : row.tempQuota
row.isEditing = true
}
}
const handleInput = (row) => {
if (row.tempQuota !== '0' && +row.tempQuota < +row.quota) {
ElMessageBox.alert('配额数不可减少!', '警告', {
confirmButtonText: '确定'
})
row.tempQuota = row.quota
}
row.isEditing = false
}
const handleDeleteRecoverChange = () => {
const key = 'deleteRecover'
const value = deleteRecoverValue.value
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
const handleNoDisplayChange = () => {
const key = 'noDisplay'
const value = noDisplayValue.value
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
const handleQuotaChange = () => {
optionData.value.forEach((item) => {
item.quota = item.tempQuota
delete item.tempQuota
})
}
const cleanTempQuota = () => {
optionData.value.forEach((item) => {
delete item.tempQuota
})
}
watch(
() => props.moduleConfig,
(val) => {
moduleConfig.value = val
optionData.value = val.options
deleteRecoverValue.value = val.deleteRecover
noDisplayValue.value = val.noDisplay
},
{ immediate: true, deep: true }
)
</script>
<style lang="scss" scoped>
.quota-wrapper {
width: 100%;
display: flex;
justify-content: space-between;
}
.quota-title {
font-size: 14px;
color: #606266;
margin-bottom: 20px;
font-weight: bold;
align-items: center;
}
.quota-config {
color: #ffa600;
cursor: pointer;
font-size: 14px;
}
.dialog {
width: 41vw;
.dialog-title {
color: #292a36;
font-size: 20px;
}
}
</style>

View File

@ -41,7 +41,7 @@ const questionConfig = computed(() => {
const { type, field, options, ...rest } = cloneDeep(moduleConfig)
// console.log(field,'formValuechange')
let alloptions = options
if (type === QUESTION_TYPE.VOTE) {
if (type === QUESTION_TYPE.VOTE || NORMAL_CHOICES.includes(type)) {
const { options, voteTotal } = useVoteMap(field)
const voteOptions = unref(options)
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))

View File

@ -143,7 +143,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)
}
}