feat: 完成 preview 组件的功能

- 新增 preview 组件相关的资源
- 调用 web 端部分 API
- 相关题目添加答案配置
This commit is contained in:
Huangzhe
2025-03-12 20:38:13 +08:00
parent b228fc8767
commit 691007d9f6
34 changed files with 8470 additions and 15 deletions

View File

@@ -0,0 +1,58 @@
<template>
<div v-if="isPreview"
v-html="nodes"
style="min-width: 22px"
:class="isMobile ? 'mobile-text' : ''"
@click="onClick"></div>
<div v-else v-html="nodes"></div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
// 富文本内容
nodes: {
type: String,
default: ''
},
// 是否开启预览图片功能
isPreview: {
type: Boolean,
default: false
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
}
},
methods: {
onClick(evt) {
const targetTagName = (evt.target || evt.srcElement).tagName || '';
if (targetTagName.toLowerCase() === 'img') {
evt.stopPropagation()
evt.cancelBubble = true
}
}
}
});
</script>
<style lang="scss" scoped>
:deep(p) {
margin-bottom: 0;
}
:deep(img) {
cursor: pointer;
}
.mobile-text {
:deep(img) {
max-width: 90% !important;
height: auto !important;
}
}
</style>

View File

@@ -1 +1 @@
export const surveyQuestion = '/api/api/console/surveys/RWNK9BYp/questions';
export const surveyQuestion = 'https://yls-api-uat.dctest.digitalyili.com/api/console/surveys/RWNK9BYp/questions';

View File

