Administrator
发布于 2024-10-29 / 12 阅读
0
0

Next.js 零基础教程16 实现 Server Action 更新功能

书接上回,上回实现了使用Server Action 对数据进行增加存储到数据库当中,同时在存储的同时进行了数据格式类型进行校验,这章节我们将会对数据进行更新。

更新发票表单与创建发票表单类似,但您需要传递发票id来更新数据库中的记录。让我们看看如何获取和传递发票id。

以下是更新发票需采取的步骤:

  1. 使用发票创建新的动态路线段id。
  2. id从页面参数中读取发票。
  3. 从数据库中获取特定的发票。
  4. 使用发票数据预先填充表格。
  5. 更新数据库中的发票数据。

使用发票创建动态路线段id

Next.js 提供了动态路由功能,让您能够根据实际数据灵活创建路由,这在处理如博客文章、产品页面等内容时特别有用。要创建动态路由,只需将文件夹名称用方括号括起来,例如 [id][post][slug]。这种方法使得您无需预先知道确切的路由名称,而可以根据数据动态生成URL。这不仅增加了网站结构的灵活性,还能更好地适应不断变化的内容需求。

在您的/app/dashboard/invoices文件夹中,创建一个名为的新动态路由,然后创建一个名为file 的[id]新路由,您的文件结构应如下所示:/app/dashboard/invoices/[id]/edit/page.tsx

创建完编辑页面先不管它,我们现在要在/app/ui/invoices/buttons.tsx下创建一个按钮组件,用于触发用户的修改请求。

import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';

export function CreateInvoice() {
    return (
        <Link
            href="/dashboard/invoices/create"
            className="flex h-10 items-center rounded-lg bg-blue-600 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
        >
            <span className="hidden md:block">Create Invoice</span>{' '}
            <PlusIcon className="h-5 md:ml-4" />
        </Link>
    );
}

export function UpdateInvoice({ id }: { id: string }) {
    return (
        <Link
            href={`/dashboard/invoices/${id}/edit`}
            className="rounded-md border p-2 hover:bg-gray-100"
        >
            <PencilIcon className="w-5" />
        </Link>
    );
}

export function DeleteInvoice({ id }: { id: string }) {
    return (
        <>
            <button className="rounded-md border p-2 hover:bg-gray-100">
                <span className="sr-only">Delete</span>
                <TrashIcon className="w-5" />
            </button>
        </>
    );
}

然后创建/app/ui/invoices/status.tsx组件,这个组件可以很方便地在发票列表或详情页面中使用,以直观地展示每个发票的当前状态。

import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';

export default function InvoiceStatus({ status }: { status: string }) {
  return (
    <span
      className={clsx(
        'inline-flex items-center rounded-full px-2 py-1 text-xs',
        {
          'bg-gray-100 text-gray-500': status === 'pending',
          'bg-green-500 text-white': status === 'paid',
        },
      )}
    >
      {status === 'pending' ? (
        <>
          Pending
          <ClockIcon className="ml-1 w-4 text-gray-500" />
        </>
      ) : null}
      {status === 'paid' ? (
        <>
          Paid
          <CheckIcon className="ml-1 w-4 text-white" />
        </>
      ) : null}
    </span>
  );
}

现在还需要一个form组件用于提交信息,接下来创建一个/app/invoices/edit-from.tsx组件。

'use client';

import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { Button } from '@/app/ui/button';


export type CustomerField = {
  id: string;
  name: string;
};

export type InvoiceForm = {
  id: string;
  customer_id: string;
  amount: number;
  status: string;
};


export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  return (
    <form>
      <div className="rounded-md bg-gray-50 p-4 md:p-6">
        {/* Customer Name */}
        <div className="mb-4">
          <label htmlFor="customer" className="mb-2 block text-sm font-medium">
            Choose customer
          </label>
          <div className="relative">
            <select
              id="customer"
              name="customerId"
              className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
              defaultValue={invoice.customer_id}
            >
              <option value="" disabled>
                Select a customer
              </option>
              {customers.map((customer) => (
                <option key={customer.id} value={customer.id}>
                  {customer.name}
                </option>
              ))}
            </select>
            <UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
          </div>
        </div>

        {/* Invoice Amount */}
        <div className="mb-4">
          <label htmlFor="amount" className="mb-2 block text-sm font-medium">
            Choose an amount
          </label>
          <div className="relative mt-2 rounded-md">
            <div className="relative">
              <input
                id="amount"
                name="amount"
                type="number"
                step="0.01"
                defaultValue={invoice.amount}
                placeholder="Enter USD amount"
                className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
              />
              <CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
            </div>
          </div>
        </div>

        {/* Invoice Status */}
        <fieldset>
          <legend className="mb-2 block text-sm font-medium">
            Set the invoice status
          </legend>
          <div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
            <div className="flex gap-4">
              <div className="flex items-center">
                <input
                  id="pending"
                  name="status"
                  type="radio"
                  value="pending"
                  defaultChecked={invoice.status === 'pending'}
                  className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
                />
                <label
                  htmlFor="pending"
                  className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600"
                >
                  Pending <ClockIcon className="h-4 w-4" />
                </label>
              </div>
              <div className="flex items-center">
                <input
                  id="paid"
                  name="status"
                  type="radio"
                  value="paid"
                  defaultChecked={invoice.status === 'paid'}
                  className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
                />
                <label
                  htmlFor="paid"
                  className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white"
                >
                  Paid <CheckIcon className="h-4 w-4" />
                </label>
              </div>
            </div>
          </div>
        </fieldset>
      </div>
      <div className="mt-6 flex justify-end gap-4">
        <Link
          href="/dashboard/invoices"
          className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
        >
          Cancel
        </Link>
        <Button type="submit">Edit Invoice</Button>
      </div>
    </form>
  );
}

