【北大开源实践】-选项限制 (#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:
parent
400aee9fda
commit
b5bcb7ff7e
@ -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
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
9
server/src/modules/mutex/mutex.module.ts
Normal file
9
server/src/modules/mutex/mutex.module.ts
Normal 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 {}
|
28
server/src/modules/mutex/services/mutexService.service.ts
Normal file
28
server/src/modules/mutex/services/mutexService.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
86
web/components.d.ts
vendored
Normal 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']
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -28,8 +28,13 @@ const meta = {
|
||||
value: '',
|
||||
min: 'minNum',
|
||||
contentClass: 'input-number-config'
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "quotaConfig",
|
||||
name: "quotaConfig",
|
||||
type: "QuotaConfig",
|
||||
}
|
||||
],
|
||||
editConfigure: {
|
||||
|
@ -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) => {
|
||||
|
@ -36,6 +36,11 @@ const meta = {
|
||||
options: [],
|
||||
keys: 'extraOptions',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
key: "quotaConfig",
|
||||
name: "quotaConfig",
|
||||
type: "QuotaConfig",
|
||||
}
|
||||
],
|
||||
editConfigure: {
|
||||
|
190
web/src/materials/setters/widgets/QuotaConfig.vue
Normal file
190
web/src/materials/setters/widgets/QuotaConfig.vue
Normal 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>
|
@ -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 || NORMAL_CHOICES.includes(type)) {
|
||||
const { options, voteTotal } = useVoteMap(field)
|
||||
const voteOptions = unref(options)
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user