书接上回,上回实现了使用Server Action 对数据进行增加存储到数据库当中,同时在存储的同时进行了数据格式类型进行校验,这章节我们将会对数据进行更新。
更新发票表单与创建发票表单类似,但您需要传递发票id来更新数据库中的记录。让我们看看如何获取和传递发票id。
以下是更新发票需采取的步骤:
- 使用发票创建新的动态路线段id。
- id从页面参数中读取发票。
- 从数据库中获取特定的发票。
- 使用发票数据预先填充表格。
- 更新数据库中的发票数据。
使用发票创建动态路线段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.ts
的updateInvoice
组件函数,编辑/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” 微信公众号