feat(component): 优化 contenteditable组件功能

- 添加 showAction 控制编辑按钮显示
- 实现文本域聚焦和失焦时的编辑按钮显示和隐藏
-优化键盘弹出和收起时的编辑按钮显示逻辑
-修复文档中描述的产品问卷配置- 优化问卷设计页面的题目编辑功能
This commit is contained in:
陈昱达
2025-03-07 13:27:39 +08:00
parent 5686c295e1
commit eb9f6aa7ed
12 changed files with 90 additions and 81 deletions

3
components.d.ts vendored
View File

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

View File

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

View File

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

View File

@@ -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: '',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
>&#xe86c;</i >&#xe86c;</i>
>
<template v-if="!isLastPage"> <template v-if="!isLastPage">
<i class="iconfont moverQues" style="margin-right: 16px">&#xe71b;</i> <i class="iconfont moverQues" style="margin-right: 16px">&#xe71b;</i>
<i class="iconfont" @click="deleteHandle">&#xe6c5;</i> <i class="iconfont" @click="deleteHandle">&#xe6c5;</i>

View File

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