ElmentUI-textarea实现自定义变量文本模板配置组件

ElmentUI-textarea实现自定义变量文本模板配置组件

初阶版本

简介

TemplateEditor 是一个 Vue 组件,主要用于创建和编辑包含变量的文本模板。它提供了一个用户界面,允许用户选择预定义的变量并将其插入到模板中,同时提供了模板内容的实时预览功能。

功能模块

  1. 变量选择面板
    • 显示一组预定义的变量标签,每个标签对应一个变量。
    • 这些变量存储在 templateVariableOptions 数组中,包含 dictLabel(显示名称)和 dictValue(实际值)。
    • 用户点击变量标签时,会触发 insertVariable 方法将变量插入到文本模板中。
  2. 文本模板编辑区域
    • 使用 <textarea> 元素让用户输入和编辑模板内容。
    • 监听 clickkeyupinput 事件,分别用于保存光标位置和更新父组件的模板内容。
    • saveCursorPosition 方法会记录用户当前的光标位置,以便在插入变量时能准确地将变量插入到该位置。
  3. 预览区域
    • 显示模板内容的实时预览。
    • parsedPreview 方法会使用正则表达式将模板中的变量(以 {} 包裹)替换为对应的 dictLabel,并将替换后的内容以 HTML 格式显示在预览区域。
  4. 数据绑定和更新
    • 通过 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 方法将对应的变量插入到输入框中。
  • 文本输入框:用户可以在该输入框中输入内容,支持多行输入。输入框绑定了多个事件,如 inputblurfocus 等,用于处理输入内容的变化、光标位置的变更等。
  • 预览区域:实时显示解析后的输入内容,将输入框中的变量替换为对应的标签名称,并以特定样式显示。
脚本部分 (<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>

组件属性

属性名类型默认值说明
valueString‘’用于初始化输入框的内容,支持双向绑定。

组件数据

数据名类型说明
localValueString本地模板内容,用于存储输入框的当前内容。
editorBlurNumber光标位置,记录输入框失去焦点时光标的位置。
templateVariableOptionsArray模板变量选择项列表,存储预设的变量选项。
variableMapObject变量选择项映射,用于将变量值映射到对应的标签名称。

组件计算属性

计算属性名返回值类型说明
parsedPreviewString解析后的预览内容,将输入框中的变量替换为对应的标签名称,并以特定样式显示。

组件方法

1. initOptions()
  • 说明:初始化模板变量选择项列表,通过调用 getDicts 方法获取变量选项,并生成变量选择项映射。
  • 示例:在组件创建时自动调用该方法。
2. addVariable(variable)
  • 参数
    • variable:一个对象,包含 dictValuedictLabel 属性,代表要插入的变量。
  • 说明:将选中的变量插入到输入框的当前光标位置,并更新光标位置和父组件的 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 攻击。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值