文章目录
- 效果
- 获取粘贴的文件
- 获取拖拽的文件
- 发送请求 生成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)};
}
`;