文件上传 = 拖拽 + 多文件 + 文件夹

news2024/11/26 2:54:24

从前往后看都是努力,从后往前看都是命运

大家好,我是「柒八九」。一个「专注于前端开发技术/RustAI应用知识分享」Coder

前言

今天呢,和大家聊点耳熟能详的东西。「文件上传」

讲到这里,大家不要嗤之以鼻,认为这不是分分钟就用组件库实现的吗?确实,现在很多成熟的组件库都提供了「文件上传」的功能,但是呢,它们只提供部分的功能。比方说,

  1. 执行{多}文件上传
  2. 拖拽上传
  3. 针对文件夹内容上传
  4. {多}文件上传 + 文件夹上传

但是呢,这些框架只是提供了上面的部分功能,而不是将上面的功能全部一网打尽。

我们来看一下Antd的文件上传的功能。 alt

Antd_Upload[1]能实现上述功能,但是不能将上面所有功能糅合到一起。因为多文件上传文件夹上传它们实现原理是不同的。(Arco_Upload[2]也是如此)。

所以,今天我们就来自己手搓一个「文件上传」。它所拥有的能力如下

  1. 支持{多}文件上传
  2. 拖拽上传
  3. 文件内容上传
  4. {多}文件上传 + 文件夹上传

也就是说,我们的文件上传可以上传你本地的任何文件。(除了系统文件,这个我们会提到)。

实现效果如下所示: alt

alt
alt

有人会说,人家都给你提供了组件,你为啥不用,你这不是重复造轮子吗?其实还真不是,之所以会出现这个问题,是因为现有的解决方案不支持我们的需求,我们才会大费周折的去找解决方案。

最后但同样重要的是:本文会提供一种解决方案,并且也会实现上述的所有功能,但是到后面文件上传到服务器的部分,这里就不做介绍了。也就是说,我们最后,通过操作能获取到Files信息,就认为这个组件封装成功了。

好了,天不早了,干点正事哇。

alt

我们能所学到的知识点

  1. 项目初始化
  2. 拖拽功能
  3. 处理input
  4. 处理文件&回调
  5. 唤起弹窗

1. 项目初始化

因为,我们在做项目展示的时候,需要用到一些组件库和工具库,所以我们就抛弃vite/cra了,我们这里就直接使用我们的f_cli[3]直接构建一个前端项目。

f_cli create upload_demo

如果你是一个老粉,你就知道,我们的f_cli是支持组件库选择的。在项目初始化时,我们可以选择组件库。

选择UI库
选择UI库

针对此次的demo我们就选择antd。然后,其他配置都按照你的心意来就完事了。

一顿操作之后,我们就有了一个功能完备的前端项目。 alt

随后,我们可以执行yarn dev进行前端项目开发了。

也就说,我们下面的代码讲解和项目组织都是基于f_cli生成项目的基础上。


2. 拖拽功能

其实,针对拖拽功能的处理,我们有很多解决方案。

  1. 利用原生特性- 在DOM原生上新增draggable属性,然后监听dragstart/dragend等。可行吗,必须可行。但是,你需要处理和监听的事情很多。

    • 如果对这块还有些陌生,可以参考 MDN_drag [4]对这块的解释
  2. 利用库,有很多业界比较出名的拖拽库能处理我们的问题,使用库的好处就是我们通过简单的API能够获取我们想要的数据和要实现的功能操作。(所以,我们就是用第三方库实现拖拽功能)下面就列举几个比较常见的拖拽库。

    • react-dnd [5]
    • react-draggable [6]
    • react-dropzone [7]

alt 通过npm_trend得知,react-dropzone独占鳌头。所以,我们就选用react-dropzone作为我们的拖拽解决方案。

alt

拖拽组件

既然,材料和食谱都已经确定,那我们就需要烹饪我们的膳食了。

现在,我们把我们的上传场景再做一次限定,我们可以将我们整个页面作为我们的拖拽区域,这样我们就不必拘泥于特定组件了。(当然,这个区域是可以变更的)。

