feat: nps评分 (#79)
* feat: nps评分功能 * feat: nps样式添加 * feat: 添加nps评分icon * feat: 基于修改建议修改nps评分组件 * feat: 将自定义类移入至设置器
This commit is contained in:
parent
071afbfec1
commit
05b9416cd9
@ -10,6 +10,8 @@ import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { getListHeadByDataList } from '../utils';
|
||||
@Injectable()
|
||||
export class DataStatisticService {
|
||||
private radioType = ['radio-star', 'radio-nps'];
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SurveyResponse)
|
||||
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||
@ -66,7 +68,7 @@ export class DataStatisticService {
|
||||
}
|
||||
// 处理选项的更多输入框
|
||||
if (
|
||||
itemConfig.type === 'radio-star' &&
|
||||
this.radioType.includes(itemConfig.type) &&
|
||||
!data[`${itemConfigKey}_custom`]
|
||||
) {
|
||||
data[`${itemConfigKey}_custom`] =
|
||||
|
BIN
web/public/imgs/question-type-snapshot/radio-nps.webp
Normal file
BIN
web/public/imgs/question-type-snapshot/radio-nps.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -29,6 +29,10 @@ export const defaultQuestionConfig = {
|
||||
starStyle: 'star',
|
||||
starMin: 1,
|
||||
starMax: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
minMsg: '极不满意',
|
||||
maxMsg: '十分满意',
|
||||
// 分值对应的开关和输入框配置,因为不是固定从0开始,用对象比较合适
|
||||
rangeConfig: {},
|
||||
// 选项
|
||||
|
@ -41,6 +41,13 @@ const menuItems = {
|
||||
icon: 'tixing-pingfen',
|
||||
title: '评分',
|
||||
},
|
||||
'radio-nps': {
|
||||
type: 'radio-nps',
|
||||
path: 'NpsModule',
|
||||
snapshot: '/imgs/question-type-snapshot/radio-nps.webp',
|
||||
icon: 'NPSpingfen',
|
||||
title: 'nps评分',
|
||||
},
|
||||
vote: {
|
||||
type: 'vote',
|
||||
path: 'VoteModule',
|
||||
@ -57,7 +64,14 @@ const menuGroup = [
|
||||
},
|
||||
{
|
||||
title: '选择类题型',
|
||||
questionList: ['radio', 'checkbox', 'binary-choice', 'radio-star', 'vote'],
|
||||
questionList: [
|
||||
'radio',
|
||||
'checkbox',
|
||||
'binary-choice',
|
||||
'radio-star',
|
||||
'radio-nps',
|
||||
'vote',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
@form-change="onFormChange($event, item)"
|
||||
:inline="true"
|
||||
labelPosition="left"
|
||||
:class="item.contentClass"
|
||||
></SettersField>
|
||||
</template>
|
||||
<Component
|
||||
@ -32,7 +33,11 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script>
|
||||
import { get as _get, pick as _pick, isFunction as _isFunction } from 'lodash-es';
|
||||
import {
|
||||
get as _get,
|
||||
pick as _pick,
|
||||
isFunction as _isFunction,
|
||||
} from 'lodash-es';
|
||||
|
||||
import FormItem from '@/materials/setters/widgets/FormItem.vue';
|
||||
import setterLoader from '@/materials/setters/setterLoader';
|
||||
@ -170,4 +175,16 @@ export default {
|
||||
.config-form {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.nps-customed-config {
|
||||
.el-form-item {
|
||||
margin-right: 0px;
|
||||
::v-deep .el-form-item__label {
|
||||
width: 70px !important;
|
||||
margin-right: 8px;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
width: 234px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -6,6 +6,7 @@ export default {
|
||||
checkbox: 'CheckboxModule',
|
||||
'binary-choice': 'BinaryChoiceModule',
|
||||
'radio-star': 'StarModule',
|
||||
'radio-nps': 'NpsModule',
|
||||
city: 'CityModule',
|
||||
vote: 'VoteModule',
|
||||
'matrix-checkbox': 'GroupModule',
|
||||
|
@ -5,6 +5,7 @@ const config = {
|
||||
radio: '单选',
|
||||
checkbox: '多选',
|
||||
'radio-star': '评分',
|
||||
'radio-nps': 'NPS评分',
|
||||
city: '城市选择',
|
||||
vote: '投票',
|
||||
'binary-choice': '判断',
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import OptionConfig from '@/materials/questions/components/AdvancedConfig/OptionConfig.vue';
|
||||
import RateConfig from '../AdvancedConfig/RateConfig.vue';
|
||||
import { defineComponent, ref, inject } from 'vue';
|
||||
import { defineComponent, ref, computed, inject } from 'vue';
|
||||
import ExtraIcon from '@/materials/questions/components/ExtraIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@ -45,6 +45,28 @@ export default defineComponent({
|
||||
const openRateConfig = () => {
|
||||
rateConfigVisible.value = true;
|
||||
};
|
||||
|
||||
const isNps = computed(() => {
|
||||
return moduleConfig.value.type === 'radio-nps';
|
||||
});
|
||||
|
||||
const min = computed(() => {
|
||||
const { min, starMin } = moduleConfig.value;
|
||||
return isNps.value ? min : starMin;
|
||||
});
|
||||
|
||||
const max = computed(() => {
|
||||
const { max, starMax } = moduleConfig.value;
|
||||
return isNps.value ? max : starMax;
|
||||
});
|
||||
|
||||
const explain = computed(() => {
|
||||
const { type } = moduleConfig.value;
|
||||
if (type == 'radio-start') return true;
|
||||
if (isNps.value) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
addOther,
|
||||
optionConfigVisible,
|
||||
@ -54,10 +76,22 @@ export default defineComponent({
|
||||
handleChange,
|
||||
moduleConfig,
|
||||
rateConfigVisible,
|
||||
min,
|
||||
max,
|
||||
isNps,
|
||||
explain,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const { showOthers, hasAdvancedConfig, hasAdvancedRateConfig } = this;
|
||||
const {
|
||||
showOthers,
|
||||
hasAdvancedConfig,
|
||||
hasAdvancedRateConfig,
|
||||
min,
|
||||
max,
|
||||
explain,
|
||||
isNps,
|
||||
} = this;
|
||||
return (
|
||||
<div class="option-edit-bar-wrap">
|
||||
<div class="option-edit-bar">
|
||||
@ -100,16 +134,17 @@ export default defineComponent({
|
||||
)}
|
||||
{this.rateConfigVisible && (
|
||||
<RateConfig
|
||||
min={this.moduleConfig.starMin}
|
||||
max={this.moduleConfig.starMax}
|
||||
min={min}
|
||||
max={max}
|
||||
rangeConfig={this.moduleConfig.rangeConfig}
|
||||
visible={this.rateConfigVisible}
|
||||
onVisibleChange={(val) => {
|
||||
this.rateConfigVisible = val;
|
||||
}}
|
||||
explain={true}
|
||||
explain={explain}
|
||||
dialogWidth="800px"
|
||||
onConfirm={this.handleChange}
|
||||
class={[isNps ? 'nps-rate-config' : '']}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -147,6 +182,17 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.nps-rate-config {
|
||||
::v-deep .row {
|
||||
height: 47px;
|
||||
}
|
||||
::v-deep .text {
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pop-title {
|
||||
font-family: PingFangSC-Medium;
|
||||
font-size: 14px;
|
||||
|
148
web/src/materials/questions/widgets/NpsModule/index.vue
Normal file
148
web/src/materials/questions/widgets/NpsModule/index.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="nps-wrapper-main">
|
||||
<div class="nps-row-msg">
|
||||
<div class="nps-msg left">{{ minMsg }}</div>
|
||||
<div class="nps-msg right">{{ maxMsg }}</div>
|
||||
</div>
|
||||
<BaseRate
|
||||
:name="props.field"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:readonly="props.readonly"
|
||||
:value="indexValue"
|
||||
iconClass="number"
|
||||
@change="confirmNps"
|
||||
/>
|
||||
<QuestionWithRule
|
||||
v-if="isShowInput"
|
||||
:showTitle="false"
|
||||
:moduleConfig="moduleConfig"
|
||||
@change="onMoreDataChange"
|
||||
></QuestionWithRule>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, computed } from 'vue';
|
||||
import QuestionWithRule from '@/materials/questions/widgets/QuestionRuleContainer';
|
||||
import BaseRate from '../BaseRate';
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
minMsg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxMsg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rangeConfig: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const rating = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
const key = props.field;
|
||||
emit('change', {
|
||||
key,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const confirmNps = (num) => {
|
||||
if (props.readonly) return;
|
||||
rating.value = num + '';
|
||||
};
|
||||
|
||||
const minMsg = computed(() => {
|
||||
return props.minMsg || '极不满意';
|
||||
});
|
||||
|
||||
const maxMsg = computed(() => {
|
||||
return props.maxMsg || '十分满意';
|
||||
});
|
||||
|
||||
const indexValue = computed(() => {
|
||||
return props.value !== '' ? props.value : -1;
|
||||
});
|
||||
const currentRangeConfig = computed(() => {
|
||||
return props.rangeConfig[rating.value];
|
||||
});
|
||||
const isShowInput = computed(() => {
|
||||
return currentRangeConfig.value?.isShowInput;
|
||||
});
|
||||
const moduleConfig = computed(() => {
|
||||
return {
|
||||
type: 'selectMoreModule',
|
||||
field: `${props.field}_${rating.value}`,
|
||||
placeholder: props.rangeConfig[rating.value]?.text,
|
||||
value: props.rangeConfig[rating.value]?.othersValue || '',
|
||||
};
|
||||
});
|
||||
|
||||
const onMoreDataChange = (data) => {
|
||||
const { key, value } = data;
|
||||
emit('change', {
|
||||
key,
|
||||
value,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.nps-wrapper-main {
|
||||
.nps-row-msg {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.2rem;
|
||||
.nps-msg {
|
||||
flex: 1;
|
||||
font-size: 0.22rem;
|
||||
color: #92949d;
|
||||
&.left {
|
||||
text-align: left;
|
||||
}
|
||||
&.right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .star-wrapper-main {
|
||||
.star-item {
|
||||
&:hover {
|
||||
background-color: $primary-color;
|
||||
}
|
||||
}
|
||||
&:has(.star-item:hover) .star-item:not(:hover, :hover ~ *) {
|
||||
background-color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
95
web/src/materials/questions/widgets/NpsModule/meta.js
Normal file
95
web/src/materials/questions/widgets/NpsModule/meta.js
Normal file
@ -0,0 +1,95 @@
|
||||
import basicConfig from '../../common/config/basicConfig';
|
||||
import { Message } from 'element-ui';
|
||||
|
||||
const meta = {
|
||||
title: '评分',
|
||||
questExtra: ['listenMerge'],
|
||||
type: 'radio-nps',
|
||||
componentName: 'NpsModule',
|
||||
formConfig: [
|
||||
basicConfig,
|
||||
{
|
||||
name: 'min',
|
||||
label: 'NPS量表最小值',
|
||||
labelStyle: {
|
||||
'font-weight': 'bold',
|
||||
},
|
||||
contentClass: 'nps-select-config',
|
||||
key: 'min',
|
||||
type: 'Select',
|
||||
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
})),
|
||||
valueSetter: (val, moduleConfig) => {
|
||||
if (moduleConfig['max'] && val >= moduleConfig['max']) {
|
||||
Message({
|
||||
type: 'info',
|
||||
message: '最小值不可大于最大值',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max',
|
||||
label: 'NPS量表最大值',
|
||||
labelStyle: {
|
||||
'font-weight': 'bold',
|
||||
},
|
||||
key: 'max',
|
||||
type: 'Select',
|
||||
contentClass: 'nps-select-config',
|
||||
options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
})),
|
||||
valueSetter: (val, moduleConfig) => {
|
||||
if (moduleConfig['min'] && val <= moduleConfig['min']) {
|
||||
Message({
|
||||
type: 'info',
|
||||
message: '最大值不可小于最小值',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'npsMsg',
|
||||
label: 'NPS两级文案',
|
||||
labelStyle: {
|
||||
'font-weight': 'bold',
|
||||
},
|
||||
contentClass: 'nps-customed-config',
|
||||
type: 'Customed',
|
||||
content: [
|
||||
{
|
||||
label: '最小值文案',
|
||||
type: 'Input',
|
||||
key: 'minMsg',
|
||||
direction: 'horizon',
|
||||
},
|
||||
{
|
||||
label: '最大值文案',
|
||||
type: 'Input',
|
||||
key: 'maxMsg',
|
||||
direction: 'horizon',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
editConfigure: {
|
||||
optionEdit: {
|
||||
show: false,
|
||||
},
|
||||
optionEditBar: {
|
||||
show: true,
|
||||
configure: {
|
||||
showOthers: false,
|
||||
showAdvancedRateConfig: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
@ -9,6 +9,7 @@
|
||||
@change="changeData"
|
||||
popper-class="option-list-width"
|
||||
:disabled="formConfig.disabled"
|
||||
:class="formConfig.contentClass"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
@ -34,6 +35,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
moduleConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
formConfig: {
|
||||
@ -57,8 +62,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
changeData(value) {
|
||||
const key = this.formConfig.key;
|
||||
|
||||
const { key, valueSetter } = this.formConfig;
|
||||
if (valueSetter && typeof valueSetter == 'function') {
|
||||
let status = valueSetter(value, this.moduleConfig);
|
||||
if (status) {
|
||||
this.validValue = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, {
|
||||
key,
|
||||
value,
|
||||
@ -71,7 +82,9 @@ export default {
|
||||
.option-list-width {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.nps-select-config {
|
||||
width: 312px;
|
||||
}
|
||||
.select-option-quote,
|
||||
.originType {
|
||||
font-family: PingFangSC-Regular;
|
||||
|
@ -34,6 +34,7 @@ const checkBoxTipSame = '请选择#min#项,少选择了#less#项';
|
||||
const textRangeMinTip = '至少输入#min#字';
|
||||
const numberRangeMinTip = '数字最小为#min#';
|
||||
const numberRangeMaxTip = '数字最大为#max#';
|
||||
const radioType = ['radio-star', 'radio-nps'];
|
||||
|
||||
// 多选题的选项数目限制
|
||||
export function optionValidator(value, minNum, maxNum) {
|
||||
@ -198,7 +199,7 @@ const generateOthersKeyMap = (question) => {
|
||||
const { type, field, options, rangeConfig } = question;
|
||||
let othersKeyMap = undefined;
|
||||
|
||||
if (['radio-star'].includes(type)) {
|
||||
if (radioType.includes(type)) {
|
||||
othersKeyMap = {};
|
||||
for (const key in rangeConfig) {
|
||||
if (rangeConfig[key].isShowInput) {
|
||||
@ -257,7 +258,7 @@ export default function (questionConfig) {
|
||||
|
||||
// 对于选择题支持填写更多信息的,需要做是否必填的校验
|
||||
if (_keys(othersKeyMap).length) {
|
||||
if (['radio-star'].includes(type)) {
|
||||
if (radioType.includes(type)) {
|
||||
if (rangeConfig) {
|
||||
for (const key in rangeConfig) {
|
||||
if (rangeConfig[key].isShowInput && rangeConfig[key].required) {
|
||||
@ -275,7 +276,6 @@ export default function (questionConfig) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign(validMap, pre);
|
||||
}, {});
|
||||
return { rules };
|
||||
|
Loading…
Reference in New Issue
Block a user