(demo)在输入框里上传图片,表单提交并渲染

235 阅读3分钟

需求:实现在输入框里上传图片,表单提交并渲染

如图所示:

12.png

表单结构

<el-form :model="addForm" :rules="addFormRules" ref="addForm">
  <template v-if="fieldlist?.length">
    <div
      class="form-item"
      v-for="item in fieldlist"
      :key="item.tpfieldname"
    >
      <el-form-item
        :label="item.tpfielddesc"
        :prop="`fieldlistParams.${item.tpfieldname}`"
      >
        ...
        <!-- inputtype为5, 支持输入框传图片 -->
        <div v-if="item.inputtype == 5">
          <div id="editor" contenteditable="true"></div>
          <el-upload
            action="#"
            :auto-upload="false"
            :on-change="uploadFile"
            :show-file-list="false"
            ref="upload"
          >
            <el-button type="primary" size="small">上传</el-button>
          </el-upload>
        </div>
        ...
      </el-form-item>
    </div>
  </template>
  <div class="form-item addForm">
    <el-form-item
      prop="uploadImg"
      :label="$t('upload.content')"
    >
      <el-upload
        action="#"
        :auto-upload="false"
        :on-change="imgUpload"
        :file-list="fileUploadList"
        ref="upload"
        :on-remove="handleRemove"
      >
        <el-button type="primary" size="small">上传对象</el-button>
      </el-upload>
    </el-form-item>
  </div>
</el-form>

上传图片

export default {
  data() {
    return {
      // 输入框的图片文件
      fileListObj: {},
      // 用于监视DOM变化的MutationObserver
      observer: null,
    };
  },
  mounted() {
    this.observeEditor();
  },
  beforeDestroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  },

  methods: {
    // 删除图片时,删除对应的file
    observeEditor() {
      const editor = document.getElementById("editor");
      this.observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.removedNodes.length > 0) {
            mutation.removedNodes.forEach((node) => {
              if (
                node.nodeName === "IMG" &&
                node.hasAttribute("data-filename")
              ) {
                const filename = node.getAttribute("data-filename");
                if (filename && this.fileListObj[filename]) {
                  delete this.fileListObj[filename];
                }
              }
            });
          }
        });
      });

      const config = { childList: true, subtree: true };
      this.observer.observe(editor, config);
    },

    uploadFile(event) {
      const file = event.raw;
      this.fileListObj[file.name] = file;
      // 调用函数以查找光标所在的 <div> 并插入图片
      this.findDivToInsertImage(file);
    },
    findDivToInsertImage(file) {
      // 创建并插入图片元素
      const windowURL = window.URL || window.webkitURL;
      console.log(file);
      const dataURL = windowURL.createObjectURL(file);
      console.log(dataURL);
      console.log("---------------");
      const img = document.createElement("img");
      img.setAttribute("data-filename", file.name);
      img.src = dataURL;
      img.width = 50;
      img.height = 50;
      // 获取光标位置
      const selection = window.getSelection();
      const range = selection.getRangeAt(0);
      // console.log(selection?.focusNode)
      //确保上传图片时,光标在编辑器内,否则鼠标点击其他地方也会插入图片
      if (
        selection?.focusNode.id == "editor" ||
        selection?.focusNode.parentNode?.id == "editor"
      ) {
        range.insertNode(img);
      }
    },
  },
};

提交表单

提交时,图文输入框的值需要处理成一个数组,图片和其他文件一起上传,后端通过数组里的文件名来匹配文件

