feat: 增加签名问卷的支持
- 增加 SignQuestion 组件 - 增加响应的内容数据 - 在 Index 页面正常挂载组件显示
This commit is contained in:
@@ -6377,6 +6377,43 @@ export const useCommonStore = defineStore('common', {
|
|||||||
question_tag: '',
|
question_tag: '',
|
||||||
planet_id: '',
|
planet_id: '',
|
||||||
permissions: null
|
permissions: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '17852690',
|
||||||
|
title: 'Q7',
|
||||||
|
stem: '请留下您的姓名',
|
||||||
|
other: '',
|
||||||
|
question_index: 10,
|
||||||
|
question_type: 22,
|
||||||
|
config: {
|
||||||
|
is_required: 1,
|
||||||
|
select_random: 0,
|
||||||
|
float_window: 0,
|
||||||
|
float_window_content: '',
|
||||||
|
popup_window: 0,
|
||||||
|
popup_window_content: '',
|
||||||
|
is_show: [],
|
||||||
|
quick_type: 0
|
||||||
|
},
|
||||||
|
created_at: '2025-03-06T15:51:13',
|
||||||
|
created_user_id: 1281,
|
||||||
|
updated_user_id: null,
|
||||||
|
survey_id: 9482,
|
||||||
|
logic_config: {
|
||||||
|
expect: '',
|
||||||
|
order: 0,
|
||||||
|
type: 0,
|
||||||
|
stay_time: ''
|
||||||
|
},
|
||||||
|
options: [],
|
||||||
|
associate: [],
|
||||||
|
logics_has: null,
|
||||||
|
last_option_index: 0,
|
||||||
|
question_code: '',
|
||||||
|
question_value: '',
|
||||||
|
question_tag: '',
|
||||||
|
planet_id: '',
|
||||||
|
permissions: null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
cycle_pages: null
|
cycle_pages: null
|
||||||
|
|||||||
@@ -1,57 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="design-create">
|
<div class="design-create">
|
||||||
<draggable
|
<draggable
|
||||||
v-model:data="questionInfo.questions"
|
v-model:data="questionInfo.questions" item-key="id" handle=".moverQues" chosenClass="chosen"
|
||||||
item-key="id"
|
animation="300" :scroll="true"
|
||||||
handle=".moverQues"
|
|
||||||
chosenClass="chosen"
|
|
||||||
animation="300"
|
|
||||||
:scroll="true"
|
|
||||||
>
|
>
|
||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<choose-question
|
<choose-question
|
||||||
:element="element"
|
:element="element" :questions="questionInfo.questions" :index="index"
|
||||||
:questions="questionInfo.questions"
|
:chooseQuestionId="chooseQuestionId" @get-choose-question-id="getChooseQuestionId"
|
||||||
:index="index"
|
|
||||||
:chooseQuestionId="chooseQuestionId"
|
|
||||||
@get-choose-question-id="getChooseQuestionId"
|
|
||||||
>
|
>
|
||||||
<!-- 选择题 -->
|
<!-- 选择题 -->
|
||||||
<Choice
|
<Choice
|
||||||
v-if="element.question_type === 1 || element.question_type === 2"
|
v-if="element.question_type === 1 || element.question_type === 2" :element="element"
|
||||||
:element="element"
|
|
||||||
:active="chooseQuestionId === element.id"
|
:active="chooseQuestionId === element.id"
|
||||||
></Choice>
|
></Choice>
|
||||||
<!-- 填空题 -->
|
<!-- 填空题 -->
|
||||||
<Completion
|
<Completion
|
||||||
v-if="element.question_type === 4"
|
v-if="element.question_type === 4" :element="element" :active="chooseQuestionId === element.id"
|
||||||
:element="element"
|
|
||||||
:active="chooseQuestionId === element.id"
|
|
||||||
sn="lXEBBpE2"
|
sn="lXEBBpE2"
|
||||||
></Completion>
|
></Completion>
|
||||||
|
|
||||||
|
<!-- 矩阵题 -->
|
||||||
<martrix-question
|
<martrix-question
|
||||||
v-if="
|
v-if="
|
||||||
element.question_type === 8 ||
|
element.question_type === 8 ||
|
||||||
element.question_type === 9 ||
|
element.question_type === 9 ||
|
||||||
element.question_type === 10
|
element.question_type === 10
|
||||||
"
|
" :element="element" :active="chooseQuestionId === element.id"
|
||||||
:element="element"
|
|
||||||
:active="chooseQuestionId === element.id"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 签名题 -->
|
||||||
|
<sign-question
|
||||||
|
v-if="element.question_type === 22" :element="element"
|
||||||
|
:active="chooseQuestionId === element.id"
|
||||||
|
></sign-question>
|
||||||
|
|
||||||
<!-- 打分题 -->
|
<!-- 打分题 -->
|
||||||
<Rate
|
<Rate
|
||||||
v-if="element.question_type === 5"
|
v-if="element.question_type === 5" :element="element" :active="chooseQuestionId === element.id"
|
||||||
:element="element"
|
|
||||||
:active="chooseQuestionId === element.id"
|
|
||||||
sn="lXEBBpE2"
|
sn="lXEBBpE2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!--图文-->
|
<!--图文-->
|
||||||
<TextWithImages
|
<TextWithImages
|
||||||
v-if="element.question_type === 6"
|
v-if="element.question_type === 6" :element="element"
|
||||||
:element="element"
|
|
||||||
:active="chooseQuestionId === element.id"
|
:active="chooseQuestionId === element.id"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -59,11 +51,7 @@
|
|||||||
<template #action="{ element: el }">
|
<template #action="{ element: el }">
|
||||||
<div class="flex slot-actions">
|
<div class="flex slot-actions">
|
||||||
<template v-for="(item, optionIndex) in actionOptions">
|
<template v-for="(item, optionIndex) in actionOptions">
|
||||||
<div
|
<div v-if="item.question_type.includes(el.question_type)" :key="optionIndex" class="flex">
|
||||||
v-if="item.question_type.includes(el.question_type)"
|
|
||||||
:key="optionIndex"
|
|
||||||
class="flex"
|
|
||||||
>
|
|
||||||
<template v-for="(act, actIndex) in item.actions" :key="actIndex">
|
<template v-for="(act, actIndex) in item.actions" :key="actIndex">
|
||||||
<div class="flex align-center action-item" @click="actionEvent(act, el)">
|
<div class="flex align-center action-item" @click="actionEvent(act, el)">
|
||||||
<van-icon :name="act.icon"></van-icon>
|
<van-icon :name="act.icon"></van-icon>
|
||||||
@@ -79,11 +67,8 @@
|
|||||||
<!-- {{ element.question_type }}-->
|
<!-- {{ element.question_type }}-->
|
||||||
<!-- {{questionInfo.survey.pages.length}}-->
|
<!-- {{questionInfo.survey.pages.length}}-->
|
||||||
<paging
|
<paging
|
||||||
v-if="!element.question_type && questionInfo.survey.pages.length > 1"
|
v-if="!element.question_type && questionInfo.survey.pages.length > 1" :info="element" :index="index"
|
||||||
:info="element"
|
:active="pageIsActive(activeIndex, questionInfo.questions, element.page)" @click.stop=""
|
||||||
:index="index"
|
|
||||||
:active="pageIsActive(activeIndex, questionInfo.questions, element.page)"
|
|
||||||
@click.stop=""
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -102,6 +87,7 @@ import Completion from './components/Questions/Completion.vue';
|
|||||||
import MartrixQuestion from './components/Questions/MartrixQuestion.vue';
|
import MartrixQuestion from './components/Questions/MartrixQuestion.vue';
|
||||||
import Rate from './components/Questions/Rate.vue';
|
import Rate from './components/Questions/Rate.vue';
|
||||||
import TextWithImages from '@/views/Design/components/Questions/TextWithImages.vue';
|
import TextWithImages from '@/views/Design/components/Questions/TextWithImages.vue';
|
||||||
|
import SignQuestion from './components/Questions/SignQuestion.vue';
|
||||||
|
|
||||||
const activeIndex = ref(-1);
|
const activeIndex = ref(-1);
|
||||||
/**
|
/**
|
||||||
|
|||||||
225
src/views/Design/components/Questions/SignQuestion.vue
Normal file
225
src/views/Design/components/Questions/SignQuestion.vue
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
const signatureCanvas = useTemplateRef('signatureCanvas');
|
||||||
|
|
||||||
|
const canvasWidth = ref(100);
|
||||||
|
const canvasHeight = computed(() => canvasWidth.value / 2.5);
|
||||||
|
const showSignText = ref(true);
|
||||||
|
const isEraser = ref(false);
|
||||||
|
|
||||||
|
let ctx: CanvasRenderingContext2D;
|
||||||
|
let isDrawing = false;
|
||||||
|
const undoStack = ref<ImageData[]>([]);
|
||||||
|
const currentStep = ref(-1);
|
||||||
|
|
||||||
|
// 保存当前状态
|
||||||
|
const saveState = () => {
|
||||||
|
if (!ctx || !signatureCanvas.value) return;
|
||||||
|
const imageData = ctx.getImageData(0, 0, signatureCanvas.value.width, signatureCanvas.value.height);
|
||||||
|
currentStep.value += 1;
|
||||||
|
// 移除当前步骤之后的所有状态(处理在撤销后又进行了新的绘制的情况)
|
||||||
|
undoStack.value.splice(currentStep.value);
|
||||||
|
undoStack.value.push(imageData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置画笔样式
|
||||||
|
const setPenStyle = () => {
|
||||||
|
if (!ctx) return;
|
||||||
|
if (isEraser.value) {
|
||||||
|
ctx.globalCompositeOperation = 'destination-out';
|
||||||
|
ctx.lineWidth = 20;
|
||||||
|
} else {
|
||||||
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换画笔/橡皮擦
|
||||||
|
const togglePen = () => {
|
||||||
|
isEraser.value = !isEraser.value;
|
||||||
|
setPenStyle();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!signatureCanvas.value) return;
|
||||||
|
// 将 canvas 宽度和窗口的宽度保持一致
|
||||||
|
canvasWidth.value = window.innerWidth - 50;
|
||||||
|
|
||||||
|
// 获取 canvas 上下文
|
||||||
|
ctx = signatureCanvas.value.getContext('2d')!;
|
||||||
|
if (!ctx) return;
|
||||||
|
setPenStyle();
|
||||||
|
|
||||||
|
// 保存初始空白状态
|
||||||
|
saveState();
|
||||||
|
|
||||||
|
// 触摸开始,开始绘制适用于移动设备
|
||||||
|
signatureCanvas.value?.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault(); // 防止页面滚动
|
||||||
|
isDrawing = true;
|
||||||
|
const rect = signatureCanvas.value!.getBoundingClientRect();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(
|
||||||
|
touch.clientX - rect.left,
|
||||||
|
touch.clientY - rect.top
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('touchmove', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const rect = signatureCanvas.value!.getBoundingClientRect();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
ctx.lineTo(
|
||||||
|
touch.clientX - rect.left,
|
||||||
|
touch.clientY - rect.top
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('touchend', () => {
|
||||||
|
if (isDrawing) {
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
isDrawing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('touchcancel', () => {
|
||||||
|
isDrawing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 鼠标事件处理
|
||||||
|
signatureCanvas.value?.addEventListener('mousedown', (e) => {
|
||||||
|
isDrawing = true;
|
||||||
|
const rect = signatureCanvas.value!.getBoundingClientRect();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const rect = signatureCanvas.value!.getBoundingClientRect();
|
||||||
|
ctx.lineTo(
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('mouseup', () => {
|
||||||
|
if (isDrawing) {
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
isDrawing = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
signatureCanvas.value?.addEventListener('mouseleave', () => {
|
||||||
|
if (isDrawing) {
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
isDrawing = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除画布
|
||||||
|
*/
|
||||||
|
const clearCanvas = () => {
|
||||||
|
if (!ctx || !signatureCanvas.value) return;
|
||||||
|
ctx.clearRect(0, 0, signatureCanvas.value.width, signatureCanvas.value.height);
|
||||||
|
saveState();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存画布
|
||||||
|
*/
|
||||||
|
const saveCanvas = () => {
|
||||||
|
if (!ctx || !signatureCanvas.value) return;
|
||||||
|
return signatureCanvas.value.toDataURL('image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销
|
||||||
|
*/
|
||||||
|
const undo = () => {
|
||||||
|
if (!ctx || !signatureCanvas.value || currentStep.value <= 0) return;
|
||||||
|
currentStep.value -= 1;
|
||||||
|
const imageData = undoStack.value[currentStep.value];
|
||||||
|
if (imageData) {
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<van-cell>
|
||||||
|
<div class="sign-question">
|
||||||
|
<canvas
|
||||||
|
ref="signatureCanvas" :width="canvasWidth" :height="canvasHeight"
|
||||||
|
style="border: 1px solid #ccc; border-radius: 4px;"
|
||||||
|
>
|
||||||
|
</canvas>
|
||||||
|
<div class="sign-text" :class="{ 'show': showSignText }">
|
||||||
|
<span @click="clearCanvas">清空</span>
|
||||||
|
<span @click="undo">撤销</span>
|
||||||
|
<span @click="togglePen">{{ isEraser ? '画笔' : '橡皮擦' }}</span>
|
||||||
|
<span @click="saveCanvas">完成并上传</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sign-question {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
// canvas {
|
||||||
|
// width: 100%;
|
||||||
|
// touch-action: none;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.sign-text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user