使用nextjs官方提供的脚手架创建一个项目模版
npx create-next-app@latest next-crud --use-npm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"
启动项目
pnpm dev
1.在app目录下创建global.css文件,并写入以下全局样式代码:
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
2.在app/layout.tsx中引入
import '@/app/ui/global.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}
1.新建app/ui/fonts.ts,引入字体并导出
import { Inter, Lusitana } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'] });
export const lusitana = Lusitana({
weight: ['400', '700'],
subsets: ['latin'],
});
2.将字体添加到 /app/layout.tsx 中的 <body> 元素:
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}
可以看到设置的全局字体了
在nextjs中可以使用next/image设置图像。
1.将图片文件放到 public文件夹下
2.使用next/image组件,引入
import Image from 'next/image';
<Image
src="/hero-desktop.png"
width={1000}
height={760}
className="hidden md:block"
alt="Screenshots of the dashboard project showing desktop version"
/>
Next.js 使用文件系统路由,其中文件夹用于创建嵌套路由。每个文件夹代表一个映射到 URL 段的路由段。
1.nextjs默认 app/page.tsx 是根路由
2.新建 app/dashboard/page.tsx文件
export default function Page() {
return <p>Dashboard Page</p>;
}
3.访问http://localhost:3000/dashboard 就是dashboard路由对应的页面了
共享导航:在app/dashboard下面创建layout.tsx
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}
这样/dashboard下面的都会共享同一个布局,即同一个 SideNav 左侧导航
访问:http://localhost:3000/dashboard 查看效果
使用nextjs导航,当组件更新的时候,布局不会重新渲染
现在导航切换使用a标签,点击会重新加载页面,使用Link标签替换
// /app/ui/dashboard/nav-links.tsx
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{
name: 'Invoices',
href: '/dashboard/invoices',
icon: DocumentDuplicateIcon,
},
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];
export default function NavLinks() {
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
1.使用next/navigation提供的usePathname()
由于 usePathname() 是一个钩子,因此需要将 nav-links.tsx 转换为客户端组件。将 React 的 "use client" 指令添加到文件顶部,然后从 next/navigation 导入 usePathname() :
'use client';
import {
UserGroupIcon,
HomeIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
// ...
2.获取当前pathname
const pathname = usePathname();
3.当 link.href 与 pathname 匹配时,链接以蓝色文本和浅蓝色背景显示。
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
{
'bg-sky-100 text-blue-600': pathname === link.href,
},
)}
1.创建github项目,推送代码
2.创建一个vercel帐号
3.在vercel中部署github项目
4.部署完成:https://next-crud-two-psi.vercel.app/dashboard
5.选择storage:Connect Store → Create New → Postgres → Continue.
6.获取数据库相关数据
7.将上面数据粘贴到.env中
8.安装postgres
npm i @vercel/postgres
9.运行脚本创建数据表,把本地数据导入到vercel 数据库中
"seed": "node -r dotenv/config ./scripts/seed.js"
10.可以从vercel数据库看到数据
可以查询数据:
npm install antd --save
将 antd 首屏样式按需抽离并植入到 HTML 中,以避免页面闪动的情况。
1.安装 @ant-design/nextjs-registry
npm install @ant-design/nextjs-registry --save
2.在 app/layout.tsx 中使用
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
import { AntdRegistry } from '@ant-design/nextjs-registry';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>
<AntdRegistry>{children}</AntdRegistry>
</body>
</html>
);
}
1.编写查询数据库方法
export async function fetchLatestInvoices() {
noStore();
try {
const data = await sql<LatestInvoiceRaw>`
SELECT * FROM customers;`;
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}
2.在page.tsx中获取数据
data = await fetchLatestInvoices();
3.获取data后进行渲染
<div className="bg-white px-6">
{data.map((invoice, i) => (
<div
key={invoice.id}
className={clsx(
'flex flex-row items-center justify-between py-4',
{
'border-t': i !== 0,
},
)}
>
<div className="flex items-center">
<Image
src={invoice.image_url}
alt={`${invoice.name}'s profile picture`}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.name}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.email}
</p>
</div>
</div>
<Delete invoice={invoice} handleDelete={handleDelete} />
<p
className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
>
{invoice.amount}
</p>
</div>
))}
</div>
1.熟悉编写ui组件,一个输入框和一个按钮,用户在输入框输入内容,点击按钮调用插入数据的方法
编写客户端组件Add
'use client';
import React, { useState } from 'react';
import { Button, Input } from 'antd';
interface IProps {
handleAdd: (value: string) => void;
}
const Add = (props: IProps) => {
const { handleAdd } = props;
const [value, setValue] = useState('');
const handleClick = () => {
handleAdd && handleAdd(value);
};
return (
<>
<Input onChange={(e) => setValue(e.target.value)} />
<Button onClick={handleClick}>添加</Button>
</>
);
};
export default Add;
2.编写插入数据方法
export async function insertInvoices(name: string) {
noStore();
try {
const data = await sql<LatestInvoiceRaw>`
INSERT INTO customers (name, email, image_url)
VALUES (${name},'email','/customers/balazs-orban.png');`;
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}
3.在page.tsx中引入Add组件
<Add handleAdd={handleAdd} />
3.编写handleAdd方法,这里注意因为page.tsx是服务端组件,所以在handleAdd方法中指明是服务端
const handleDelete = async (record: any) => {
'use server';
await deleteInvoices(record.id);
};
1.首先编写客户端删除按钮组件
'use client';
import React from 'react';
import { Button } from 'antd';
interface IProps {
handleDelete: (invoice: any) => void;
invoice: any;
}
const Delete = (props: IProps) => {
const { handleDelete, invoice } = props;
const handleClick = () => {
console.log('aa');
handleDelete && handleDelete(invoice);
};
return (
<>
<Button onClick={handleClick}>删除</Button>
</>
);
};
export default Delete;
2.在page.tsx中引入删除按钮组件
<Delete invoice={invoice} handleDelete={handleDelete} /> 3.编写删除数据操作export async function deleteInvoices(id: string) {
noStore();
try {
const data = await sql<LatestInvoiceRaw>`
DELETE FROM customers
WHERE id=${id};`;
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}
4.最后编写删除操作
const handleDelete = async (record: any) => {
'use server';
await deleteInvoices(record.id);
};
这样就完成了crud操作。
将代码提交到github上,使用vercel会自动部署
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。