那么,我们为这个组件起一个霸气侧漏的名字 -- FullScreenDropZone。看这名字多气派,FullScreen,它支持全屏范围内拖拽。也就是说,不管你把文件拖拽到页面的哪个位置,都可以触发文件上传功能。

"全屏"? 按照SPA的尿性,那岂不是需要在一个路由的组件的根部。没错,它就是这样的。

组件挂载位置

我们先把内部代码扔下,我们先来讲讲FullScreenDropZone是从哪里被调用的。

alt

上面的代码中,只是展示了FullScreenDropZone在何处调用,不是最终的代码。我们可以看到,我们就是把它挂载到了某个路由下的根部。并且,该页面的子组件用children先展示替代。当然,我们也可以把children的逻辑留下来,然后将组件在Router中配置。

为了大家做验证,把代码贴到了下面

import React from "react";
import FullScreenDropZone from "@/components/FullScreenDropZone";
import { useDropzone } from "react-dropzone";

const Upload: React.FC<React.PropsWithChildren> = ({ children }) => {
    const {
        getRootProps: getDragAndDropRootProps,
        getInputProps: getDragAndDropInputProps,
        acceptedFiles: dragAndDropFiles,
    } = useDropzone({
        noClicktrue,
        noKeyboardtrue,
        disabledfalse,
    });
    
  return (
      <FullScreenDropZone
            getDragAndDropRootProps={getDragAndDropRootProps}
        >
   
          {children}
        </FullScreenDropZone>

  );
};

export default Upload;

上面代码中,还有一点我们需要提前说明下,我们不是选中了react-dropzone作为我们的拖拽方案了吗。

我们使用useDropzone来收集拖拽过程中所产生的数据信息。对于更具体的参数,可以参考react-dropzone_api[8]

组件内部逻辑

从之前的代码中我们得知,FullScreenDropZone接收了一个从useDropzone中返回的属性getRootProps(我们为其取了一个别名getDragAndDropRootProps)

见名知意,该属性是为了获取在根元素上设置的属性和方法的。这是react-dropzone的语法,这里也不在过多解释。

type Props = React.PropsWithChildren<{
    getDragAndDropRootProps: any;
}>;

