您的位置:首页 > 文旅 > 旅游 > 武汉网站开发哪家好_企业网站源码英文_深圳百度推广客服电话多少_互联网产品运营推广方案

武汉网站开发哪家好_企业网站源码英文_深圳百度推广客服电话多少_互联网产品运营推广方案

2025/3/18 3:20:31 来源:https://blog.csdn.net/qq_37954693/article/details/145916915  浏览:    关键词:武汉网站开发哪家好_企业网站源码英文_深圳百度推广客服电话多少_互联网产品运营推广方案
武汉网站开发哪家好_企业网站源码英文_深圳百度推广客服电话多少_互联网产品运营推广方案

next.js-学习4

    • 11.添加搜索和分页
      • 添加搜索框
      • 搜索表格数据
      • 防抖
      • 分页
    • 12.增删改票据
      • 1. 创建票据
        • 1.1测试添加
      • 2. 更新票据
        • 2.1测试更新
      • 3. 删除票据
        • 3.1测试删除
    • 13.错误处理
      • 1.添加try/catch
        • 1.1 测试删除失败
      • 2.处理错误
      • 错误处理文档链接
    • 14.提高易用性
      • 1. eslint-plugin-jsx-a11y
      • 2. 表单验证
        • 2.1客户端验证
        • 2.2服务端验证
          • 2.2.1添加
        • 3.2 编辑

更新下home菜单的路由,app/ui/dashboard/nav-links.tsx中link的 { name: ‘Home’, href: ‘/dashboard’, icon: HomeIcon },改为 { name: ‘Home’, href: ‘/dashboard/overview’, icon: HomeIcon },

11.添加搜索和分页

添加搜索框

  1. /dashboard/invoices/page.tsx中添加

    import Pagination from '@/app/ui/invoices/pagination';
    import Search from '@/app/ui/search';
    import Table from '@/app/ui/invoices/table';
    import { CreateInvoice } from '@/app/ui/invoices/buttons';
    import { lusitana } from '@/app/ui/fonts';
    import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
    import { Suspense } from 'react';export default async function Page() {return (<div className="w-full"><div className="flex w-full items-center justify-between"><h1 className={`${lusitana.className} text-2xl`}>Invoices</h1></div><div className="mt-4 flex items-center justify-between gap-2 md:mt-8"><Search placeholder="Search invoices..." /><CreateInvoice /></div>{/*  <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}><Table query={query} currentPage={currentPage} /></Suspense> */}<div className="mt-5 flex w-full justify-center">{/* <Pagination totalPages={totalPages} /> */}</div></div>);
    }
    
  2. 添加个搜索/app/ui/search.tsx中

    //export default function Search({ placeholder }: { placeholder: string }) {中添加
    function handleSearch(term: string) {console.log(term);}//input中 placeholder={placeholder}下边添加修改监听事件onChange={(e) => {handleSearch(e.target.value);}}
    

    跳转到http://localhost:3000/dashboard/invoices路由搜索就会看到搜索的输入内容打印。

    在这里插入图片描述

    继续修改/app/ui/search.tsx,导入组件,修改Search方法,useSearchParams可以封装url参数

    import { useSearchParams } from 'next/navigation';
    export default function Search() {const searchParams = useSearchParams();function handleSearch(term: string) {const params = new URLSearchParams(searchParams);if (term) {params.set('query', term);} else {params.delete('query');}}// ...
    }
    

    继续增加路由path,这样输入搜索在导航栏会显示

    import { useSearchParams, usePathname, useRouter } from 'next/navigation';//更新导入的组件
    //searchParams下加入const pathname = usePathname();const { replace } = useRouter();//在handleSearch函数最后一行写入,pathname是请求的路径dashboard/invoices,params.toString()是请求的参数就说问号后边的那一串replace(`${pathname}?${params.toString()}`);
    

    在这里插入图片描述

    为了确保输入字段与URL同步,并在共享时填充,您可以通过从searchParams中读取defaultValue来传递输入:

    defaultValue={searchParams.get('query')?.toString()}//加到onChange事件下边
    

    /app/dashboard/invoices/page.tsx中,Page函数增加参数

    props: {searchParams?: Promise<{query?: string;page?: string;}>;
    }
    

    函数第一行,获取参数

     const searchParams = await props.searchParams;const query = searchParams?.query || '';const currentPage = Number(searchParams?.page) || 1;
    

搜索表格数据

  1. 打开注释,给/app/ui/invoices/table.tsx传参,返回表格数据

     <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}><Table query={query} currentPage={currentPage} /></Suspense>
    

    可以看到页面搜索出来数据了在这里插入图片描述

