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)};
  }
`;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值