当前位置: 首页 > news >正文

文件上传、分片上传结合antdProComponents表格展示,点击上传

上传组件:

// ChunkUpload.jsx
import React, { useCallback, forwardRef, useState, useImperativeHandle, useEffect, useRef } from 'react';
import { UploadDropZone } from '@rpldy/upload-drop-zone';
import {ChunkedUploady,useUploady,useChunkStartListener,useChunkFinishListener,useRequestPreSend,useAbortItem,useItemAbortListener,useAbortAll,useAbortBatch,useBatchAbortListener,useBatchAddListener,useAllAbortListener,useBatchStartListener,useBatchFinishListener,
} from '@rpldy/chunked-uploady';
import retryEnhancer, { useRetry } from "@rpldy/retry-hooks";
import { useItemProgressListener, useItemFinalizeListener } from '@rpldy/uploady';
import { Button } from "antd";
import { asUploadButton } from "@rpldy/upload-button";const ChunkUpload = forwardRef((props, ref) => {const [activeBatches, setActiveBatches] = useState([]);const [abortResult, setAbortResult] = useState();const abortItemBtnRef = useRef();
const flattenBatchItems = (batch) => {if (!batch || !batch.items) return [];const parentData = { ...batch };delete parentData.items;return batch.items.map(item => {const newItem = { ...item };Object.keys(parentData).forEach(key => {newItem[`parent_${key}`] = parentData[key];});newItem[`file_name`] = item.file?.name || item.fileName || '';newItem[`file_id`] = '';return newItem;});};// ---- listeners (most are kept as your original components) ----const ChunkUploadStartListenerComponent = () => {useChunkStartListener(async (data) => { /* nothing special here */ });return null;};const ChunkUploadFinishListenerComponent = () => {useChunkFinishListener(({ item, chunk, uploadData }) => { });return null;};// 当 batch 被添加时触发(立即通知父组件)const ChunkUploadAddListener = ({ onBatchStart }) => {useBatchAddListener((batch) => {onBatchStart(batch);});return null;};const ChunkedUploadAbortAllListener = () => {useAllAbortListener(() => {// console.log("调用了abortAll,全部停止上传");
    });return null;};const BatchUploadStartListener = () => {useBatchStartListener((batch) => { });return null;};const BatchFinishListener = ({ onBatchFinish }) => {useBatchFinishListener((batch) => {onBatchFinish(batch);});return null;};const BatchAbortListener = ({ onBatchAbort }) => {useBatchAbortListener((batch) => {onBatchAbort(batch);});return null;};const UploadAbortItemListener = ({ onAbortItem }) => {useItemAbortListener((item) => {onAbortItem(item);});return null;};// 给可拖拽项添加点击上传const MyClickableDropZone = forwardRef((props, ref) => {const { onClick, ...buttonProps } = props;const onZoneClick = useCallback(e => { if (onClick) onClick(e); }, [onClick]);return (<UploadDropZone{...buttonProps}ref={ref}onDragOverClassName="drag-active"extraProps={{ onClick: onZoneClick }}groupedmaxGroupSize={10}/>
    );});const DropZoneButton = asUploadButton(MyClickableDropZone);// 停止单个上传按钮(隐藏测试用)const UploadAbortItemButton = forwardRef((props, ref) => {const abort = useAbortItem();useImperativeHandle(ref, () => ({ abort: (id) => abort(id) }));return (<Button className="w-16 h-6 border rounded-sm p-6" style={{ display: "none" }}>取消上传</Button>
    );});const handleAbortItem = (item) => {setAbortResult(item);};const handleBatchAbort = (batch) => {let abortBatchResult = batch.items.map(item => item.id);setAbortResult(abortBatchResult);setActiveBatches(prevActiveBatches => prevActiveBatches.filter((it) => it.id !== batch.id));};const onBatchFinish = (batch) => {setActiveBatches(prevActiveBatches => prevActiveBatches.filter((it) => it.id !== batch.id));};// InnerUploader — 暴露控制接口给父组件(并支持 setRequestPreSend)const InnerUploader = forwardRef(({ activeBatches, preSendDataRef }, ref) => {const uploader = useUploady();const abortAll = useAbortAll();const abortBatch = useAbortBatch();const abortItem = useAbortItem();const preSendRef = useRef(null);const retryItem = useRetry();useRequestPreSend(({ options, items }) => {const itemId = items?.[0]?.id;const match = preSendDataRef.current[itemId] || {};return {options: {...options,params: {...options.params,video_id: match.file_id,name: match.file_name,},},};});useImperativeHandle(ref, () => ({setRequestPreSend: (cb) => { preSendRef.current = cb; },startUpload: () => uploader.processPending(),stopAll: () => abortAll(),stopBatch: (batchId) => abortBatch(batchId),stopItem: (itemId) => abortItem(itemId),retryItem: (itemId) => retryItem(itemId),getBatches: () => activeBatches,}), [activeBatches, uploader, abortBatch, abortItem]);return null;});// 文件上传进度监听const ItemProgressListener = ({ onItemProgress }) => {useItemProgressListener((item) => {onItemProgress?.(item);});return null;};// 文件上传完成监听const ItemFinalizeListener = ({ onItemFinalize }) => {useItemFinalizeListener((item) => {onItemFinalize?.(item);});return null;};// 当 batch 添加 -> 保存 activeBatches 并立即通知父组件(持久化由父组件完成)const handleBatchStart = (batch) => {setActiveBatches(prev => [...prev, batch]);// 立刻扁平化并发回父组件,父组件负责去重/持久化const flat = flattenBatchItems(batch);props.onBatchAdd?.(flat); // <-- 父组件接收并持久化
  };useEffect(() => {props.onBatchesChange?.(activeBatches);}, [activeBatches]);return (<ChunkedUploadyautoUpload={false}accept='video/*'chunked={false} // 是否开启分片上传destination={{url: '/api/uploads',headers: { 'Authorization': localStorage.getItem('token') }}}sendWithFormData={true}concurrentmaxConcurrent={5}parallel={1}retries={5}enhancer={retryEnhancer}{...props}multiple><ChunkedUploadAbortAllListener /><ChunkUploadAddListener onBatchStart={handleBatchStart} /><ChunkUploadStartListenerComponent /><ChunkUploadFinishListenerComponent /><BatchUploadStartListener /><BatchAbortListener onBatchAbort={handleBatchAbort} /><UploadAbortItemListener onAbortItem={handleAbortItem} /><BatchFinishListener onBatchFinish={onBatchFinish} /><UploadAbortItemButton ref={abortItemBtnRef} />{/* 本地 Item 进度/完成监听,透传给父组件 */}<ItemProgressListener onItemProgress={props.onItemProgress} /><ItemFinalizeListener onItemFinalize={props.onItemFinalize} /><InnerUploader ref={ref} activeBatches={activeBatches} preSendDataRef={props.preSendDataRef} /><DropZoneButton><div className='w-full h-auto flex flex-col justify-center items-center p-4 rounded-md border border-inherit cursor-pointer select-none' ><div className='text-lg' style={{ marginBottom: '1rem' }}>单击或拖动文件到此区域进行上传</div><div className='text-sm text-gray-500'>支持单个文件上传</div></div></DropZoneButton></ChunkedUploady>
  );
});export default ChunkUpload;