防抖

/app/ui/search.tsx中使用防抖,handleSearch函数上边添加useDebouncedCallback回调0.3秒不输入才去搜索

下载React 防抖

pnpm i use-debounce
// 直接在组件内定义 useDebouncedCallbackconst debouncedSearch = useDebouncedCallback((term: string) => {console.log(`Searching... ${term}`);const params = new URLSearchParams(searchParams.toString()); // 确保是字符串格式if (term) {params.set('query', term);} else {params.delete('query');}replace(`${pathname}?${params.toString()}`);}, 300);// 事件处理函数调用 debouncedSearchfunction handleSearch(term: string) {debouncedSearch(term);}

分页

  1. /app/dashboard/invoices/page.tsx中加入

    import { fetchInvoicesPages } from '@/app/lib/data';<Pagination totalPages={totalPages} />//打开注释
    
  2. /app/ui/invoices/pagination.tsx加入

    import { usePathname, useSearchParams } from 'next/navigation';
    //Pagination函数中加入,解开这个页面的注释const pathname = usePathname();const searchParams = useSearchParams();const currentPage = Number(searchParams.get('page')) || 1;//创建搜索urlconst createPageURL = (pageNumber: number | string) => {const params = new URLSearchParams(searchParams);params.set('page', pageNumber.toString());return `${pathname}?${params.toString()}`;};
    
  3. 如果想让用户输入搜索的时候页签是第一个可以/app/ui/search.tsx加入

    const params = new URLSearchParams(searchParams);//参数下边加入
    params.set('page', '1');
    

12.增删改票据

首先学会使用from和action

例子:

// Server Component
export default function Page() {// Actionasync function create(formData: FormData) {'use server';// Logic to mutate data...}// Invoke the action using the "action" attributereturn <form action={create}>...</form>;
}

1. 创建票据

  1. /dashboard/invoices/create/page.tsx

    import Form from '@/app/ui/invoices/create-form';
    import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
    import { fetchCustomers } from '@/app/lib/data';export default async function Page() {const customers = await fetchCustomers();//breadcrumbs一个面包屑导航组件,帮助用户了解当前页面在网站中的位置。比如从“发票”页面到“创建发票”页面的导航//fetchCustomers() 异步获取客户数据,确保在渲染页面之前获取到相关的客户信息return (<main><Breadcrumbsbreadcrumbs={[{ label: 'Invoices', href: '/dashboard/invoices' },{label: 'Create Invoice',href: '/dashboard/invoices/create',active: true,},]}/><Form customers={customers} /></main>);
    }
    

创建Server Actions,新建/app/lib/actions.ts

'use server';export async function createInvoice(formData: FormData) {}

在这里插入图片描述

导入action,/app/ui/invoices/create-form.tsx

import { createInvoice } from '@/app/lib/actions';
//from改为<form action={createInvoice}>

在/app/lib/actions.ts中

//编辑异步函数,如果字段多用Object.fromEntries()
export async function createInvoice(formData: FormData) {const rawFormData = {customerId: formData.get('customerId'),amount: formData.get('amount'),status: formData.get('status'),};// Test it out:console.log(rawFormData);
}

在/app/lib/definitions.ts中有Invoice的定义

/app/lib/actions.ts中,使用验证库验证入参

'use server';import { z } from 'zod';const FormSchema = z.object({id: z.string(),customerId: z.string(),amount: z.coerce.number(),status: z.enum(['pending', 'paid']),date: z.string(),
});const CreateInvoice = FormSchema.omit({ id: true, date: true });export async function createInvoice(formData: FormData) {// ...
}

在/app/lib/definitions.ts中,开始验证

// ...
export async function createInvoice(formData: FormData) {const { customerId, amount, status } = CreateInvoice.parse({customerId: formData.get('customerId'),amount: formData.get('amount'),status: formData.get('status'),});
}
//console.log(rawFormData);

在/app/lib/definitions.ts中,用分消除浮点错误

//createInvoice函数最后一行加入 
const amountInCents = amount * 100;

在/app/lib/definitions.ts中,创建日期

  //createInvoice函数最后一行加入 const date = new Date().toISOString().split('T')[0];

在/app/lib/actions.ts中,插入数据库

 import postgres from 'postgres';const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
// ...//createInvoice函数最后一行加入 
await sql`INSERT INTO invoices (customer_id, amount, status, date)VALUES (${customerId}, ${amountInCents}, ${status}, ${date})`;

在/app/lib/actions.ts中,由于要更新发票路由中显示的数据,因此需要清除此缓存并触发对服务器的新请求

