16_模板字符替换

实现效果

  1. 选择对应模板,文案会自动填充到多行文本框,且可替换占位字符会高亮
  2. 在替换编辑里,编辑替换占位字符
  3. 可添加、编辑、删除模板

具体操作流程及规则:

  1. 点击活动公告-添加或某个活动编辑处;找到添加模板

  2. 在模板界面可以随意添加或修改公告模板;也可以删除不再需要的模板

或者在【具体模板】输入要添加的模板标签,然后在模板框内填入模板内容点保存即可

  1. 点击保存后回到公告界面,选择某个模板后可自动录入模板内容,且可替换占位字符会高亮

  2. 替换逻辑是,根据字母表 aaaa 到 zzzz;都可以替换(看模板上怎么放这些字母)

然后在替换编辑直接输入日常那些道具名字;替换的字数不限制,比如活动日期

  1. 弄好替换的文案直接点【一键录入】;就会录入文案

  2. 注意事项:一键录入后再修改替换框内的内容是修改不了的;因为已经替换过字母了没这么智能;

若一键录入后需要修改替换内容,则自行在公告内容内修改

组件实现

1. 公告模板组件

edit-template-modal.vue

选择框+输入框的结合:可实现编辑已有模板 或 新增模板
<AutoComplete
  v-model="templateInfo.name"
  :data="templateOptions"
  @on-change="handleChangeContent"
  class="w-150px"
></AutoComplete>
props: {
  templateList: Array
},
data() {
  return {
    templateInfo: {
      name: '',
      // ...
    },
  }
},
computed: {
  templateOptions() {
    return this.templateList.map(item => item.name);
  }
},
methods: {
  handleChangeContent(value) {
    // 遍历 templateList 存在 name===value,则处理回显信息,且记录id -> 编辑
    if (this.templateOptions.includes(value)) {
      this.templateList.map(item => {
        if (item.name === value) {
          // ...
        }
      });
      this.type = 'edit';
    } else {
      this.type = 'add';
    }
  },
}
删除已有模板
<div class="ml-5 select-wrap">
  <Button
    class="select-button"
    type="primary"
    icon="md-arrow-dropdown"
    v-click-outside="handdleHideOption"
    @click="showOption = !showOption"
    >已有模板</Button
  >
  <transition name="vertical-toggle">
    <ul v-show="showOption" class="select-option">
      <li class="select-item" v-for="item in templateList" :key="item.id">
          <span>{{ item.name }}</span>
          <div class="select-remove" @click.stop.prevent="handleRemoveTemplate(item.id)">
            <Icon type="md-close" />
          </div>
      </li>
    </ul>
  </transition>
</div>
props: {
  templateList: Array
},
data() {
  return {
    showOption: false,
  }
},
directives: {
  'click-outside': {
    bind: function (el, binding, vnode) {
      el.clickOutsideEvent = function (event) {
        // 检查点击事件是否发生在元素外部
        if (!(el === event.target || el.contains(event.target))) {
          // 如果是,调用提供的方法
          vnode.context[binding.expression](event);
        }
      };
      document.body.addEventListener('click', el.clickOutsideEvent);
    },
    unbind: function (el) {
      document.body.removeEventListener('click', el.clickOutsideEvent);
    }
  }
},
methods: {
  handdleHideOption() {
    this.showOption = false;
  },
  // 删除模板
  handleRemoveTemplate(id) {
    this.$Modal.confirm({
      title: '提示',
      content: '<p>删除模板标题后,对应的模板内容也会删除</p><p>是否继续当前操作?</p>',
      onOk: async () => {
        const params = { id };
        const { data: result } = await this.$axios.post(this.apis.remove, params);
        if (result.code === 1) {
          this.$Message.success(result.msg);
          this.showOption = false;
          this.$emit('getTemplateList');
        } else {
          this.$Message.warning(result.msg);
        }
      }
    });
  }
}

2. 多行文本框占位符高亮

hightlight-box.vue

  • 实现思路:

    1. div 跟 textarea 重叠样式和位置保持完全一致
    2. 同时 textarea 文字跟背景透明
    3. textarea 负责输入
    4. div 负责高亮显示
  • 需要注意的点:

    1. textarea 需要把文字跟背景设置成透明的;
    2. 上层负责显示的 div,通过 pointer-events: none; 将鼠标事件设置为失效
