需求:实现在输入框里上传图片,表单提交并渲染
如图所示:
表单结构
<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;
},
如图:
渲染
后端返回的字符串"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;
},
如图
这个输入框的样式修改:
#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);