feat[survey]: NPS 内容调整

- NPS value使用 hooks 方式.
- 解决 preview 的 NPS 组件无法调整的问题
- PreviewNPS 组件实现方式调整
- 增加 nps组件相应的类型文件
This commit is contained in:
Huangzhe
2025-03-18 13:17:18 +08:00
parent 1f0ffa679b
commit 6fe96b9935
6 changed files with 214 additions and 59 deletions

View File

@@ -30,7 +30,6 @@
<p>{{ element.config.prompt_right }}</p> <p>{{ element.config.prompt_right }}</p>
</div> </div>
<RateCharacter <RateCharacter
v-model="answerValue"
:config="element.config" :config="element.config"
:index="optionIndex" :index="optionIndex"
@change="handleRateChange" @change="handleRateChange"
@@ -47,7 +46,7 @@ import { ref } from 'vue';
import RateCharacter from './RateCharacter.vue'; import RateCharacter from './RateCharacter.vue';
const isPreview = defineModel('isPreview', { default: false, type: Boolean }); const isPreview = defineModel('isPreview', { default: false, type: Boolean });
const props = defineProps({ /*const props = */ defineProps({
index: { index: {
type: Number, type: Number,
default: 0 default: 0
@@ -60,11 +59,10 @@ const props = defineProps({
questionType: { type: [String, Number], default: 4 } questionType: { type: [String, Number], default: 4 }
}); });
// answer 的答案以 矩阵形式存储, 例如 [4,7],上层更新答案的时候也容易 /**
const rates = defineModel('rates', { default: [], type: Array }); * element === question
const rate = ref(0); * @type {ModelRef<Object, string, Object, Object>}
const answerValue = ref(); */
const element = defineModel('element', { const element = defineModel('element', {
type: Object, type: Object,
default: () => { default: () => {
@@ -72,29 +70,13 @@ const element = defineModel('element', {
} }
}); });
// 不知道的 BUG ,开始的时候不能重置颜色。 故如此
setTimeout(() => {
rate.value = localStorage.getItem(props.sn);
// console.log(`rate value:`, rate.value);
// if (rates.value[0] !== undefined) {
// console.log(`rates value:`, rates.value);
// rate.value = rates.value[0]
// }
// else return
}, 1000);
/** /**
* *
* @param index {number} 索引 * @param index {number} 索引
* @param rate {number} 具体数值 * @param rate {number} 具体数值
*/ */
function handleRateChange(index, rate) { function handleRateChange(/* index, rate */) {
// 如果没有查询到对应索引的数值, 那么就直接push一个直到有数值为止 // 占位
while (rates.value.length < index) {
rates.value.push(NaN);
}
rates.value[index] = rate;
localStorage.setItem(props.sn, rate.value);
} }
const chooseId = ref(''); const chooseId = ref('');

View File

@@ -16,6 +16,7 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { value as model } from '@/views/Design/components/Questions/hooks/useNPSHooks';
const rateItem = ref([ const rateItem = ref([
{ {
@@ -52,10 +53,6 @@ const props = defineProps({
} }
}); });
const model = defineModel('model', {
type: Number
});
const index = defineModel('index', { const index = defineModel('index', {
type: Number type: Number
}); });
@@ -74,12 +71,14 @@ const renderScore = (min, max, interval) => {
} }
rateItem.value = result; rateItem.value = result;
}; };
// 重置颜色 // 重置颜色
function getItem(value) { function getItem(value) {
model.value = value.label; // console.log(value.label);
rateItem.value.forEach((item, index) => { rateItem.value.forEach((item, index) => {
rateItem.value[index].active = item.label <= value.label; rateItem.value[index].active = item.label <= value.label;
}); });
model.value = value.label;
} }
watch( watch(

View File

@@ -0,0 +1,3 @@
import { ref } from 'vue';
export const value = ref<number>(-1);

View File

@@ -1208,6 +1208,174 @@ async function answer(callback, callbackBeforePage) {
callback(); callback();
} }
} }
// 关联引用
function onRelation(
// 避免出现参数 undefined 情况
{ options, value, list } = {},
{ question_type: _questionType, question_index: _questionIndex, related, answer } = {}
) {
// 关联
related.forEach((relationItem) => {
let relationOptions = [];
if (_questionType === 9 || _questionType === 10) {
// 矩阵选择
list.forEach((item) => {
if (item.type === relationItem.cite_type) {
relationOptions = [...relationOptions, ...item.options];
}
if (relationItem.relation_type === 1) {
relationOptions = relationOptions.filter((option) =>
_questionType === 9 ? option.value : option.value?.length
);
} else if (relationItem.relation_type === 2) {
relationOptions = relationOptions.filter((option) =>
_questionType === 9 ? !option.value : !option.value?.length
);
}
});
} else if (_questionType === 11) {
// 矩阵打分
list.forEach((item) => {
if (item.type === relationItem.cite_type) {
relationOptions = [...relationOptions, ...item.options];
}
});
} else if (_questionType === 25 || _questionType === 26) {
// 热区题
relationOptions = options.filter((option) => {
if (relationItem.relation_type === 1) {
return option.status === 1;
} else if (relationItem.relation_type === 2) {
return option.status === 2;
} else if (relationItem.relation_type === 3) {
return !option.status;
}
return true;
});
} else if (_questionType === 105) {
// MXD
options.forEach((currentOptions) => {
currentOptions.forEach((option) => {
const index = relationOptions.findIndex(
(relationOption) => relationOption.option_key === option.option_key
);
if (index === -1) {
// 全部项
if (relationItem.relation_type === 0) {
return relationOptions.push(option);
}
// 高相关
if (relationItem.relation_type === 3) {
return option.value === 'b' && relationOptions.push(option);
}
// 不相关
if (relationItem.relation_type === 4) {
return option.value === 'w' && relationOptions.push(option);
}
}
});
});
} else if (relationItem.relation_type === 0) {
// 全部选项
relationOptions = options;
} else {
// 过滤选中/未选中选项
relationOptions = options.filter((option) => {
if (_questionType === 1) {
// 单选
if (relationItem.relation_type === 1) return value === option.option_key;
return value !== option.option_key;
} else if (_questionType === 2) {
// 多选
if (relationItem.relation_type === 1) return value.includes(option.option_key);
return !value.includes(option.option_key);
}
return true;
});
}
// 找到关联题
const question = questionsData.value.questions.find(
(question) => question.question_index === relationItem.relation_question_index
);
// 深拷贝关联选项
const copyRelationOptions = JSON.parse(JSON.stringify(relationOptions));
// 更新关联选项key
copyRelationOptions.forEach((option) => {
if (option.option_key[0] !== 'Q') {
let letter = 'A';
// 矩阵题行、列
if (_questionType >= 9 && _questionType <= 11) {
letter = relationItem.cite_type === 1 ? 'R' : 'C';
}
option.option_key = `Q${_questionIndex}${letter}${option.option_key}`;
}
// 其他项特殊处理
if (option.is_other && option.value) {
option.is_other = 0;
option.option = option.value;
}
delete option.value;
delete option.status;
});
// 更新关联题列表
const relatedList = question.list.find(
(relatedListItem) => relatedListItem.relation_question_index === _questionIndex
);
relatedList.options = answer ? copyRelationOptions : [];
});
}
function jumpImmediately() {
const code = questionsData.value.action?.code;
if (page.value !== pages.value.length + 1 && ![20004, 20011, 20016].includes(code)) {
return;
}
const survey = questionsData.value.survey;
let countTime = 0;
let url = '';
if (code === 20004 && survey.screening_end_url_select && survey.screening_end_url) {
countTime = survey.screening_standing_time;
url = survey.screening_end_url;
}
if (code === 20011 && survey.success_end_url_select && survey.success_end_url) {
countTime = survey.success_standing_time;
url = survey.success_end_url;
}
if (code === 20016 && survey.quota_end_url_select && survey.quota_end_url) {
countTime = survey.quota_standing_time;
url = survey.quota_end_url;
}
// 跳转链接
if (countTime <= 0 && url) {
questionsData.value.action.code = -1 * code; // 防止 AnswerMob AnswerPc 组件里显示最后一页
url = url.replaceAll('#sn#', questionsData.value.answer.sn);
url = url.replaceAll('#user#', questionsData.value.answer.respondent);
url = url.replaceAll('#survey_sn#', questionsData.value.answer.survey_sn);
if (proxy.$route.query.source === 'YILI_APP_WANGYI') {
Object.keys(proxy.$route.query).forEach((key) => {
if (!['sn', 'source', 'is_template', 'channelUCode'].includes(key)) {
url += `${url.indexOf('?') === -1 ? '?' : '&'}${key}=${proxy.$route.query[key]}`;
}
});
}
// 判断是否小程序路径
if (url[0] === '/') {
// 判断是否在小程序环境
wx.miniProgram.getEnv(() => {
wx.miniProgram.redirectTo({ url });
});
} else {
if (url.indexOf('http://') === -1 && url.indexOf('https://') === -1) {
url = `http://${url}`;
}
open(url, '_self');
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
::v-deep .van-cell::after { ::v-deep .van-cell::after {

View File

@@ -1,43 +1,45 @@
<template> <template>
<n-p-s v-model:element="question" v-model:rates="rates" :active="false" :isPreview="true" /> <n-p-s v-model:element="question" v-model:value="value" :active="false" :isPreview="true" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import NPS from '@/views/Design/components/Questions/NPS.vue'; import NPS from '@/views/Design/components/Questions/NPS.vue';
import { ref, watch } from 'vue'; import { watch } from 'vue';
import { value } from '@/views/Design/components/Questions/hooks/useNPSHooks';
const question = defineModel<question>('question', { default: { config: { is_required: false } } });
const answer = defineModel<{ [key: string]: number }>('answer', { default: undefined });
// rates 数值取决与 answer 没有数据重新建立一个对应长度的数组
const rates = ref(answer.value ? getRates() : new Array(question.value.list[0].options!.length));
// // 预览新增 emit ['changeAnswer', 'previous', 'next'] // // 预览新增 emit ['changeAnswer', 'previous', 'next']
const emit = defineEmits(['changeAnswer', 'previous', 'next', 'update:element']); const emit = defineEmits(['changeAnswer', 'previous', 'next', 'update:element']);
// 获取 rates const question = defineModel<question>('question', { default: { config: { is_required: false } } });
function getRates() {
const keys = Object.keys(answer.value); /**
return keys.map((item) => { * answer 的答案类型
return answer.value[item]; * {
}); * "1": 10
* }
*/
const answer = defineModel<NPSAnswerType>('answer', { default: undefined });
// 解析答案
// function parseAnswer() {
// return answer.value[`1`];
// }
/**
* 生成NPS答案
*/
function genAnswer(value: number): NPSAnswerType {
const res: any = {};
res[`1`] = value;
return res;
} }
// console.log(answer.value && getRates()); watch(value, (newValue) => {
watch( // console.log(genAnswer(newValue as number));
rates, answer.value = genAnswer(newValue as number);
() => { // 提交答案
const res = {}; emit('changeAnswer', answer.value);
rates.value.map((item, index) => {
// index 是 key, item 是 value
res[index + 1] = item;
}); });
answer.value = res;
emit('changeAnswer', res);
},
{
deep: true
}
);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -0,0 +1 @@
type NPSAnswerType = { [key: string]: number };