You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React版Tip-Tap编辑器能否实现文件上传?如何操作?

React版Tip-Tap编辑器实现文件上传方案

React版Tip-Tap本身没有原生的通用文件上传功能,但可以通过自定义扩展或手动触发上传+插入内容的方式实现,下面是两种实用的实现方案:

方案1:快速实现基础文件上传(插入链接)

这种方式无需自定义编辑器节点,通过按钮触发文件选择,上传后直接在编辑器中插入文件链接,适合简单场景。

实现步骤

  1. 在组件中添加隐藏的文件选择输入框和触发按钮
  2. 编写文件上传逻辑(对接后端接口)
  3. 上传完成后,调用Tip-Tap的API插入链接内容

代码示例

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';

const MessageEditor = () => {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '',
  });

  const handleFileUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    // 替换为你的实际后端上传接口
    const formData = new FormData();
    formData.append('file', file);
    try {
      const response = await fetch('/api/file-upload', {
        method: 'POST',
        body: formData,
      });
      const result = await response.json();
      const fileUrl = result.downloadUrl;

      // 在编辑器中插入文件链接
      editor?.chain().focus().insertContent(`
        <a href="${fileUrl}" target="_blank" rel="noopener noreferrer">
          📄 ${file.name}
        </a>
      `).run();
    } catch (err) {
      console.error('文件上传失败:', err);
      // 这里可以添加错误提示逻辑
    }
  };

  return (
    <div className="editor-container">
      <button onClick={() => document.getElementById('file-upload-input').click()}>
        上传文件
      </button>
      <input
        id="file-upload-input"
        type="file"
        style={{ display: 'none' }}
        onChange={handleFileUpload}
        // 可选:限制文件类型,比如 accept=".pdf,.docx,.xlsx"
      />
      <EditorContent editor={editor} />
    </div>
  );
};

export default MessageEditor;

方案2:自定义文件卡片节点(更美观的展示)

如果需要更友好的文件展示样式(比如带文件名、大小、删除按钮的卡片),可以自定义Tip-Tap的节点扩展,让编辑器支持文件卡片类型。

步骤1:定义FileCard扩展

import { Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';

// 自定义文件卡片渲染组件
const FileCardComponent = ({ node, deleteNode }) => {
  const formatFileSize = (size) => {
    if (size < 1024) return `${size}B`;
    if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)}KB`;
    return `${(size / (1024 * 1024)).toFixed(2)}MB`;
  };

  return (
    <div style={{ 
      padding: '10px', 
      border: '1px solid #e5e7eb', 
      borderRadius: '6px', 
      margin: '8px 0',
      display: 'flex',
      alignItems: 'center',
      gap: '10px'
    }}>
      <span>📄</span>
      <a 
        href={node.attrs.url} 
        target="_blank" 
        rel="noopener noreferrer"
        style={{ flex: 1, color: '#3b82f6' }}
      >
        {node.attrs.name}
      </a>
      <span style={{ color: '#6b7280', fontSize: '14px' }}>
        {formatFileSize(node.attrs.size)}
      </span>
      <button 
        onClick={deleteNode}
        style={{ 
          background: 'none', 
          border: 'none', 
          cursor: 'pointer',
          color: '#ef4444'
        }}
      >
        ×
      </button>
    </div>
  );
};

// 定义Tip-Tap节点扩展
export const FileCard = Node.create({
  name: 'fileCard',
  group: 'block',
  atom: true, // 标记为原子节点,避免被拆分

  addAttributes() {
    return {
      url: {
        default: '',
        required: true,
      },
      name: {
        default: '',
        required: true,
      },
      size: {
        default: 0,
        required: true,
      },
    };
  },

  // 解析HTML时的规则
  parseHTML() {
    return [
      {
        tag: 'div[data-file-card]',
        getAttrs: (dom) => ({
          url: dom.getAttribute('data-url'),
          name: dom.getAttribute('data-name'),
          size: parseInt(dom.getAttribute('data-size'), 10),
        }),
      },
    ];
  },

  // 渲染HTML时的规则
  renderHTML({ node }) {
    return [
      'div',
      {
        'data-file-card': '',
        'data-url': node.attrs.url,
        'data-name': node.attrs.name,
        'data-size': node.attrs.size,
      },
    ];
  },

  // 绑定React组件作为节点视图
  addNodeView() {
    return ReactNodeViewRenderer(FileCardComponent);
  },
});

步骤2:在编辑器中使用FileCard扩展

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { FileCard } from './FileCard';

const MessageEditor = () => {
  const editor = useEditor({
    extensions: [StarterKit, FileCard], // 注册自定义扩展
    content: '',
  });

  const handleFileUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    // 上传逻辑同方案1
    const formData = new FormData();
    formData.append('file', file);
    try {
      const response = await fetch('/api/file-upload', {
        method: 'POST',
        body: formData,
      });
      const result = await response.json();
      const fileUrl = result.downloadUrl;

      // 插入自定义文件卡片节点
      editor?.chain().focus().insertContent({
        type: 'fileCard',
        attrs: {
          url: fileUrl,
          name: file.name,
          size: file.size,
        },
      }).run();
    } catch (err) {
      console.error('文件上传失败:', err);
    }
  };

  return (
    <div className="editor-container">
      <button onClick={() => document.getElementById('file-upload-input').click()}>
        上传文件
      </button>
      <input
        id="file-upload-input"
        type="file"
        style={{ display: 'none' }}
        onChange={handleFileUpload}
      />
      <EditorContent editor={editor} />
    </div>
  );
};

export default MessageEditor;

注意事项

  • 上传逻辑需要对接你的后端服务,确保返回可访问的文件URL
  • 可以根据需求添加文件类型限制(通过input的accept属性)、上传进度提示、错误重试等功能
  • 如果需要支持拖拽上传,可以结合拖拽相关库实现拖拽区域,再复用上述上传和插入逻辑

内容的提问来源于stack exchange,提问作者Atul Bhatt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 11:10:32