import { revalidatePath } from 'next/cache';
//createInvoice函数最后一行加入 
revalidatePath('/dashboard/invoices');

在/app/lib/actions.ts中,重定向回invoices页

import { redirect } from 'next/navigation';
  //createInvoice函数最后一行加入redirect('/dashboard/invoices');
1.1测试添加

在这里插入图片描述

2. 更新票据

/app/ui/invoices/table.tsx中更新按钮会将id传入更新页面,InvoicesTable函数返回的td中的

/app/ui/invoices/buttons.tsx中,UpdateInvoice函数,Link修改

href={`/dashboard/invoices/${id}/edit`}

创建一个新的动态路由,/app/dashboard/invoices/[id]/edit/page.tsx

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';export default async function Page() {return (<main><Breadcrumbsbreadcrumbs={[{ label: 'Invoices', href: '/dashboard/invoices' },{label: 'Edit Invoice',href: `/dashboard/invoices/${id}/edit`,active: true,},]}/><Form invoice={invoice} customers={customers} /></main>);
}

/app/dashboard/invoices/[id]/edit/page.tsx中,给page加上入参

export default async function Page(props: { params: Promise<{ id: string }> }) {const params = await props.params;const id = params.id;// ...
}

/dashboard/invoices/[id]/edit/page.tsx中,根据id获取数据

import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';//引入
//Page函数中获取数据
export default async function Page(props: { params: Promise<{ id: string }> }) {// ...const [invoice, customers] = await Promise.all([fetchInvoiceById(id),fetchCustomers(),]);// ...

访问: http://localhost:3000/dashboard/invoices,点击编辑就会展示数据,url改变为http://localhost:3000/dashboard/invoice/uuid/edit。

在这里插入图片描述

在/app/ui/invoices/edit-form.tsx中

//这样传值是错误的
//<form action={updateInvoice(id)}>
// ...
import { updateInvoice } from '@/app/lib/actions';export default function EditInvoiceForm({invoice,customers,
}: {invoice: InvoiceForm;customers: CustomerField[];
}) {const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);return <form action={updateInvoiceWithId}>{/* ... */}</form>;
}

在/app/lib/actions.ts创建个UpdateInvoice操作

// Use Zod to update the expected types
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;await sql`UPDATE invoicesSET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}WHERE id = ${id}`;revalidatePath('/dashboard/invoices');redirect('/dashboard/invoices');
}
2.1测试更新

在这里插入图片描述

3. 删除票据

在/app/ui/invoices/buttons.tsx中,传入id

import { deleteInvoice } from '@/app/lib/actions';// ...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-5" /></button></form>);
}

/app/lib/actions.ts创建一个deleteInvoice操作,因为没跳转页面不需要redirect

export async function deleteInvoice(id: string) {await sql`DELETE FROM invoices WHERE id = ${id}`;revalidatePath('/dashboard/invoices');
}
3.1测试删除

在这里插入图片描述

13.错误处理

1.添加try/catch

/app/lib/actions.ts中,sql一般需要加上

