feat: nps评分 (#79)

* feat: nps评分功能

* feat: nps样式添加

* feat: 添加nps评分icon

* feat: 基于修改建议修改nps评分组件

* feat: 将自定义类移入至设置器
This commit is contained in:
chaorenluo 2024-04-01 17:11:52 +08:00 committed by sudoooooo
parent 071afbfec1
commit 05b9416cd9
12 changed files with 355 additions and 14 deletions

View File

@ -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`] =

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -29,6 +29,10 @@ export const defaultQuestionConfig = {
starStyle: 'star',
starMin: 1,
starMax: 5,
min: 0,
max: 10,
minMsg: '极不满意',
maxMsg: '十分满意',
// 分值对应的开关和输入框配置因为不是固定从0开始用对象比较合适
rangeConfig: {},
// 选项

View File

@ -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',
],
},
];

View File

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

View File

@ -6,6 +6,7 @@ export default {
checkbox: 'CheckboxModule',
'binary-choice': 'BinaryChoiceModule',
'radio-star': 'StarModule',
'radio-nps': 'NpsModule',
city: 'CityModule',
vote: 'VoteModule',
'matrix-checkbox': 'GroupModule',

View File

@ -5,6 +5,7 @@ const config = {
radio: '单选',
checkbox: '多选',
'radio-star': '评分',
'radio-nps': 'NPS评分',
city: '城市选择',
vote: '投票',
'binary-choice': '判断',

View File

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

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

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

View File

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

View File

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