@@ -1,7 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router';
import layout from '@/layouts/index.vue';
import Design from '@/views/Design/Index.vue';
import Preview from '@/views/Design/Preview.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@@ -50,7 +49,7 @@ const router = createRouter({
path: '/preview',
name: 'preview',
meta: {},
component: Preview
component: ()=> import ( '@/views/Survey/views/Preview/Index.vue')
},
{
path: '/create',

View File

@@ -0,0 +1,50 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useQuestionStore = defineStore('questionStore', () => {
const questionsData = ref();
// styleInfo 主题样式
const styleInfo = computed(() => questionsData.value?.survey?.style || {});
// 当前页数
const page = ref(0);
// 分页
const pages = computed(() => questionsData.value?.answer?.pages || []);
// 加载
const loading = ref(false);
// 上一页时的加载状态
const prevLoading = ref(false);
// 分页计时器
const localPageTimer = ref({});
// 当前页问卷
const questions = computed(() => {
const currentPages = pages.value[page.value - 1] || [];
return (questionsData.value?.questions || []).filter((quetion: any) =>
currentPages.find((index: any) => quetion.question_index === index)
);
});
// 是否显示分页器
const showPage = computed(() => {
return ![102, 104, 105, 201].includes(questions.value[0]?.question_type);
});
// 作用未知
const l = ref({});
// 主题颜色
const themeColor = ref({});
return {
questionsData,
styleInfo,
page,
pages,
l,
themeColor,
loading,
prevLoading,
localPageTimer,
questions,showPage
};
});

View File

@@ -0,0 +1,11 @@
import type { Directive } from 'vue';
/**
* 自定义指令,用于在元素挂载后自动获取焦点
*/
export const vFocus: Directive = {
mounted(el: HTMLInputElement) {
el.focus();
}
};

126
src/utils/utils.js Normal file
View File

@@ -0,0 +1,126 @@
/**
* 生成一个按数字顺序的名字名字1名字2名字3
* @param names {string[]} 要生成的名字前缀列表
* @param list {string[]} 已经有的名字列表
* @param options {object}
* @returns {string[]}
*/
export function getSequenceName(names, list, options) {
const result = [];
const start = options?.start ?? 1; // 从数字几开始
names.forEach((name) => {
const sequence = [];
if (!list.length) {
result.push(name + start);
return;
}
list
.map((i) => getDomText(i))
.filter((i) => !!i)
.forEach((item) => {
if (item.startsWith(name)) {
const index = Number(item.substring(name.length));
if (!isNaN(index) && index >= start) {
sequence.push(index);
}
}
});
sequence.sort((a, b) => a - b);
if (!sequence.includes(start)) {
result.push(`${name}${start}`);
return;
}
for (let i = 0; i < sequence.length; i += 1) {
if (!sequence[i + 1] || sequence[i] + 1 !== sequence[i + 1]) {
result.push(`${name}${sequence[i] + 1}`);
break;
}
}
});
return result;
}
/**
* 获取富文本中的文字
* @param dom {string} 富文本
* @returns {string}
*/
export function getDomText(dom) {
const div = document.createElement('div');
div.innerHTML = dom;
return div.textContent;
}
/**
* 判断富文本是否与列表中的富文本重复。重复则返回 true不同则返回 false
* @param list {string[]} 待判断的富文本列表
* @param comparedList {string[]} 用于比较的富文本列表
* @param options {object}
* @returns {boolean[]}
*/
export function isRichRepeat(list, comparedList, options) {
const imageAsDifference = options?.imageAsDifference !== false; // 不比较图片。只要存在图片,则认为两段富文本不同
return list.map((rich) => {
return comparedList.some((compared) => {
if (imageAsDifference) {
if (rich.includes('<img') || compared.includes('<img')) {
return false;
}
}
return getDomText(rich) === getDomText(compared);
});
});
}
/**
* 判断富文本是否为空。
* @param list {string[]} 待判断的富文本列表
* @param options {object}
* @returns {boolean[]}
*/
export function isRichEmpty(list, options) {
const imageAsEmpty = options?.imageAsEmpty === true; // 不判断图片。只要存在图片,则认为两段富文本不为空
return list.map((rich) => {
if (!imageAsEmpty) {
if (rich.includes('<img')) {
return false;
}
}
return !getDomText(rich);
});
}
/**
* 使数组 options 里的内容按随机的方式,重新排序。不改变原数组,返回新的排好序的数组。
* @param options
* @param isRandom
*/
export function randomOptions(options, isRandom = true) {
if (!isRandom) {
return options;
}
return [...(options || [])].sort(() => Math.random() - 0.5);
}
/**
* 根据 field 字段,判断两个对象数组是否相同
* @param arr1
* @param arr2
* @param field
*/
export function compareArrayByField(arr1, arr2, field) {
const arr1Fields = Array.from(new Set(arr1.map((i) => i[field])));
const arr2Fields = Array.from(new Set(arr2.map((i) => i[field])));
if (arr1Fields.length !== arr2Fields.length) {
return false;
}
return arr1Fields.every((i) => arr2Fields.includes(i));
}

View File

@@ -14,7 +14,7 @@
</template>
<template #input>
<template v-for="(item, optionIndex) in element.options" :key="item.id">
<van-radio-group v-if="element.question_type === 1">
<van-radio-group v-if="element.question_type === 1" v-model="choiceValue">
<option-action
v-model:data="element.options[optionIndex]"
:active="active"
@@ -28,6 +28,7 @@
:disabled="it.disabled"
icon-size="0.45rem"
>
<!-- 自定义文本 -->
<template #default>
<div class="flex align-center van-cell">
<contenteditable v-model="it.option" :active="active"></contenteditable>
@@ -73,6 +74,8 @@
<script setup>
import OptionAction from '@/views/Design/components/ActionCompoents/OptionAction.vue';
import { defineAsyncComponent, toRefs } from 'vue';
const choiceValue = ref('checked');
const Contenteditable = defineAsyncComponent(() => import('@/components/contenteditable.vue'));
const props = defineProps({
element: {

View File

@@ -0,0 +1,99 @@
<template>
<table class="matrix-table">
<thead>
<tr>
<th></th>
<td v-for="col in columns" :key="col.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
v-if="col.editor"
v-model="col.option"
v-focus
type="text"
@focusout="col.editor = false"
@click="handleRowNameChange(col.option!)"
/>
<span v-else @click="handleRowNameChange(col.option!)" v-html="col.option"/>
</td>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in rows" :key="rowIndex">
<th v-html="row.option"/>
<td v-for="(col, colIndex) in columns" :key="colIndex">
<input
type="checkbox"
:value="`${rowIndex + 1}_${colIndex + 1}`"
:checked="isOptionChecked(rowIndex, colIndex)"
@change="handleColNameChange(row.option, col.option, $event)"
/>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue';
import {vFocus} from '@/utils/directives/useVFocus';
const props = defineProps<{
rows: { option: string }[];
columns: { option: string, editor?: boolean }[];
questionType: number;
matrixAnswer: { [key: string]: any };
rowRecord: (number | string)[];
}>();
const emits = defineEmits(['update:matrixAnswer', 'update:rowRecord']);
const isOptionChecked = (rowIndex: number, colIndex: number): boolean => {
const key = `R${rowIndex + 1}_C${colIndex + 1}`;
return !!props.matrixAnswer[key];
};
const handleRowNameChange = (value: string) => {
console.log(`row change: ${value}`);
// 你可以在这里添加其他逻辑
};
const handleColNameChange = (rowOption: string, colOption: string, e: Event) => {
const target = e.target as HTMLInputElement;
const col = props.columns.findIndex(option => option.option === colOption);
const row = props.rows.findIndex(option => option.option === rowOption);
if (props.questionType === 10) {
if (target.checked) {
props.rowRecord[col] = (props.rowRecord[col] || []).concat(row + 1);
} else {
props.rowRecord[col] = (props.rowRecord[col] || []).filter(item => item !== row + 1);
}
props.matrixAnswer = {};
props.rows.forEach((rowOption, rowIndex) => {
const colOptions = props.rowRecord[rowIndex];
if (colOptions) {
colOptions.forEach((col: any) => {
props.matrixAnswer[`R${rowIndex + 1}_C${col}`] = true;
});
}
});
}
emits('update:matrixAnswer', props.matrixAnswer);
emits('update:rowRecord', props.rowRecord);
};
</script>
<style scoped lang="scss">
.matrix-table {
width: 100%;
border-collapse: collapse;
}
.matrix-table th,
.matrix-table td {
padding: 8px;
border: 1px solid #ddd;
text-align: center;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<table class="matrix-table">
<thead>
<tr>
<th></th>
<td v-for="col in columns" :key="col.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
v-if="col.editor"
v-model="col.option"
v-focus
type="text"
@focusout="col.editor = false"
@click="handleRowNameChange(col.option!)"
/>
<span v-else @click="handleRowNameChange(col.option!)" v-html="col.option"></span>
</td>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in rows" :key="rowIndex">
<th v-html="row.option"></th>
<td v-for="(col, colIndex) in columns" :key="colIndex">
<input
type="radio"
:name="`R${rowIndex + 1}`"
:value="`${rowIndex + 1}_${colIndex + 1}`"
:checked="isOptionChecked(rowIndex, colIndex)"
@change="handleColNameChange(row.option, col.option, $event)"
/>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue';
import {vFocus} from '@/utils/directives/useVFocus';
let props = defineProps<{
rows: { option: string }[];
columns: { option: string, editor?: boolean }[];
questionType: number;
matrixAnswer: { [key: string]: any };
rowRecord: (number | string)[];
}>();
const emits = defineEmits(['update:matrixAnswer', 'update:rowRecord']);
const isOptionChecked = (rowIndex: number, colIndex: number): boolean => {
const key = `R${rowIndex + 1}_C${colIndex + 1}`;
return !!props.matrixAnswer[key];
};
const handleRowNameChange = (value: string) => {
console.log(`row change: ${value}`);
// 你可以在这里添加其他逻辑
};
const handleColNameChange = (rowOption: string, colOption: string, e: Event) => {
const target = e.target as HTMLInputElement;
const col = props.columns.findIndex(option => option.option === colOption);
const row = props.rows.findIndex(option => option.option === rowOption);
if (props.questionType === 9) {
props.rowRecord[col] = row + 1;
props.matrixAnswer = {};
props.rowRecord.forEach((row, index) => {
props.matrixAnswer[`${index + 1}_${row}`] = 1;
});
}
emits('update:matrixAnswer', props.matrixAnswer);
emits('update:rowRecord', props.rowRecord);
};
</script>
<style scoped lang="scss">
.matrix-table {
width: 100%;
border-collapse: collapse;
}
.matrix-table th,
.matrix-table td {
padding: 8px;
border: 1px solid #ddd;
text-align: center;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<table class="matrix-table">
<thead>
<tr>
<th></th>
<td v-for="col in columns" :key="col.option">
<!-- 编辑状态单次点击出输入框失焦后关闭 -->
<input
v-if="col.editor"
v-model="col.option"
v-focus
type="text"
@focusout="col.editor = false"
@click="handleRowNameChange(col.option!)"
/>
<span v-else @click="handleRowNameChange(col.option!)" v-html="col.option"></span>
</td>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in rows" :key="rowIndex">
<th v-html="row.option"></th>
<td v-for="(col, colIndex) in columns" :key="colIndex">
<input
type="text"
:value="matrixAnswer[`${rowIndex + 1}_${colIndex + 1}`]"
@change="handleColNameChange(row.option, col.option, $event, rowIndex, colIndex)"
/>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue';
import {vFocus } from "@/utils/directives/useVFocus"
const props = defineProps<{
rows: { option: string }[];
columns: { option: string, editor?: boolean }[];
questionType: number;
matrixAnswer: { [key: string]: any };
}>();
const emits = defineEmits(['update:matrixAnswer']);
const handleRowNameChange = (value: string) => {
console.log(`row change: ${value}`);
// 你可以在这里添加其他逻辑
};
const handleColNameChange = (rowOption: string, colOption: string, e: Event, rowIndex: number, colIndex: number) => {
const target = e.target as HTMLInputElement;
const key = `R${rowIndex + 1}_C${colIndex + 1}`;
const newMatrixAnswer = { ...props.matrixAnswer, [key]: target.value };
emits('update:matrixAnswer', newMatrixAnswer);
};
</script>
<style scoped lang="scss">
.matrix-table {
width: 100%;
border-collapse: collapse;
}
.matrix-table th,
.matrix-table td {
padding: 8px;
border: 1px solid #ddd;
text-align: center;
}
</style>

View File

@@ -24,7 +24,7 @@
:key="optionIndex"
@click="chooseOption(item)"
>
<RateCharacter :config="element.config"></RateCharacter>
<RateCharacter v-model="answerValue" :config="element.config"></RateCharacter>
<div class="tips">
<p>{{ element.config.prompt_left }}</p>
<p>{{ element.config.prompt_center }}</p>
@@ -38,7 +38,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import RateCharacter from './RateCharacter.vue';
const props = defineProps({
@@ -56,6 +56,16 @@ const props = defineProps({
sn: { type: String, default: '' },
questionType: { type: [String, Number], default: 4 }
});
const answerValue = ref()
// NPS 的答案
const NPSAnswer = ref({
'question_index': props.index,
'answer': {
'1': answerValue.value
}
});
const element = ref(props.element);
const chooseId = ref('');
// 创建一个本地副本以保存更改

View File

@@ -47,6 +47,8 @@ const props = defineProps({
}
});
const model = defineModel();
const renderScore = (min, max, interval) => {
const result = [];
for (let i = min; i <= max; i += interval) {
@@ -61,21 +63,24 @@ const renderScore = (min, max, interval) => {
}
rateItem.value = result;
};
// 重置颜色
const getItem = (value) => {
// chooseId.value = value.id;
model.value = value.label;
rateItem.value.forEach((item, index) => {
rateItem.value[index].active = item.label <= value.label;
});
};
watch(model, () => {
getItem({ label: model.value, active: false });
});
// 监听 min、max 和 score_interval 的变化
watch(
() => [props.config.min, props.config.max, props.config.score_interval],
(newValues) => {
const [newMin, newMax, newScoreInterval] = newValues;
// this.renderScore();
renderScore(newMin, newMax, newScoreInterval);
// 在这里可以添加对这些值变化的处理逻辑
},
{ immediate: true }
);
@@ -83,7 +88,6 @@ watch(
<style scoped lang="scss">
ul {
// border: 1px solid red;
display: flex;
margin-bottom: 10px;
}
@@ -95,8 +99,6 @@ ul {
border: 1px solid #ddd;
border-radius: 4px;
color: #666;
// border: 1px solid red;
}
.active_item {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
<template>
<slot></slot>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { BrowsingRecordApi } from '../js/api';
const has3dPages = ['/answer'];
export default defineComponent({
components: {
},
props: {
isStemMiddle: {
type: Boolean,
default: false
},
// 题干
stem: {
type: String,
default: ''
},
// 配置
config: {
type: Object,
default: () => ({})
},
// 样本SN
answerSn: {
type: String,
default: ''
},
// 问卷SN
answerSurveySn: {
type: String,
default: ''
},
question: {
type: Object,
default: () => ({})
},
cartWaresLength: {
type: Number,
default: 0
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
}
},
computed: {
canUse3D() {
return this.config.is_three_dimensions && has3dPages.includes(this.$route.path);
},
surveyId() {
return this.config.scene;
},
// scene() {
// return this.config.scene_information;
// },
shelves() {
if (!this.scene) return [];
return this.scene.shelves || [];
},
shelf() {
if (!this.config.shelf) return this.shelves[0];
return this.shelves.find((x) => x.planetid == this.config.shelf);
},
wares() {
if (!this.shelf) return [];
return this.shelf.wares;
},
ware() {
return this.wares.find((x) => x.planetid == this.config.ware);
},
options() {
var options = [];
try {
this.question.list.forEach((item) => {
item.options.forEach((option) => {
// question_index为0代表当前题型为了支持复制题时引用的question_index错误的问题
var question_index = item.relation_question_index || 0;
options.push({
...option,
question_index
});
});
});
if (this.question.hideOptions) {
options = options.filter((option) => !this.question.hideOptions.includes(option.option_key));
}
} catch (e) {
return [];
}
return options;
},
defaultWare() {
return this.ware;
}
},
data() {
return {
scene: null,
shopData: null,
page: null,
page_shelves: null,
sceneAction: null,
elCart: null,
hold: null,
// 用于控制下一页点击
pager: {
// 判断能否点击
activate: false,
timeoutId: 0,
// 初始化点击延迟
init(time = 3000) {
this.activate = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = 0;
}
this.timeoutId = setTimeout(() => {
this.activate = true;
}, time);
},
// 清空延迟
clear() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = 0;
}
}
},
showTimeTimeoutId: 0
};
},
beforeUnmount() {
this.pager.clear();
if (this.showTimeTimeoutId) clearTimeout(this.showTimeTimeoutId);
},
async mounted() {
if (!this.canUse3D) return;
if (this.config.is_binding_goods) {
this.$refs.SceneGuidance?.show({
isSelect: true,
isMobile: this.isMobile
});
} else {
this.$refs.SceneGuidance?.show({
isSelect: false,
isMobile: this.isMobile
});
}
this.pager.init();
this.scene = JSON.parse(this.config.scene_information);
// 解决缓存问题,答卷时加载场景信息
if (!this.scene) {
var res = await BrowsingRecordApi.getSurveysScene({
sn: this.$route.query.sn,
question_index: this.question.question_index
});
this.scene = JSON.parse(res.data?.scene_information);
}
console.log('scene', this.scene);
this.shopData = buildShopDataDemo(this.scene);
this.$refs.SceneSurveyViewer.init();
if (this.config.is_show_time && this.config.show_time) {
this.showTimeTimeoutId = setTimeout(() => {
this.showTimeTimeoutId = 0;
this.$emit('next');
}, this.config.show_time * 1000);
}
},
methods: {
next() {
console.log(this.pager.activate);
if (!this.pager.activate) {
return;
}
this.pager.init();
this.$emit(
'next',
() =>
new Promise((resolve) => {
this.sceneAction = {
action: 'hold_to_shelf'
};
setTimeout(() => resolve(), 100);
})
);
},
previous() {
if (!this.pager.activate) {
return;
}
this.pager.init();
this.sceneAction = {
action: 'hold_to_shelf'
};
setTimeout(() => {
this.$emit('previous');
}, 100);
},
onLoadingCompletion() {
this.shopData.shelves.forEach((shelf) => {
var wares = shelf.wares;
// 选项随机
if (this.options && this.options.length && this.config.is_binding_goods) {
wares = this.options
.filter((option) => !option.is_other)
.map((option) => {
return this.wares.find(
(ware) => ware.question_index == option.question_index && ware.option_index == option.option_index
);
});
wares = wares.filter((x) => x);
}
if (shelf.layoutType == 'multiCols') {
shelf.cells = shelf.cells.map((cell, index) => {
return {
...cell,
...wares[Math.floor(index / shelf.originCells.length) % wares.length],
showSign: !!this.config.is_price_tag,
showLogo: !!this.config.is_brand
};
});
} else {
shelf.cells = this.shelf.cells.map((cell, index) => {
return {
...cell,
...wares[index % this.wares.length],
showSign: !!this.config.is_price_tag,
showLogo: !!this.config.is_brand
};
});
}
});
this.page_shelves = {
shelves: this.shopData.shelves
};
},
onFromSceneHoldToShelf() {
this.hold = null;
// this.sceneAction = {
// action: "hold_to_shelf",
// };
},
onPageCompletion() {
this.elCart = this.$refs.cart;
},
onUiHoldToCart() {
if (!this.hold) return;
this.sceneAction = {
action: 'hold_to_cart'
};
var hold = this.hold;
this.hold = null;
var ware = this.wares.find((ware) => ware.planetid == hold.data.surveyWare.id);
if (!ware) return;
var option = null;
this.question.list.forEach((qgroup) => {
// 获取question_index考虑到关联选项
var question_index = qgroup.relation_question_index || 0;
if (question_index != ware.question_index) return;
qgroup.options.forEach((qoption) => {
if (qoption.option_index != ware.option_index) return;
option = qoption;
});
});
if (!option) return;
this.$emit('onHoldToCart', option);
},
onHold(hold) {
this.hold = hold;
},
// 用户行为记录
onBehaviorFlush(data) {
// 判断是否记录
if (!this.config.is_behavior) return;
if (!data[0]) return;
if (!this.answerSn || !this.answerSurveySn) return;
BrowsingRecordApi.browsingRecordSurveys({
sn: this.answerSurveySn,
data: {
sn: this.answerSn,
question_index: this.question.question_index,
question_type: this.question.question_type,
planet_id: data[0].wareId,
shelve_planet_id: data[0].shelfId,
action_info: data
}
}).catch((e) => console.error(e));
}
}
});
</script>
<style lang="scss" scoped>
@import 'viewer.utils';
.answerviewer {
top: 0;
left: 0;
// width: 100vw;
// height: 100vh;
// position: fixed;
background-color: #fff;
// z-index: 99;
}
// .answerviewerVague {
// .q-content-mask {
// position: fixed;
// left: 0;
// right: 0;
// top: 0;
// bottom: 0;
// background-color: rgba($color: #000000, $alpha: 0.3);
// }
// :deep(canvas) {
// filter: blur(5px);
// }
// }
</style>

View File

@@ -0,0 +1,114 @@
<template>
<SceneSurveyViewer
ref="SceneSurveyViewer"
:shopData="shopData"
:surveyId="surveyId"
:hidden="true"
@onLoadingCompletion="onLoadingCompletion"
:page="page"
:page_shelves="page_shelves"
/>
</template>
<script>
import { BrowsingRecordApi } from '../js/api';
import SceneSurveyViewer from './SceneSurveyViewerPage/Index.vue';
const has3dPages = ['/answer'];
export default {
components: {
SceneSurveyViewer
},
props: {
// 问题列表
questions: {
type: Array,
default: () => []
}
},
data() {
return {
first3DQuestion: null,
sceneInformation: null,
// 场景信息
shopData: null,
// 场景ID
surveyId: null,
// 商品摆放信息
page: null,
page_shelves: null
};
},
computed: {
canUse3D() {
return has3dPages.includes(this.$route.path);
}
},
watch: {
questions: {
async handler(arr) {
if (!this.canUse3D) return;
// 3D资源预获取
try {
var target = arr.find((question) => question?.config?.is_three_dimensions);
this.first3DQuestion = target;
if (!target) return;
this.sceneInformation = JSON.parse(target?.config?.scene_information);
if (!this.sceneInformation) {
// 解决缓存问题,答卷时加载场景信息
var res = await BrowsingRecordApi.getSurveysScene({
sn: this.$route.query.sn,
question_index: target.question_index
});
this.sceneInformation = JSON.parse(res.data?.scene_information);
}
if (!this.sceneInformation) return;
// 初始化
this.surveyId = target.config.scene;
this.$refs.SceneSurveyViewer.init();
} catch (e) {
console.warn(e);
}
},
deep: true,
immediate: true
}
},
methods: {
async onLoadingCompletion() {
// 3D资源预获取
try {
var target = this.first3DQuestion;
if (!target) return;
var scene = this.sceneInformation;
if (!scene) return;
scene.shelves.forEach((shelf) => {
var wares = shelf.wares;
shelf.cells = shelf.cells.map((cell, index) => {
return {
...cell,
...wares[index % wares.length],
showSign: !!target.config?.is_price_tag,
showLogo: false
};
});
});
this.page_shelves = {
shelves: this.shopData.shelves
};
} catch (e) {
console.warn(e);
}
}
}
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div>
<slot name="prefix" />
<template v-for="(item, index) in text" :key="index">
{{ item }}
<br v-if="index !== text.length - 1" />
</template>
<slot name="suffix" />
</div>
</template>
<script setup>
import { computed, defineProps } from 'vue';
import { getLanguage } from '../js/language';
import { storeToRefs } from 'pinia';
import { useQuestionStore } from '@/stores/Questions/useQuestionStore';
const props = defineProps({
langTypes: { type: Array, default: () => undefined },
translateKey: { type: String, default: '' },
translateParams: { type: Array, default: () => [] },
fullText: { type: String, default: '' }
});
const {questionsData : data } = storeToRefs(useQuestionStore())
const text = computed(() => {
if (props.fullText) {
return props.fullText.split('\n');
}
if (!props.translateKey) {
return [];
}
console.log(data);
const style = data.value.survey?.style || {};
const styleTypes = data.value.languageType
? data.value.languageType
: [style.is_en_tips ? 'en' : '', style.is_cn_tips ? 'zh' : ''].filter((i) => !!i);
const types = props.langTypes ? props.langTypes : styleTypes;
const result = getLanguage(types)[props.translateKey];
if (typeof result === 'string') {
return result.split('\n');
} else {
return result(...(props.translateParams || [])).split('\n');
}
});
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="pagination-mob">
<!-- mob端 -->
<a-spin
v-show="showPrevious && page > min && page < pages"
:spinning="prevLoading"
wrapperClassName="spin prev-spin"
>
<div
class="pfe-button btn previous my-btn"
@click="previous"
:style="`color: ${buttonTextColor};background-color: ${buttonColor}`"
>
<i class="iconfont">&#xe67f;</i>
<span>{{ prevButtonText }}</span>
</div>
</a-spin>
<a-spin v-show="page < pages" :spinning="loading" wrapperClassName="spin">
<div
class="pfe-button btn next my-btn"
:class="nextDisabled ? 'disabled' : ''"
@click="next"
:style="`color: ${buttonTextColor};background-color: ${buttonColor}`"
>
<span>{{
showStart && page === 0
? startText
: showSubmit && page + 1 === pages
? submitText
: nextText || nextButtonText
}}</span>
<i v-show="page + 1 !== pages" class="iconfont">&#xe670;</i>
</div>
</a-spin>
</div>
</template>
<script>
import { computed, defineComponent } from 'vue';
import {storeToRefs} from 'pinia';
import PfeButton from './PfeButton.vue';
import { useQuestionStore } from '@/stores/Questions/useQuestionStore';
// 引入 Spin 组件
import { Spin as ASpin } from 'ant-design-vue';
export default defineComponent({
components: { PfeButton },
props: {
// 当前页数
page: {
type: Number,
default: 1
},
// 总页数
pages: {
type: Number,
default: 1
},
// 最小页数
min: {
type: Number,
default: 1
},
// 显示上一页按钮
showPrevious: {
type: [Boolean, Number],
default: false
},
// 显示开始按钮
showStart: {
type: [Boolean, Number],
default: false
},
// 开始按钮文字
startText: {
type: String,
default: '开始'
},
// 显示提交按钮
showSubmit: {
type: [Boolean, Number],
default: false
},
// 提交按钮文字
submitText: {
type: String,
default: '提交'
},
// 按钮背景颜色
buttonColor: {
type: String,
default: ''
},
// 按钮文字颜色
buttonTextColor: {
type: String,
default: ''
},
// 下一页按钮文字
nextText: {
type: String,
default: ''
},
// 下一页按钮禁用
nextDisabled: {
type: Boolean,
default: false
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
prevLoading: {
type: Boolean,
default: false
}
},
setup(props, context) {
const {questionsData} = storeToRefs(useQuestionStore())
const prevButtonText = computed(() => questionsData.value?.survey?.style?.up_button_text || '上一页');
const nextButtonText = computed(() => questionsData.value?.survey?.style?.next_button_text || '下一页');
// 上一页
function previous() {
if (props.page > props.min) {
context.emit('previous');
context.emit('update:page', props.page - 1);
}
}
// 下一页
function next() {
if (props.nextDisabled) return;
if (props.page < props.pages) {
context.emit('next');
context.emit('update:page', props.page + 1);
}
}
return {
prevButtonText,
nextButtonText,
previous,
next
};
}
});
</script>
<style lang="scss" scoped>
.pagination {
display: flex;
align-items: center;
justify-content: center;
.p-btn {
min-width: 96px;
margin: 0 12px;
border: 0;
:deep(span) {
white-space: pre;
}
}
}
.pagination-mob {
display: flex;
align-items: center;
justify-content: center;
.spin {
width: calc(50% - 30px);
.btn {
width: 100%;
}
}
.prev-spin {
margin-right: 20px;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
width: calc(50% - 30px);
height: 44px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
.iconfont {
font-size: 16px;
}
}
.previous {
background-color: #fff;
color: #70b936;
// border: 1px solid $yili-default-color;
//margin-right: 20px;
.iconfont {
margin-right: 8px;
}
}
.next {
background: #70b936;
color: #fff;
.iconfont {
margin-left: 8px;
}
}
}
.my-btn:not(.disabled):active {
opacity: 0.8;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div class="pfe-button" :class="`type-${type} size-${size}`">
<i v-if="icon" class="iconfont left-icon">{{ icon }}</i>
<span>{{ text }}</span>
<i v-if="rightIcon" class="iconfont right-icon">{{ rightIcon }}</i>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
type: {
type: String,
default: 'default'
},
size: {
type: String,
default: 'default'
},
text: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
},
rightIcon: {
type: String,
default: ''
}
},
setup() {
return {};
}
});
</script>
<style lang="scss" scoped>
.pfe-button {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 4px;
cursor: pointer;
.left-icon {
margin-right: 6px;
font-size: 20px;
}
.right-icon {
margin-left: 6px;
font-size: 20px;
}
}
.type-primary {
border: 1px solid #70b936;
background: #70b936;
color: #fff;
}
.type-primary:hover {
border: 1px solid #70b936;
background: #70b936;
}
.type-default {
border: 1px solid #70b936;
background-color: #fff;
color: #70b936;
}
.type-default:hover {
color: #70b936;
}
.size-default {
height: 42px;
padding: 0 20px;
font-size: 16px;
line-height: 24px;
}
.size-small {
height: 32px;
padding: 0 16px;
font-size: 14px;
line-height: 22px;
.left-icon {
margin-right: 4px;
font-size: 16px;
}
.right-icon {
margin-left: 4px;
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="progress">
<div class="bar" :style="`width: ${percent}%;background-color: ${color}`" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
percent: {
type: Number,
default: 0
},
color: {
type: String,
default: '#fff'
}
}
});
</script>
<style lang="scss" scoped>
.progress {
height: 8px;
background-color: rgba(255, 255, 255, 0.3);
.bar {
height: 100%;
border-radius: 8px;
transition: width 0.5s;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="remark-section">
<div class="icon" v-if="isPreview">
<i class="iconfont icon-pinglun" @click="openDrawer"></i>
</div>
</div>
</template>
<script>
import { defineComponent, ref, computed } from 'vue';import { useRoute, useRouter } from 'vue-router';
import {storeToRefs} from 'pinia';
// import { useStore } from 'vuex';
// import {} from "@/stores/counter.js"
// 屏蔽 cheerio
// import * as cheerio from 'cheerio';
// import useEmitter from '@/composables/useEmitter';
import { ElMessage as message } from 'element-plus';
import { useCommonStore } from '@/stores/modules/common';
export default defineComponent({
props: {
title: {
type: String,
default: ''
},
type: {
type: [Number, String],
default: ''
},
questionIndex: {
type: Number,
default: undefined
}
},
setup(props) {
const { visible } = storeToRefs(useCommonStore());
const route = useRoute();
const router = useRouter();
const sn = computed(() => route.query.sn);
const isLogin = ref(window.localStorage.getItem('plantToken'));
const isPreview = computed(() => route.path === '/preview');
function openDrawer() {
if (!isLogin.value) {
message.success('您尚未登陆,请先登陆,再评论');
router.push({
name: 'login'
});
return;
}
const title = props.title.trim();
visible.value = true;
}
return {
sn,
// visible,
openDrawer,
isLogin,
isPreview
};
}
});
</script>
<style lang="scss" scoped>
.remark-section {
cursor: pointer;
.icon-jieshuyu {
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<div class="base">
<div ref="target" class="container-viewer-1-"></div>
<div ref="elCart" style="position: absolute; right: 80px; bottom: 80px"></div>
<div v-if="spinning" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: #fff">
<a-spin style="position: absolute; top: 50%; left: 50%" />
</div>
<div v-if="freezeRotY" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0"></div>
</div>
</template>
<script>
const viewer_ = null;
export default {
components: {
// JoyStick,
// WayPointsList
},
data() {
return {
surveyId: null,
shopData: null,
page: null,
page_shelves: null,
sceneAction: null,
elCart: null,
defaultWare: null,
isLocked: null,
freezeRotY: false,
spinning: true,
// 此变量只是
hold: null,
isWayPointMode: false,
useJoyStick: false
};
},
watch: {
page(newVal /* oldVal */) {
// console.log('page ........', newVal, oldVal)
if (viewer_) {
viewer_.flyAnimation = !(this.defaultWare?.planetid && this.isLocked);
viewer_.frostFarScene = this.isLocked;
viewer_.arrange(newVal).then(() => {
// this.$emit("onPageCompletion");
setTimeout(() => {
this.spinning = false;
}, 200);
window.parent.postMessage(
JSON.stringify({
command: 'onPageCompletion'
}),
'*'
);
// #20221018
viewer_.hold({
wareId: this.defaultWare?.planetid,
keepHold: this.isLocked
// 组件内置的固定角度有问题,单独通过遮罩实现
// freezeRotY: this.freezeRotY ? "0" : undefined,
});
});
}
},
page_shelves(newVal, oldVal) {
console.log('page ........', newVal, oldVal);
if (viewer_) {
viewer_.arrange_shelves(newVal).then(() => {
// this.$emit("onPageCompletion");
setTimeout(() => {
this.spinning = false;
}, 200);
window.parent.postMessage(
JSON.stringify({
command: 'onPageCompletion'
}),
'*'
);
// #20221018
viewer_.hold({
wareId: this.defaultWare?.planetid,
keepHold: this.isLocked
// 组件内置的固定角度有问题,单独通过遮罩实现
// freezeRotY: this.freezeRotY ? "0" : undefined,
});
});
}
},
sceneAction(newVal /* oldVal */) {
if (viewer_ && newVal) {
viewer_.action(newVal.action);
}
},
elCart(newVal /* oldVal */) {
if (viewer_ && newVal) {
// viewer_.elCart = newVal;
viewer_.elCart = this.$refs.elCart;
}
}
},
mounted() {
window.addEventListener('message', (e) => {
let data = null;
try {
data = JSON.parse(e.data);
} catch (e) {
// mark
}
if (data?.command === 'setData') {
Object.keys(data.data).forEach((key) => {
this[key] = data.data[key];
});
this.$nextTick(() => this.tryInitView());
}
});
window.parent.postMessage(
JSON.stringify({
command: 'getData'
}),
'*'
);
},
// beforeUnmount: function () {
// if (viewer_) {
// viewer_.dispose();
// viewer_ = null;
// }
// },
methods: {
debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
},
joyChange(e) {
if (viewer_) {
viewer_.doJoystick(e);
}
},
gotoWayPoint(e) {
if (viewer_) {
viewer_.gotoWayPoint(e.wayPoint);
}
},
tryInitView() {
if (!viewer_) {
// const isHall = this.shopData.type === 'hall';
// this.useJoyStick = this.shopData.type == "hall";
// useJoyStick: isHall && this.shopData?.hall.useRoaming // #20230802
/* useJoyStick: isHall; *//* && isHallCanWalk(this.shopData.hall); */ // #20230811
const sd = JSON.parse(JSON.stringify(this.shopData || {}));
sd.shelves.forEach((shelve) => {
shelve.hideWhenSurvey = true; // hideWhenSurvey 改成 true 时,不显示展台的紫色边框
});
// viewer_ = new SurveyViewer({
// container: this.$refs.target,
// surveyId: this.surveyId,
// shopData: sd,
//
// prefixAsset: '/shelves-v5-asset' // v5, #20230104
// });
viewer_.on('loadingCompletion', () => {
// this.$emit("onLoadingCompletion");
window.parent.postMessage(
JSON.stringify({
command: 'onLoadingCompletion'
}),
'*'
);
});
const changeHoldDebounce = this.debounce((d) => (this.hold = d), 500);
viewer_.on('hold', (d) => {
this.hold = null;
// this.$emit("onHold", d);
window.parent.postMessage(
JSON.stringify({
command: 'onHold',
data: d
}),
'*'
);
changeHoldDebounce(d);
});
viewer_.on('from_scene_hold_to_shelf', () => {
// this.$emit("onFromSceneHoldToShelf");
if (!this.hold) return;
this.sceneAction = {
action: 'hold_to_shelf'
};
window.parent.postMessage(
JSON.stringify({
command: 'onFromSceneHoldToShelf'
}),
'*'
);
this.hold = null;
});
viewer_.on('behavior_flush', (q1) => {
// this.$emit("onBehaviorFlush", q1);
window.parent.postMessage(
JSON.stringify({
command: 'onBehaviorFlush',
data: q1
}),
'*'
);
});
viewer_.startup();
}
return viewer_;
}
}
};
</script>
<style>
html,
body {
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
<style scoped lang="scss">
.base {
position: relative;
width: 100%;
height: 100%;
}
div.container-viewer-1- {
width: 100%;
height: 100%;
}
div.container-viewer-1-:focus {
outline: none;
}
//div.container-viewer-1- > > > canvas:focus {
// outline: none;
//}
.bottom-points-list {
bottom: 140px;
:deep(.yo-thumb .yo-obj-fit-cover) {
object-fit: cover;
width: 100% !important;
height: 100%;
}
:deep(.yo-thumb) {
cursor: pointer;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,137 @@
<template>
<div class="question-introduce-container">
<div v-if="showTitle" class="title-part">
<rich-text
isPreview
:nodes="title"
class="title"
:class="isMobile ? 'm-title' : ''"
:style="`color: ${themeColor.stemColor}`"
/>
<Remark :title="label + '标题评论'" :type="1" v-if="!isAnswer" />
</div>
<div v-if="showDesc" class="desc-part" :class="isMobile ? 'm-desc-part' : ''">
<rich-text
isPreview
:nodes="desc"
:class="isMobile ? 'm-desc' : 'desc'"
:style="`color: ${themeColor.stemColor}`"
/>
<div style="margin-top: 32px" v-if="desc">
<Remark :title="label + '介绍语评论'" :type="2" v-if="!isAnswer" />
</div>
</div>
<!-- <AnswerViewerPrefetch :questions="questions" />-->
</div>
</template>
<script>
import { defineComponent } from 'vue';
import RichText from '@/components/RichText.vue';
import Remark from '../../components/Remark/index.vue';
import AnswerViewerPrefetch from '../AnswerViewerPrefetch.vue';
import { useQuestionStore } from '@/stores/Questions/useQuestionStore';
import { storeToRefs } from 'pinia';
export default defineComponent({
components: { RichText, Remark, AnswerViewerPrefetch },
props: {
title: {
type: String,
default: ''
},
desc: {
type: String,
default: ''
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
},
// 是否答题模式
isAnswer: {
type: Boolean,
default: false
},
// 显示标题
showTitle: {
type: [Boolean, Number],
default: false
},
// 显示介绍语
showDesc: {
type: [Boolean, Number],
default: false
},
label: {
type: String,
default: ''
},
//
questions: {
type: Array,
default: () => []
}
},
setup() {
const { themeColor } = storeToRefs(useQuestionStore());
return { themeColor };
}
});
</script>
<style lang="scss" scoped>
.question-introduce-container {
display: flex;
flex-direction: column;
.title-part {
display: flex;
align-items: center;
justify-content: space-between;
.title {
flex: 1;
}
}
.desc-part {
display: flex;
align-items: center;
justify-content: space-between;
.title {
flex: 1;
}
}
.m-desc-part {
margin-bottom: 30px;
}
}
.title {
font-size: 24px;
}
.desc {
flex: 1;
margin-top: 32px;
}
.m-title {
margin-top: 10px;
}
.m-desc {
flex: 1;
margin-top: 30px;
:deep(img) {
max-width: 100% !important;
height: auto !important;
}
}
</style>

View File

@@ -0,0 +1,325 @@
<template>
<div class="question-concluse-wrapper">
<div class="content">
<div v-if="isTemplate" v-html="survey.success_end_content" />
<div v-if="code === 20004" v-html="survey.screening_end_content" />
<div v-else-if="code === 20011" v-html="survey.success_end_content" />
<div v-else-if="code === 20016" v-html="survey.quota_end_content" />
</div>
<div v-if="!isAnswer" class="comment">
<!-- <Remark v-if="code === 20004" title="结束语提前结束评论" :type="6" />-->
<!-- <Remark v-if="code === 20011" title="结束语成功完成评论" :type="5" />-->
<!-- <Remark v-if="code === 20016" title="配额超限页评论" :type="8" />-->
</div>
<!-- 立即跳转动画 -->
<div v-if="isEndUrl" class="animation-wrapper">
<div ref="animation" class="animation" @click="toEndUrl"></div>
</div>
<!-- 跳转弹窗 -->
<div v-if="isEndUrl && countTime >= 0" class="mask">
<div class="modal">
<div class="m-title">
<LangTranslate
v-if="countTime > 0"
translate-key="QLast_RedirectingIn5Seconds"
:translate-params="[countTime]"
/>
<LangTranslate v-else translate-key="QLast_IosRedirectingPrompt" />
</div>
<div class="m-bottom">
<LangTranslate v-if="countTime > 0" translate-key="QLast_Stay" class="m-btn border-right" @click="stopJump" />
<LangTranslate translate-key="QLast_Redirect" class="m-btn" @click="jump" />
</div>
</div>
</div>
<LangTranslate
v-if="isAnswer && questionsData.survey?.style?.is_yip"
translate-key="PoweredByDigitalTechnologyCenterYIP"
class="footer"
>
<template #prefix>
<img src="https://diaoyan-files.automark.cc/yili_logo.png" alt="" class="yip-icon" />
</template>
</LangTranslate>
</div>
</template>
<script>
import { computed, defineComponent, getCurrentInstance, ref, inject, onMounted } from 'vue';
// import Remark from '@/views/Survey/views/components/Remark/index.vue';
// 屏蔽动效库
// import lottie from 'lottie-web';
import redJson from '@/views/Survey/views/Preview/components/json/red.json';
// import lottieJson from './json/lottie.json';
import LangTranslate from '../LangTranslate.vue';
import {storeToRefs} from 'pinia';
import { useQuestionStore} from "@/stores/Questions/useQuestionStore"
export default defineComponent({
props: {
action: {
type: Object,
default: () => {}
},
code: {
type: Number,
default: 0
},
survey: {
type: Object,
default: () => {}
},
// 是否答题模式
isAnswer: {
type: Boolean,
default: false
},
isTemplate: { type: Boolean, default: false }
},
components: {
// Remark,
LangTranslate
},
setup(props) {
const { proxy } = getCurrentInstance();
// const questionsData = inject('questionsData'); // 问卷数据
const {questionsData} = storeToRefs(useQuestionStore());
console.log(11, questionsData, props.survey);
let countTimer; // 跳转计时器
const animation = ref(redJson); // 立即跳转动画
const countTime = ref(0); // 跳转倒计时
// 是否有跳转链接
const isEndUrl = computed(() => {
const code = props.action ? props.action.code : props.code;
return (
(code === 20004 && props.survey.screening_end_url_select && props.survey.screening_end_url)
|| (code === 20011 && props.survey.success_end_url_select && props.survey.success_end_url)
|| (code === 20016 && props.survey.quota_end_url_select && props.survey.quota_end_url)
);
});
// 跳转
function toEndUrl() {
switch (props.action.code) {
case 20004: // 被甄别
if (props.survey.screening_end_url_select && props.survey.screening_end_url) {
const url = props.survey.screening_end_url;
toUrl(url);
}
break;
case 20011: // 成功
if (props.survey.success_end_url_select && props.survey.success_end_url) {
const url = props.survey.success_end_url;
toUrl(url);
}
break;
case 20016: // 配额超限
if (props.survey.quota_end_url_select && props.survey.quota_end_url) {
const url = props.survey.quota_end_url;
toUrl(url);
}
break;
default:
break;
}
}
// 跳转链接
function toUrl(url) {
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');
}
}
// 跳转倒计时
function cuontDown() {
if (isEndUrl.value) {
if (props.action.code === 20004) {
countTime.value = props.survey.screening_standing_time;
} else if (props.action.code === 20011) {
countTime.value = props.survey.success_standing_time;
} else if (props.action.code === 20016) {
countTime.value = props.survey.quota_standing_time;
}
countTimer = setInterval(() => {
if (countTime.value === 0) {
toEndUrl();
clearInterval(countTimer);
} else {
countTime.value -= 1;
}
}, 1000);
}
}
// 停止跳转
function stopJump() {
countTime.value = -1;
clearInterval(countTimer);
}
// 立即跳转
function jump() {
toEndUrl();
stopJump();
}
function loadAnimation() {
// 立即跳转动画
if (animation.value) {
// lottie.loadAnimation({
// container: animation.value,
// animationData: lottieJson
// });
}
}
onMounted(() => {
cuontDown();
loadAnimation();
});
return {
questionsData,
animation,
isEndUrl,
toEndUrl,
countTime,
stopJump,
jump
};
}
});
</script>
<style lang="scss" scoped>
.question-concluse-wrapper {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
height: 100%;
.content {
flex: 1;
}
.comment {
width: 30px;
}
}
.animation-wrapper {
width: 100%;
}
.animation {
width: 75px;
height: 75px;
margin: 50px auto 0;
cursor: pointer;
}
.mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: rgba(1, 21, 53, 0.7);
//filter: alpha(opacity=50);
.red-animation {
width: 100%;
max-width: 300px;
}
.receive {
width: 120px;
height: 34px;
border-radius: 20px;
background: linear-gradient(135deg, #f78f4f 0%, #e94435 100%);
color: #fff;
font-size: 14px;
line-height: 34px;
text-align: center;
cursor: pointer;
}
.modal {
width: 80%;
max-width: 300px;
border-radius: 10px;
background-color: #fff;
text-align: center;
.m-title {
padding: 30px 20px;
font-size: 16px;
}
.m-bottom {
display: flex;
.m-btn {
flex: 1;
padding: 20px 0;
border-top: 1px solid #f5f5f5;
color: #1c6fff;
font-size: 16px;
cursor: pointer;
}
.m-btn.border-right {
border-right: 1px solid #f5f5f5;
}
}
}
}
.footer {
bottom: 0;
left: 0;
width: 100%;
height: 50px;
color: #a3a3a3;
font-size: 12px;
text-align: center;
.yip-icon {
width: auto !important;
height: 20px !important;
margin-right: 6px;
margin-bottom: 0 !important;
transform: translateY(-2px);
}
}
</style>

View File

@@ -0,0 +1,378 @@
<template>
<a-radio-group v-model:value="value" @change="changeValue" :disabled="disabled" v-if="customRadio">
<div class="radio-group">
<div class="group" v-for="(group, groupIndex) in optionGroups" :key="groupIndex">
<div
v-if="group.title && !config.option_groups?.is_hide"
v-show="showGroupTitle(group.options)"
class="group-title answer-color"
>
{{ group.title }}
</div>
<div
class="radio theme-hover-default"
:class="group.title && !config.option_groups?.is_hide ? 'margin-left' : ''"
v-for="option in group.options"
:key="option.option_key"
:style="isMobile ? 'width: 100%' : `width: calc(100% / ${config.each_number || 1} - 33px)`"
v-show="!hideOptions.includes(option.option_key)"
@click="onChangeValue(option.option_key)"
>
<a-radio :value="option.option_key" @click.stop />
<div class="option scrollbar answer-color">
<rich-text :nodes="option.option" isPreview />
</div>
<a-input
v-if="option.is_other"
v-model:value="option.value"
@change="changeInput($event, option.option_key)"
:disabled="disabled"
/>
</div>
</div>
</div>
</a-radio-group>
<choice :element="question" v-else />
</template>
<script setup>
import Choice from '@/views/Design/components/Questions/Choice.vue';
const customRadio = true;
</script>
<script>
import RichText from '@/components/RichText.vue';
import { computed, defineComponent, ref, watch } from 'vue';
import AnswerViewer from '@/views/Survey/views/Preview/AnswerViewer.vue';
import { compareArrayByField, randomOptions } from '@/utils/utils.js';
export default defineComponent({
components: { RichText, AnswerViewer },
props: {
// 题干
stem: {
type: String,
default: ''
},
// 列表
list: {
type: Array,
default: () => []
},
// 配置
config: {
type: Object,
default: () => {
}
},
// 答案
answer: {
type: Object,
default: () => {
}
},
// 答案索引
answerIndex: {
type: String,
default: ''
},
// 隐藏选项
hideOptions: {
type: Array,
default: () => []
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
},
// 样本SN
answerSn: {
type: String,
default: ''
},
answerSurveySn: {
type: String,
default: ''
},
question: {
type: Object,
default: () => ({})
}
},
setup(props, context) {
const value = ref(''); // 值
const options = ref([]); // 选项
const optionGroups = ref([]); // 分组
console.log(`radio input question: `, props.question);
// 初始化
function init() {
props.list.forEach((list) => {
options.value = [...options.value, ...list.options];
});
if (props.answer) {
value.value = Object.keys(props.answer)[0];
options.value.forEach((option) => {
if (option.is_other && option.option_key === value.value) {
option.value = props.answer[value.value];
}
});
}
}
init();
// 选项分组
function setOptionGroups() {
optionGroups.value = [];
const copyOptions = JSON.parse(JSON.stringify(options.value));
props.config.option_groups?.option_group.forEach((optionGroup) => {
const group = {
title: optionGroup.title,
options: []
};
optionGroup.groups.forEach((groups) => {
const index = copyOptions.findIndex((option) => option.option_key === groups.option_key);
if (index === -1) return;
group.options.push(copyOptions.splice(index, 1)[0]);
});
group.options = sortOptions(
group.options,
props.question.config.select_random && props.question.config.option_group_random_inside
);
optionGroups.value.push(group);
});
optionGroups.value = sortOptions(
optionGroups.value,
props.question.config.select_random && props.question.config.option_group_random_outside
);
// 若是 group 是undefined ,默认给一个空对象
const group = optionGroups.value.find((group) => !group.title) ?? {};
// console.log(group);
group.options = sortOptions(copyOptions, props.question.config.select_random);
}
// 排序(固定项和其他项不参与随机)
function sortOptions(oldOptions, isRandom) {
const sorts = [];
const fixed = [];
const others = [];
const removeOther = [];
oldOptions.forEach((option) => {
if (option.is_remove_other) {
removeOther.push(option);
} else if (option.is_other) {
others.push(option);
} else if (option.is_fixed) {
fixed.push(option);
} else {
sorts.push(option);
}
});
return [...randomOptions(sorts, isRandom), ...fixed, ...others, ...removeOther];
}
// 选择回调
function changeValue(e) {
// 更新答案
const option = options.value.find((option) => option.option_key === e.target.value);
context.emit('update:answer', {
[e.target.value]: option.value || (option.is_other ? '' : '1')
});
// 清空未选中项输入框值
options.value.forEach((option) => {
if (option.is_other && option.option_key !== e.target.value) {
option.value = '';
}
});
}
function onChangeValue(key) {
value.value = key;
changeValue({ target: { value: key } });
}
// 输入回调
function changeInput(e, key) {
const option = options.value.find((option) => option.option_key === key);
option.value = e.target.value;
value.value = key;
// 更新答案
context.emit('update:answer', { [key]: e.target.value });
}
// 监听答案
watch(
() => props.answer,
() => {
context.emit('changeAnswer', {
options: optionGroups.value.flatMap((group) => group.options || []),
value: value.value
});
// 质量控制
const timer = setTimeout(() => {
if (value.value) {
const index = optionGroups.value
.flatMap((group) => group.options.map((option) => option))
.findIndex((option) => option.option_key === value.value);
context.emit('update:answerIndex', index + '');
} else if (props.answerIndex) {
context.emit('update:answerIndex', '');
}
clearTimeout(timer);
});
},
{
deep: true,
immediate: true
}
);
// 监听list更新关联选项
watch(
() => props.list,
() => {
// 更新关联题选项
let newOptions = [];
props.list.forEach((list) => {
newOptions = [...newOptions, ...list.options];
});
// 其他项
newOptions.forEach((option) => {
if (option.is_other && option.option_key === value.value) {
const timer = setTimeout(() => {
option.value = props.answer[value.value];
clearTimeout(timer);
});
}
});
if (
!compareArrayByField(options.value, newOptions, 'option_key')
|| !compareArrayByField(options.value, newOptions, 'option')
) {
options.value = newOptions;
}
// 清空值和答案
if (value.value && options.value.findIndex((option) => option.option_key === value.value) === -1) {
// 清空值
value.value = '';
// 清空答案
context.emit('update:answer', null);
}
},
{
deep: true
}
);
// 监听list更新关联选项
watch(
() => options.value,
(val, oldVal) => {
if (compareArrayByField(val, oldVal || [], 'option_key') && compareArrayByField(val, oldVal || [], 'option')) {
return;
}
setOptionGroups();
},
{
deep: true,
immediate: true
}
);
const onHoldToCart = (target) => {
value.value = target.option_key;
changeValue({
target: {
value: target.option_key
}
});
};
const cartWaresLength = computed(() => {
if (!props.answer) return 0;
return Object.keys(props.answer).length;
});
console.log(`car wares length:`, cartWaresLength.value);
// 显示分组标题
function showGroupTitle(groupOptions) {
const option = groupOptions.find((option) => !props.hideOptions.includes(option.option_key));
return !!option;
}
return {
value,
options,
optionGroups,
changeValue,
onChangeValue,
changeInput,
onHoldToCart,
cartWaresLength,
showGroupTitle
};
}
});
</script>
<style lang="scss" scoped>
.radio-group {
display: flex;
flex-wrap: wrap;
margin-bottom: -18px;
.group {
display: flex;
flex-wrap: wrap;
width: 100%;
.group-title {
margin-bottom: 18px;
}
}
.radio {
display: flex;
align-items: center;
margin-right: 32px;
margin-bottom: 6px;
padding: 6px 8px;
border-radius: 3px;
:deep(.ant-radio-wrapper) {
margin-right: 0;
}
.option {
overflow: auto;
margin-right: 8px;
padding-left: 8px;
}
}
.margin-left {
padding-left: 32px;
}
.ant-input {
width: 225px;
}
}
.ant-radio-group {
width: 100%;
}
</style>

View File

@@ -0,0 +1,447 @@
<template>
<div class="question">
<!-- 高级题型不显示 -->
<div class="question-inner-wrapper" v-if="questionType <= 100">
<div class="title" :style="`color: ${themeColor.stemColor}`">
<span v-if="showTitle" class="question-inner-span" v-html="title"></span>
<!-- question.stem -->
<div class="stem">
<rich-text :nodes="`${newTitle}${tip}`" isPreview :isMobile="isMobile" />
</div>
</div>
<!-- <Remark-->
<!-- :title="title + '问题评论'"-->
<!-- :type="3"-->
<!-- :questionIndex="questionIndex"-->
<!-- v-if="!isAnswer"-->
<!-- style="margin-bottom: 22px"-->
<!-- />-->
</div>
<LangTranslate v-if="error && questionType <= 100" :full-text="error" class="error" />
<LangTranslate v-if="warning" :full-text="warning" class="error warning" />
<!-- -->
<slot />
</div>
</template>
<script>
import RichText from '@/components/RichText.vue';
import { computed, defineComponent } from 'vue';
// import Remark from '../components/Remark/index.vue';
import LangTranslate from '../LangTranslate.vue';
import { useQuestionStore } from '@/stores/Questions/useQuestionStore';
import { storeToRefs } from 'pinia';
export default defineComponent({
components: { RichText, /*Remark,*/ LangTranslate },
props: {
// 标题
stem: {
type: String,
default: ''
},
title: {
type: String,
default: ''
},
// 标题
tip: {
type: String,
default: ''
},
// 错误
error: {
type: String,
default: ''
},
// 警告
warning: {
type: String,
default: ''
},
// 题型
questionType: {
type: Number,
default: 0
},
// 题号
questionIndex: {
type: Number,
default: 0
},
// 问卷列表
questions: {
type: Array,
default: () => []
},
// 是否显示题号
showTitle: {
type: [Boolean, Number],
default: false
},
// 是否移动端
isMobile: {
type: Boolean,
default: false
},
// 是否答题模式
isAnswer: {
type: Boolean,
default: false
}
},
setup(props) {
// const themeColor = inject('themeColor'); // 主题颜色
const { themeColor } = storeToRefs(useQuestionStore());
const buttonColor = themeColor.value?.buttonColor; // 按钮颜色
const buttonTextColor = themeColor.value?.buttonTextColor; // 按钮文字颜色
const answerColor = themeColor.value?.answerColor || '#333333'; // 答案颜色
const answerColor30 = toRgba(answerColor, 0.3); // 背景颜色(透明度30s)
const answerColor10 = toRgba(answerColor, 0.1); // 背景颜色(透明度10)
const answerBorder = answerColor === '#333333' ? '#d9d9d9' : answerColor; // 边框颜色
const answerPlaceholder = toRgba(answerColor, 0.3); // placeholder颜色
// 鼠标 hover 时的背景色,目前用于:单选、多选、图片单选、图片多选的选项
const hoverBackgroundColor = themeColor.value?.buttonColor + '1E';
// 16进制转rgba
function toRgba(color, opacity) {
return (
'rgba('
+ parseInt(color.substring(1, 3), 16)
+ ','
+ parseInt(color.substring(3, 5), 16)
+ ','
+ parseInt(color.substring(5, 7), 16)
+ ','
+ opacity
+ ')'
);
}
const replaceStr = (str, firIndex, lastIndex, char) => {
if (str.length == 0) {
return '';
}
if (str.indexOf('<p>') < 0) {
return str;
}
const strAry = str.split('');
strAry[firIndex] = char;
strAry[lastIndex] = char;
return strAry.join('');
};
const newTitle = computed(() => {
const cycleIndexStr = props.title?.match(/\.\d/)?.[0] || ''; // 有循环体组的题,需要在没找到被关联的题目情况下,在循环题组内再查找一次
let title = props.stem;
const matchArr = title.match(/(\[%cite\(.*?\)%\])/g) || [];
matchArr.forEach((matchValue) => {
const value = matchValue.replace('[%cite(', '').replace(')%]', '');
let replacement = ''; // 替换文本
// 查找引用问题
const question = props.questions.find((question) => {
// 矩阵题
if (question.question_type >= 8 && question.question_type <= 11) {
return question.title === value.split('_R')[0].split('_C')[0];
}
// 排序题
if (question.question_type === 16) {
return question.title === value.split('_A')[0];
}
return question.title === value;
})
|| props.questions.find((question) => {
// 矩阵题
if (question.question_type >= 8 && question.question_type <= 11) {
return question.title === (value + cycleIndexStr).split('_R')[0].split('_C')[0];
}
// 排序题
if (question.question_type === 16) {
return question.title === (value + cycleIndexStr).split('_A')[0];
}
return question.title === value + cycleIndexStr;
});
if (question) {
let options = []; // 选项
question.list.forEach((list) => {
options = [...options, ...list.options];
});
if (question.answer) {
const { answer } = question;
if (question.question_type === 1) {
// 查找引用选项(单选)
const option = options.find((option) => option.option_key === Object.keys(answer)[0]);
if (answer[option.option_key] === '1') {
replacement = option.option;
} else {
replacement = answer[option.option_key];
}
} else if (question.question_type === 2 && Object.keys(answer).length >= question.config.min_select) {
// 查找引用选项(多选)
options.forEach((option) => {
if (answer[option.option_key] === '1') {
replacement += option.option;
} else if (answer[option.option_key]) {
replacement += answer[option.option_key];
}
});
} else if (question.question_type === 4) {
// 查找引用选项(填空)
replacement = answer.value;
} else if (question.question_type === 5 && options.length > 0) {
// 查找引用选项(打分)
options.forEach((option) => {
replacement += option.value;
});
} else if (question.question_type === 17) {
// 查找引用选项(恒定总和)
options.forEach((option) => {
replacement += option.option;
});
} else if (question.question_type >= 8 && question.question_type <= 11) {
// 查找引用选项(矩阵题)
// const val = value.split("_").reverse()[0];
// let row = [];
// let col = [];
// question.list.forEach((list) => {
// if (list.type === 1) {
// row = [...row, ...list.options];
// } else if (list.type === 2) {
// col = [...col, ...list.options];
// }
// });
// if (val[0] === "R") {
// col.forEach((colItem) => {
// replacement += colItem.option;
// });
// } else if (val[0] === "C") {
// row.forEach((rowItem) => {
// replacement += rowItem.option;
// });
// }
} else if (question.question_type === 16) {
// 查找引用选项(排序)
let optionKey;
const sort = value.split('_A')[1] * 1;
Object.keys(answer).forEach((key) => {
if (answer[key] === sort) {
optionKey = key;
}
});
const option = options.find((option) => option.option_key === optionKey);
replacement += option?.option || '';
}
}
}
// 引用的p标签变成span标签
replacement = replaceStr(replacement, 1, replacement.length - 2, 'span');
replacement = replacement.replace(/<\/{0,1}(?!img).+?>/gi, '');
title = title.replace(`${matchValue}`, replacement);
// .replaceAll("<label><p ", "<label><span ")
// .replaceAll("<label><p>", "<label><span>")
// .replaceAll("</p></label>", "</span></label>");
console.log('title', title);
});
return title;
});
return {
newTitle,
buttonColor,
buttonTextColor,
themeColor,
answerColor,
answerColor30,
answerColor10,
hoverBackgroundColor,
answerBorder,
answerPlaceholder
};
}
});
</script>
<style lang="scss" scoped>
.question {
.question-inner-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
display: flex;
flex: 1;
min-height: 24px;
margin-bottom: 24px;
> span {
flex-shrink: 0;
}
.stem {
flex: 1;
}
}
.error {
margin: -22px 0 10px;
padding-left: 2em;
color: #ff374f;
font-size: 12px;
line-height: 20px;
}
.warning {
color: #f7860f;
}
:deep(p) {
margin-bottom: 0;
}
}
// .question-inner-span {
// width: 100%;
// }
</style>
<style lang="scss" scoped>
:deep(.theme-hover-default) {
&:hover {
background-color: v-bind(hoverbackgroundcolor);
cursor: pointer;
}
}
:deep(.ant-radio-inner) {
border-color: v-bind(answercolor) !important;
background-color: transparent;
}
:deep(.ant-radio-inner::after) {
background-color: v-bind(answercolor);
}
:deep(.ant-checkbox-inner) {
border-color: v-bind(answercolor) !important;
background-color: transparent;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: v-bind(answercolor);
}
:deep(.ant-input) {
border-color: v-bind(answerborder) !important;
background-color: transparent;
color: v-bind(answercolor);
}
:deep(.ant-input-number) {
border-color: v-bind(answerborder) !important;
background-color: transparent;
color: v-bind(answercolor);
}
:deep(.ant-select-selector) {
border-color: v-bind(answerborder) !important;
background-color: transparent !important;
color: v-bind(answercolor);
}
:deep(.ant-input:hover) {
border-color: v-bind(answercolor);
}
:deep(.ant-input::placeholder) {
color: v-bind(answerplaceholder) !important;
}
:deep(.ant-select-selection-placeholder) {
color: v-bind(answerplaceholder) !important;
}
// :deep(.ant-rate) {
// color: v-bind(answerColor) !important;
// }
// :deep(.ant-rate-star-zero .ant-rate-star-second) {
// color: v-bind(answerColor30);
// }
// 打分组件样式 start
:deep(.num-item), // 旧的组件的样式
:deep(.rate-wrapper .number-item .content) {
border-color: v-bind(answercolor) !important;
color: v-bind(answercolor) !important;
}
:deep(.num-item-active), // 旧的组件的样式
:deep(.rate-wrapper .number-item.active .content) {
border-color: v-bind(answercolor) !important;
background-color: v-bind(answercolor) !important;
color: #fff !important;
}
:deep(.rich-rate-item-active) {
border-color: v-bind(answercolor) !important;
background-color: v-bind(answercolor) !important;
}
:deep(.step-inner) {
border-color: v-bind(answercolor) !important;
}
:deep(.ant-slider-track), // 旧的组件的样式
:deep(.slider-bar::before) {
background-color: v-bind(answercolor) !important;
}
:deep(.slider-step .slider-dot.selected .content::before) {
border-color: v-bind(answercolor) !important;
}
:deep(.ant-slider-handle) {
border-color: v-bind(answercolor) !important;
}
:deep(.ant-slider-dot-active) {
border-color: v-bind(answercolor) !important;
}
:deep(.ant-slider-mark-text) {
color: v-bind(answercolor);
}
// 打分组件样式 end
:deep(.ant-upload) {
border-color: v-bind(answerborder) !important;
background-color: v-bind(answercolor10) !important;
}
:deep(.pfe-button) {
background-color: v-bind(buttoncolor) !important;
color: v-bind(buttontextcolor) !important;
}
// 公共样式
:deep(.answer-color) {
color: v-bind(answercolor) !important;
}
:deep(.answer-border) {
border-color: v-bind(answerborder) !important;
}
:deep(.answer-background30) {
background-color: v-bind(answercolor30) !important;
}
:deep(.answer-background10) {
background-color: v-bind(answercolor10) !important;
}
</style>

View File

@@ -0,0 +1,516 @@
{
"cells": [
{
"name": "商品1",
"box": {
"position": { "x": -0.6093719155636137, "y": 1.1925115714503842, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.029369916622918046, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware1",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1.png"
},
"surveyLogo": "",
"surveyPrice": 190
},
{
"name": "商品2",
"box": {
"position": { "x": -0.18520626992561548, "y": 1.1908614160723439, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.03174731410176801, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware2",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2.png"
},
"surveyLogo": "",
"surveyPrice": 225
},
{
"name": "商品3",
"box": {
"position": { "x": 0.2287325535366465, "y": 1.1892382329257407, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.028571714546698213, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware3",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3.png"
},
"surveyLogo": "",
"surveyPrice": 210
},
{
"name": "商品4",
"box": {
"position": { "x": 0.661393068886308, "y": 1.1909523674363642, "z": 0.4537964589333506 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.03136253199426353, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware4",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4.png"
},
"surveyLogo": "",
"surveyPrice": 195
},
{
"name": "商品1",
"box": {
"position": { "x": -0.605500512913119, "y": 0.8538858659191146, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.0027324557304382324,
"y": -0.03184773059511403,
"z": 0.1743204391002655
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.1499999999999999, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware1",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1.png"
},
"surveyLogo": "",
"surveyPrice": 190
},
{
"name": "商品2",
"box": {
"position": {
"x": -0.18658027999214435,
"y": 0.8512643658064176,
"z": 0.45017966628074646
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": { "x": 0, "y": -0.02759083972388149, "z": 0.1743204391002655 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.1499999999999999, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware2",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2.png"
},
"surveyLogo": "",
"surveyPrice": 225
},
{
"name": "商品3",
"box": {
"position": { "x": 0.2230628973331703, "y": 0.8511807287232012, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.005585700273513794,
"y": -0.032213962561231047,
"z": 0.1743204391002655
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.1499999999999999, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware3",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3.png"
},
"surveyLogo": "",
"surveyPrice": 210
},
{
"name": "商品4",
"box": {
"position": { "x": 0.6551478924840007, "y": 0.8483097259524031, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.02673691703609271, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware4",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4.png"
},
"surveyLogo": "",
"surveyPrice": 195
},
{
"name": "商品1",
"box": {
"position": { "x": -0.60433011332925, "y": 0.5125224579062782, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.0027324557304382324,
"y": -0.034508354476310954,
"z": 0.1743204391002655
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware1",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1.png"
},
"surveyLogo": "",
"surveyPrice": 190
},
{
"name": "商品2",
"box": {
"position": { "x": -0.1914175066620415, "y": 0.5095275839341008, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": { "x": 0, "y": -0.027170852492222397, "z": 0.1743204391002655 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware2",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2.png"
},
"surveyLogo": "",
"surveyPrice": 225
},
{
"name": "商品3",
"box": {
"position": { "x": 0.22360906333448538, "y": 0.5101818344822482, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.005585700273513794,
"y": -0.030842453533999448,
"z": 0.1743204391002655
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware3",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3.png"
},
"surveyLogo": "",
"surveyPrice": 210
},
{
"name": "商品4",
"box": {
"position": { "x": 0.6482247451401058, "y": 0.504394091002413, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.02471250348374021, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware4",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4.png"
},
"surveyLogo": "",
"surveyPrice": 195
},
{
"name": "商品1",
"box": {
"position": { "x": -0.6134215690979794, "y": 0.1945505440235138, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.0027324557304382324,
"y": -0.024228371171756358,
"z": 0.1846828854084015
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware1",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can1.png"
},
"surveyLogo": "",
"surveyPrice": 190
},
{
"name": "商品2",
"box": {
"position": {
"x": -0.19111044416229633,
"y": 0.1945505440235138,
"z": 0.45017966628074646
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": { "x": 0, "y": -0.021009267483951266, "z": 0.1846828854084015 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware2",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can2.png"
},
"surveyLogo": "",
"surveyPrice": 225
},
{
"name": "商品3",
"box": {
"position": { "x": 0.21581759477578777, "y": 0.1945505440235138, "z": 0.45017966628074646 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 0.37358155846595764, "y": 0.225911445915699, "z": 0.21794167160987854 },
"child": {
"sign": {
"position": {
"x": -0.005585700273513794,
"y": -0.020448554878266023,
"z": 0.1846828854084015
},
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 0.7393795847892761, "y": 0.05661093816161156, "z": 0.05661093816161156 }
},
"logo": { "position": { "x": 0, "y": 0.15000000000000002, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware3",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can3.png"
},
"surveyLogo": "",
"surveyPrice": 210
},
{
"name": "商品4",
"box": {
"position": { "x": 0.6432047193626257, "y": 0.19147498005043284, "z": 0.45 },
"scale": { "x": 1, "y": 1, "z": 1 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"size": { "x": 0.3736, "y": 0.2259, "z": 0.2179 },
"child": {
"sign": {
"position": { "x": -0.006, "y": -0.013996807328698846, "z": 0.1847 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"logo": { "position": { "x": 0, "y": 0.15, "z": 0 } }
}
},
"pileNumber": 6,
"pileRotationY": 0,
"planetid": "ware4",
"surveyWare": {
"type": 1,
"commodity": {
"url": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4-lod0.glb"
},
"texture": null,
"urlThumb": "https://diaoyanxingqiu.oss-cn-zhangjiakou.aliyuncs.com/3d/shelves/shelves-v3-store/v3.4/ware/milkpowder-can4.png"
},
"surveyLogo": "",
"surveyPrice": 195
}
]
}

View File

@@ -0,0 +1,301 @@
/* PC端样式 - 开始 */
.pc .page {
z-index: 999;
vertical-align: top;
width: 180px;
height: 52px;
padding: 0;
border-radius: 5px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 24px;
line-height: 52px;
text-align: center;
opacity: 1;
cursor: pointer;
user-select: none;
}
.pc .page .iconfont {
vertical-align: bottom;
font-size: 26px;
line-height: 52px;
}
.pc .cart .iconfont {
font-size: 36px;
}
.pc .pageNext {
position: absolute;
bottom: 0;
left: calc(50% + 20px);
transform: translate(0, -50%);
}
.pc .pagePre {
position: absolute;
right: calc(50% + 20px);
bottom: 0;
transform: translate(0, -50%);
}
.pc .pageNext::before {
content: '下一页';
vertical-align: top;
padding-right: 10px;
line-height: 50px;
}
.pc .pagePre::after {
content: '上一页';
vertical-align: top;
padding-left: 10px;
line-height: 50px;
}
.pc .cart {
position: fixed;
right: 72px;
bottom: 160px;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 92px;
height: 92px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.pc .cart-num {
position: absolute;
top: 5px;
right: 6px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #f00;
color: #ffff;
line-height: 20px;
text-align: center;
}
.pc .q-content {
position: absolute;
top: 50%;
left: 50%;
z-index: 998;
display: inline-block;
width: 60%;
text-align: center;
transform: translate(-50%, -50%);
}
.pc .q-content-top {
position: absolute;
top: 0;
left: 50%;
z-index: 997;
padding: 10px 20px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.5);
line-height: 30px;
opacity: 1;
transform: translate(-50%, 0);
}
//.pc .q-content-top * {
//}
.pc .q-content-top > *:nth-child(2) {
/* margin-top: 10px; */
}
.pc .q-content-top .ant-radio {
height: 16px;
font-size: 16px;
}
.pc .q-content-top .title {
color: #fff;
font-weight: normal;
}
.pc .q-content-top .ant-radio-inner::after {
background-color: #fff !important;
}
/* PC端样式 - 结束 */
/* 移动端样式 - 开始 */
.mobile .page {
z-index: 999;
vertical-align: top;
width: 120px;
padding: 0;
border-radius: 5px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
line-height: 42px;
text-align: center;
opacity: 1;
cursor: pointer;
}
.mobile .page .iconfont {
position: relative;
bottom: -2px;
font-size: 20px;
}
.mobile .cart .iconfont {
font-size: 20px;
}
.mobile .pageNext {
position: absolute;
right: 32px;
bottom: 0;
padding-left: 10px;
transform: translate(0, -50%);
}
.mobile .pagePre {
position: absolute;
bottom: 0;
left: 32px;
padding-right: 10px;
transform: translate(0, -50%);
}
.mobile .pageNext::before {
content: '下一页';
vertical-align: top;
padding-right: 10px;
line-height: 43px;
}
.mobile .pagePre::after {
content: '上一页';
vertical-align: top;
padding-left: 10px;
line-height: 43px;
}
.mobile .cart {
position: absolute;
right: 30px;
bottom: 90px;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.mobile .cart-num {
position: absolute;
top: 4px;
right: 4px;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: #f00;
color: #ffff;
font-size: 10px;
line-height: 14px;
text-align: center;
}
.mobile .q-content {
position: absolute;
top: 50%;
left: 50%;
z-index: 998;
display: inline-block;
min-width: 350px;
text-align: center;
transform: translate(-50%, -50%);
}
.mobile .q-content-top {
position: absolute;
top: 20px;
left: 50%;
z-index: 997;
width: 90%;
padding: 10px 20px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.5);
line-height: 30px;
opacity: 1;
transform: translate(-50%, 0);
}
//.mobile .q-content-top * {
//}
.mobile .q-content-top > *:nth-child(2) {
/* margin-top: 10px; */
}
//.mobile .q-content-top .ant-radio {
//}
.mobile .q-content-top .title {
color: #fff;
font-weight: normal;
}
.mobile .q-content-top .ant-radio-inner::after {
background-color: #fff !important;
}
/* 移动端样式 - 结束 */
/* 公共样式 - 开始 */
.q-content-top .radio-group {
margin-bottom: 0 !important;
}
.q-content-top .radio-group > .radio {
width: auto !important;
margin-right: 16px !important;
margin-bottom: 0 !important;
color: #fff !important;
}
.q-content-top .ant-radio-inner {
border-color: #fff !important;
}
.q-content-top .ant-checkbox-inner {
border-color: #fff !important;
}
.q-content-top .checkbox-group > .checkbox {
width: auto !important;
color: #fff !important;
}
.q-content-top p {
color: #fff !important;
}
.q-content-top .ant-input {
color: #fff !important;
}
.q-content-top .ant-input::-webkit-input-placeholder {
color: #ccc !important;
}
/* 公共样式 - 结束 */

View File

@@ -0,0 +1,126 @@
import request from '@/utils/request';
import { useCounterStore } from '@/stores/counter';
class AnswerApi {
/* 获取问卷 */
static getQuetions(params) {
return request({
method: 'GET',
url: `/answer/surveys/${params.id}`,
params: { ...params.data, time: Date.now() },
headers: {
'Cache-Control': 'no-cache',
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id')
}
});
}
/* 获取问卷详情 */
static getQuetionDetail(params) {
return request({
method: 'GET',
url: `/console/surveys/${params.id}/detail`,
params: params.data,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id') //store.state.answer.answerSessionId,
}
});
}
/* 答题 */
static answer(params) {
return request({
method: 'POST',
url: `/answer/surveys/${params.id}`,
data: params.data,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id')
}
});
}
/* 答题 */
static surveyPrevious(params) {
return request({
method: 'POST',
url: `/answer/surveys/${params.id}/prev`,
data: params.data,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id')
}
});
}
/* 获取验证码 */
static getCode(params) {
return request({
method: 'POST',
url: `/system/sms_codes`,
data: params.data,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id') //store.state.answer.answerSessionId,
}
});
}
/* 问卷密码 */
static password(params) {
return request({
method: 'PATCH',
url: `/answer/surveys/${params.id}/password`,
data: params.data,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id') //store.state.answer.answerSessionId,
}
});
}
/* word导出 */
static download(sn, params = '') {
return request({
method: 'get',
url: `/console/survey_word_export/${sn}?${params}`,
headers: {
'answer-session-id': AnswerApi.getLocalStorageWithExpiration('answer-session-id') //store.state.answer.answerSessionId,
}
});
}
// 获取localStorage数据并检查过期时间
static getLocalStorageWithExpiration(key) {
const item = JSON.parse(localStorage.getItem(key));
if (!item) {
return useCounterStore?.state?.answer?.answerSessionId || '';
}
if (!item || new Date().getTime() > item.expirationDate) {
// 数据过期,删除数据
localStorage.removeItem(key);
return null;
}
return item.value;
}
}
class BrowsingRecordApi {
/* 用户行为记录 */
static browsingRecordSurveys(params) {
return request({
method: 'post',
url: `/console/browsingRecord/surveys/${params.sn}/add`,
data: params.data
});
}
/* 获取3D资源 */
static getSurveysScene(params) {
return request({
method: 'get',
url: `/answer/surveys/${params.sn}/scene?question_index=${params.question_index}`
});
}
}
export { AnswerApi, BrowsingRecordApi };

View File

@@ -0,0 +1,586 @@
export const language = {
PoweredByDigitalTechnologyCenterYIP: {
en: 'Powered by Yili Data Technology Center',
zh: '由数字科技中心YIP提供支持'
},
PleaseSelectAtLeastOneOptionsPerLine: {
en: (count) => `Please select at least ${count} answer option${count > 1 ? 's' : ''} per row.`,
zh: (count) => `每行最少选${count}个。`
},
PleaseSelectAtLeastOneOptionsPerColumn: {
en: (count) => `Please select at least ${count} answer option${count > 1 ? 's' : ''} per column.`,
zh: (count) => `每列最少选${count}个。`
},
PleaseCategorizeAllOptions: {
en: 'Please categorize all answer options.',
zh: '部分选项未归类。'
},
ThisIsARequiredQuestion: {
en: 'This is compulsory.',
zh: '这是一道必答题。'
},
PleaseInputAValue: {
en: 'Please input a value.',
zh: '请输入。'
},
PleaseSelectAtLeastOneOptions: {
en: (count) => `Please select at least ${count} answer option${count > 1 ? 's' : ''}.`,
zh: (count) => `最少选择${count}个。`
},
PleaseSelectAtLeastOneOptionsInTitleGroup: {
en: (count, title) => `Please select at least ${count} answer option${count > 1 ? 's' : ''} in ${title} group.`,
zh: (count, title) => `${title}最少选择${count}个。`
},
PleaseSelectAtMostOneOptionsInTitleGroup: {
en: (count, title) => `Please select at most ${count} answer option${count > 1 ? 's' : ''} in ${title} group.`,
zh: (count, title) => `${title}最多选择${count}个。`
},
PleaseSelectAtLeastOnePictures: {
en: (count) => `Please select at least ${count} picture${count > 1 ? 's' : ''}.`,
zh: (count) => `最少选择${count}个。`
},
PleaseLetTheSumEqualToTotal: {
en: (count) => `Please let the sum equal to ${count}.`,
zh: (count) => `请让总和等于${count}`
},
PleaseUploadAtLeastOneFiles: {
en: (count) => `Please upload at least ${count} file${count > 1 ? 's' : ''}.`,
zh: (count) => `最少上传${count}个文件。`
},
PleaseEnterEnglishLetters: {
en: 'Please enter English letters.',
zh: '请输入字母。'
},
PleaseEnterChineseWords: {
en: 'Please enter Chinese characters.',
zh: '请输入中文。'
},
PleaseEnterACorrectEmail: {
en: 'Please enter a valid email.',
zh: '请输入正确的email。'
},
PleaseEnterACorrectPhone: {
en: 'Please enter a valid phone number.',
zh: '请输入正确的手机号。'
},
PleaseEnterACorrectID: {
en: 'Please enter a valid ID card number.',
zh: '请输入正确的身份证号。'
},
PleaseEnterMoreThanOneCharacters: {
en: (count) => `Please enter more than ${count} character${count > 1 ? 's' : ''}.`,
zh: (count) => `请输入大于${count}个字符。`
},
// 质量控制
PleaseAnswerCarefully: {
en: 'Please answer carefully.',
zh: '请认真作答。'
},
ContinueAnswer: {
en: 'Continue',
zh: '继续作答'
},
ReviseAnswer: {
en: 'Revise',
zh: '去修改'
},
TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise: {
en: (count, type) => `The answer is continuously repeated more than ${count} times, please revise.`,
zh: (count, type) => `答案连续重复超过${count}个,${type ? '建议' : '请'}修改。`
},
// 预览
QuestionRemark: {
en: (title) => `${title} remark`,
zh: (title) => `${title}问题评论`
},
// 问卷结束的各种提示
SurveyEndNotice: {
en: 'This survey has ended. We look forward to your participation next time.',
zh: '本次调研活动已经结束,感谢您的参与,请您持续关注'
},
SurveyNotStartNotice: {
en: 'This survey has not yet started.',
zh: '答题时间还没有开始'
},
SurveyTemporarilySuspendedNotice: {
en: 'This survey is temporarily suspended for collection',
zh: '此问卷暂停收集'
},
LinkDisabledNotice: {
en: 'The provider has not enabled this link. Please try again later',
zh: '投放者未启用该链接,请稍后重试'
},
FeedbackNotice: {
en: 'Thank you for participating in this survey. We look forward to your participation next time',
zh: '感谢您的反馈,期待您下次参与'
},
SubmittedNotice: {
en: 'You have submitted the questionnaire. We look forward to your participation next time.',
zh: '您已提交答卷,期待您下次反馈'
},
SurveyFilledNotice: {
en: 'You have already filled out this questionnaire',
zh: '您已填过此问卷'
},
FillOutNewSurvey: {
en: 'Fill out new one',
zh: '填份新的'
},
ReviseSurvey: {
en: 'Revise',
zh: '修改内容'
},
// 问题的提示文字
UploadSuccessful: {
en: 'Upload successful.',
zh: '上传成功'
},
UploadFailed: {
en: 'Upload failed.',
zh: '上传失败'
},
Loading: {
en: 'Loading',
zh: '加载中'
},
Start: {
en: 'Start',
zh: '开始'
},
PleaseAnswerCarefullyOtherwiseRestart: {
en:
'Please be sure to read the questions carefully. Consecutive answers may invalidate the entire questionnaire. Your answer to each question is of great significance to us.',
zh:
'您在每一题的态度与观点均对我们有非常重要的意义,请您务必仔细阅读题目后回答,连续一致的答案可能会导致整个问卷的作废,请您重新作答。'
},
IGotIt: {
en: 'Got it',
zh: '我知道啦'
},
// BPTO
Product: {
en: 'Product',
zh: '产品'
},
Character: {
en: 'Character',
zh: '特性'
},
Price: {
en: 'Price',
zh: '价格'
},
PleaseSelect: {
en: 'Please select',
zh: '请选择'
},
// CBC
YouHaveNotAnsweredThisQuestionAnswerToContinue: {
en: 'You have not answered this question. You can continue after answering!',
zh: '您没有对该问题进行回答,回答后可继续做答!'
},
ProductNumber: {
en: 'Product Number',
zh: '商品编号'
},
ProductName: {
en: 'Product Name',
zh: '商品名称'
},
ProductDescription: {
en: 'Product Description',
zh: '商品介绍'
},
ProductDetail: {
en: 'Product detail',
zh: '商品详情'
},
Above: {
en: 'Above',
zh: '以上'
},
ChooseNeither: {
en: 'Choose neither',
zh: '都不选'
},
// KANO
HaveSomeFeature: {
en: (feature) => `Have the feature ${feature}`,
zh: (feature) => `具备${feature}`
},
DoNotHaveSomeFeature: {
en: (feature) => `Do not have the feature ${feature}`,
zh: (feature) => `不具备${feature}`
},
Dislike: {
en: 'Dislike',
zh: '不喜欢'
},
Tolerable: {
en: 'Tolerable',
zh: '能忍受'
},
DoesNotMatter: {
en: 'Does not matter',
zh: '无所谓'
},
ShouldBe: {
en: 'Should be',
zh: '理应如此'
},
Like: {
en: 'Like',
zh: '喜欢'
},
// MaxDiff
PleaseSelectSome: {
en: (option) => `Please select ${option}`,
zh: (option) => `请选择${option}`
},
// PSM
PleaseSelectAValueThatIsNoLessThanOneAndNoMoreThanOne: {
en: (min, max) => `Please select a value that is no less than ${min} and no more than ${max}`,
zh: (min, max) => `请选择不小于${min}不大于${max}的值`
},
PleaseSelectAValueThatIsNoMoreThanOne: {
en: (min, max) => `Please select a value that is no more than ${max}`,
zh: (min, max) => `请选择不大于${max}的值`
},
// 密码
PleaseEnterPassword: {
en: 'Please enter password',
zh: '请输入密码'
},
// 填空题
PleaseInputANumber: {
en: 'Please input a number',
zh: '请输入数字'
},
PleaseEnterANumberGreaterThanOrEqualToOne: {
en: (count) => `Please enter a number greater than or equal to ${count}`,
zh: (count) => `请输入大于等于${count}的数字`
},
PleaseEnterANumberLessThanOrEqualToOne: {
en: (count) => `Please enter a number less than or equal to ${count}`,
zh: (count) => `请输入小于等于${count}的数字`
},
// 级联题
PleaseSelectAProvince: {
en: 'Please select a province',
zh: '请选择省份'
},
PleaseSelectACity: {
en: 'Please select a city',
zh: '请选择城市'
},
PleaseSelectADistrict: {
en: 'Please select a district',
zh: '请选择区县'
},
// 分类题
Option: {
en: 'Answer Option',
zh: '选项'
},
Category: {
en: 'Category',
zh: '分类'
},
BracketDragAndDropOptionsHere: {
en: '(Drag and drop options here)',
zh: '(选项拖入此处)'
},
// 知情同意书题
OneSecondsLeftToRead: {
en: (count) => `${count} seconds left to read`,
zh: (count) => `还需阅读${count}`
},
// 热区题
PleaseSelectOneAreas: {
en: (count) => `Please select ${count} areas`,
zh: (count) => `请选择${count}个选区`
},
PleaseSelectOneFavoriteAreas: {
en: (count) => `Please select ${count} areas you like`,
zh: (count) => `请选择${count}个喜欢选区`
},
PleaseSelectOneDislikeAreas: {
en: (count) => `Please select ${count} areas you dislike`,
zh: (count) => `请选择${count}个不喜欢选区`
},
// 图片题
PleaseClickToViewAllPictures: {
en: 'Please click to view all pictures',
zh: '请点击查看所有图片'
},
ClickToViewPicture: {
en: 'Click to view picture',
zh: '点击查看图片'
},
AutoDestructionAfterViewingTimeout: {
en: 'Auto destruction after viewing timeout',
zh: '查看超时后自动焚毁'
},
ImageHasExpired: {
en: 'Image has expired',
zh: '图片已失效'
},
PleaseClickToViewPicture: {
en: 'Click to view picture',
zh: '请点击查看图片'
},
NoteCantViewAfterTimeLimit: {
en: 'Note: Can\'t view after time limit',
zh: '注意:超过显示时间限制后将无法再次查看'
},
DisplayTimeLimitExceeded: {
en: 'Display time limit exceeded',
zh: '已超过显示时间限制'
},
AutomaticallyCloseAfterOneSecond: {
en: (count) => `Automatically close after ${count} seconds`,
zh: (count) => `${count}秒后自动关闭`
},
// 多项填空
ContentIsRequired: {
en: 'is required',
zh: '必答'
},
ContentShouldNotBeTheSameAsOtherBlanks: {
en: 'should not be the same as other blanks',
zh: '的内容请勿与其他空相同'
},
TheBlankIs: {
en: (message) => `The blank ${message}`,
zh: (message) => `填空${message}`
},
// 地图题
ObtainGeographicalLocation: {
en: 'Obtain geographical location',
zh: '获取地理位置'
},
LongitudeAndLatitude: {
en: (lng, lat) => `Longitude: ${lng}, Latitude: ${lat}`,
zh: (lng, lat) => `经度:${lng}, 纬度:${lat}`
},
PleaseEnterTheLocationOrStreetYouWantToSearchFor: {
en: 'Please enter the location or street you want to search for',
zh: '请输入要搜索的地点、街道'
},
// 密码题
PleaseEnterAtLeastOneCharacters: {
en: (count) => `Please enter at least ${count} characters.`,
zh: (count) => `请至少输入${count}`
},
ThePasswordNeedsToIncludeLettersNumbersOrBoth: {
en: 'The password needs to include letters, numbers, or both letters and numbers.',
zh: '密码需要包含字母/数字/字母和数字'
},
ThePasswordNeedsToIncludeLettersNumbersSymbolsOrCombination: {
en: 'The password needs to include letters, numbers, symbols, or a combination of letters, numbers, and symbols.',
zh: '密码需要包含字母/数字/符号/字母和数字/字母和符号/符号和数字/字母、数字和符号'
},
ThePasswordNeedsToIncludeUpperLowerLettersNumbersSymbolsOrCombination: {
en:
'The password needs to include uppercase letters, lowercase letters, numbers, symbols, or a combination of uppercase and lowercase letters, numbers and symbols.',
zh: '密码需要包含大/小写字母/数字/符号/大/小写字母和数字/大/小写字母和符号/符号和数字/大/小写字母、数字和符号'
},
ThePasswordMustBeACombinationOfLettersAndNumbers: {
en: 'The password must be a combination of letters and numbers.',
zh: '密码必须为字母和数字'
},
ThePasswordsEnteredDoNotMatch: {
en: 'The passwords entered do not match.',
zh: '两次密码输入不一致'
},
// 手机号题
GetCaptcha: {
en: 'Get Captcha',
zh: '获取验证码'
},
SentSuccessfully: {
en: 'Sent successfully.',
zh: '发送成功'
},
PleaseEnterAValidPhoneNumber: {
en: 'Please enter a valid phone number.',
zh: '请输入正确的手机号'
},
// 签名题
Clear: {
en: 'Clear',
zh: '清空'
},
Undo: {
en: 'Undo',
zh: '撤销'
},
Eraser: {
en: 'Eraser',
zh: '橡皮'
},
Brush: {
en: 'Brush',
zh: '画笔'
},
CompleteAndUpload: {
en: 'Upload',
zh: '完成并上传'
},
// 恒定总和题
Sum: {
en: 'Sum',
zh: '总和'
},
// 上传文件题的提示
ClickOrDragTheFileHereToUpload: {
en: 'Click or drag the file here to upload.',
zh: '点击或将文件拖拽到这里上传'
},
UploadFileDesc: {
en: (min, max, minSize, maxSize, type) => {
let result = `Upload at least ${min} files, up to a maximum of ${max} files, with each file size between ${minSize}MB and ${maxSize}MB`;
if (type) {
result += `, and the file format limited to ${type}`;
}
result += '.';
return result;
},
zh: (min, max, minSize, maxSize, type) => {
let result = `最少上传${min}个文件,最多上传${max}个文件,每个文件不小于${minSize}M不大于${maxSize}M`;
if (type) {
result += `,文件格式限制为${type}`;
}
result += '。';
return result;
}
},
FileSizeIsLessThanOneMB: {
en: (size) => `File size is less than ${size}MB.`,
zh: (size) => `文件小于${size}M`
},
FileSizeIsGreaterThanOneMB: {
en: (size) => `File size is greater than ${size}MB.`,
zh: (size) => `文件大于${size}M`
},
FileFormatIsIncorrect: {
en: 'The file format is incorrect.',
zh: '文件格式不正确'
},
UploadFileSuccessful: {
en: (title) => `Upload ${title} successful.`,
zh: (title) => `${title} 上传成功`
},
UploadFileFailed: {
en: (title) => `Upload ${title} failed.`,
zh: (title) => `${title} 上传失败`
},
EquipmentNotice: {
en: (title) =>
`Sorry, the number of submissions you have made under the current IP address has reached the limit. Thank you for participating.`,
zh: (title) => `很抱歉,您使用当前设备填写问卷已达到限制次数,感谢参与`
},
SubmitIpNotice: {
en: (title) =>
`Sorry, the number of submissions you have made under the current IP address has reached the limit. Thank you for participating.`,
zh: (title) => `很抱歉您在当前IP地址下填写问卷已达到限制次数感谢参与`
},
PrivatizationWxWorkIdentityNotice: {
en: (title) =>
`Sorry, the current environment does not allow you to answer this questionnaire.\nPlease click on the link or scan the code to answer through the privatized enterprise WeChat.`,
zh: (title) => `很抱歉,当前环境无法作答该问卷\n请通过私有化企业微信点击链接或扫码作答`
},
// QLast 提示
QLast_Stay: {
en: 'Cancel',
zh: '停止跳转'
},
QLast_Redirect: {
en: 'Continue',
zh: '立即跳转'
},
QLast_RedirectingIn5Seconds: {
en: (countdown) => `You will be redirecting to another page in ${countdown} seconds.`,
zh: (countdown) => `发布者设置了作答后跳转,页面将于【${countdown}】秒后跳转`
},
QLast_IosRedirectingPrompt: {
en: 'If you are using an Apple (iOS) deviceplease click the button below to jump now.',
zh: '页面跳转中如您使用苹果iOS设备或页面长时间未跳转请手动点击下方按钮进行跳转'
}
};
export const allLanguageTypes = Object.keys(language[Object.keys(language)[0]]);
export const languageTypes = ['zh'];
export function setLanguageTypes(types) {
languageTypes.push(...types);
}
export function getLanguage(langArr = languageTypes) {
const l = [];
if (!langArr) {
l.push('zh');
} else if (Array.isArray(langArr)) {
if (langArr.length) {
l.push(...langArr);
} else {
l.push('zh');
}
} else {
l.push(langArr);
}
const result = {};
Object.keys(language).forEach((key) => {
result[key] = '';
let tempStr = [];
let tempFunc = [];
l.forEach((lang) => {
const res = language[key][lang];
if (typeof res === 'string') {
tempStr.push(language[key][lang]);
} else {
tempFunc.push(language[key][lang]);
}
});
if (tempStr.length) {
result[key] = tempStr.join('\n');
}
if (tempFunc.length) {
result[key] = (...rest) => tempFunc.map((func) => func(...rest)).join('\n');
}
});
return result;
}

View File

@@ -0,0 +1,35 @@
import { createVNode } from 'vue';
import {ElMessage as message} from "element-plus"
function getNode(text) {
const lines = (text || '').split('\n').filter((i) => !!i);
return createVNode(
'div',
{
style: 'display:inline-flex;flex-direction:column;text-align:left;'
},
lines.map((i) => createVNode('div', {}, i))
);
}
function getOptions(options, duration, onClose) {
if (typeof options === 'string') {
return {
content: getNode(options),
duration,
onClose
};
}
return {
...(options || {}),
content: getNode(options?.content || ''),
duration: duration ?? options?.duration,
onClose: onClose ?? options?.onClose
};
}
export default {
error: (...rest) => message.error(getOptions(...rest)),
warn: (...rest) => message.warn(getOptions(...rest)),
success: (...rest) => message.success(getOptions(...rest))
};

View File

@@ -0,0 +1,130 @@
import getlogicStatus from './logical';
// 更新code
function updateCode(action, logic, pages, page) {
const { question_index, skip_question_index } = logic;
const startIndex = pages.findIndex(
(page) => page.findIndex((questionIndex) => questionIndex === question_index) !== -1
);
if (startIndex < page) {
if (skip_question_index === -1) {
action.code = 20011;
action.msg = '成功结束页';
} else if (skip_question_index === -2) {
action.code = 20004;
action.msg = '甄别结束页';
} else if (skip_question_index === -3) {
action.code = 20016;
action.msg = '配额超限页';
}
}
}
// 更新分页pages(题后跳转逻辑)
function updatePagesAfter(pages, logic, jumpTo, page) {
const { question_index, skip_question_index } = logic;
const startIndex = pages.findIndex(
(page) => page.findIndex((questionIndex) => questionIndex === question_index) !== -1
);
const endIndex = pages.findIndex(
(page) => page.findIndex((questionIndex) => questionIndex === skip_question_index) !== -1
);
if (endIndex !== -1) {
const endQuestionIndex = pages[endIndex].findIndex((questionIndex) => questionIndex === skip_question_index);
pages[endIndex].splice(0, endQuestionIndex);
// 跳转到某页
if (startIndex > endIndex && startIndex < page) {
jumpTo.question_index = question_index;
return (jumpTo.question_page = endIndex + 1);
}
pages.splice(startIndex + 1, endIndex - startIndex - 1);
}
}
// 更新分页pages(题前设置逻辑)
function updatePagesBefore(pages, hideQuestionIndex) {
const pagesIndex = pages.findIndex(
(page) => page.findIndex((questionIndex) => questionIndex === hideQuestionIndex) !== -1
);
if (pagesIndex === -1) return;
if (pages[pagesIndex].length === 1) {
pages.splice(pagesIndex, 1);
} else {
const pageIndex = pages[pagesIndex].findIndex((questionIndex) => questionIndex === hideQuestionIndex);
pages[pagesIndex].splice(pageIndex, 1);
}
}
// 自动填写
function autoFill(answerAutoFill, logic) {
answerAutoFill.push({
answer: logic.autofill.answer_insert,
question_index: logic.question_index,
question_type: logic.autofill.question_type
});
}
// 选项隐藏逻辑
function updateOptionHidden(hide_options, logic) {
const { question_index, hide_option_index } = logic;
hide_options.question_index = question_index;
if (logic.logicStatus) {
hide_options.option_key.push(...hide_option_index.map((opt) => opt.option_key));
}
}
// 模拟答题接口
export default function answerMock(questionsData, page) {
const data = {
action: { code: 20010, msg: '答案已记录' },
jump_to: {},
hide_options: {
option_key: []
},
answer_info_autofill: [],
pages: JSON.parse(JSON.stringify(questionsData.answer.pages_init))
};
const logics = getlogicStatus(questionsData);
logics.forEach((logic) => {
if (logic.logicStatus && logic.skip_type === 0) {
// 20240731
// fix: Q1,Q2,Q3,Q4,Q5. Q1 A 跳转至 Q3; Q1 B 跳转至 Q2; Q2 always 跳转至 Q5. 当 Q1 选择 A, 页面空白/作答结束
if (!data.pages[page - 1].includes(logic.question_index)) {
return;
}
// 题后跳转逻辑
if (logic.skip_question_index < 0) {
return updateCode(data.action, logic, data.pages, page);
}
updatePagesAfter(data.pages, logic, data.jump_to, page);
} else if (logic.logicStatus && logic.skip_type === 1) {
// 题前设置逻辑
updatePagesBefore(data.pages, logic.question_index);
} else if (logic.logicStatus && logic.skip_type === 3) {
// 自动填写逻辑
autoFill(data.answer_info_autofill, logic);
} else if (logic.skip_type === 4) {
// 只计算跳转后所在页面的隐藏逻辑,否则会出现只返回最后一道隐藏选项题目的情况,导致失效
const toPage = page + 1;
const hasHiddenLogicQuizPage = data.pages.findIndex((page) => page.includes(logic.question_index)) + 1;
if (hasHiddenLogicQuizPage === toPage) {
// 选项隐藏逻辑
updateOptionHidden(data.hide_options, logic);
}
}
});
// 更新问卷状态
if (page === data.pages.length) {
data.action.code = 20011;
data.action.msg = '成功结束页';
}
// 拒绝知情同意书
// const refuseIndex = questionsData.questions.findIndex(
// (question) => question.question_type === 23 && question.answer?.value === "2"
// );
// if (refuseIndex !== -1) {
// data.action.code = 20013;
// data.action.msg = "不同意继续参与,已结束作答";
// }
// 返回数据
return data;
}

View File

@@ -0,0 +1,875 @@
import { AnswerApi } from '../js/api';
import { computed, getCurrentInstance, defineComponent, provide, ref, watch } from 'vue';
import answerMock from './mock';
import { useRoute } from 'vue-router';
import msg from './message';
import LangTranslate from '../components/LangTranslate.vue';
import { useQuestionStore } from '@/stores/Questions/useQuestionStore.ts';
import { storeToRefs } from 'pinia';
export default defineComponent({
components: {
LangTranslate
},
props: {
// 是否答题模式
isAnswer: {
type: Boolean,
default: false
},
// 是否模板预览
isTemplate: {
type: Boolean,
default: false
}
},
setup(props) {
const questionStore = useQuestionStore()
/**
* styleInfo 主题样式
*/
const {questionsData, styleInfo, page, pages,l} = storeToRefs(questionStore)
const route = useRoute();
// const questionsData = inject('questionsData'); // 问卷数据
const translatedText = computed(() => questionsData.value.language || {});
const startAnswerTime = new Date().getTime(); // 开始答题时间戳
const { proxy } = getCurrentInstance();
const scrollbar = ref(null);
let localPageInterval;
watch(
styleInfo,
(style) => {
console.log(`style value: `, style);
if (questionsData.value?.survey?.style && !style.is_home && !page.value) {
page.value = 1;
}
},
{ immediate: true }
);
// 主题颜色
const themeColor = computed(() => ({
stemColor: styleInfo.value.stem_color,
answerColor: styleInfo.value.answer_color,
buttonColor: styleInfo.value.button_color,
buttonTextColor: styleInfo.value.button_text_color
}));
provide('themeColor', themeColor);
// 当前页问卷
const questions = computed(() => {
const currentPages = pages.value[page.value - 1] || [];
return (questionsData.value?.questions || []).filter((quetion) =>
currentPages.find((index) => quetion.question_index === index)
);
});
// 是否显示分页器
const showPage = computed(() => {
return [102, 104, 105, 201].includes(questions.value[0]?.question_type) ? false : true;
});
// 分页计时器
watch(
page,
() => {
localPageTimer.value = {};
clearInterval(localPageInterval);
const lastQuestionIndex = questions.value.slice(-1)[0]?.question_index;
console.log(questionsData.value);
const localPage = questionsData.value?.survey?.local_pages.find(
(localPage) => localPage.question_index === lastQuestionIndex && localPage.timer_config.is_short_time
);
if (localPage) {
localPageTimer.value = localPage.timer_config;
localPageTimer.value.short_time = localPageTimer.value.short_time * 1;
localPageInterval = setInterval(() => {
if (localPageTimer.value.short_time === 0) {
// 停止计时
return clearInterval(localPageInterval);
}
localPageTimer.value.short_time -= 1;
}, 1000);
}
},
{
deep: true,
immediate: true
}
);
// 要搜索的数组和要查找的连续数字的数量n
function hasNConsecutiveNumbers(arr, n, onTrue, onFalse) {
let count = 1;
let warnStart = 0;
let prevNum = arr[0];
for (let i = 1; i < arr.length; i++) {
if (prevNum && arr[i] === prevNum) {
count++;
} else {
count = 1;
warnStart = i;
}
if (count === n) {
if (onTrue) onTrue(warnStart, warnStart + n);
return true;
}
prevNum = arr[i];
}
if (onFalse) onFalse();
return false;
}
// 答题
async function answer(callback, callbackBeforePage) {
if ((questions.value.length || !questionsData.value.questions.length) && !props.isTemplate) {
// 表单验证(当前页)
const errors = questions.value.filter((question) => {
const { config, answer, question_type: questionType } = question;
let isError = false;
if (config.is_required && !answer) {
isError = true;
if (questionType === 10) {
// 矩阵多选题
if (question.config.is_change_row_cell) {
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerColumn(config.min_select || 1);
} else {
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerLine(config.min_select || 1);
}
} else if (questionType === 12) {
// 图片显示题,如果开启了仅显示并且开启了单图显示时长,需要确保用户每一个图片都点开看过了才能下一页
if (!answer?.read && config.picture_type === 0 && config.countdown_type === 1) {
isError = true;
question.error = translatedText.value.PleaseClickToViewAllPictures;
}
} else if (questionType === 15) {
// 分类题
question.error = translatedText.value.PleaseCategorizeAllOptions;
} else if (!question.error) {
question.error = translatedText.value.ThisIsARequiredQuestion;
}
} else if (answer && questionType === 1 && Object.keys(answer).findIndex((value) => !answer[value]) !== -1) {
// 单选题
isError = true;
question.error = translatedText.value.PleaseInputAValue;
} else if (answer && questionType === 2) {
// 多选题
if (Object.keys(answer).length < config.min_select) {
// 选项数量
let options = [];
question.list.forEach((list) => {
options = [...options, ...list.options];
});
const index = options.findIndex(
(option) => option.option_key === Object.keys(answer)[0] && option.is_remove_other
);
if (index === -1) {
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOneOptions(config.min_select);
} else {
question.error = '';
}
} else if (Object.keys(answer).findIndex((value) => !answer[value]) !== -1) {
isError = true;
question.error = translatedText.value.PleaseInputAValue;
} else {
question.error = '';
}
if (!question.error) {
// 选项分组最少选项数量
config.option_groups?.option_group.some((optionGroup) => {
const { min, title } = optionGroup;
const select = optionGroup.groups.filter((groups) => Object.keys(answer).includes(groups.option_key));
if (title && select.length < min) {
// 判断隐藏选项
question.hideOptionsSet = new Set(question.hideOptions);
const options = question.list.flatMap((list) =>
list.options
.filter(({ option_key }) =>
optionGroup.groups.some(
({ option_key: groupKey }) =>
groupKey === option_key && !question.hideOptionsSet.has(groupKey)
)
)
.map(({ option_key }) => option_key)
);
if (options.length >= min) {
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsInTitleGroup(min, title);
}
}
return isError;
});
}
} else if (answer && questionType === 10) {
// 矩阵多选题,列分组时,校验选项数量
const cellGroups = (config?.cell_option_groups?.option_group || []).filter((i) => i.groups?.length);
if (cellGroups.length) {
const rows = question.list.reduce((p, c) => [...p, ...(c.type === 1 ? c.options || [] : [])], []);
const cols = question.list.reduce((p, c) => [...p, ...(c.type === 2 ? c.options || [] : [])], []);
const freeCols = cols.filter(
(col) => !cellGroups.some((g) => g.groups.find((c) => c.option_key === col.option_key))
);
const arr = rows.map((row) => {
return cellGroups.map((group) => {
return group.groups.map((col) => {
return `${row.option_key}_${col.option_key}`;
});
});
});
arr.forEach((a, idx) => a.push(freeCols.map((col) => `${rows[idx].option_key}_${col.option_key}`)));
const answered = Object.keys(answer);
cellGroups.forEach((group, idx) => {
if (!group.groups?.length || (!group.max && !group.min) || isError) {
return;
}
for (let i = 0; i < arr.length; i += 1) {
const row = arr[i];
const selectedCount = row[idx].filter((key) => answered.includes(key)).length;
if (group.min && selectedCount < group.min) {
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsInTitleGroup(
group.min,
group.title
);
break;
}
if (group.max && selectedCount > group.max) {
isError = true;
question.error = translatedText.value.PleaseSelectAtMostOneOptionsInTitleGroup(
group.max,
group.title
);
break;
}
}
});
}
// 最少选择数量校验
const minSelect = +config.min_select || 0;
const perLineSelectedCount = Object.keys(answer).reduce(
(p, c) => {
if (+answer[c]) {
p[+c.split('_')[0] - 1] += 1;
}
return p;
},
question.list.filter((i) => i.type === 1).flatMap((i) => i.options.map((i) => 0))
);
if (minSelect && minSelect > Math.max(...perLineSelectedCount)) {
if (question.config.is_change_row_cell) {
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerColumn(config.min_select || 1);
} else {
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerLine(config.min_select || 1);
}
isError = true;
}
console.log('===', minSelect, Object.keys(answer), answer, perLineSelectedCount);
} else if (answer && questionType === 12) {
question.error = '';
} else if (answer && questionType === 14 && Object.keys(answer).length < config.min_select) {
// 图片多选题
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOnePictures(config.min_select);
} else if (answer && questionType === 16) {
// 排序题
if (Object.keys(answer).length < (+config.min_select || 0)) {
// 选项数量
isError = true;
question.error = translatedText.value.PleaseSelectAtLeastOneOptions(config.min_select);
}
} else if (answer && questionType === 17) {
// 恒定总和题
let sum = 0;
Object.keys(answer).forEach((key) => {
sum += answer[key] * 1;
});
if (sum === config.total) {
question.error = '';
} else {
isError = true;
question.error = translatedText.value.PleaseLetTheSumEqualToTotal(config.total);
}
} else if (answer && questionType === 18 && answer.length < config.min_number) {
// 文件上传题
isError = true;
question.error = translatedText.value.PleaseUploadAtLeastOneFiles(config.min_number);
} else if (answer && questionType === 4) {
question.error = '';
// 填空题
const { value } = answer;
let newValue = value.replace(/\n|\r|\r\n/g, '');
switch (config.text_type) {
case 3: // 字母
isError = config.include_mark == 1
? !/^[a-zA-Z·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]]+$/.test(
newValue
) || !newValue.length
: !/^[a-zA-Z]+$/.test(newValue) || !newValue.length;
question.error = isError ? translatedText.value.PleaseEnterEnglishLetters : '';
break;
case 4: // 中文
isError = config.include_mark == 1
? !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]])+$/.test(
newValue
) || !newValue.length
: !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/.test(
newValue
) || !newValue.length;
question.error = isError ? translatedText.value.PleaseEnterChineseWords : '';
break;
case 5: // 邮箱
isError = !/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
);
question.error = isError ? translatedText.value.PleaseEnterACorrectEmail : '';
break;
case 6: // 手机号
isError = !/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value);
question.error = isError ? translatedText.value.PleaseEnterACorrectPhone : '';
break;
case 7: // 身份证号
isError = !/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(
value
);
question.error = isError ? translatedText.value.PleaseEnterACorrectID : '';
break;
default:
break;
}
if (!isError && value.length < config.min && ![1, 2].includes(config.text_type)) {
isError = true;
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
}
} else if (answer && questionType === 8) {
// 矩阵填空题
question.error = '';
Object.keys(answer).forEach((key) => {
const value = answer[key];
switch (config.text_type) {
case 3: // 字母
if (
!/^[a-zA-Z·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]]+$/.test(
value
)
)
question.error = translatedText.value.PleaseEnterEnglishLetters;
break;
case 4: // 中文
if (
!/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]])+$/.test(
value
)
)
question.error = translatedText.value.PleaseEnterChineseWords;
break;
case 5: // 邮箱
if (
!/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
)
)
question.error = translatedText.value.PleaseEnterACorrectEmail;
break;
case 6: // 手机号
if (!/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value))
question.error = translatedText.value.PleaseEnterACorrectPhone;
break;
case 7: // 身份证号
if (
!/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(value)
)
question.error = translatedText.value.PleaseEnterACorrectID;
break;
default:
break;
}
if (!question.error && value.length < config.min && ![1, 2].includes(config.text_type)) {
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
}
});
if (question.error) isError = true;
} else if (questionType === 27 && question.error) {
// 多项填空题
isError = true;
} else {
question.error = '';
}
return isError;
});
if (!errors.length) {
// 质量控制
const question = questions.value.find((question) => {
const { config, answer, question_type: questionType } = question;
if (questionType === 9 && config.is_repeat) {
// 矩阵单选
const arr = Object.keys(answer).map((key) => key.split('_')[1]);
return hasNConsecutiveNumbers(arr, config.allow_repeat_num + 1);
} else if (questionType === 10 && config.is_repeat) {
// 矩阵多选
let arr = [];
const obj = {};
for (const key in answer) {
const [index, value] = key.split('_');
if (obj[index] === undefined) {
obj[index] = arr.length;
arr.push([value]);
} else {
arr[obj[index]].push(value);
}
}
arr = arr.map((subArr) => subArr.sort().join());
return hasNConsecutiveNumbers(arr, config.allow_repeat_num + 1);
}
return false;
});
if (question) {
await new Promise((resolve) => {
Modal[question.config.repeat_type ? 'confirm' : 'info']({
class: 'custom-modal custom-modal-title-notice',
title: translatedText.value.PleaseAnswerCarefully,
content: question.config.alert_text,
cancelText: translatedText.value.ContinueAnswer,
okText: translatedText.value.ReviseAnswer,
onCancel: () => {
resolve();
}
});
});
}
// 质量控制(选择题)
const allPages = pages.value.flatMap((currentPages) => currentPages);
const allQuestions = allPages.map((questionIndex) => {
return questionsData.value.questions.find((question) => question.question_index === questionIndex);
});
const repeat = questionsData.value.survey.repeat_list?.find((repeat) =>
repeat.question_indexes.find(({ first_index, last_index }) => {
const firstIndex = allPages.findIndex((questionIndex) => questionIndex === first_index);
const lastIndex = allPages.findIndex((questionIndex) => questionIndex === last_index);
const currentQuestions = allQuestions
.slice(firstIndex, lastIndex + 1)
.filter((currentQuestions) => currentQuestions.question_type === repeat.question_type);
const answerIndexes = currentQuestions.map((question) => question.answerIndex);
return hasNConsecutiveNumbers(
answerIndexes,
repeat.allow_repeat_num + 1,
(warnStart, warnEnd) => {
currentQuestions.forEach((question, index) => {
if (index >= warnStart && index < warnEnd) {
if (repeat.repeat_type) {
question.warning = translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise(
repeat.allow_repeat_num,
repeat.repeat_type
);
} else {
question.error = translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise(
repeat.allow_repeat_num,
repeat.repeat_type
);
}
} else {
question.warning = '';
question.error = '';
}
});
},
() => {
currentQuestions.forEach((question) => {
question.warning = '';
});
}
);
})
);
if (repeat) {
await new Promise((resolve) => {
Modal[repeat.repeat_type ? 'confirm' : 'info']({
class: 'custom-modal custom-modal-title-notice',
title: translatedText.value.PleaseAnswerCarefully,
content: repeat.alert_text,
cancelText: translatedText.value.ContinueAnswer,
okText: translatedText.value.ReviseAnswer,
onCancel: () => {
questions.value.forEach((question) => (question.answerIndex = ''));
resolve();
}
});
});
}
// 判断是作答还是预览
if (!props.isAnswer) {
loading.value = true;
try {
// 模拟接口
const data = answerMock(questionsData.value, page.value);
console.log('模拟作答数据', data);
// 更新答案
updateAnswer(data.answer_info_autofill);
// 更新分页数组
questionsData.value.answer.pages = data.pages;
// 选项隐藏
hideOptions(data.hide_options);
// 更新action
questionsData.value.action = data.action;
if ([20004, 20011, 20016].includes(data.action.code)) {
return (page.value = pages.value.length + 1);
}
callback(data.jump_to);
} catch (error) {
console.log(error);
}
return (loading.value = false);
}
// 表单验证通过,开始答题
const cycle = [];
const questionsAnswer = [];
questions.value.forEach((question) => {
questionsAnswer.push({
question_index: question.question_index,
answer: question.answer || {}
});
// 循环
if (question.question_type === 1 || question.question_type === 2) {
const optionKeys = [];
question.list.forEach((list) => {
list.options.forEach((option) => {
optionKeys.push(option.option_key);
});
});
cycle.push({
question_index: question.question_index,
options: {
a: optionKeys.join()
}
});
}
});
loading.value = true;
try {
if (callbackBeforePage) await callbackBeforePage();
const { data } = await AnswerApi.answer({
id: proxy.$route.query.sn,
data: {
cycle,
answer: JSON.stringify(questionsAnswer),
is_test: proxy.$route.query.is_test
}
});
// 更新答案
updateAnswer(data.answer_info_autofill);
// 更新分页数组
questionsData.value.answer.pages = data.pages;
// 京东跳转
if (data.action.code === 302) {
location.href = data.action.msg;
}
// 更新问卷状态
if (data.action.code === 20014) {
const timer = setTimeout(() => {
const url = data.action.msg;
toUrl(url);
clearTimeout(timer);
});
}
// 判断抽奖
if (data.action.lottery) {
const endAnswerTime = new Date().getTime();
const answerTime = endAnswerTime - startAnswerTime;
proxy.$router.push({
path: '/luck',
query: {
user_id: proxy.$route.query.sn,
id: questionsData.value.answer.sn,
time_num: parseInt(answerTime / 1000),
is_test: proxy.$route.query.is_test || 0
}
});
}
// 选项隐藏
hideOptions(data.hide_options);
// 更新action
questionsData.value.action = data.action;
callback(data.jump_to);
// 写入关联选项缓存
if (props.isAnswer && questionsData.value.survey.is_breakpoint) {
if (data.action.code === 20010) {
const questionsCache = JSON.parse(localStorage.getItem('questionsCache')) || {};
questionsCache[proxy.$route.query.sn] = questionsData.value.questions;
localStorage.setItem('questionsCache', JSON.stringify(questionsCache));
} else {
const questionsCache = JSON.parse(localStorage.getItem('questionsCache'));
if (questionsCache?.[proxy.$route.query.sn]) {
delete questionsCache[proxy.$route.query.sn];
localStorage.setItem('questionsCache', JSON.stringify(questionsCache));
}
}
}
} catch (error) {
console.log(error);
}
loading.value = false;
} else {
console.log(errors);
const { error, title, question_index } = errors[0];
const lines = (error || '')
.split('\n')
.filter((i) => !!i)
.map((i) => `${title}${i}`);
msg.error(lines.join('\n'));
// 锚点
const anchor = document.querySelector(`#questionIndex${question_index}`);
console.log(anchor, scrollbar.value);
scrollbar.value.scrollTo(0, anchor.offsetTop - (props.isMobile ? 20 : 40));
}
} else {
callback();
}
}
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');
}
}
}
// 选项隐藏
function hideOptions(hide) {
const questionIndex = hide?.question_index;
if (questionIndex) {
const qustion = questionsData.value.questions.find((qustion) => qustion.question_index === questionIndex);
qustion.hideOptions = hide.option_key || [];
}
}
// 关联引用
function onRelation({ options, value, list }, { question_type, question_index, related, answer }) {
// 关联
related.forEach((relationItem) => {
let relationOptions = [];
if (question_type === 9 || question_type === 10) {
// 矩阵选择
list.forEach((item) => {
if (item.type === relationItem.cite_type) {
relationOptions = [...relationOptions, ...item.options];
}
if (relationItem.relation_type === 1) {
relationOptions = relationOptions.filter((option) =>
question_type === 9 ? option.value : option.value?.length
);
} else if (relationItem.relation_type === 2) {
relationOptions = relationOptions.filter((option) =>
question_type === 9 ? !option.value : !option.value?.length
);
}
});
} else if (question_type === 11) {
// 矩阵打分
list.forEach((item) => {
if (item.type === relationItem.cite_type) {
relationOptions = [...relationOptions, ...item.options];
}
});
} else if (question_type === 25 || question_type === 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 (question_type === 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 (question_type === 1) {
// 单选
if (relationItem.relation_type === 1) return value === option.option_key;
return value !== option.option_key;
} else if (question_type === 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 (question_type >= 9 && question_type <= 11) {
letter = relationItem.cite_type === 1 ? 'R' : 'C';
}
option.option_key = `Q${question_index}${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 === question_index
);
relatedList.options = answer ? copyRelationOptions : [];
});
}
// 更新答案
function updateAnswer(auto) {
if (auto) {
auto.forEach((autoItem) => {
const question = questionsData.value.questions.find(
(question) => question.question_index === autoItem.question_index
);
question.answer = JSON.parse(autoItem.answer);
// 隐藏的题目,自动填写时,并被后续题目关联选项,会出现关联选项未正确展示的情况,此暂时解决方法
const evt1 = {};
if ([1].includes(question.question_type)) {
evt1.value = Object.keys(question.answer)
.map((key) => (question.answer[key] ? key : undefined))
.filter((i) => !!i)?.[0] || undefined;
evt1.options = question.list.flatMap((i) => i.options);
onRelation(evt1, question);
}
});
}
}
// 跳转链接
function toUrl(url) {
// 判断是否小程序路径
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);
}
}
watch(
() => route.query,
() => {
console.log('route.query.sn', route.query);
location.reload();
},
{
deep: true
}
);
return {
page,
pages,
loading,
prevLoading,
showPage,
styleInfo,
questions,
scrollbar,
questionsData,
localPageTimer,
answer,
previous,
next,
onRelation
};
}
});

View File

@@ -139,7 +139,7 @@ function shareLink() {
function downLoadImg() {
const { title, url } = publishInfo.value.download_url;
if (utils.getCookie('xToken')) {
appBridge.save2Album(url, (result: any) => {
appBridge.save2Album(url, () => {
showSuccessToast('下载成功');
});
} else {