async submitContent() {
  let fieldlist = [...this.fieldlist];
  fieldlist.forEach((item) => {
    // 如果当前为图文输入框的值,需要特殊处理
    if (item.inputtype == 5) {
      // 生成数组
      const extractedArray = this.extractTextAndFilenames();
      const fileArr = [];
      extractedArray.forEach((item) => {
        if (this.fileListObj[item]) {
          //存在,说明能找到file
          fileArr.push(this.fileListObj[item]);
        }
      });
      // console.log("extractedArray", extractedArray);
      item["fieldvalue"] = [...extractedArray];
    } else {
      item["fieldvalue"] = this.addForm.fieldlistParams[item.tpfieldname];
    }
  });

  let paramsObj = {
    // 需要携带的其他参数
    // userid: this.userInfo.userid,

    fieldlist: fieldlist,
  };

  let paramsForm = new FormData();
  paramsForm.append("jsonStr", JSON.stringify(paramsObj));
  // 其他的上传对象
  let files = this.addForm.formData.getAll("files");
  files?.forEach((item) => {
    paramsForm.append("files", item);
  });

  // 获取输入框上传的文件
  let inputFiles = Object.values(this.fileListObj);
  inputFiles?.forEach((item) => {
    paramsForm.append("files", item);
  });

  this.$refs["addForm"].validate(async (valid) => {
    if (valid) {
      const res = await AddItem(paramsForm);
      if (res.code == 200) {
        this.$message.success("提交成功!");
      }
    }
  });
},
extractTextAndFilenames() {
  const div = document.getElementById("editor");
  const nodes = div.childNodes;

  let result = [];

  nodes.forEach((node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      // 如果是文本节点,则直接将文本内容推入结果数组
      if (node.textContent.trim() !== "") {
        result.push(node.textContent.trim());
      }
    } else if (node.nodeName === "IMG") {
      // 如果是<img>元素,则获取其data-filename属性值并推入结果数组
      const filename = node.getAttribute("data-filename");
      if (filename) {
        result.push(filename);
      }
    } else if (node.nodeName === "DIV") {
      // 如果是<div>元素,则递归处理其子节点
      const subResult = this.extractTextAndFilenamesFromDiv(node);
      if (subResult.length > 0) {
        result = result.concat(subResult);
      }
    }
  });

  return result;
},
// 辅助函数,处理<div>内部的内容
extractTextAndFilenamesFromDiv(divNode) {
  const nodes = divNode.childNodes;
  let subResult = [];

  nodes.forEach((node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      // 如果是文本节点,则直接将文本内容推入子结果数组
      if (node.textContent.trim() !== "") {
        subResult.push(node.textContent.trim());
      }
    } else if (node.nodeName === "IMG") {
      // 如果是<img>元素,则获取其data-filename属性值并推入子结果数组
      const filename = node.getAttribute("data-filename");
      if (filename) {
        subResult.push(filename);
      }
    } else if (node.nodeName === "DIV") {
      // 如果是<div>元素,则递归处理其子节点
      const deeperSubResult = this.extractTextAndFilenamesFromDiv(node);
      if (deeperSubResult.length > 0) {
        subResult = subResult.concat(deeperSubResult);
      }
    }
  });

  return subResult;
},

如图:

3.png

渲染

后端返回的字符串"le spectacle^/1.png^musical^"分割成传过去的这个数组,拼成html结构渲染

getInputValue(fieldvalue) {
  // 分割、过滤空值
  const values = fieldvalue.split("^").filter((value) => value !== "");
  const imgExtensions = [".png", ".jpg", ".jpeg", ".gif"];

  const html = values
    .map((value) => {
      if (imgExtensions.some((ext) => value.endsWith(ext))) {
        return `<img src="/xxx?path=${value}" />`;
      } else {
        return `<span>${value}</span>`;
      }
    })
    .join("");

  return html;
},

如图

4.png

这个输入框的样式修改:

#editor {
    &:focus-visible {
        outline: 0;
        border: 1px solid #409EFF;
    }
}

补充优化

解决:当配置多个图文输入框时,因为写死了id导致提交内容都一样的bug

监听粘贴事件,确保以纯文本形式插入而不是带样式

  mounted() {
    this.$nextTick(() => {
      this.fieldlist?.forEach((item) => {
        if (item.inputtype === 5) {
          const editor = document.getElementById(item.tpfieldname);
          if (editor) {
            this.observeEditor(editor);

            // 监听粘贴事件,确保以纯文本形式插入
            editor.addEventListener("paste", function (e) {
              e.preventDefault();
              const text = (e.clipboardData || window.clipboardData).getData(
                "text/plain"
              );
              document.execCommand("insertText", false, text);
            });
          }
        }
      });
    });
  },

传入并使用不同的id

    extractTextAndFilenames(id) {
      const div = document.getElementById(id);
      const nodes = div.childNodes;
      ...
    }
    observeEditor(editor) {
      ...
    }
    uploadFile(event, editorId) {
      ...
      // 调用函数以查找光标所在的 <div> 并插入图片
      this.findDivToInsertImage(file, editorId);
    },
    findDivToInsertImage(file, editorId) {
      ...
      if (
        selection?.focusNode.id == editorId ||
        selection?.focusNode.parentNode?.id == editorId
      ) {
        range.insertNode(img);
      }
    },

upload的回调这么写:

<el-upload action="#" :auto-upload="false"
  :on-change="(event) => { uploadFile(event, item.tpfieldname) }" :show-file-list="false"
  ref="upload">
  <el-button type="primary" size="small">{{ $t("upload") }}</el-button>
</el-upload>

别忘记调extractTextAndFilenames时传一下

const extractedArray = this.extractTextAndFilenames(item.tpfieldname);