页面组件 --> antdProComponents

// index.jsx
import { useRef, useState } from 'react';
import { EditableProTable, ModalForm } from '@ant-design/pro-components';
import { Button, Form, message, Popconfirm } from "antd";
import {SelectOutlined,UploadOutlined,LoadingOutlined,ClockCircleOutlined,CheckCircleOutlined,CloseCircleOutlined,
} from "@ant-design/icons";
import ChunkUpload from "../components/ChunkUpload";const VideoMaterial = () => {const [editableKeys, setEditableRowKeys] = useState([]);const [chooseFile, setChooseFile] = useState(false);const materialRef = useRef();const uploadRef = useRef();const preSendDataRef = useRef({});const [batchesMap, setBatchesMap] = useState([]);const [form] = Form.useForm();// 父端接收 ChunkUpload 的批次添加事件const handleBatchAdd = (flatItems) => {setBatchesMap(prev => {const map = new Map(prev.map(i => [i.id, i]));flatItems.forEach(item => {if (map.has(item.id)) {map.set(item.id, { ...(map.get(item.id)), ...item });} else {map.set(item.id, { ...item });}});return Array.from(map.values());});// 打开编辑态让用户填写setEditableRowKeys(prev => [...prev, ...flatItems.map(it => it.id)]);};// 上传进度回调const handleItemProgress = (item) => {// item: { id, loaded, total, completed? }setBatchesMap(prev => prev.map(row => {if (row.id === item.id) {const completed = item.completed ?? Math.round((item.loaded / Math.max(item.total || 1, 1)) * 100);return { ...row, completed, state: 'uploading' };}return row;}));};// 上传状态回调 (TODO 有BUG待完善)const handleItemFinalize = (item) => {setBatchesMap(prev => prev.map(row => {if (row.id === item.id) {// 如果 finalize 时 completed === 100 认为成功const completed = item.completed ?? row.completed;const newState = item.state ?? (completed === 100 && 'finished');return { ...row, completed, state: newState };}return row;}));};// 保存编辑回调(当用户手动点击保存)const submitForm = async (row) => {// row 已包含 file_id / file_name 等字段setBatchesMap(prev => prev.map(item => item.id === row.id ? { ...item, ...row } : item));return true;};// 当点击开始上传const handleStartUpload = async () => {var resultMap;try {// 1) validateFields 以确保表单校验通过并获取最新字段const validateValues = await form.validateFields();// validateValues值的结构为 { id: { file_id, file_name } }const resultArr = Object.entries(validateValues).map(([id, obj]) => ({ id, ...obj }));// 确认是否经过第1步的校验获取到数据if (resultArr.length) {resultMap = Object.fromEntries(resultArr.map(r => [r.id, r]));} else {resultMap = Object.fromEntries(batchesMap.map(r => [r.id, r]));}// 2) 合并到 batchesMapsetBatchesMap(prev => prev.map(item => ({ ...item, ...(resultMap[item.id] || {}) })));// 3) 保存所有编辑行for (const key of editableKeys) {if (materialRef.current?.saveEditable) {await materialRef.current.saveEditable(key);}}// 4) 设置 requestPreSend(每个 item 上传时根据 item.id 找对应的参数)preSendDataRef.current = resultMap;// 5) 退出编辑态并开始上传
      setEditableRowKeys([]);uploadRef.current.startUpload();// message.success('开始上传');} catch (error) {// console.error(error);message.error('请先填写并保存所有必填字段');}};// 删除/取消上传const abortUpload = (id) => {uploadRef.current.stopItem(id);setBatchesMap(prev => prev.map(it => it.id === id ? { ...it, state: 'error' } : it));};// 重试上传const retryUpload = async (id) => {setBatchesMap(prev => prev.map(it => it.id === id ? { ...it, state: 'reloading' } : it));uploadRef.current && uploadRef.current.retryItem(id);};// 清除并停止所有上传const clearAll = () => {if (uploadRef.current) {uploadRef.current.stopAll()setBatchesMap([]);setEditableRowKeys([]);}};const confirm = (id) => {abortUpload(id)setBatchesMap(prev => prev.filter(item => item.id !== id))};const cancel = () => { };// 表格列(你现有的列,略微调整 renderText)const columns = [{title: '序列ID',dataIndex: 'id',width: 150,align: 'center',editable: false,},{title: '视频ID',dataIndex: 'file_id',width: 100,align: 'center',editable: true,trigger: ['onBlur'],formItemProps: { rules: [{ required: true, message: '请填写视频ID' }] },},{title: '视频名称',dataIndex: 'file_name',editable: true,width: 230,trigger: ['onBlur'],formItemProps: { rules: [{ required: true, message: '请填写视频名称' }] },},{title: '状态',dataIndex: 'state',editable: false,width: 130,render: (text, rowData) => {switch (rowData.state) {case 'pending':return <><ClockCircleOutlined style={{ color: '#faad14' }} /> 等待上传</>;case 'uploading':return <><LoadingOutlined style={{ color: '#1890ff' }} spin /> 上传中</>;case 'reloading':return <><LoadingOutlined style={{ color: '#faad14' }} /> 重新上传中</>case 'aborted':return <><CloseCircleOutlined style={{ color: '#faad14' }} /> 已取消</>;case 'finished':return <><CheckCircleOutlined style={{ color: '#52c41a' }} /> 上传成功</>;case 'error':return <><CloseCircleOutlined style={{ color: '#ff4d4f' }} /> 上传失败</>;default: return rowData.state;}}},{title: '上传进度',dataIndex: 'completed',valueType: 'progress',editable: false,width: 150,renderText: (val) => Math.max(0, Math.min(100, Math.round(val || 0))),},// {//   title: '原名称',//   dataIndex: 'file',//   editable: false,//   render: (colData) => colData?.name || '',// },
    {title: '操作',dataIndex: 'option',valueType: 'option',width: 180,render: (text, record, _, action) => {const UPLOAD_RETRY = record.state !== 'pending' && record.state !== 'finished' && record.state !== 'uploading';// 等待上传、上传成功或者正在上传不可以重试const UPLOAD_ABORT = record.state === 'uploading';// 上传中可以取消return [<a key="editable" onClick={() => action?.startEditable?.(record.id)}>编辑</a>,UPLOAD_RETRY && (<a key="retry" onClick={async () => { retryUpload(record.id); await action?.saveEditable?.(record.id); }}>重试</a>),UPLOAD_ABORT && (<a key="abort" onClick={() => { abortUpload(record.id); action?.saveEditable?.(record.id); }}>取消上传</a>),<Popconfirmkey="delete"title="确定删除吗?"onConfirm={() => confirm(record.id)}onCancel={cancel}okText="确定"cancelText="取消"><a>删除</a></Popconfirm>
          ,]}},];return (<><EditableProTablescroll={{ x: 800 }}columns={columns}actionRef={materialRef}headerTitle="视频素材列表"rowKey="id"search={false}value={batchesMap}onChange={(value) => setBatchesMap(value)}recordCreatorProps={false}revalidateOnFocus={false}pagination={{ pageSize: 10 }}toolBarRender={() => [// <Button//   type="primary"//   danger//   key="danger"//   onClick={() => clearAll()}//   icon={<StopOutlined />}// >//   全部停止// </Button>,<Buttontype="primary"key="primary"disabled={batchesMap.length === 0}onClick={handleStartUpload}icon={<UploadOutlined />}>开始上传</Button>,<Buttontype='default'key="choose"onClick={() => setChooseFile(true)}icon={<SelectOutlined />}>选择素材</Button>
        ]}editable={{type: 'multiple',form,editableKeys,onSave: async (rowKey, data) => {await submitForm(data);// 返回 true 表示保存成功return true;},onDelete: (rowKey, data) => {abortUpload(data.id);},onChange: setEditableRowKeys,actionRender: (row, config, dom) => [dom.save, dom.delete]}}/><ModalFormtitle={'选择素材'}width={800}open={chooseFile}onOpenChange={setChooseFile}submitter={{render: (props, defaultDoms) => {return [<Buttonkey="confirm"type='primary'onClick={() => {setChooseFile(false);}}>确认</Button>,
            ];},}}><ChunkUploadref={uploadRef}destination={{ url: '/localApi/upload/simple' }}onBatchAdd={handleBatchAdd}                    // 新增批次时父端持久化onBatchesChange={() => { }}                     // 保持兼容(可选)onItemProgress={handleItemProgress}            // 进度回调onItemFinalize={handleItemFinalize}            // 完成回调preSendDataRef={preSendDataRef}/></ModalForm></>
  );
};export default VideoMaterial;

 

