ElmentUI-textarea实现自定义变量文本模板配置组件
初阶版本
简介
TemplateEditor
是一个 Vue 组件,主要用于创建和编辑包含变量的文本模板。它提供了一个用户界面,允许用户选择预定义的变量并将其插入到模板中,同时提供了模板内容的实时预览功能。
功能模块
- 变量选择面板:
- 显示一组预定义的变量标签,每个标签对应一个变量。
- 这些变量存储在
templateVariableOptions
数组中,包含dictLabel
(显示名称)和dictValue
(实际值)。 - 用户点击变量标签时,会触发
insertVariable
方法将变量插入到文本模板中。
- 文本模板编辑区域:
- 使用
<textarea>
元素让用户输入和编辑模板内容。 - 监听
click
、keyup
和input
事件,分别用于保存光标位置和更新父组件的模板内容。 saveCursorPosition
方法会记录用户当前的光标位置,以便在插入变量时能准确地将变量插入到该位置。
- 使用
- 预览区域:
- 显示模板内容的实时预览。
parsedPreview
方法会使用正则表达式将模板中的变量(以{}
包裹)替换为对应的dictLabel
,并将替换后的内容以 HTML 格式显示在预览区域。
- 数据绑定和更新:
- 通过
props
接收父组件传递的value
,并将其存储在localValue
中进行本地编辑。 - 当
localValue
发生变化时,updateParent
方法会触发input
事件,将更新后的模板内容传递给父组件。
- 通过
组件代码
<template>
<div class="editor-wrapper">
<!-- 变量选择面板 -->
<div class="variable-panel">
<el-tag
v-for="(variable, index) in templateVariableOptions"
:key="index"
type="info"
effect="plain"
class="variable-tag"
@click="insertVariable(variable)">
{{ variable.dictLabel }}
</el-tag>
</div>
<!-- textarea 模板内容 -->
<textarea
ref="templateEditor"
v-model="localValue"
class="template-editor"
placeholder="输入模板内容..."
@click="saveCursorPosition"
@keyup="saveCursorPosition"
@input="updateParent"
></textarea>
<!-- 预览区域 -->
<div class="preview-area" v-html="parsedPreview()"></div>
</div>
</template>
<script>
export default {
name: 'TemplateEditor',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
// 光标位置
cursorPos: 0,
// 本地模板内容
localValue: this.value,
// 模板变量选择项列表
templateVariableOptions: [
{ dictLabel: "合同号", dictValue: "contractNo" },
{ dictLabel: "被申请人名称", dictValue: "defendantName" },
{ dictLabel: "调解金额", dictValue: "overdueAmt" },
{ dictLabel: "逾期天数", dictValue: "overdueDays" }
],
// 变量选择项映射
variableMap: {}
};
},
watch: {
value(newValue) {
this.localValue = newValue;
}
},
created() {
// 初始化选项
this.initOptions()
},
methods: {
initOptions() {
if (this.templateVariableOptions && this.templateVariableOptions.length > 0) {
this.variableMap = {};
this.templateVariableOptions.forEach(variable => {
this.variableMap[variable.dictValue] = variable.dictLabel;
});
}
},
/**
* 保存光标位置
*/
saveCursorPosition() {
this.cursorPos = this.$refs.templateEditor.selectionStart;
},
/**
* 变量插入
*/
insertVariable(variable) {
const editor = this.$refs.templateEditor;
const variableText = `{${variable.dictValue}}`;
// 在光标位置插入变量
const value = this.localValue || '';
const newValue = value.slice(0, this.cursorPos) + variableText + value.slice(this.cursorPos);
// 更新数据并保持光标位置
this.$nextTick(() => {
this.localValue = newValue;
this.cursorPos += variableText.length;
editor.focus();
editor.setSelectionRange(this.cursorPos, this.cursorPos);
this.updateParent();
});
},
/**
* 预览区域内容处理
* @return {*|string} 处理后的预览内容
*/
parsedPreview() {
if (!this.localValue) {
return ""
}
// 使用正则表达式替换变量
return this.localValue.replace(/\{([^}]+)\}/g, (match, dictValue) => {
const dictLabel = this.variableMap[dictValue] || dictValue;
return `<span class="variable">${dictLabel}</span>`;
});
},
/**
* 更新父组件的模板内容
*/
updateParent() {
this.$emit('input', this.localValue);
}
}
}
</script>
<style>
.editor-wrapper {
position: relative;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 5px;
}
.variable-panel {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.variable-tag {
cursor: pointer;
transition: all 0.3s;
}
.variable-tag:hover {
background-color: #f4f4f5;
transform: scale(1.05);
}
.template-editor {
width: 100%;
min-height: 120px;
padding: 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
resize: vertical;
}
.preview-area {
margin-top: 10px;
padding: 8px;
border: 1px solid #ebeef5;
border-radius: 4px;
min-height: 60px;
}
.preview-area .variable {
color: #409eff;
padding: 0 4px;
margin: 0 2px;
background-color: #f0f7ff;
}
</style>
引用代码
<el-form-item label="短信模板内容:" prop="smsTempContext">
<template-editor v-model="smsTempContext"/>
</el-form-item>
<script>
export default {
name: 'SmsTemplate',
data() {
return {
// 模板内容
smsTempContext: ''
};
}
}
<script>
进阶版本
概述
TemplateEditor
是一个自定义的 Vue 组件,用于提供一个支持插入预设变量的文本输入框,并具备实时预览功能。用户可以通过点击预设的变量标签将变量插入到输入框中,输入框中的变量会在预览区域以特定样式显示。
源码
<template>
<div class="editor-wrapper">
<!-- 变量选择区域 -->
<div class="variable-panel">
<el-tag
v-for="(variable, index) in templateVariableOptions"
:key="index"
type="info"
effect="plain"
class="variable-tag"
@click="addVariable(variable)">
{{ variable.dictLabel }}
</el-tag>
</div>
<!-- 内容输入区域 -->
<el-input
ref="templateEditor"
type="textarea"
v-model="localValue"
:rows="5"
placeholder="请输入内容"
@input.native="inputChange"
@blur="blurChange"
@focus="focusChange"
@click.native="focusChange"
@keydown.up.down.left.right.native="focusChange"
@select.native="selectEditor"
class="template-editor"
/>
<!-- 预览区域 -->
<div class="preview-area" v-html="parsedPreview"></div>
</div>
</template>
<script>
export default {
/** 自定义变量文本输入框 */
name: "TemplateEditor",
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
// 本地模板内容
localValue: this.value,
// 光标位置
editorBlur: 0,
// 模板变量选择项列表
templateVariableOptions: [],
// 变量选择项映射
variableMap: {}
};
},
/**
* 计算属性
*/
computed: {
/**
* 计算解析预览区域内容
* @return {*|string} 预览区域内容
*/
parsedPreview() {
if (!this.localValue) {
return "";
}
// 使用正则表达式替换变量
return this.localValue.replace(/\{([^}]+)}/g, (match, dictValue) => {
const dictLabel = this.variableMap[dictValue] || dictValue;
return `<span class="variable">${dictLabel}</span>`;
});
}
},
// 监视值变化
watch: {
value(newValue) {
this.localValue = newValue;
}
},
created() {
// 初始化选项
this.initOptions();
},
methods: {
/**
* 初始化选项
*/
initOptions() {
// 模板变量选择项列表
this.getDicts({dictType: 'TEMPLATE_VARIABLE'}).then(response => {
this.templateVariableOptions = response.data.data;
if (this.templateVariableOptions && this.templateVariableOptions.length > 0) {
this.variableMap = {};
this.templateVariableOptions.forEach(variable => {
this.variableMap[variable.dictValue] = variable.dictLabel;
});
}
})
},
/**
* 插入变量
*/
addVariable(variable) {
let before = this.localValue.slice(0, this.editorBlur);
let after = this.localValue.slice(
this.editorBlur,
this.localValue.length
);
let variableText = `{${variable.dictValue}}`;
this.editorBlur = this.editorBlur + variableText.length;
this.localValue = before + variableText + after;
// 修改父组件值
this.updateParentValue();
},
/**
* 光标位置变更
*/
blurChange(e) {
this.editorBlur = e.target.selectionStart;
},
/**
* 删除元素剩余部分
*/
inputChange(e) {
// deleteContentBackward==退格键 deleteContentForward==del键
if (e.inputType === "deleteContentBackward" || e.inputType === "deleteContentForward") {
let beforeIndex = 0;
let afterIndex = 0;
// 光标位置往前
for (let i = e.target.selectionStart - 1; i >= 0; i--) {
if (this.localValue[i] === "{") {
beforeIndex = i;
afterIndex = e.target.selectionStart;
break;
}
if (this.localValue[i] === "}") {
break;
}
}
// 光标位置往后
for (let i = e.target.selectionStart; i < this.localValue.length; i++) {
if (this.localValue[i] === "}") {
afterIndex = i + 1;
beforeIndex = e.target.selectionStart;
break;
}
if (this.localValue[i] === "{") {
break;
}
}
if (beforeIndex === 0 && afterIndex === 0) {
return
}
let beforeStr = this.localValue.slice(0, beforeIndex)
let afterStr = this.localValue.slice(afterIndex)
this.localValue = beforeStr + afterStr
this.editorBlur = beforeStr.length
this.$nextTick(() => {
this.updateFocus(e.target, this.editorBlur, this.editorBlur);
});
}
// 修改父组件值
this.updateParentValue();
},
/**
* 选择元素剩余部分
*/
selectEditor(e) {
// 光标开始位置往前
for (let i = e.target.selectionStart - 1; i >= 0; i--) {
if (this.localValue[i] === "{") {
this.updateFocus(e.target, i, e.target.selectionEnd);
break;
}
if (this.localValue[i] === "}") {
break;
}
}
// 光标结束位置往后
for (let i = e.target.selectionEnd; i < this.localValue.length; i++) {
if (this.localValue[i] === "}") {
this.updateFocus(e.target, e.target.selectionStart, i + 1);
break;
}
if (this.localValue[i] === "{") {
break;
}
}
},
/**
* 焦点变更
*/
focusChange(e) {
setTimeout(() => {
let selStart = e.target.selectionStart;
let beforeArrLength = this.localValue.slice(0, selStart).split("{").length;
let afterArrLength = this.localValue.slice(0, selStart).split("}").length;
// 根据'{'和'}'生成两个数组 判断数组长度 是否相等 不相等就不成对就移动光标
if (beforeArrLength !== afterArrLength) {
let pos = this.localValue.indexOf("}", selStart) + 1;
if (beforeArrLength > afterArrLength && e.code === 'ArrowLeft') {
// 按下按键左箭头
pos = this.localValue.lastIndexOf("{", selStart);
}
this.updateFocus(e.target, pos, pos);
}
}, 100);
},
/**
* 修改光标位置
* @param target 目标元素
* @param start 光标开始位置
* @param end 光标结束位置
*/
updateFocus(target, start, end) {
if (target.setSelectionRange) {
target.setSelectionRange(start, end);
} else {
let selection = window.getSelection();
let range = document.createRange();
range.setStart(target, start);
range.setEnd(target, end);
selection.removeAllRanges();
selection.addRange(range);
}
},
/**
* 修改父组件值
*/
updateParentValue() {
this.$emit('input', this.localValue);
}
}
};
</script>
<style>
.editor-wrapper {
position: relative;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 5px;
}
.variable-panel {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.variable-tag {
cursor: pointer;
transition: all 0.3s;
}
.variable-tag:hover {
background-color: #f4f4f5;
transform: scale(1.05);
}
.template-editor {
width: 100%;
min-height: 120px;
padding: 0;
border: 0 solid #dcdfe6;
border-radius: 0;
resize: vertical;
}
.preview-area {
margin-top: 10px;
padding: 8px;
border: 1px solid #ebeef5;
border-radius: 4px;
min-height: 60px;
}
.preview-area .variable {
color: #409eff;
padding: 0 4px;
margin: 0 2px;
background-color: #f0f7ff;
}
</style>
组件介绍
模板部分 (<template>
)
- 变量选择面板:显示一个包含多个标签的面板,每个标签代表一个模板变量选项。用户点击标签时,会调用
addVariable
方法将对应的变量插入到输入框中。 - 文本输入框:用户可以在该输入框中输入内容,支持多行输入。输入框绑定了多个事件,如
input
、blur
、focus
等,用于处理输入内容的变化、光标位置的变更等。 - 预览区域:实时显示解析后的输入内容,将输入框中的变量替换为对应的标签名称,并以特定样式显示。
脚本部分 (<script>
)
- props:接收一个
value
属性,用于初始化输入框的内容。 - data:定义了多个数据属性,包括本地模板内容
localValue
、光标位置editorBlur
、模板变量选择项列表templateVariableOptions
和变量选择项映射variableMap
。 - computed:计算属性
parsedPreview
用于解析输入框中的内容,将变量替换为对应的标签名称,并返回解析后的预览内容。 - watch:监听
value
属性的变化,当value
发生变化时,更新localValue
。 - created:在组件创建时调用
initOptions
方法,初始化模板变量选择项列表。 - methods:
initOptions
:通过调用getDicts
方法获取模板变量选择项列表,并将其存储在templateVariableOptions
中,同时生成变量选择项映射variableMap
。addVariable
:将选中的变量插入到输入框的当前光标位置,并更新光标位置和父组件的value
属性。blurChange
:记录输入框失去焦点时光标的位置。inputChange
:处理输入框内容的变化,当用户按下退格键或删除键时,自动删除变量的完整部分,并更新光标位置和父组件的value
属性。selectEditor
:处理输入框内容的选择事件,自动扩展选择范围到变量的完整部分。focusChange
:处理输入框的焦点变更事件,当光标位置不在变量的完整部分时,自动调整光标位置。updateFocus
:修改输入框的光标位置。updateParentValue
:触发input
事件,更新父组件的value
属性。
样式部分 (<style>
)
定义了组件的样式,包括编辑器包装器、变量选择面板、变量标签、输入框和预览区域的样式。
组件使用
1. 引入组件
在需要使用 TemplateEditor
组件的 Vue 文件中引入该组件:
import TemplateEditor from './TemplateEditor.vue';
export default {
components: {
TemplateEditor
},
// 其他配置
}
2. 使用组件
在模板中使用 TemplateEditor
组件,并绑定 value
属性:
<template>
<div>
<TemplateEditor v-model="templateValue"/>
</div>
</template>
<script>
export default {
data() {
return {
templateValue: ''
};
}
};
</script>
组件属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | String | ‘’ | 用于初始化输入框的内容,支持双向绑定。 |
组件数据
数据名 | 类型 | 说明 |
---|---|---|
localValue | String | 本地模板内容,用于存储输入框的当前内容。 |
editorBlur | Number | 光标位置,记录输入框失去焦点时光标的位置。 |
templateVariableOptions | Array | 模板变量选择项列表,存储预设的变量选项。 |
variableMap | Object | 变量选择项映射,用于将变量值映射到对应的标签名称。 |
组件计算属性
计算属性名 | 返回值类型 | 说明 |
---|---|---|
parsedPreview | String | 解析后的预览内容,将输入框中的变量替换为对应的标签名称,并以特定样式显示。 |
组件方法
1. initOptions()
- 说明:初始化模板变量选择项列表,通过调用
getDicts
方法获取变量选项,并生成变量选择项映射。 - 示例:在组件创建时自动调用该方法。
2. addVariable(variable)
- 参数:
variable
:一个对象,包含dictValue
和dictLabel
属性,代表要插入的变量。
- 说明:将选中的变量插入到输入框的当前光标位置,并更新光标位置和父组件的
value
属性。
3. blurChange(e)
- 参数:
e
:事件对象,包含输入框的相关信息。
- 说明:记录输入框失去焦点时光标的位置。
4. inputChange(e)
- 参数:
e
:事件对象,包含输入框的相关信息。
- 说明:处理输入框内容的变化,当用户按下退格键或删除键时,自动删除变量的完整部分,并更新光标位置和父组件的
value
属性。
5. selectEditor(e)
- 参数:
e
:事件对象,包含输入框的相关信息。
- 说明:处理输入框内容的选择事件,自动扩展选择范围到变量的完整部分。
6. focusChange(e)
- 参数:
e
:事件对象,包含输入框的相关信息。
- 说明:处理输入框的焦点变更事件,当光标位置不在变量的完整部分时,自动调整光标位置。
7. updateFocus(target, start, end)
- 参数:
target
:目标元素,即输入框元素。start
:光标开始位置。end
:光标结束位置。
- 说明:修改输入框的光标位置,支持跨浏览器兼容性。
8. updateParentValue()
- 说明:触发
input
事件,更新父组件的value
属性。
组件样式
类名 | 说明 |
---|---|
.editor-wrapper | 组件的外层包装器,设置了边框、圆角和内边距。 |
.variable-panel | 变量选择面板,使用 flex 布局,支持换行显示。 |
.variable-tag | 变量标签,设置了鼠标指针样式和过渡效果。 |
.template-editor | 输入框,设置了宽度、最小高度、边框和可调整大小属性。 |
.preview-area | 预览区域,设置了边框、圆角和内边距。 |
.preview-area .variable | 预览区域中的变量,设置了文本颜色、内边距和背景颜色。 |
注意事项
- 组件依赖
getDicts
方法来获取模板变量选择项列表,请确保该方法在组件中可用。 - 输入框中的变量使用
{}
包裹,例如{variable}
。 - 预览区域使用
v-html
指令显示解析后的内容,请确保输入内容的安全性,避免 XSS 攻击。