<div class="highlight-box">
  <div class="textarea-outer" ref="textareaOuter" :style="{ height: `${maxHeight}px` }">
    <div ref="outerInner" class="outer-inner" style="white-space: pre-wrap" v-html="highlightHtml(value)"></div>
  </div>
  <Input
    ref="textareaBox"
    placeholder="请输入内容..."
    v-model.trim="inputContent"
    type="textarea"
    :autosize="{ minRows: 8, maxRows: 8 }"
    :style="{ height: `${maxHeight}px` }"
    @keyup.enter="syncScrollTop"
  ></Input>
</div>
props: {
  content: String,
  highlightKey: Array,
  maxHeight: {
    type: Number,
    default: 170
  }
},
data() {
  return {
    value: '',
    inputContent: ''
  };
},
watch: {
  content(newVal) {
    this.inputContent = newVal;
  },
  inputContent(newVal) {
    this.value = newVal ? newVal.replace(/</g, '&lt;').replace(/>/g, '&gt;') : newVal;
  }
}
  • 生成 div 展示的内容
methods: {
  highlightHtml(str) {
    if (str) {
      let rebuild = str;
      if (this.highlightKey.filter(item => ~str.indexOf(item)).length) {
        let regExp = null;
        this.highlightKey.forEach(item => {
          regExp = new RegExp(item, 'g');
          rebuild = rebuild.replace(regExp, `<span class="hight-light-text">${item}</span>`);
        });
      }
      return rebuild;
    }
    return str;
  },
}
  • 监听滚动事件,令div跟textare的滚动高度保持一致
mounted() {
  const wrap = this.$refs.textareaBox.$el.getElementsByTagName('textarea')[0];
  wrap.addEventListener('scroll', this.syncScrollTop);
  wrap.addEventListener('mousewheel', this.syncScrollTop);
},
beforeDestory() {
  const wrap = this.$refs.textareaBox.$el.getElementsByTagName('textarea')[0];
  wrap.removeEventListener('scroll', this.syncScrollTop);
  wrap.removeEventListener('mousewheel', this.syncScrollTop);
},
methods: {
  syncScrollTop() {
    const wrap = this.$refs.textareaBox.$el.getElementsByTagName('textarea')[0];
    const outerWrap = this.$refs.textareaOuter;
    const outerInner = this.$refs.outerInner;

    if (wrap.scrollHeight > this.maxHeight && outerInner.scrollHeight !== wrap.scrollHeight) {
      outerInner.style.height = `${wrap.scrollHeight}px`;
    }
    if (wrap.scrollTop !== outerWrap.scrollTop) {
      outerWrap.scrollTop = wrap.scrollTop;
    }
  },
}
  • 多行文本框样式
@width: 600px;
::v-deep textarea.ivu-input {
  width: @width;
  line-height: 20px;
  resize: none;
  // 光标的颜色
  color: #333333;
  // 文本颜色
  text-shadow: 0 0 0 rgba(0, 0, 0, 0);
  -webkit-text-fill-color: transparent;
  background: transparent;
  border-radius: 5px;
  border: 1px solid #e0e0e0;
  padding: 4px 8px;
  &::placeholder {
    -webkit-text-fill-color: #999999;
  }
  &:hover {
    border-color: #4c84ff;
  }
  &:focus {
    border-color: #4c84ff;
    box-shadow: 0 0 0 2px #dbe4ff;
    outline: none;
  }
}
::v-deep .hight-light-text {
  color: #f27c49;
}
  • 其它样式
@width: 600px;
.highlight-box {
  position: relative;
  font-size: 14px;
  width: @width;
  position: relative;
  color: #515a61;
  background: #ffffff;
  border-radius: 5px;
  overflow: hidden;
  .textarea-outer {
    width: @width;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    border: 1px solid transparent;
    border-top: 0;
    // 鼠标事件失效 ie6-10不支持
    pointer-events: none;
    cursor: text;
    overflow-y: auto;
    line-height: 20px;
    word-break: break-word;
    .outer-inner {
      padding: 4px 8px;
      width: 100%;
      &:hover {
        border-color: #4c84ff;
      }
    }
  }
}

