Skip to content

vue3+naive-ui+jsx函数/命令式文件监听复制文件上传组件示例

支持功能

  • 支持复制粘贴文件上传
  • 支持拖拽
  • 打开文件管理器点击上传

函数式组件代码

jsx
import {ArchiveOutline} from '@vicons/ionicons5'
import {ref} from "vue";
import _ from 'lodash-es'
import {v4 as uuidv4} from 'uuid'

/**
 * 文件上传组件
 * @param {Object} options - 配置选项
 * @param {Object} options.modal - naive-ui modal实例
 * @param {Object} options.loadingBar - naive-ui loadingBar实例
 * @param {Object} [options.multiple] - 默认false 是否支持多个文件 多选
 * @param {Object} [options.max] - naive-ui 默认undefined(不做限制) 限制上传文件数量
 * @param {Function} options.uploadFunc - 上传方法,点击上传后会传入文件信息file
 * @param {Object} [options.emits] - 事件发射器
 * @param {Function} [options.emits.uploadSuccess] - 事件发射器,上传成功防抖回调,会在所有文件上传后执行
 * @param {Object} [options.otherNUploadProps] - 其它n-upload属性配置 ,通过v-bind绑定
 * @returns {ModalReactive}
 */
export default (options) => {
    const {modal, loadingBar, emits, uploadFunc, multiple = false, max, otherNUploadProps = {}} = options
    const uploadContainer = ref(null)     // 上传容器引用
    const fileList = ref([])             // 文件列表
    const uploadRef = ref(null)          // 上传组件引用
    let cleanupInterval = null           // 清理定时器

    /* 弹窗出现回调*/
    async function onAfterEnter() {
        if (otherNUploadProps?.disabled === true) {
            return
        }
        // 初始化剪贴板监听
        uploadContainer.value?.addEventListener('paste', handlePaste)
        uploadContainer.value?.focus()

        // 保持容器焦点(解决部分浏览器粘贴需要焦点的问题)
        cleanupInterval = setInterval(() => {
            uploadContainer.value?.focus()
        }, 500)
    }

    function onBeforeLeave() {
        // 资源清理
        clearInterval(cleanupInterval)
        uploadContainer.value?.removeEventListener('paste', handlePaste)
    }

    // 处理复制事件
    function handlePaste(event) {
        console.log('handlePaste')
        // 处理剪贴板粘贴事件
        const clipboardData = event.clipboardData
        if (!clipboardData) {
            return
        }
        const newFiles = Array.from(clipboardData.items)
            .filter(item => item.kind === 'file')
            .map((item, index) => {
                const file = item.getAsFile();
                // 创建标准化文件对象
                const uuid = uuidv4().substring(0, 8)
                return {
                    batchId: uuid,
                    file,
                    fullPath: '',
                    id: uuid,
                    name: `${uuid}_${file.name}`,
                    percentage: 0,
                    status: 'pending',
                    thumbnailUri: null,
                    type: file.type,
                    url: null
                }
            })

        if (newFiles.length > 0) {
            event.preventDefault()
            fileList.value = [...fileList.value, ...newFiles]
        }
    }

    /* 上传逻辑 */
    async function customUpload(options) {
        try {
            await uploadFunc(options)
            handleUploadComplete()
        } catch (error) {
            loadingBar.error()
        }
    }

    function handleUploadComplete() {
        try {
            $message.success('上传成功')
            resetUploadState()
            notifyUploadSuccess()
        } finally {
            loadingBar.finish()
        }
    }

    function resetUploadState() {
        // 重置上传状态
        uploadRef.value?.clear()
        fileList.value = []
    }

    const notifyUploadSuccess = _.debounce(() => {
        emits?.uploadSuccess?.()
    }, 1000)

    /* 渲染模态框 */
    const instance = modal.create({
        title: '上传文件',
        preset: 'card',
        style: {width: '600px', padding: '1rem'},
        onAfterEnter,
        onBeforeLeave,
        maskClosable: false,
        content: () => (
            <div
                tabindex="-1"
                ref={uploadContainer}
                style="height: 100%; display: flex; flex-direction: column; gap: 1rem"
            >
                <n-upload multiple={multiple} max={max} {...otherNUploadProps}
                          file-list={fileList.value}
                          ref={uploadRef}
                          custom-request={customUpload}
                          default-upload={false}
                          onChange={(options) => {
                              // options 包含之前的文件和本次上传的文件
                              fileList.value = options.fileList
                          }}
                          directory-dnd
                >
                    <n-upload-dragger>
                        <div style="margin-bottom: 12px">
                            <n-icon size="48" depth={3}>
                                <ArchiveOutline/>
                            </n-icon>
                        </div>
                        <n-text style="font-size: 16px">
                            复制 | 点击 | 拖动文件到该区域来上传
                        </n-text>
                    </n-upload-dragger>
                </n-upload>
                <n-button
                    disabled={fileList.value.length === 0}
                    onClick={() => uploadRef.value?.submit()}
                >
                    上传
                </n-button>
            </div>
        )
    })

    return instance
}

使用示例

js
// 点击上传文件
function handleClickUpload() {
    clearSelectFile()
    const instance = funcUploadFile({
        modal,
        loadingBar,
        multiple: true,
        async uploadFunc(options) {
            console.log('options', options)
            const formData = new FormData()
            formData.append('file', options.file.file);
            const filename = currentFolder.value
                ? `${currentFolder.value}/${options.file.name}`
                : options.file.name;
            formData.append('filename', filename);
            const res = await request.loadingRequest({
                loadingBar,
                method: 'post',
                url: '/upload',
                data: formData,

            });
            console.log('res', res)
            return res
        },
        emits: {
            uploadSuccess() {
                instance.destroy()
                getList()
            }
        },
    })
}
/src/technology/dateblog/2025/04/20250425-vue3-naive-ui-jsx%E5%87%BD%E6%95%B0%E5%91%BD%E4%BB%A4%E5%BC%8F%E6%96%87%E4%BB%B6%E7%9B%91%E5%90%AC%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E7%BB%84%E4%BB%B6%E7%A4%BA%E4%BE%8B.html