完成这一切之后,就可以切换回page.tsx页面,然后实现完整的Edit Invocie页面


import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';

export default async function Page({ params }: { params: { id: string } }) {
    const id = params.id;
    const [invoice, customers] = await Promise.all([
        fetchInvoiceById(id),
        fetchCustomers(),
    ]);

    return (
        <main>
            <Breadcrumbs
                breadcrumbs={[
                    { label: 'Invoices', href: '/dashboard/invoices' },
                    {
                        label: 'Edit Invoice',
                        href: `/dashboard/invoices/${id}/edit`,
                        active: true,
                    },
                ]}
            />
            <Form invoice={invoice} customers={customers} />
        </main>
    );
}

现在,测试一切是否正确连接。访问http://172.16.100.104/dashboard/invoices并点击铅笔图标编辑发票。
导航后,您应该会看到一个预先填充了发票详细信息的表单:

UUID 与自动递增键
我们使用 UUID 而不是递增键(例如 1、2、3 等),这会使 URL更长;但是,UUID 消除了ID冲突的风险,具有全局唯一性,并降低了枚举攻击的风险 - 使其成为大型数据库的理想选择。但是,如果您更喜欢更清晰的URL,您可能更喜欢使用自动递增键。

将Form的内容通过actions.ts传递到数据库

与新增数据一样,编辑/app/lib/action.ts

'use server';
import { z } from 'zod';
import { query } from '@/app/lib/db';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

const FormSchema = z.object({
    id: z.string(),
    customerId: z.string(),
    amount: z.coerce.number(),
    status: z.enum(['pending', 'paid']),
    date: z.string(),
});

{/* 不更改之前的代码 添加下面的内容*/}

const UpdateInvoice = FormSchema.omit({ id: true, date: true });

export async function updateInvoice(id: string, formData: FormData) {
    const { customerId, amount, status } = UpdateInvoice.parse({
        customerId: formData.get('customerId'),
        amount: formData.get('amount'),
        status: formData.get('status'),
    });

    const amountInCents = amount * 100;
    try {
        await query(
            'UPDATE invoices SET customer_id = $1, amount = $2, status = $3 WHERE id = $4',
            [customerId, amountInCents, status, id]
        );
    } catch (error) {
        console.error('发票更新失败:', error);
        throw new Error('发票更新失败');
    }

    revalidatePath('/dashboard/invoices');
    redirect('/dashboard/invoices');
}

添加完整之后在edit-form上面引用actions.tsupdateInvoice组件函数,编辑/app/ui/edit-form

完成之后就可以看到修改数据的时候有个API在请求后端数据库

删除操作

同理,要使用服务器操作删除发票,请将删除按钮包装在元素中<form>,然后使用以下命令将其传递id给服务器操作bind。
编辑/app/lib/actions.ts,实现数据库删除信息

{/* 不修改之前的代码,新增下面代码 */}
export async function deleteInvoice(id: string) {
    try {
        await query('DELETE FROM invoices WHERE id = $1', [id]);
    } catch (error) {
        console.error('发票删除失败:', error);
        throw new Error('发票删除失败');
    }
}

将删除按钮包装在元素中<form>,编辑/app/ui/buttons.tsx

export function DeleteInvoice({ id }: { id: string }) {
    const deleteInvoiceWithId = deleteInvoice.bind(null, id);
    return (
        <form action={deleteInvoiceWithId}>
            <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
                <span className="sr-only">Delete</span>
                <TrashIcon className="w-4" />
            </button>
        </form>
    );
}

在14-16章中,学习了如何使用服务器操作来改变数据,学习了如何使用revalidatePathAPI 重新验证 Next.js 缓存并将redirect用户重定向到新页面。

如果你想看更多内容或者能够看到技术更新的内容,请百度搜索:曲速引擎 warp drive csdn 在首页找到我的地址访问即可,一线更新内容将会在我的个人博客上面更新,谢谢大家。

更详细内容查看

独立博客 https://www.dataeast.cn/
CSDN博客 https://blog.csdn.net/siberiaWarpDrive
B站视频空间 https://space.bilibili.com/25871614?spm_id_from=333.1007.0.0
关注 “曲速引擎 Warp Drive” 微信公众号


评论