文章目录

  • ​​效果​​
  • ​​获取粘贴的文件​​
  • ​​获取拖拽的文件​​
  • ​​发送请求 生成markdown 语句​​
  • ​​实现逻辑代码(主要实现)​​
  • ​​后端代码​​

效果

js 监听 复制图片 & 拖拽上传文件 并填充到markdown编辑器_上传图片

获取粘贴的文件

const { clipboardData } = e;
file = clipboardData.items[0].getAsFile();
const Paste = useCallback(
e => {
uploadFileBy("paste")(e);
},
[uploadFileBy]
);

<div onPaste={Paste} />

获取拖拽的文件

const { dataTransfer } = e;
file = dataTransfer.files[0];
const Drop = useCallback(
e => {
uploadFileBy("drop")(e);
},
[uploadFileBy]
);

<div
onDragEnter={...}
onDragLeave={...}
onDrop={Drop}
/>

发送请求 生成markdown 语句

后端返回 上传文件名 前端生成 ​​![..](..)​​格式语句

export const baseURL = process.env.REACT_APP_URL;
export const getImagePath = baseURL + "/getImage/";

export const uploadFile = async (file: File): Promise<string | undefined> => {
const formData = new FormData();
formData.append("file", file);
const { data } = await request("/uploadFile_md", formData);
return data ? `!["当图片不显示时展示的文字"](${getImagePath}${data})` : undefined;
};

实现逻辑代码(主要实现)

​uploadFileBy​​根据type执行 粘贴 和 拖拽上传 逻辑基本一致 区别只是获取file的方式不同

type TDragEvent = React.DragEvent<HTMLDivElement>;
type TClipEvent = React.ClipboardEvent<HTMLDivElement>;

type TUploadFile = (
type: "paste" | "drop"
) => ((e: TDragEvent) => void) | ((e: TClipEvent) => void);

const allowUploadType = ["image/gif", "image/jpeg", "image/png"];
const maxFileSize = 10 * 1024 * 1024; // 10M

const uploadFileBy: TUploadFile = useCallback(
type => (e: TDragEvent | TClipEvent) => {
let file;
if (type === "drop") {
setDragging(false);
const { dataTransfer } = e as TDragEvent;
file = dataTransfer.files[0];
} else {
const { clipboardData } = e as TClipEvent;
file = clipboardData?.items[0]?.getAsFile();
console.log("file:", file);
}
if (file && allowUploadType.includes(file.type)) {
//需要时阻止默认事件 否则 粘贴文字等操作失效
e.stopPropagation();
e.preventDefault();
if (file.size < maxFileSize) {
//上传图片 获得图片地址
handleUploadFile(file);
} else NotificationWarn({ message: "文件最大10M" });
}
},
[handleUploadFile]
);

​handleUploadFile​​ 上传后端

const handleUploadFile = useCallback(
async (file: File) => {
setUploading(true);
const data = await uploadFile(file);
if (data) {
NotificationSuccess({ message: "上传成功" });
//通知 父组件 让 nav 触发加入这段文字的方法
handleInsertFile(data);
}
setUploading(false);
},
[handleInsertFile]
);

后端代码

const fs = require("fs");
const multer = require('multer');

const imagePath = `${__dirname}/public/images/`;
//文件上传到服务器的位置
const multerInstance = multer({ dest: imagePath });
app.use(multerInstance.any());

//上传图片
app.post('/uploadFile_md',(req, res) => {
const {
files: [{ path, mimetype, filename }],
} = req;
// mimetype: 'image/png',
// filename: '846764f3318fb3d40ee80c343b42bf29',
//# 避免中文名 容易出现特殊字符请求不到文件
const extName = mimetype.match(/\/(\w+)$/)[1];
//# 不改名 也可以获取图片 不过 浏览器里输入地址就查看不到图片 而是下载文件了
fs.rename(path, `${path}.${extName}`, (err) => {
if (err) {
console.error("fs rename err:", err);
res.status(500).send({ message: "文件保存失败" });
} else {
const fileName = `${filename}.${extName}`;
console.log("图片保存成功:", fileName);
res.json(fileName);
}
});
})

全部代码 仅供参考

import {
ReactElement,
useState,
useEffect,
useRef,
forwardRef,
useMemo,
useCallback,
useImperativeHandle,
} from "react";
import useMount from "../../hooks/useMount";
import { editRefProps } from "../../pages/md";
import styled from "styled-components";
import { NotificationSuccess, NotificationWarn } from "../common/Notification";
import { uploadFile } from "../../api/mdApi";

