增加商机线索管理修复若干bug
This commit is contained in:
1 parent
b9cdabe045
commit
9c07af2b59
39 files changed
+6219
-42
No files matched your search
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="rich-editor">
|
||||
<div class="editor-toolbar">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUploadRequest"
|
||||
accept="image/*"
|
||||
:disabled="uploading"
|
||||
>
|
||||
<el-button size="small" :icon="Picture" :loading="uploading">上传图片</el-button>
|
||||
</el-upload>
|
||||
<span class="editor-tip">支持 Ctrl+V 直接粘贴截图</span>
|
||||
</div>
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="editor-body"
|
||||
contenteditable="true"
|
||||
:style="{ minHeight: minHeight + 'px' }"
|
||||
:data-placeholder="placeholder"
|
||||
@input="handleInput"
|
||||
@paste="handlePaste"
|
||||
@keyup="saveSelection"
|
||||
@mouseup="saveSelection"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Picture } from "@element-plus/icons-vue";
|
||||
import { uploadFile } from "@/api/file";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: "" },
|
||||
placeholder: { type: String, default: "请输入内容,可粘贴或上传图片" },
|
||||
minHeight: { type: Number, default: 120 },
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const editorRef = ref(null);
|
||||
const uploading = ref(false);
|
||||
let savedRange = null;
|
||||
|
||||
function syncFromModel() {
|
||||
if (!editorRef.value) return;
|
||||
const html = props.modelValue || "";
|
||||
if (editorRef.value.innerHTML !== html) {
|
||||
editorRef.value.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(syncFromModel);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
// 编辑中不同步,避免光标跳动;失焦或外部赋值时才同步
|
||||
if (editorRef.value && document.activeElement !== editorRef.value) {
|
||||
syncFromModel();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleInput() {
|
||||
emit("update:modelValue", editorRef.value?.innerHTML || "");
|
||||
}
|
||||
|
||||
function saveSelection() {
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0 && editorRef.value?.contains(sel.anchorNode)) {
|
||||
savedRange = sel.getRangeAt(0).cloneRange();
|
||||
}
|
||||
}
|
||||
|
||||
function insertHtml(html) {
|
||||
const editor = editorRef.value;
|
||||
if (!editor) return;
|
||||
editor.focus();
|
||||
const sel = window.getSelection();
|
||||
let range = null;
|
||||
if (sel && sel.rangeCount > 0 && editor.contains(sel.anchorNode)) {
|
||||
range = sel.getRangeAt(0);
|
||||
} else if (savedRange && editor.contains(savedRange.startContainer)) {
|
||||
range = savedRange;
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
if (!range) {
|
||||
editor.insertAdjacentHTML("beforeend", html);
|
||||
handleInput();
|
||||
return;
|
||||
}
|
||||
range.deleteContents();
|
||||
const holder = document.createElement("div");
|
||||
holder.innerHTML = html;
|
||||
const frag = document.createDocumentFragment();
|
||||
let node;
|
||||
let lastNode = null;
|
||||
while ((node = holder.firstChild)) {
|
||||
lastNode = frag.appendChild(node);
|
||||
}
|
||||
range.insertNode(frag);
|
||||
if (lastNode) {
|
||||
range.setStartAfter(lastNode);
|
||||
range.setEnd(range.endContainer, range.endOffset);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
savedRange = range.cloneRange();
|
||||
}
|
||||
handleInput();
|
||||
}
|
||||
|
||||
async function uploadImage(file) {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const res = await uploadFile(fd, { scope: "user" });
|
||||
if (res?.code !== 200 && res?.code !== 201) {
|
||||
throw new Error(res?.msg || "图片上传失败");
|
||||
}
|
||||
const data = res?.data || {};
|
||||
return data.url || data.src || "";
|
||||
}
|
||||
|
||||
async function insertImageFile(file) {
|
||||
uploading.value = true;
|
||||
try {
|
||||
const url = await uploadImage(file);
|
||||
if (!url) throw new Error("图片地址为空");
|
||||
insertHtml(`<img src="${url}" style="max-width:100%;height:auto;" />`);
|
||||
ElMessage.success("图片已插入");
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || "图片上传失败");
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleUploadRequest(options) {
|
||||
return insertImageFile(options?.file);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
const items = Array.from(e.clipboardData?.items || []);
|
||||
const files = items
|
||||
.filter((it) => it.kind === "file" && String(it.type || "").startsWith("image/"))
|
||||
.map((it) => it.getAsFile())
|
||||
.filter(Boolean);
|
||||
if (!files.length) return;
|
||||
e.preventDefault();
|
||||
saveSelection();
|
||||
insertImageFile(files[0]);
|
||||
}
|
||||
|
||||
defineExpose({ getHtml: () => editorRef.value?.innerHTML || "" });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rich-editor {
|
||||
width: 100%;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 10px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.editor-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
padding: 8px 10px;
|
||||
outline: none;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
overflow-y: auto;
|
||||
max-height: 320px;
|
||||
|
||||
&:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user