增加商机线索管理修复若干bug

This commit is contained in:
hero920103 committed 2026-09-11 11:11:43 +08:00
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>