http://www.wxhsa.cn/company.asp?id=1143

相关文章:

  • 2025 年 PLM 市场新锐崛起:五家厂商以创新技术引领行业变革新路径
  • 2025 年国产 PLM 系统发展全景:厂商实力与核心功能深度解读
  • 开发效率翻倍!编码助手+云效 AI 评审如何破解代码质量与速度难题?
  • SSL部署完成,https显示连接不安全如何处理?
  • 各省简称
  • 完整教程:HDFS基准测试与数据治理
  • var code = 76cb2b4f-5a26-4a70-a3bf-dc8f2ae5162f
  • 解放双手!三端通用的鼠标连点神器
  • 用 C# 与 Tesseract 实现验证码识别系统
  • 【9月19日最终截稿,SPIE出版】2025年信息工程、智能信息技术与人工智能国际学术会议(IEITAI 2025)
  • Dockerfile:如何用CMD同时启动两个进程
  • 启动GA-Event Activated,结束GA-End Ability
  • VMware Avi Load Balancer 31.1.2 发布 - 多云负载均衡平台
  • C# WinForms 使用 CyUSB.dll 访问 USB 设备
  • NKOJ全TJ计划——NP3990
  • Linux redis 8.2.1源码编译
  • MarkDown学习
  • 202003_MRCTF_千层套娃
  • 基于MATLAB的粒子群算法优化广义回归神经网络的实现
  • MySql EXPLAIN 详解
  • Transformer完整实现及注释
  • 数据策略与模型算法
  • 25fall-cs101 作业图床 - Amy
  • 在使用代理的时候,可以使用更简单的C++语法代替FGameplayAttribute代理,使用TStaticFuncPtr T
  • 从 url 到 PPT 一键生成:Coze 工作流,颠覆你的内容创作方式!
  • [WPF学习笔记]多语言切换-001
  • Shell 语法摘要
  • 软件设计师知识点总结(一)
  • 智能引擎驱动:DRS.Editor让汽车诊断设计效率跃升!
  • 【译】Visual Studio 2026 Insider 来了!