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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:07:14