3. 替换编辑

replace-item-select.vue

<div class="row-y-center mb-5" v-for="(item, index) in data" :key="item.keyId">
  <span class="t-nowrap t-right" style="width: 50px">{{ labelList[index] }}:</span>
  <Input class="t-nowrap ml-5 w-100px" type="text" v-model="item.content" />
  <Button
    class="col-auto ml-5"
    type="primary"
    shape="circle"
    size="small"
    icon="md-add"
    @click="addItem(index)"
  ></Button>
  <Button
    class="col-auto ml-5"
    type="error"
    shape="circle"
    size="small"
    icon="md-remove"
    @click="deleteItem(index)"
  ></Button>
</div>
  • 生成小写字母
function getGenerateSmall() {
  var str = [];
  for (var i = 97; i < 123; i++) {
    str.push(String.fromCharCode(i).repeat(4));
  }
  return str;
}
const labelList = getGenerateSmall();
  • 创建一个新选项
let autoAddId = 0; // 自增id
function createUnit() {
  return {
    keyId: autoAddId++,
    content: ''
  };
}
  • 对外暴露初始化
function initList(min) {
  let units = [];
  while (units.length < min) {
    units.push(createUnit());
  }
  return units;
}
  • 其他属性
props: {
  data: Array,
  addItem: {
    type: Function,
    default(index) {
      // input插入1, 但是keyId 是最后+1
      this.data.splice(index + 1, 0, createUnit());
    }
  },
  deleteItem: {
    type: Function,
    default(index) {
      if (index == 0) {
        this.$Message.warning('只剩一条数据的情况下不允许删除...');
        return false;
      }
      this.data.splice(index, 1);
    }
  }
},
data() {
  return {
    labelList
  };
}

应用

<FormItem>
  <hightlight-box
    :content.sync="editInfo.content"
    :highlightKey="templateData.labelList"
    @preview="handlePreview"
  >
    <!-- 有模板则显示 -->
    <Select
      v-model="templateData.content"
      class="mb-5 w-150px"
      @on-change="handleChangeContent"
    >
      <Option
        v-for="item in templateData.list"
        :value="item.content"
        :key="item.id"
        >{{ item.name }}</Option
      >
    </Select>
    <Button class="mb-5 ml-5" type="primary" icon="ios-add" @click="handleEditTemplate">
      点击加号添加模板
    </Button>
  </hightlight-box>
</FormItem>
<FormItem label="替换编辑:">
  <div class="replace-item">
    <Button
      class="mt-5"
      style="margin-left: 250px"
      size="small"
      type="primary"
      @click="handleReplace"
    >一键录入</Button>
    <replace-items-select :data="templateData.replaceList"></replace-items-select>
  </div>
  </FormItem>
data() {
  return {
    editInfo: {
      content: null,
      // ...
    },
    templateData: {
      list: [], // 模板列表
      labelList: [],
      content: '',
      showReplace: false,
      // 0-表示未使用模板 1-表示使用模板未做替换 2-使用模板且已替换[但不一定已全替换]
      replaceStep: 0,
      replaceList: [],
      controller: {
        handleOpen: null
      }
    },
  }
},
methods: {
  // 选择具体模板
  handleChangeContent() {
    // 公告内容
    this.editInfo.content = value;
    // 展示-替换编辑
    this.templateData.showReplace = true;
    this.templateData.replaceStep = 1;
    this.templateData.replaceList = ReplaceItemsSelect.initList(7);
  },
  // 点击加号添加模板
  handleEditTemplate() {
    this.templateData.controller.handleOpen();
  },
  // 一键录入
  handleReplace() {
    const replaceList = this.templateData.replaceList;
    let content = this.editInfo.content;
    replaceList.map((item, index) => {
      if (item.content) {
        const str = new RegExp(this.templateData.labelList[index], 'g');
        content = content.replace(str, item.content);
      }
    });
    this.editInfo.content = content;
    this.templateData.replaceStep = 2;
  },
  // 预览
  handlePreview() {},
}

posted on 2024-01-08 10:36  pleaseAnswer  阅读(16)  评论(0编辑  收藏  举报