//createInvoice操作
try {await sql`INSERT INTO invoices (customer_id, amount, status, date)VALUES (${customerId}, ${amountInCents}, ${status}, ${date})`;} catch (error) {// We'll log the error to the console for nowconsole.error(error);}
//updateInvoice操作try {await sql`UPDATE invoicesSET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}WHERE id = ${id}`;} catch (error) {// We'll log the error to the console for nowconsole.error(error);}

注意如何在try/catch块之外调用redirect。这是因为重定向通过抛出错误来工作,而错误将被catch块捕获。为了避免这种情况,可以在try/catch之后调用redirect。只有在try成功时才能访问重定向。举个例子(这个不需要复制到代码中,只是加深大家理解,这里项目中是会遇到的):

export async function createInvoice(formData: FormData) {let redirectUrl = '';  // 用于存储重定向的 URLtry {// 解析并验证表单数据const parsedData = CreateInvoice.parse(Object.fromEntries(formData));// 如果解析成功,进行发票创建的逻辑console.log('Parsed Data:', parsedData);// 模拟保存发票或其他处理逻辑// 假设这里是保存发票到数据库的代码// 如果一切顺利,设置成功重定向的 URLredirectUrl = '/success';  // 设定成功后的重定向页面} catch (error) {if (error instanceof z.ZodError) {// 处理验证错误console.error('验证失败:', error.errors);redirectUrl = '/error';  // 如果验证失败,重定向到错误页面} else {// 处理其他未预料的错误console.error('发生了一个意外错误:', error);redirectUrl = '/error';  // 遇到其他错误时重定向到错误页面}}// 在 try/catch 之后进行重定向if (redirectUrl) {redirect(redirectUrl);  // 执行重定向}
}

/app/lib/actions.ts中deleteInvoice操作使用手动抛出异常的方式

export async function deleteInvoice(id: string) {throw new Error('Failed to Delete Invoice');// Unreachable code block//await sql`DELETE FROM invoices WHERE id = ${id}`;//revalidatePath('/dashboard/invoices');
}
1.1 测试删除失败

在这里插入图片描述

2.处理错误

使用error.tsx处理所有错误,创建个/dashboard/invoices/error.tsx,reset 按钮

  • 点击后会执行 reset(),尝试重新渲染组件(在 Next.js 中,它会重新加载页面)。
'use client';import { useEffect } from 'react';export default function Error({error,reset,
}: {error: Error & { digest?: string };reset: () => void;
}) {useEffect(() => {// Optionally log the error to an error reporting serviceconsole.error(error);}, [error]);return (<main className="flex h-full flex-col items-center justify-center"><h2 className="text-center">Something went wrong!</h2><buttonclassName="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"onClick={// Attempt to recover by trying to re-render the invoices route() => reset()}>Try again</button></main>);
}

点击删除按钮,会有try again的按钮,点击try again会继续返回到删除按钮的列表页面,就是删除之前的页面。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用notFound函数处理404错误,在/app/lib/data.ts中,加个打印看看uuid

export async function fetchInvoiceById(id: string) {try {// ...console.log(invoice); // Invoice is an empty array []return invoice[0];} catch (error) {console.error('Database Error:', error);throw new Error('Failed to fetch invoice.');}
}

访问http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit,这个时候还是返回try again

/dashboard/invoices/[id]/edit/page.tsx中,添加

import { notFound } from 'next/navigation';//加入
//Page函数使用Promise.all返回invoice后,判断数据是否存在,下边加上if (!invoice) {notFound();}

创建个/dashboard/invoices/[id]/edit/not-found.tsx页面,notFound优先于error.tsx

import Link from 'next/link';
import { FaceFrownIcon } from '@heroicons/react/24/outline';export default function NotFound() {return (<main className="flex h-full flex-col items-center justify-center gap-2"><FaceFrownIcon className="w-10 text-gray-400" /><h2 className="text-xl font-semibold">404 Not Found</h2><p>Could not find the requested invoice.</p><Linkhref="/dashboard/invoices"className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400">Go Back</Link></main>);
}

这个时候访问http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit,会返回notFound在这里插入图片描述

把app/lib/actions.ts中deleteInvoice改回来吧,测试结束

错误处理文档链接

  • Error Handling
  • error.js API Reference
  • notFound() API Reference
  • not-found.js API Reference

14.提高易用性

1. eslint-plugin-jsx-a11y

Next.js包含eslint-plugin-jsx-a11y,在ESLint配置中添加插件,以帮助及早发现可访问性问题

/package.json中增加

//scripts中加入
"lint": "next lint"

运行

pnpm lint

​ 显示,不然你提交到vercel会部署失败

✔ No ESLint warnings or errors

验证ESlint,/app/ui/invoices/table.tsx中

<Imagesrc={invoice.image_url}className="rounded-full"width={28}height={28}alt={`${invoice.name}'s profile picture`} // 删除这行
/>

你会发现错误

./app/ui/invoices/table.tsx
88:23  Warning: Image elements must have an alt prop, either with meaningful text, or an empty string for decorative images.  jsx-a11y/alt-text

2. 表单验证

在http://localhost:3000/dashboard/invoices/create页面,不填任何东西,点击提交按钮,请求服务端直接报错,

2.1客户端验证

我们可以在客户端加个required属性

/app/ui/invoices/create-form.tsx中,加入required,测试

<inputid="amount"name="amount"type="number"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"required
/>

在这里插入图片描述

验证后删除这个验证,开始测试服务端验证。

2.2服务端验证

也可以使用服务器端验证,通过验证服务器上的表单,可以解决:

  1. 确保您的数据在发送到数据库之前是预期的格式
  2. 降低恶意用户绕过客户端验证的风险
  3. 对于被认为有效的数据,有一个真实的来源

使用React的useActionState钩子来处理表单错误

useActionState钩子:

•有两个参数:(action, initialState)。

•返回两个值:[state, formAction] -表单状态,以及表单提交时调用的函数。

2.2.1添加

/app/ui/invoices/create-form.tsx中,

'use client';//标记为客户端// ...
import { useActionState } from 'react'; // 导入 useActionState hook(可能是自定义 hook)export default function Form({ customers }: { customers: CustomerField[] }) {// 使用 useActionState hook 来创建表单的状态和动作(创建发票的动作)const [state, formAction] = useActionState(createInvoice, initialState);// ...return <form action={formAction}>...</form>;// 渲染一个表单,表单的动作由 formAction 提供
}

/app/ui/invoices/create-form.tsx中,可以定义个initialState

// ...
import { createInvoice, State } from '@/app/lib/actions'; // 从指定路径导入 createInvoice 函数和 State 类型export default function Form({ customers }: { customers: CustomerField[] }) {// 定义初始状态const initialState: State = { message: null, errors: {} }; // ...

/app/lib/actions.ts中,修改FormSchema,加入异常返回参数

const FormSchema = z.object({id: z.string(),customerId: z.string({invalid_type_error: 'Please select a customer.',}),amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }),status: z.enum(['pending', 'paid'], {invalid_type_error: 'Please select an invoice status.',}),date: z.string(),
});

在/app/lib/actions.ts中,加入状态

// ...
export type State = {errors?: {customerId?: string[];amount?: string[];status?: string[];};message?: string | null;
};export async function createInvoice(prevState: State, formData: FormData) {//使用状态// ...
}

/app/lib/actions.ts中,safeParse()将返回一个包含成功或错误字段的对象。这将有助于更优雅地处理验证,而无需将此逻辑放入try/catch块中。

//createInvoice函数中,CreateInvoice.parse改为const validatedFields = CreateInvoice.safeParse({

/app/lib/actions.ts中,validatedFields属性下边继续加验证

if (!validatedFields.success) {return {errors: validatedFields.error.flatten().fieldErrors,message: 'Missing Fields. Failed to Create Invoice.',};}

/app/lib/actions.ts中,验证后使用validatedFields.data获取数据

 const { customerId, amount, status } = validatedFields.data;//结构数据

/app/ui/invoices/create-form.tsx中的select标签属性id为customer的加入属性

 aria-describedby="customer-error"

/app/ui/invoices/create-form.tsx中,在

标签后加入,展示错误提示

<div id="customer-error" aria-live="polite" aria-atomic="true">{state.errors?.customerId &&state.errors.customerId.map((error: string) => (<p className="mt-2 text-sm text-red-500" key={error}>{error}</p>))}</div>

/app/ui/invoices/create-form.tsx中的select标签属性id为amount的加入属性

aria-describedby="amount-error"

/app/ui/invoices/create-form.tsx中,在

标签后加入,展示错误提示

<div id="amount-error" aria-live="polite" aria-atomic="true">{state.errors?.amount &&state.errors.amount.map((error: string) => (<p className="mt-2 text-sm text-red-500" key={error}>{error}</p>))}</div>

/app/ui/invoices/create-form.tsx中的select标签属性id为pending和paid都加入属性

aria-describedby="status-error"

/app/ui/invoices/create-form.tsx中,在fieldset标签中加入,展示错误提示

<div id="amount-error" aria-live="polite" aria-atomic="true">{state.errors?.amount &&state.errors.amount.map((error: string) => (<p className="mt-2 text-sm text-red-500" key={error}>{error}</p>))}</div>

在这里插入图片描述

3.2 编辑

/app/ui/invoices/edit-form.tsx也跟添加一样,代码如下

// ...
import { updateInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';export default function EditInvoiceForm({invoice,customers,
}: {invoice: InvoiceForm;customers: CustomerField[];
}) {const initialState: State = { message: null, errors: {} };const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);const [state, formAction] = useActionState(updateInvoiceWithId, initialState);return <form action={formAction}>{/* ... */}</form>;
}

/app/lib/actions.ts

export async function updateInvoice(id: string,prevState: State,formData: FormData,
) {const validatedFields = UpdateInvoice.safeParse({customerId: formData.get('customerId'),amount: formData.get('amount'),status: formData.get('status'),});if (!validatedFields.success) {return {errors: validatedFields.error.flatten().fieldErrors,message: 'Missing Fields. Failed to Update Invoice.',};}const { customerId, amount, status } = validatedFields.data;const amountInCents = amount * 100;try {await sql`UPDATE invoicesSET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}WHERE id = ${id}`;} catch (error) {return { message: 'Database Error: Failed to Update Invoice.' };}revalidatePath('/dashboard/invoices');redirect('/dashboard/invoices');
}

在/app/ui/invoices/edit-form.tsx和添加一样的加法在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com