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']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheck: typeof import('vant/es')['Check']
VanCheckbo: typeof import('vant/es')['Checkbo']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanCol: typeof import('vant/es')['Col']
VanDialog: typeof import('vant/es')['Dialog']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanIcon: typeof import('vant/es')['Icon']

View File

@@ -1,39 +1,40 @@
<template>
<div
ref="editor"
contenteditable="true"
:contenteditable="active"
class="van-field"
@focus="showToolbar"
@blur="save"
v-html="modelValue"
></div>
<div ref="editorAction" class="editor-action">
<button v-for="item in actions" :key="item.name" @click="funEvent(item)">{{ item.label }}</button>
<div v-if="showAction && active" ref="editorAction" class="editor-action">
<button v-for="item in actions" :key="item.name" @click="funEvent(item,$event)">{{ item.label }}</button>
</div>
</template>
<script setup>
import { defineEmits, ref, onMounted, onBeforeUnmount } from 'vue';
import { defineEmits, ref, onMounted, watch } from 'vue';
defineProps({
const props = defineProps({
modelValue: {
type: String,
default: ''
},
active: {
type: Boolean,
default: false
}
});
const editor = ref(null);
const editorAction = ref(null);
const emit = defineEmits(['update:modelValue']);
let lastHeight = window.innerHeight;
const save = (e) => {
emit('update:modelValue', e.target.innerHTML);
emit('update:modelValue', e.innerHTML);
};
const functions = {
// todo 点击按钮之后 如何判断 才能让按钮再次点击 不消失 获取重新聚焦 再次选中文本?
boldModern: () => {
document.execCommand('bold', false, null);
document.execCommand('bold', true, null);
},
underLine: () => {
document.execCommand('underline', false, null);
@@ -47,6 +48,21 @@ const funEvent = (item) => {
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 = [
{
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(() => {
window.addEventListener('resize', handleResize);
editor.value.addEventListener('focus', () => {
showAction.value = true;
});
document.addEventListener('resize', () => {
showAction.value = false;
});
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
// onBeforeUnmount(() => {
// editor.value.removeEventListener('resize', handleResize);
// });
</script>
<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#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.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
format('svg');
format('svg');
}
.logo {

View File

@@ -797,7 +797,7 @@ export const useCommonStore = defineStore('common', {
stem: '<p>请问,您的实足年龄在以下哪个区间呢?指的是您上一次过生日时的年龄</p>',
other: '',
question_index: 27,
question_type: 1,
question_type: 6,
config: {
placeholder: '',
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>',
other: '概念诊断问卷配置概念诊断1',
question_index: 1,
question_type: 1,
question_type: 6,
config: {
placeholder: '',
version: '',

View File

@@ -33,8 +33,8 @@
<martrix-question
v-if="
element.question_type === 8 ||
element.question_type === 9 ||
element.question_type === 10
element.question_type === 9 ||
element.question_type === 10
"
:element="element"
:active="chooseQuestionId === element.id"

View File

@@ -117,23 +117,23 @@ const openMoveModel = (item, index) => {
// 上下移动
const optionMove = (action) => {
switch (action.action) {
case 'up':
if (activeIndex.value === 0) {
moveShow.value = false;
return false;
}
// 向上移动
element.value.splice(activeIndex.value - 1, 0, element.value.splice(activeIndex.value, 1)[0]);
activeIndex.value -= 1;
break;
case 'down':
if (activeIndex.value === element.value.length - 1) {
moveShow.value = false;
return false;
}
element.value.splice(activeIndex.value + 1, 0, element.value.splice(activeIndex.value, 1)[0]);
activeIndex.value += 1;
break;
case 'up':
if (activeIndex.value === 0) {
moveShow.value = false;
return false;
}
// 向上移动
element.value.splice(activeIndex.value - 1, 0, element.value.splice(activeIndex.value, 1)[0]);
activeIndex.value -= 1;
break;
case 'down':
if (activeIndex.value === element.value.length - 1) {
moveShow.value = false;
return false;
}
element.value.splice(activeIndex.value + 1, 0, element.value.splice(activeIndex.value, 1)[0]);
activeIndex.value += 1;
break;
}
};

View File

@@ -196,8 +196,8 @@ const getSkipTypeText = (skipType) => {
const ls = [];
logics.map((item) => {
if (
item.skip_type === skipType &&
item.question_index === activeQuestion.value.question_index
item.skip_type === skipType
&& item.question_index === activeQuestion.value.question_index
) {
ls.push(item);
}
@@ -213,13 +213,13 @@ const getSkipTypeText = (skipType) => {
const questionSetting = (type) => {
switch (type) {
case 'before':
questionBeforeShow.value = true;
case 'before':
questionBeforeShow.value = true;
break;
case 'after':
questionBeforeShow.value = true;
break;
break;
case 'after':
questionBeforeShow.value = true;
break;
}
skipType.value = type === 'before' ? 1 : 0;
};

View File

@@ -108,9 +108,9 @@ function isSurplus() {
return false;
} else {
return (
parseFloat(((localConfig.value.max - localConfig.value.min) * 1000).toFixed(4, 10)) %
parseFloat((localConfig.value.score_interval * 1000).toFixed(4, 10)) ===
0
parseFloat(((localConfig.value.max - localConfig.value.min) * 1000).toFixed(4, 10))
% parseFloat((localConfig.value.score_interval * 1000).toFixed(4, 10))
=== 0
);
}
}

View File

@@ -6,13 +6,16 @@
label-align="top"
class="base-select"
>
<template #label>
<template #left-icon>
<div
class="van-filed"
v-html="element.title"
:contenteditable="active"
class="van-field"
@blur="saveStem($event, element)"
v-html="element.stem"
></div>
@blur="saveStem($event, element, 'title')"
/>
</template>
<template #label>
<contenteditable v-model="element.stem" :active="active"></contenteditable>
</template>
<template #input>
<template v-for="(item, index) in element.options" :key="index">
@@ -76,7 +79,8 @@
</template>
<script setup>
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({
element: {
type: Object,
@@ -96,8 +100,8 @@ const element = ref(props.element);
const saveOption = (e, ele) => {
ele.option = e.target.innerHTML;
};
const saveStem = (e, ele) => {
ele.stem = e.target.innerHTML;
const saveStem = (e, ele, key) => {
ele[key] = e.target.innerHTML;
};
</script>
<style scoped lang="scss">

View File

@@ -8,7 +8,8 @@
class="base-select"
>
<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>
</van-field>
</div>

View File

@@ -11,8 +11,7 @@
class="iconfont active-icon"
:style="{ marginRight: isLastPage ? '0' : '16px' }"
@click="activePage"
>&#xe86c;</i
>
>&#xe86c;</i>
<template v-if="!isLastPage">
<i class="iconfont moverQues" style="margin-right: 16px">&#xe71b;</i>
<i class="iconfont" @click="deleteHandle">&#xe6c5;</i>

View File

@@ -3,9 +3,7 @@
<div v-for="item in 10" :key="item" class="template">
<img src="https://picsum.photos/131/128" width="110" height="100" alt="" />
<span>报名/签到模板</span>
<span style="color: rgb(127, 127, 127)"
>报名签到 | 引用 {{ item }} | 创建人: {{ '张三' }}</span
>
<span style="color: rgb(127, 127, 127)">报名签到 | 引用 {{ item }} | 创建人: {{ '张三' }}</span>
</div>
</div>
</template>