export default function FullScreenDropZone(props: Props{
  const [isDragActive, setIsDragActive] = useState(false);
  const onDragEnter = () => setIsDragActive(true);
  const onDragLeave = () => setIsDragActive(false);

  useEffect(() => {
      window.addEventListener("keydown", (event) => {
          if (event.code === "Escape") {
              onDragLeave();
          }
      });
  }, []);

  return (
      <DropDiv
          {...props.getDragAndDropRootProps({
              onDragEnter,
          })}
      >

          {isDragActive && (
              <Overlay 
                onDrop={onDragLeave} 
                onDragLeave={onDragLeave}
              >

                  <CloseButtonWrapper onClick={onDragLeave}>
                      x
                  </CloseButtonWrapper>
                  拖拽文件以上传
              </Overlay>
          )}
          {props.children}
      </DropDiv>

  );
}

上面的代码大致分为三部分

  1. 定义了状态( isDragActive)和方法
  2. useEffect中监听了 keydownEscape
  3. return定义了布局展示逻辑

针对第一部分和第二部分,一目了然,这里就不再赘述。

我们,来简单解释一下第三部分。首先,映入眼帘的是一堆我们之前从没讲过的组件DropDiv/Overlay/CloseButtonWrapper

其实嘛,这就是一堆普通的div原生,只不过我们使用了styled-components为其添加了一些样式。(对于如何styled-components的使用,我们前几天在styled-components不完全手册有过解释)

这里我们就直接把它们的代码贴到下面。

import { styled } from "styled-components";
const CloseButtonWrapper = styled("div")`
    position: absolute;
    top: 10px;
    right: 10px;
    cursor: pointer;
`
;
const DropDiv = styled("div")`
    flex: 1;
    display: flex;
    flex-direction: column;
`
;
const Overlay = styled("div")`
    border-width: 8px;
    left: 0;
    top: 0;
    outline: none;
    transition: border 0.24s ease-in-out;
    height: 100%;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    color: #fff;
    font-size: 24px;
    font-weight: 900;
    text-align: center;
    position: absolute;
    border-color: #51cd7c;
    border-style: solid;
    background: rgba(0, 0, 0, 0.9);
    z-index: 3000;
`
;

然后,剩余的逻辑呢,就是在组件的顶部(DropDiv),接收调用处传来的getDragAndDropRootProps并且将内部方法onDragEnter配置到内部。

随后,就是基于isDragActive判断Overlay的显示和隐藏,从上面代码中我们可以得知,Overlay就是当页面处于拖拽状态时,出现的蒙层。其中还有一个小细节就是,当我们在拖拽过程中想终止上传,我们可以将文件拖拽到CloseButtonWrapper(页面右上角),然后就会触发类似「关闭」的效果。

当然,因为我们的FullScreenDropZone是在页面的顶层,为了体现更好的可移植性和封装性。当我们想为一个页面或者页面部分区域做拖拽处理时候,我们就可以将其用FullScreenDropZone包裹。所以,我们在FullScreenDropZone中有children的处理。

到这里,看起来我们拖拽功能已经完事了,其实这只是完成了一部分。

查看react-dropzone的使用方式,其实我们还缺少input的处理。用于接收getInputProps

alt

但是,在上面代码中我们丝毫没看到关于inputgetInputProps的处理。只是用children将子组件进行了展示。

对咯,我们将input放置到了children中了。为什么会这么做呢,且看下面的分解。


3. 处理input

如果大家用原生写过上传,那势必就逃不过input的操作。

我们从MDN_Input_File[9]可以窥探一二。

alt

如上所示,我们可以

  • <input/>添加 type="file"属性,就可以实现一个简单的文件上传的功能。
  • 如果要实现多文件上传,可以新增 multiple属性。
  • 还可以设置 accept来指定上传的文件格式

如果我们要实现文件夹上传,我们可以通过设置webkitdirectory

alt

但是,使用webkitdirectory有兼容性问题。这块大家需要注意。

结合,在第二节中我们使用react-dropzone处理文件拖拽时,也需要一个<input/>接收返回的getInputProps属性。(<input {...getInputProps()} />)

UploadInputs

我们可以将上面三种<input/>做统一处理。也就是在这个组件中我们分别接收

  1. getDragAndDropInputProps 处理拖拽的配置信息
  2. getFileSelectorInputProps处理{多}文件上传的配置信息
  3. getFolderSelectorInputProps处理文件夹上传的配置信息
export default function UploadSelectorInputs({
    getDragAndDropInputProps,
    getFileSelectorInputProps,
    getFolderSelectorInputProps,
}
{
  return (
      <>
        <input {...getDragAndDropInputProps()} />
        <input {...getFileSelectorInputProps()} />
        <input {...getFolderSelectorInputProps()} />
      </>

  );
}

然后我们在指定的组件中进行组件的调用,这个地方就是我们之前调用<FullScreenDropZone/>的地方,也就是页面根路径(pages/Upload)

alt

从上面截图中我们看到,我们的UploadInputs消化了useDropzone返回的getInputProps属性了,但是其余的属性没有地方生成(用红框框起来的部分)

也就说,我们现在要在一个地方处理常规文件{夹}上传的属性定义。在这里我们定义一个Hook来执行这个操作。

useFileInput

我们在src/hook文件夹下新建一个useFileInput来执行上述操作。

import { useCallback, useRef, useState } from "react";

export interface FileWithPath extends File {
    readonly path?: string;
}

export default function useFileInput({ directory }: { directory?: boolean }{
    const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
    const inputRef = useRef<HTMLInputElement>();

    const openSelectorDialog = useCallback(() => {
        if (inputRef.current) {
            inputRef.current.value = null;
            inputRef.current.click();
        }
    }, []);

    const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
        event,
    ) => {
        if (!!event.target && !!event.target.files) {
            const files = [...event.target.files].map((file) =>
                toFileWithPath(file),
            );
            setSelectedFiles(files);
        }
    };

    const getInputProps = useCallback(
        () => ({
            type"file",
            multipletrue,
            style: { display"none" },
            ...(directory ? { directory""webkitdirectory"" } : {}),
            ref: inputRef,
            onChange: handleChange,
        }),
        [],
    );

    return {
        getInputProps,
        open: openSelectorDialog,
        selectedFiles: selectedFiles,
    };
}

上面就是一个功能的封装,按照返回值来看,其中有三个重要的功能点

  1. getInputProps:返回 input中的各个属性,其中根据 directory属性值来区分是否是文件夹上传。
  2. open: 定义了一个方法,用于在外部执行文件获取的弹窗
  3. selectedFiles: 收集用户选择的文件信息

然后,我们就可以在指定地方(src/Upload)中执行了。

alt

这样,我们就将三个input的属性都配置好了。它们分别能处理useDropzone/useFileInput(非文件夹)/useFileInput(文件夹)返回的inputPorps

也就是,此时我们实现了三种能力的文件收集功能。只不过,文件拖拽我们可以通过拖拽进行处理。而文件{夹}上传需要一些操作来触发其功能。

从上面截图中我们看到(绿色部分),有两类信息,我们还未处理

  1. xxxFiles:拖拽或者选中的文件信息
  2. open: 针对文件{夹}上传的触发回调

我们还需要一个组件用于接收刚才选择的文件信息和触发文件{夹}上传的操作。


4. 处理文件&回调

我们先来看看该组件是如何调用的。

alt

如图所示,我们在Uploader中消费了webFileSelectorFiles/webFolderSelectorFiles/dragAndDropFiles/openFileSelector/openFolderSelector

其中uploadTypeSelectorView用于控制弹窗的显隐,这个我们稍后会有介绍。

先来看Uploader

Uploader

alt

如图所示,上面有几部分重要的函数。

  1. 定义了一个 state/ ref
  2. 监听 props.xxFiles
  3. 处理上传的逻辑
  4. 监听 webFiles
  5. 文件{夹}弹窗

我们就挑几个比较重要的部分来解析

监听props.xxFiles

enum PICKED_UPLOAD_TYPE {
    FILES = "files",
    FOLDERS = "folders",
}

export default function Uploader(props: Props{
  const [webFiles, setWebFiles] = useState([]);
  const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
  const isDragAndDrop = useRef(false);
  
  useEffect(() => {
      if (
          pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
          props.webFolderSelectorFiles?.length > 0
      ) {
          setWebFiles(props.webFolderSelectorFiles);
      } else if (
          pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
          props.webFileSelectorFiles?.length > 0
      ) {
          setWebFiles(props.webFileSelectorFiles);
      } else if (props.dragAndDropFiles?.length > 0) {
          isDragAndDrop.current = true;
          setWebFiles(props.dragAndDropFiles);
      }
  },  
  [
      props.dragAndDropFiles,
      props.webFileSelectorFiles,
      props.webFolderSelectorFiles,
  ]);

}

在这个effect中我们监听dragAndDropFiles/webFileSelectorFiles/webFolderSelectorFiles来根据pickedUploadType的类型来获取指定上传文件的类型,并且通过setWebFiles来更新webFiles的值。

上面的代码就是,不论是你拖拽还是文件{夹}上传,都会被存放到webFilesstate变量中。

监听webFiles

上面的操作,我们把文件放置到了webFiles中,随后我们就可以进行对应文件的处理了。

useEffect(() => {
  if (
      webFiles?.length > 0
  ) {
      if (webFiles?.length > 0) {
          toUploadFiles.current = webFiles;
          setWebFiles([]);
      } 

      toUploadFiles.current = filterOutSystemFiles(toUploadFiles.current);
      if (toUploadFiles.current.length === 0) {
          return;
      }
      let files = toUploadFiles.current;
      // if (files.length > 2) { 
      //   files = files.slice(0, 2);
      //     message.info("超出指定的数量,已经智能截取到x条")
      // }
      console.log(files);
  }
}, [webFiles]);

由于,我们在进行文件夹上传和拖拽过程中,会将整个文件进行收集,此时会有一些系统文件(以.开头),这些文件并不是我们想要的,所以我们需要将其剔除。

这里我们使用了一个工具方法。filterOutSystemFiles

export function filterOutSystemFiles(files: File[]{
    if (files[0instanceof File) {
        const browserFiles = files as File[];
        return browserFiles.filter((file) => {
            return !isSystemFile(file);
        });
    } 
}

export function isSystemFile(file: File{
    return file.name.startsWith(".");
}

在进行文件处理后,我们就可以基于需求进行文件的操作。(这里我们只是做了简单的log)。

回调的处理

前面讲过,文件拖拽就是文件收集的过程,这里不需要额外的操作,但是对于文件{夹}来将我们需要唤起对应的弹窗。

所以,我们还需要处理对应弹窗的处理。

const handleWebUpload = async (type: PICKED_UPLOAD_TYPE) => {
    pickedUploadType.current = type;
    if (type === PICKED_UPLOAD_TYPE.FILES) {
        props.showUploadFilesDialog();
    } else if (type === PICKED_UPLOAD_TYPE.FOLDERS) {
        props.showUploadDirsDialog();
    }
};


const handleUpload = (type) => () => {
    handleWebUpload(type);
};

const handleFileUpload = handleUpload(PICKED_UPLOAD_TYPE.FILES);
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);

这里多出了两个handleFileUpload/handleFolderUpload的回调。

这是我们要传人对应组件执行相关的操作。

文件{夹}弹窗

在这里就是我们消费刚才定义的几个回调的。

alt
import {Button, Modal } from 'antd'
interface Iprops {
    onClose() => void;
    show: boolean;
    uploadFiles: () => void;
    uploadFolders: () => void;
}
export default function UploadTypeSelector({
    onClose,
    show,
    uploadFiles,
    uploadFolders,
}: Iprops
{

    
return (
    <Modal open={show} onCancel={onClose}>
        <Button onClick={uploadFiles}>文件上传</Button>
        <Button onClick={uploadFolders}>文件夹上传</Button>
    </Modal>
 

);
}


5. 唤起弹窗

上面不是说过吗,针对文件{夹}上传,我们需要指派一个操作来唤起对应的文件上传弹窗。

这里,我们选择在页面中新增一个button来唤起一个弹窗,并且根据在弹窗中选择对应的上传类型来进行文件处理。

alt

从上面的我们得知几件事情

  1. Uploader中接收了 uploadTypeSelectorView相关属性
  2. UploadButton接收一个回调用于触发 uploadTypeSelectorViewtrue

UploadButton

其实,这个组件很简单,就是用于更新uploadTypeSelectorViewtrue

import { Button, ButtonProps } from "antd";
import { styled } from "styled-components";

interface Iprops {
    openUploader() => void;
    text?: string;
    color?: ButtonProps["color"];
}
function UploadButton({
    openUploader,
    text,
    color,
}: Iprops
{
    const onClickHandler = () => openUploader();

    return (
        <Wrapper
        >

            <Button
                onClick={onClickHandler}
                className="desktop-button"
                color={color ?? "secondary"}
            >

                {text ?? "上传"}
            </Button>

        </Wrapper>

    );
}

export default UploadButton;

弹窗的处理

UploadButton中我们就有一个核心逻辑就是将uploadTypeSelectorView变为true。而uploadTypeSelectorView就是表示弹窗是否显隐的标志。

对应的页面如下:(请忽略丑陋的样式) alt

处理这块的逻辑是在Uploader中,就是我们之前介绍过的。

这里我们直接来看弹窗中的代码。

import {Button, Modal } from 'antd'
interface Iprops {
    onClose() => void;
    show: boolean;
    uploadFiles: () => void;
    uploadFolders: () => void;
}
export default function UploadTypeSelector({
    onClose,
    show,
    uploadFiles,
    uploadFolders,
}: Iprops
{

    
  return (
      <Modal open={show} onCancel={onClose}>
          <Button onClick={uploadFiles}>文件上传</Button>
          <Button onClick={uploadFolders}>文件夹上传</Button>
      </Modal>
 

  );
}

其实就是渲染了两个button,然后点击不同的button来显示不同的文件上传组件。


6. TODO

其实上面的代码都是提供了一个最基本的上传操作。有些功能还是可以完善的。例如

  1. 约定文件类型
  2. 配置上传文件的大小
  3. 异步处理
  4. 在文件上传过程中,再次上传的逻辑(是失效还是进队列)
  5. 。。。。。

后记

「分享是一种态度」

「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

alt

Reference

[1]

Antd_Upload: https://ant.design/components/upload-cn

[2]

Arco_Upload: https://arco.design/react/components/upload

[3]

f_cli: https://www.npmjs.com/package/f_cli_f

[4]

MDN_drag: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API

[5]

react-dnd: https://npmjs.com/package/react-dnd

[6]

react-draggable: https://npmjs.com/package/react-draggable

[7]

react-dropzone: https://npmjs.com/package/react-dropzone

[8]

react-dropzone_api: https://react-dropzone.js.org/#src

[9]

MDN_Input_File: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1582690.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【必看】网络安全从业者书单推荐

推荐几本网络安全从业者必读的书籍 一、计算机基础 《网络硬件设备完全技术宝典》&#xff08;第3版&#xff09; 本书共768页&#xff0c;包括交换机、路由器、安全设备、网络设备等重要和常用的网络设备&#xff0c;图文并茂&#xff0c;语言流畅&#xff0c;内容及其丰富…

linux网络服务学习(6):多路径multipath解决iscsi多网卡识别错误问题

1.什么是多路径 1.1路径 物理层面一条数据的访问通道 访问方式&#xff1a; &#xff08;1&#xff09;以太网卡双绞网线以太网交换机 &#xff08;2&#xff09;HBA光纤卡光纤线光纤交换机 访问过程&#xff1a; &#xff08;1&#xff09;冗余链路failover&#xff1a…

java 23种设计模型讲解跟实例

java 23种设计模式讲解跟实例 什么是设计模式设计模式分类六大设计原则 创建型模式单例模式&#xff08;singleton&#xff09;工厂方法模式建造者模式抽象工厂模式原型模式 结构型模式适配器模式桥接模式组合模式装饰模式外观模式享元模式代理模式 行为型模式责任链模式命令模…

从零开始学Python(五)面向对象

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Python的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.类的定义 二.魔法方法 1.概念 2.常…

RabbitMQ-canal 监听本地数据库 -收不到消息解决方法

一、当我们配置好canal 的配置文件后 发现log 日志不报错&#xff0c;但是消息队列就是监听不到数据库的消息。 二、解决方法 在mysql 的ini 配置文件中加入下列代码 connect_timeout60 # 将默认值&#xff08;如30秒&#xff09;改为60秒 wait_timeout28800 # 将空闲连接超时…

安防视频监控/视频集中存储EasyCVR平台开启鉴权后设备列表不展示是为什么?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;EasyCVR基于云边端一体化架构&#xff0c;具有强大的数据接入、处理及分发能力&#xff0c;可提供视频监控直播、云端录像、云存储、录像检索与回看、智能告警、平台级联、…

阿里同学聊测试开发与测试平台

在一线大厂&#xff0c;没有测试这个岗位&#xff0c;只有测开这个岗位&#xff0c;即使是做业务测试&#xff0c;那么你的title也是测开。 所以想聊一聊测开的看法&#xff0c;但不代表这是正确的看法&#xff0c;仅供参考。 没来阿里之前我对测开的看法 一直以为专职做自动…

京东重押AI,普通人如何把握AI短视频直播的机遇?

京东重押AI&#xff0c;普通人如何把握AI短视频直播的机遇&#xff1f; 现在普通人没有大厂那样&#xff0c;有雄厚资金财力支撑的&#xff0c;但是又想在AI赛道上把握机遇&#xff0c;那选择就相当重要了&#xff0c;要选择一个真正适合自己的AI项目。 餐赞是一家专注于生活服…

截图识别公式软件都有哪些?分享3个工具!

在学术、教育、科研等领域&#xff0c;公式是不可或缺的一部分。然而&#xff0c;有时我们可能会遇到需要将纸质或屏幕上的公式快速转换为电子文本的情况。这时&#xff0c;一款能够准确识别截图中公式的软件就显得尤为重要。本文将介绍几款可以截图识别公式的软件&#xff0c;…

【数据结构与算法】:快速排序和归并排序的非递归实现

1. 递归实现的缺陷 在以前的文章中我们把快速排序和归并排序的递归实现方式进行了介绍&#xff0c;但是在校招面试和在企业的日常开发过程中&#xff0c;仅掌握递归方法是不够的&#xff0c;因为递归也有它的缺陷。 我们知道在函数调用过程中会在内存中建立栈帧&#xff0c;栈…

【多线程】线程(线程的概念+线程的创建)

文章目录 线程一、线程的概念1.引入线程的目的2.什么叫线程&#xff08;Thread&#xff09;1.线程的特点&#xff1a;2.进程和线程的区别&#xff08;面试题&#xff09;&#xff1a;3.Java的多线程编程 二、创建线程1.继承Thread重写run入口方法&#xff1a;使用jconsolesleep…

文件夹变应用?数据恢复大解密!

在日常使用电脑的过程中&#xff0c;许多用户可能都曾遭遇过这样一个奇怪的现象&#xff1a;原本用来存放文件的普通文件夹&#xff0c;突然变成了应用程序的图标。这种突如其来的变化不仅令人困惑&#xff0c;更可能导致重要数据的丢失或损坏。那么&#xff0c;究竟是什么原因…

最佳UI设计软件推荐:全球顶尖工具一览!

在这个信息化、数字化的时代&#xff0c;我们的生活、工作&#xff0c;甚至娱乐&#xff0c;都被各种各样的网站所包围。随着技术的发展&#xff0c;人们对网页UI设计的要求也越来越高。所以问题是&#xff0c;作为一个想要提高他们的UI设计能力的设计师&#xff0c;你应该如何…

渗透学习第一天:DR4G0N B4LL靶场复现

0x00 环境搭建 攻击机为kali Linux&#xff0c;IP为192.168.71.129 靶机IP地址目前不知道&#xff0c;但是是和kali同网段的 0x01 信息收集 由于不知道目标的IP地址&#xff0c;这里我采用了arp scan对本机的整个网段进行扫描 发现目标IP为192.168.71.130。对目标IP进行端…

高校人事管理系统业务分析

目标用户 大学人事部门&#xff0c;其他部门、院系、个人 解决问题 人事部门按业务划分了很多科室、数据分散、工作流程杂乱、工作效率低。 主要功能模块 人事综合管理平台、个人自助服务平台、人才招聘管理系统、薪酬管理子系统、职称评审子系统、绩效考核子系统组成。

【精选】发布应用到应用商店的基本介绍

摘要 本文旨在介绍如何在各大应用商店发布应用&#xff0c;包括市场选择、准备材料、上架步骤以及常见被拒原因及解决方法。通过详细的步骤和经验分享&#xff0c;帮助开发者顺利将应用推向市场。 引言 随着移动应用市场的不断发展&#xff0c;越来越多的开发者希望将他们的…

44-技术演进(下):软件架构和应用生命周期技术演进之路

应用、系统资源、应用生命周期管理这 3 个维度&#xff0c;构成了我们对云的所有诉求。 我会介绍下应用维度和应用生命周期管理维度的技术演进。 我们就先来看下软件架构的演进之路。 软件架构的演进 软件架构技术演进如下图所示&#xff1a; 单体架构 在单体架构中&#xff…

c++——sort()函数

一、代码和效果 #include<bits/stdc.h> using namespace std;int main() {int a[6]{1,45,2,5,456,7};sort(a,a6);for(int i0; i<6; i){cout<<a[i]<<" "<<endl;}return 0; } 二、sort函数解析 &#xff08;从小到大&#xff09; std::so…

Linux安装MuJoCo各版本及D4RL教程

Linux安装MuJoCo各版本及D4RL教程 文章目录 Linux安装MuJoCo各版本及D4RL教程Linux安装MuJoco150一、下载MuJoco1501.1 文件下载1.2 文件存放位置1.3 环境变量设置 二、安装mujoco-py三、验证mujoco-py安装是否成功 Linux安装MuJoco200一、下载MuJoco2001.1 文件下载1.2 文件存…

【随笔】Git 高级篇 -- 快速定位分支 ^|~(二十三)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…