const allowUploadType = ["image/gif", "image/jpeg", "image/png"];
const maxFileSize = 10 * 1024 * 1024; // 10M
interface IProps {
syncScroll: boolean;
setMarkdownScrollTop: (y: number) => void;
onInput: React.FormEventHandler<HTMLDivElement>;
handleInsertFile: (syntax: string) => void;
}

type TDragEvent = React.DragEvent<HTMLDivElement>;
type TClipEvent = React.ClipboardEvent<HTMLDivElement>;

type TUploadFile = (
type: "paste" | "drop"
) => ((e: TDragEvent) => void) | ((e: TClipEvent) => void);

const Edit = forwardRef<editRefProps, IProps>(
(
{ syncScroll, setMarkdownScrollTop, onInput, handleInsertFile },
ref
): ReactElement => {
const [dragging, setDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const input = useRef<HTMLInputElement | null>(null);
const inputEvent = useMemo(() => {
const event = document.createEvent("HTMLEvents");
event.initEvent("input", true, true);
return event;
}, []);

const setEditScroll = useCallback(y => {
input.current?.scrollTo(0, y);
}, []);

//强行触发oninput事件 markdown获取最新内容
const forceInput = useCallback(() => {
input.current?.dispatchEvent(inputEvent);
}, [inputEvent]);

const editGetFocus = useCallback(() => {
input.current && input.current.focus();
}, [input]);

//报漏给父级 使用
useImperativeHandle(
ref,
() => ({
setEditScroll,
forceInput,
editGetFocus,
}),
[setEditScroll, forceInput, editGetFocus]
);

useMount(() => {
editGetFocus();
});

const onEditScroll = useCallback(
e => {
setMarkdownScrollTop(e.target.scrollTop);
},
[setMarkdownScrollTop]
);

useEffect(() => {
if (syncScroll) {
input.current?.addEventListener("scroll", onEditScroll);
} else {
input.current?.removeEventListener("scroll", onEditScroll);
}
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
input.current?.removeEventListener("scroll", onEditScroll);
};
}, [onEditScroll, syncScroll]);

const DragEnter: React.DragEventHandler<HTMLDivElement> = useCallback(e => {
setDragging(true);
}, []);

const DragLeave = useCallback(e => {
setDragging(false);
}, []);

const handleUploadFile = useCallback(
async (file: File) => {
setUploading(true);
const data = await uploadFile(file);
if (data) {
NotificationSuccess({ message: "上传成功" });
//通知 父组件 让 nav 触发加入这段文字的方法
handleInsertFile(data);
}
setUploading(false);
},
[handleInsertFile]
);

const uploadFileBy: TUploadFile = useCallback(
type => (e: TDragEvent | TClipEvent) => {
let file;
if (type === "drop") {
setDragging(false);
const { dataTransfer } = e as TDragEvent;
file = dataTransfer.files[0];
} else {
const { clipboardData } = e as TClipEvent;
file = clipboardData?.items[0]?.getAsFile();
console.log("file:", file);
}
if (file && allowUploadType.includes(file.type)) {
//需要时阻止默认事件 否则 粘贴文字等操作失效
e.stopPropagation();
e.preventDefault();
if (file.size < maxFileSize) {
//上传图片 获得图片地址
handleUploadFile(file);
} else NotificationWarn({ message: "文件最大10M" });
}
},
[handleUploadFile]
);

const Drop = useCallback(
e => {
uploadFileBy("drop")(e);
},
[uploadFileBy]
);

const Paste = useCallback(
e => {
uploadFileBy("paste")(e);
},
[uploadFileBy]
);

return (
<EditBox
ref={input}
onInput={onInput}
// 拖拽相关
dragging={dragging}
uploading={uploading}
onDragEnter={DragEnter}
onDragLeave={DragLeave}
onDrop={Drop}
// 粘贴
onPaste={Paste}
/>
);
}
);

export default Edit;

const EditBox = styled.div.attrs({
contentEditable: true,
})<{ dragging: boolean; uploading: boolean }>`
position: relative;
&::before {
content: "松手上传图片";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #00000077;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 30px;
transition: opacity 0.3s linear;
opacity: ${props => (props.dragging ? 1 : 0)};
z-index: ${props => (props.dragging ? 1 : -1)};
}

&::after {
content: "图片上传中...";
width: inherit;
height: 25px;
background: #51f;
padding: 2px 20px;
font-weight: bold;
color: #fff;
/* box-shadow: -2px 0px 2px 0px #807f7fc1; */
bottom: 0;
left: 0;
position: fixed;
transform: translateY(25px);
transition: transform 0.2s linear;
${props => (props.uploading ? "transform: translateY(0);" : undefined)};
}
`;