feat(component): 优化 contenteditable组件功能
- 添加 showAction 控制编辑按钮显示 - 实现文本域聚焦和失焦时的编辑按钮显示和隐藏 -优化键盘弹出和收起时的编辑按钮显示逻辑 -修复文档中描述的产品问卷配置- 优化问卷设计页面的题目编辑功能
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -14,9 +14,12 @@ declare module 'vue' {
|
|||||||
VanButton: typeof import('vant/es')['Button']
|
VanButton: typeof import('vant/es')['Button']
|
||||||
VanCell: typeof import('vant/es')['Cell']
|
VanCell: typeof import('vant/es')['Cell']
|
||||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||||
|
VanCheck: typeof import('vant/es')['Check']
|
||||||
|
VanCheckbo: typeof import('vant/es')['Checkbo']
|
||||||
VanCheckbox: typeof import('vant/es')['Checkbox']
|
VanCheckbox: typeof import('vant/es')['Checkbox']
|
||||||
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
|
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
|
||||||
VanCol: typeof import('vant/es')['Col']
|
VanCol: typeof import('vant/es')['Col']
|
||||||
|
VanDialog: typeof import('vant/es')['Dialog']
|
||||||
VanDivider: typeof import('vant/es')['Divider']
|
VanDivider: typeof import('vant/es')['Divider']
|
||||||
VanField: typeof import('vant/es')['Field']
|
VanField: typeof import('vant/es')['Field']
|
||||||
VanIcon: typeof import('vant/es')['Icon']
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="editor"
|
ref="editor"
|
||||||
contenteditable="true"
|
:contenteditable="active"
|
||||||
class="van-field"
|
class="van-field"
|
||||||
@focus="showToolbar"
|
|
||||||
@blur="save"
|
|
||||||
v-html="modelValue"
|
v-html="modelValue"
|
||||||
></div>
|
></div>
|
||||||
<div ref="editorAction" class="editor-action">
|
<div v-if="showAction && active" ref="editorAction" class="editor-action">
|
||||||
<button v-for="item in actions" :key="item.name" @click="funEvent(item)">{{ item.label }}</button>
|
<button v-for="item in actions" :key="item.name" @click="funEvent(item,$event)">{{ item.label }}</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits, ref, onMounted, onBeforeUnmount } from 'vue';
|
import { defineEmits, ref, onMounted, watch } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const editor = ref(null);
|
const editor = ref(null);
|
||||||
const editorAction = ref(null);
|
const editorAction = ref(null);
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
let lastHeight = window.innerHeight;
|
|
||||||
|
|
||||||
const save = (e) => {
|
const save = (e) => {
|
||||||
emit('update:modelValue', e.target.innerHTML);
|
emit('update:modelValue', e.innerHTML);
|
||||||
};
|
};
|
||||||
|
|
||||||
const functions = {
|
const functions = {
|
||||||
|
// todo 点击按钮之后 如何判断 才能让按钮再次点击 不消失 获取重新聚焦 再次选中文本?
|
||||||
boldModern: () => {
|
boldModern: () => {
|
||||||
document.execCommand('bold', false, null);
|
document.execCommand('bold', true, null);
|
||||||
},
|
},
|
||||||
underLine: () => {
|
underLine: () => {
|
||||||
document.execCommand('underline', false, null);
|
document.execCommand('underline', false, null);
|
||||||
@@ -47,6 +48,21 @@ const funEvent = (item) => {
|
|||||||
functions[item.fun]();
|
functions[item.fun]();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.active,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
showAction.value = true;
|
||||||
|
} else {
|
||||||
|
save(editor.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
showAction.value = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const showAction = ref(false);
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
label: '加粗',
|
label: '加粗',
|
||||||
@@ -69,31 +85,19 @@ const actions = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const showToolbar = () => {
|
|
||||||
editorAction.value.style.display = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
const currentHeight = window.innerHeight;
|
|
||||||
if (currentHeight < lastHeight) {
|
|
||||||
// 键盘弹出
|
|
||||||
editorAction.value.style.display = '';
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 键盘收起
|
|
||||||
editorAction.value.style.display = 'none';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
lastHeight = currentHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', handleResize);
|
editor.value.addEventListener('focus', () => {
|
||||||
|
showAction.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('resize', () => {
|
||||||
|
showAction.value = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
// onBeforeUnmount(() => {
|
||||||
window.removeEventListener('resize', handleResize);
|
// editor.value.removeEventListener('resize', handleResize);
|
||||||
});
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||||
src:
|
src:
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
|
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
|
||||||
format('embedded-opentype'),
|
format('embedded-opentype'),
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
|
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
|
||||||
format('svg');
|
format('svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ export const useCommonStore = defineStore('common', {
|
|||||||
stem: '<p>请问,您的实足年龄在以下哪个区间呢?指的是您上一次过生日时的年龄</p>',
|
stem: '<p>请问,您的实足年龄在以下哪个区间呢?指的是您上一次过生日时的年龄</p>',
|
||||||
other: '',
|
other: '',
|
||||||
question_index: 27,
|
question_index: 27,
|
||||||
question_type: 1,
|
question_type: 6,
|
||||||
config: {
|
config: {
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
version: '',
|
version: '',
|
||||||
@@ -1097,7 +1097,7 @@ export const useCommonStore = defineStore('common', {
|
|||||||
stem: '<p><span style="font-family: \'PingFang SC Regular\';">假设介绍中描述的产品在您平时购物的商店或网站销售的话,以下哪句话最能描述您为自己或家人<span style="text-decoration: underline;"><strong>购买</strong><strong>这款产品A的可能性</strong></span>呢?</span></p>\n<p><img style="height: 50px; max-height: 440px;" src="https://test-cxp-public-web-1302259445.cos.ap-beijing.myqcloud.com/uat-yls/packing/imgs/1725421981418_809_mao-23.jpg" /></p>',
|
stem: '<p><span style="font-family: \'PingFang SC Regular\';">假设介绍中描述的产品在您平时购物的商店或网站销售的话,以下哪句话最能描述您为自己或家人<span style="text-decoration: underline;"><strong>购买</strong><strong>这款产品A的可能性</strong></span>呢?</span></p>\n<p><img style="height: 50px; max-height: 440px;" src="https://test-cxp-public-web-1302259445.cos.ap-beijing.myqcloud.com/uat-yls/packing/imgs/1725421981418_809_mao-23.jpg" /></p>',
|
||||||
other: '概念诊断问卷配置概念诊断1',
|
other: '概念诊断问卷配置概念诊断1',
|
||||||
question_index: 1,
|
question_index: 1,
|
||||||
question_type: 1,
|
question_type: 6,
|
||||||
config: {
|
config: {
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
version: '',
|
version: '',
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
<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"
|
:element="element"
|
||||||
:active="chooseQuestionId === element.id"
|
:active="chooseQuestionId === element.id"
|
||||||
|
|||||||
@@ -117,23 +117,23 @@ const openMoveModel = (item, index) => {
|
|||||||
// 上下移动
|
// 上下移动
|
||||||
const optionMove = (action) => {
|
const optionMove = (action) => {
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'up':
|
case 'up':
|
||||||
if (activeIndex.value === 0) {
|
if (activeIndex.value === 0) {
|
||||||
moveShow.value = false;
|
moveShow.value = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 向上移动
|
// 向上移动
|
||||||
element.value.splice(activeIndex.value - 1, 0, element.value.splice(activeIndex.value, 1)[0]);
|
element.value.splice(activeIndex.value - 1, 0, element.value.splice(activeIndex.value, 1)[0]);
|
||||||
activeIndex.value -= 1;
|
activeIndex.value -= 1;
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case 'down':
|
||||||
if (activeIndex.value === element.value.length - 1) {
|
if (activeIndex.value === element.value.length - 1) {
|
||||||
moveShow.value = false;
|
moveShow.value = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
element.value.splice(activeIndex.value + 1, 0, element.value.splice(activeIndex.value, 1)[0]);
|
element.value.splice(activeIndex.value + 1, 0, element.value.splice(activeIndex.value, 1)[0]);
|
||||||
activeIndex.value += 1;
|
activeIndex.value += 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ const getSkipTypeText = (skipType) => {
|
|||||||
const ls = [];
|
const ls = [];
|
||||||
logics.map((item) => {
|
logics.map((item) => {
|
||||||
if (
|
if (
|
||||||
item.skip_type === skipType &&
|
item.skip_type === skipType
|
||||||
item.question_index === activeQuestion.value.question_index
|
&& item.question_index === activeQuestion.value.question_index
|
||||||
) {
|
) {
|
||||||
ls.push(item);
|
ls.push(item);
|
||||||
}
|
}
|
||||||
@@ -213,13 +213,13 @@ const getSkipTypeText = (skipType) => {
|
|||||||
|
|
||||||
const questionSetting = (type) => {
|
const questionSetting = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'before':
|
case 'before':
|
||||||
questionBeforeShow.value = true;
|
questionBeforeShow.value = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'after':
|
case 'after':
|
||||||
questionBeforeShow.value = true;
|
questionBeforeShow.value = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
skipType.value = type === 'before' ? 1 : 0;
|
skipType.value = type === 'before' ? 1 : 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ function isSurplus() {
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
parseFloat(((localConfig.value.max - localConfig.value.min) * 1000).toFixed(4, 10)) %
|
parseFloat(((localConfig.value.max - localConfig.value.min) * 1000).toFixed(4, 10))
|
||||||
parseFloat((localConfig.value.score_interval * 1000).toFixed(4, 10)) ===
|
% parseFloat((localConfig.value.score_interval * 1000).toFixed(4, 10))
|
||||||
0
|
=== 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@
|
|||||||
label-align="top"
|
label-align="top"
|
||||||
class="base-select"
|
class="base-select"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #left-icon>
|
||||||
<div
|
<div
|
||||||
|
class="van-filed"
|
||||||
|
v-html="element.title"
|
||||||
:contenteditable="active"
|
:contenteditable="active"
|
||||||
class="van-field"
|
@blur="saveStem($event, element, 'title')"
|
||||||
@blur="saveStem($event, element)"
|
/>
|
||||||
v-html="element.stem"
|
</template>
|
||||||
></div>
|
<template #label>
|
||||||
|
<contenteditable v-model="element.stem" :active="active"></contenteditable>
|
||||||
</template>
|
</template>
|
||||||
<template #input>
|
<template #input>
|
||||||
<template v-for="(item, index) in element.options" :key="index">
|
<template v-for="(item, index) in element.options" :key="index">
|
||||||
@@ -76,7 +79,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import OptionAction from '@/views/Design/components/ActionCompoents/OptionAction.vue';
|
import OptionAction from '@/views/Design/components/ActionCompoents/OptionAction.vue';
|
||||||
import { ref } from 'vue';
|
import { ref, defineAsyncComponent } from 'vue';
|
||||||
|
const Contenteditable = defineAsyncComponent(() => import('@/components/contenteditable.vue'));
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
element: {
|
element: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -96,8 +100,8 @@ const element = ref(props.element);
|
|||||||
const saveOption = (e, ele) => {
|
const saveOption = (e, ele) => {
|
||||||
ele.option = e.target.innerHTML;
|
ele.option = e.target.innerHTML;
|
||||||
};
|
};
|
||||||
const saveStem = (e, ele) => {
|
const saveStem = (e, ele, key) => {
|
||||||
ele.stem = e.target.innerHTML;
|
ele[key] = e.target.innerHTML;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
class="base-select"
|
class="base-select"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<contenteditable v-model="element.stem"></contenteditable>
|
<contenteditable v-model="element.stem" :active="active"></contenteditable>
|
||||||
|
<!-- <div v-html="element.stem" v-else></div>-->
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
class="iconfont active-icon"
|
class="iconfont active-icon"
|
||||||
:style="{ marginRight: isLastPage ? '0' : '16px' }"
|
:style="{ marginRight: isLastPage ? '0' : '16px' }"
|
||||||
@click="activePage"
|
@click="activePage"
|
||||||
></i
|
></i>
|
||||||
>
|
|
||||||
<template v-if="!isLastPage">
|
<template v-if="!isLastPage">
|
||||||
<i class="iconfont moverQues" style="margin-right: 16px"></i>
|
<i class="iconfont moverQues" style="margin-right: 16px"></i>
|
||||||
<i class="iconfont" @click="deleteHandle"></i>
|
<i class="iconfont" @click="deleteHandle"></i>
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
<div v-for="item in 10" :key="item" class="template">
|
<div v-for="item in 10" :key="item" class="template">
|
||||||
<img src="https://picsum.photos/131/128" width="110" height="100" alt="" />
|
<img src="https://picsum.photos/131/128" width="110" height="100" alt="" />
|
||||||
<span>报名/签到模板</span>
|
<span>报名/签到模板</span>
|
||||||
<span style="color: rgb(127, 127, 127)"
|
<span style="color: rgb(127, 127, 127)">报名签到 | 引用 {{ item }} 次 | 创建人: {{ '张三' }}</span>
|
||||||
>报名签到 | 引用 {{ item }} 次 | 创建人: {{ '张三' }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user