Next.js 15 App Router服务端Toolbar组件按钮事件实现疑问
Next.js 15 App Router 服务端Toolbar组件交互方案
服务端组件(Server Component)无法直接绑定onClick这类客户端事件,必须通过表单提交+Server Action的方式实现交互,以下针对Import、Export、Delete三个按钮给出具体实现方案:
1. Delete按钮实现
无确认弹窗的极简版
直接用表单包裹按钮,通过隐藏字段传递要删除的项ID,提交到Server Action执行删除逻辑:
// Toolbar.tsx (服务端组件) import { deleteItems } from '@/app/actions'; export default function Toolbar({ selectedItemIds }: { selectedItemIds: string[] }) { return ( <div className="toolbar"> {/* 已实现的Add按钮 */} <form action={addItem}> <button type="submit">Add</button> </form> {/* Delete按钮 */} <form action={deleteItems}> {selectedItemIds.map(id => ( <input key={id} type="hidden" name="itemIds" value={id} /> ))} <button type="submit">Delete Selected</button> </form> </div> ); }
对应的Server Action:
// app/actions.ts 'use server'; import { redirect } from 'next/navigation'; import db from '@/lib/db'; export async function deleteItems(formData: FormData) { const itemIds = formData.getAll('itemIds') as string[]; await db.item.deleteMany({ where: { id: { in: itemIds } } }); redirect('/items'); // 删除后重定向回列表页 }
需要确认弹窗的版本
如果需要删除前的确认交互(客户端行为),单独把Delete按钮抽成客户端组件,调用Server Action:
// components/DeleteButton.tsx (客户端组件) 'use client'; import { deleteItems } from '@/app/actions'; export default function DeleteButton({ selectedItemIds }: { selectedItemIds: string[] }) { const handleDelete = async () => { if (!confirm('确定要删除选中项吗?')) return; const formData = new FormData(); selectedItemIds.forEach(id => formData.append('itemIds', id)); await deleteItems(formData); }; return <button onClick={handleDelete}>Delete Selected</button>; }
在Toolbar服务端组件中直接引入该客户端组件即可,不影响Toolbar本身的服务端属性。
2. Export按钮实现
通过Server Action生成导出文件(如CSV/Excel),并返回响应触发浏览器下载:
// Toolbar.tsx (服务端组件) import { exportItems } from '@/app/actions'; export default function Toolbar() { return ( <div className="toolbar"> <form action={exportItems}> <button type="submit">Export CSV</button> </form> </div> ); }
对应的Server Action:
// app/actions.ts 'use server'; import db from '@/lib/db'; import { generateCsv } from '@/lib/csv-generator'; // 自定义CSV生成工具 export async function exportItems() { // 从数据库获取要导出的数据 const items = await db.item.findMany(); // 生成CSV内容 const csvContent = generateCsv(items); // 返回文件响应,触发下载 return new Response(csvContent, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="items.csv"', }, }); }
如果需要导出筛选后的内容,可在表单中添加隐藏字段传递筛选参数。
3. Import按钮实现
利用表单的文件上传功能,提交到Server Action解析文件并批量插入数据:
// Toolbar.tsx (服务端组件) import { importItems } from '@/app/actions'; export default function Toolbar() { return ( <div className="toolbar"> <form action={importItems} encType="multipart/form-data"> <input type="file" name="importFile" accept=".csv,.xlsx" required /> <button type="submit">Import Items</button> </form> </div> ); }
对应的Server Action:
// app/actions.ts 'use server'; import { redirect } from 'next/navigation'; import db from '@/lib/db'; import { parseCsv } from '@/lib/csv-parser'; // 自定义CSV解析工具 export async function importItems(formData: FormData) { const file = formData.get('importFile') as File; if (!file) throw new Error('请选择要导入的文件'); // 读取并解析文件内容 const fileText = await file.text(); const items = parseCsv(fileText); // 批量插入数据库 await db.item.createMany({ data: items }); redirect('/items'); // 导入后重定向回列表页 }
注意表单必须添加encType="multipart/form-data"才能正确传递文件。
内容的提问来源于stack exchange,提问作者Varanasi Phaneendra
相关产品推荐
相关产品推荐

