从 Trello 迁移到 Notion 并不是把卡片数据导出再导入那么简单。Trello 的看板结构围绕 Board、List、Card 三层展开,而 Notion 的数据库更像一个带过滤和分组视图的表格。React 前端如果同时依赖两个系统的 API,不仅请求逻辑割裂,还容易出现状态不同步。下面围绕 Notion API、数据模型映射和拖拽交互,给出一套在 React 中实现看板与文档一体化的迁移方案。

先理清 Trello 与 Notion 的数据模型差异
迁移的第一步不是写代码,而是确认两边数据结构的对应关系。Trello 的顶级对象是 Board,下面挂多个 List,每个 List 里有多张 Card。Card 上可以放标题、描述、标签、附件、评论和自定义字段。Notion 的数据库页面没有固定的 List 概念,通常用数据库视图加一个 Status 或 Stage 字段来做分组显示。数据库里的每一行对应一个 Page,Page 内部可以继续写文档、放表格、挂附件。
这种差异会直接影响 React 里的数据层设计。例如原来从 Trello API 拉到的是列表嵌套列表的结构,而 Notion API 返回的是扁平的 page 数组,每个 page 的 properties 里才有状态和标签。下面的 JSON 对比可以直观看出两者的区别。
{
"id": "card_123",
"name": "登录接口联调",
"desc": "需要补充错误码",
"idList": "list_doing",
"labels": ["backend"]
}
而 Notion 里同一个卡片的属性结构会变成这样:
{
"object": "page",
"id": "page_456",
"properties": {
"Name": { "title": [{ "text": { "content": "登录接口联调" } }] },
"Status": { "select": { "name": "进行中" } },
"Tags": { "multi_select": [{ "name": "backend" }] }
}
}
因此在 React 状态管理中,不要再沿用 Trello 时代的 boards 加 lists 加 cards 三层嵌套 state。更合适的做法是维护一维卡片数组,每张卡片带上 status 字段,展示时按 status 分组。这样切换数据源时,组件改动量最小,也方便后续接入 Notion 的过滤和排序视图。
在 React 中接入 Notion API 并建立看板数据源
Notion 官方提供了 JavaScript SDK,但在浏览器端直接使用会暴露密钥,所以更推荐通过一个轻量的后端代理来转发请求。如果只是内部工具,且密钥只在环境变量中使用,直接调用 Notion REST API 也可以。下面这段代码演示了如何在 React 中读取数据库并转换成看板卡片格式。
const NOTION_TOKEN = process.env.REACT_APP_NOTION_TOKEN;
const DATABASE_ID = 'your_database_id';
async function fetchCards() {
const response = await fetch(`https://api.notion.com/v1/databases/${DATABASE_ID}/query`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
},
body: JSON.stringify({ page_size: 100 })
});
if (!response.ok) {
throw new Error(`Notion query failed: ${response.status}`);
}
const data = await response.json();
return data.results.map((page) => {
const props = page.properties;
return {
id: page.id,
title: props.Name?.title?.[0]?.plain_text || '未命名',
status: props.Status?.select?.name || '待处理',
tags: props.Tags?.multi_select?.map((tag) => tag.name) || []
};
});
}
请求头里的 Notion-Version 建议固定为稳定版本,避免接口行为随默认版本变化。返回结果默认按创建时间排序,如果你的看板需要按优先级或截止日期排序,可以在 body 里传入 sorts 参数,也可以在拿到数据后自己在 React 里排序。分页方面,Notion 的 query 接口返回 has_more 和 next_cursor,当卡片超过 100 张时需要循环拉取,否则看板上会静默丢卡片。
React 里的调用一般放在 useEffect 中,并用 useMemo 按 status 分组。为了避免每次拖拽后都重新请求整个数据库,可以把 Notion 响应缓存在本地状态里,只在初始化或手动刷新时拉取。这样看板交互会更流畅,也减少 Notion API 的调用次数。
实现看板拖拽与文档详情页联动
看板的核心交互是拖拽卡片切换状态。Trello 的拖拽体验很成熟,迁移到 Notion 后可以用 dnd-kit 或 react-beautiful-dnd 来替代。dnd-kit 的 API 更现代,对自定义列和卡片支持较好。下面是一个简化的卡片组件和放置容器。
import { useDraggable, useDroppable } from '@dnd-kit/core';
function KanbanCard({ card }) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: card.id
});
const style = transform
? { transform: `translate(${transform.x}px, ${transform.y}px)` }
: undefined;
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<h4>{card.title}</h4>
<span>{card.status}</span>
</div>
);
}
function KanbanColumn({ status, children }) {
const { setNodeRef } = useDroppable({ id: status });
return (
<div ref={setNodeRef} className="kanban-column">
<h3>{status}</h3>
{children}
</div>
);
}
拖拽结束时,需要根据目标列的 id 调用 Notion API 更新卡片状态。Notion 的更新接口是 PATCH /v1/pages/{page_id},把 Status 属性设置为新的 select 值。更新成功后再更新本地 state,避免整页刷新。这里要注意 select 属性的值必须已经存在于数据库的 Status 字段选项里,否则接口会返回 400 错误。迁移时可以提前在 Notion 数据库里创建好所有状态选项。
async function updateCardStatus(cardId, status) {
const response = await fetch(`https://api.notion.com/v1/pages/${cardId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
},
body: JSON.stringify({
properties: {
Status: {
select: { name: status }
}
}
})
});
if (!response.ok) {
throw new Error(`更新卡片状态失败:${response.status}`);
}
}
文档一体化是迁移到 Notion 后最大的收益。每张卡片对应的 Page 可以承载完整的需求说明、会议记录、技术方案。React 详情页可以通过 Notion 的 blocks children 接口读取页面块,再把段落、标题、代码块分别渲染成组件。这样团队成员点击卡片后不仅能看到标题和状态,还能直接在同一个界面阅读和编辑文档,不用再跳回 Trello 或单独打开文档链接。
迁移脚本与兼容性注意点
如果只是新建看板,手动复制卡片到 Notion 数据库并不现实。通常需要写一个一次性迁移脚本,先通过 Trello API 拉取所有卡片,再逐条创建 Notion page。创建时把 Trello 的 name 写入 Notion 的 Name title 属性,把 Trello 的 desc 写入 Children 的段落块,把 labels 映射到 multi_select。下面是一个简化的创建逻辑。
async function createNotionPageFromTrelloCard(card) {
const response = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
},
body: JSON.stringify({
parent: { database_id: DATABASE_ID },
properties: {
Name: {
title: [{ text: { content: card.name } }]
},
Status: {
select: { name: mapListToStatus(card.idList) }
},
Tags: {
multi_select: card.labels.map((label) => ({ name: label.name }))
}
},
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: card.desc || '' } }]
}
}
]
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`创建 Notion page 失败:${errorText}`);
}
return response.json();
}
迁移时必须重点排查几个兼容性问题。第一是 Notion 的速率限制,免费工作区每秒请求数较低,脚本里最好加入指数退避重试。第二是 Trello 的附件和评论不会自动成为 Notion 页面内容,需要在迁移前决定是放弃、下载到对象存储,还是用 Notion 的 embed 块引用外部地址。第三是字段类型转换,Trello 的日期、复选框、自定义下拉在 Notion 中分别对应 date、checkbox、select,但有些格式并不能无损迁移,建议先做小批量验证。
另外不要把旧 Trello 的成员字段直接映射成 Notion 的 person 属性后就认为迁移完成。Notion 的 person 属性依赖工作区成员记录,如果成员邮箱不一致,属性会留空。可以在迁移脚本里先做一份成员映射表,把 Trello 用户名对应到 Notion 用户 ID。权限方面,Notion 数据库的分享级别与 Trello Board 的可见范围不同,迁移后要重新确认访客是否能查看卡片内容,避免文档泄露。
ReactNotion API看板迁移修改时间:2026-09-27 17:14:32