目录
成分
科莫美国
服务器
首页 web前端 js教程 使用 Next.js 构建动态博客仪表板

使用 Next.js 构建动态博客仪表板

Dec 08, 2024 pm 05:04 PM

介绍

你好,你好吗?我是 Vítor,带着一个新项目回来了,可以帮助您提高编程技能。自从我上次发布教程以来已经有一段时间了。在过去的几个月里,我花了一些时间休息并专注于其他活动。在此期间,我开发了一个小型网络项目:博客,它成为本教程的重点。

在本指南中,我们将创建能够渲染 Markdown 的博客页面的前端。该应用程序将包括公共和私人路由、用户身份验证以及编写 Markdown 文本、添加照片、显示文章等功能。

随意定制您的应用程序,无论您喜欢什么——我什至鼓励这样做。

您可以在此处访问此应用程序的存储库:

Building a Dynamic Blog Dashboard with Next.js 冈德拉克08 / 博客平台

使用 Next.js/typescript 制作的博客平台。

博客平台

  • 文本教程

成分

  • next-auth - Next.js 的autenticação 图书馆
  • github.com/markdown-it/markdown-it - markdown biblioteca。
  • github.com/sindresorhus/github-markdown-css- Para dar estilo ao nosso markdown 编辑器。
  • github.com/remarkjs/react-markdown - Biblioteca para renderizar markdown em nosso 组件react。
  • github.com/remarkjs/remark-react/tree/4722bdf - React 中 Markdown 转换插件。
  • codemirror.net - 网络编辑器组件。
  • react-icons - 反应图标库。

科莫美国

npm i
npm run start
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

服务器

você pode encontrar o server dessa aplicação em server


在 GitHub 上查看


本教程还包括本指南中将使用的 Node.js 服务器的编写:

希望您喜欢。

编码愉快!

图书馆

以下是此项目中使用的库的摘要:

  • next-auth - Next.js 的身份验证库
  • github.com/markdown-it/markdown-it - Markdown 库。
  • github.com/sindresorhus/github-markdown-css - 用于设计我们的 Markdown 编辑器。
  • github.com/remarkjs/react-markdown - 用于在 React 组件中渲染 Markdown 的库。
  • github.com/remarkjs/remark-react/tree/4722bdf - 将 Markdown 转换为 React 的插件。
  • codemirror.net - Web 组件编辑器。
  • react-icons - React 的图标库。

创建 React 项目

我们将使用最新版本的 Next.js 框架,在编写本教程时,版本为 13.4。

运行以下命令来创建项目:

npm i
npm run start
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

安装过程中,选择模板设置。在本教程中,我将使用 TypeScript 作为编程语言,并使用 Tailwind CSS 框架来设计我们的应用程序。

配置

现在让我们安装我们将使用的所有库。

降价
npx create-next-app myblog
登录后复制
登录后复制
登录后复制
反应备注
npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
登录后复制
登录后复制
登录后复制
代码镜像
remark remark-gfm remark-react
登录后复制
登录后复制
登录后复制
图标
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
登录后复制
登录后复制
登录后复制

然后通过删除我们不会使用的所有内容来清理安装的初始结构。

建筑学

这是我们应用程序的最终结构。

npm i react-icons @types/react-icons
登录后复制
登录后复制
登录后复制

第一步

配置next.config

在项目根目录的 next.config.js 文件中,让我们配置用于访问文章图像的域地址。对于本教程,或者如果您使用本地服务器,我们将使用 localhost。

确保包含此配置以确保在应用程序中正确加载图像。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
登录后复制
登录后复制
登录后复制

配置中间件

在应用程序 src/ 的根文件夹中,创建一个 middleware.ts 以验证对私有路由的访问。

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
登录后复制
登录后复制

要了解有关中间件以及可以使用它们执行的所有操作的更多信息,请查看文档。

配置认证路由

在 /app 文件夹内,在 api/auth/[...nextauth] 中创建一个名为 Route.ts 的文件。它将包含我们的路由配置,使用 CredentialsProvider 连接到我们的身份验证 API。

CredentialsProvider 允许您处理使用任意凭据的登录,例如用户名和密码、域、双因素身份验证、硬件设备等。

首先,在项目的根目录中,创建一个 .env.local 文件并添加一个令牌,该令牌将用作我们的秘密

npm i
npm run start
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

接下来,让我们编写我们的身份验证系统,这个 NEXTAUTH_SECRET 将被添加到 src/app/auth/[...nextauth]/routes.ts 文件中的秘密中。

npx create-next-app myblog
登录后复制
登录后复制
登录后复制

认证提供者

让我们创建一个身份验证提供程序,一个上下文,它将在我们的私有路由的页面上共享用户的数据。稍后我们将使用它来包装我们的layout.tsx 文件之一。

在 src/context/auth-provider.tsx 中创建一个包含以下内容的文件:

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
登录后复制
登录后复制
登录后复制

全球风格

总的来说,在我们的应用程序中,我们将使用 Tailwind CSS 来创建我们的样式。但是,在某些地方,我们将在页面和组件之间共享自定义 CSS 类。

remark remark-gfm remark-react
登录后复制
登录后复制
登录后复制

布局

现在让我们编写私有和公共的布局。

应用程序/布局.tsx

npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
登录后复制
登录后复制
登录后复制

页面/layout.tsx

npm i react-icons @types/react-icons
登录后复制
登录后复制
登录后复制

API调用

我们的应用程序将多次调用我们的 API,您可以调整此应用程序以使用任何外部 API。在我们的示例中,我们使用本地应用程序。如果你还没有看过后端教程和服务器创建,请查看。

在 src/services/ 中,我们编写以下函数:

  1. authService.ts:负责在服务器上验证用户身份的函数。
src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
登录后复制
登录后复制
登录后复制

2.refreshAccessToken.tsx:

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
登录后复制
登录后复制
  1. getArticles.tsx:负责获取数据库中保存的所有文章的函数:
export { default } from "next-auth/middleware";
export const config = {
  matcher: ["/", "/newArticle/", "/article/", "/article/:path*"],
};
登录后复制
  1. postArticle.tsx:负责将文章数据提交到我们的服务器的函数。
.env.local
NEXTAUTH_SECRET = SubsTituaPorToken
登录后复制
  1. editArticle.tsx:负责修改数据库中特定文章的函数。
import NextAuth from "next-auth/next";
import type { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { authenticate } from "@/services/authService";
import refreshAccessToken from "@/services/refreshAccessToken";

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: {
          name: "email",
          label: "email",
          type: "email",
          placeholder: "Email",
        },
        password: {
          name: "password",
          label: "password",
          type: "password",
          placeholder: "Password",
        },
      },
      async authorize(credentials, req) {
        if (typeof credentials !== "undefined") {
          const res = await authenticate({
            email: credentials.email,
            password: credentials.password,
          });
          if (typeof res !== "undefined") {
            return { ...res };
          } else {
            return null;
          }
        } else {
          return null;
        }
      },
    }),
  ],

  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, user, account }: any) {
      if (user && account) {
        return {
          token: user?.token,
          accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10),
          refreshToken: user?.tokenRefresh,
        };
      }

      if (Date.now() < token.accessTokenExpires) {
        return token;
      } else {
        const refreshedToken = await refreshAccessToken(token.refreshToken);
        return {
          ...token,
          token: refreshedToken.token,
          refreshToken: refreshedToken.tokenRefresh,
          accessTokenExpires:
            Date.now() + parseInt(refreshedToken.expiresIn, 10),
        };
      }
    },
    async session({ session, token }) {
      session.user = token;
      return session;
    },
  },

  pages: {
    signIn: "/login",
    signOut: "/login",
  },
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
登录后复制
  1. deleteArticle.tsx:负责从数据库中删除特定文章的函数。
'use client';
import React from 'react';
import { SessionProvider } from "next-auth/react";
export default function Provider({
    children,
    session
}: {
    children: React.ReactNode,
    session: any
}): React.ReactNode {
    return (
        <SessionProvider session={session} >
            {children}
        </SessionProvider>
    )
};
登录后复制

成分

接下来,让我们编写整个应用程序中使用的每个组件。

组件/Navbar.tsx

一个带有两个导航链接的简单组件。

/*global.css*/
.container {
  max-width: 1100px;
  width: 100%;
  margin: 0px auto;
}

.image-container {
  position: relative;
  width: 100%;
  height: 5em;
  padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */
}

.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 10px solid #f3f3f3;
  border-top: 10px solid #293d71;
  border-radius: 50%;
  animation: spinner 1.5s linear infinite;
}
登录后复制

组件/Loading.tsx

一个简单的加载组件,在等待 API 调用完成时使用。

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/context/auth-provider";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Markdown Text Editor",
  description: "Created by <@vitorAlecrim>",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession(authOptions);
  return (
    <Provider session={session}>
      <html lang="en">
        <body className={inter.className}>{children}</body>
      </html>
    </Provider>
  );
}
登录后复制

组件/分页.tsx

我们页面上使用的分页组件,在我们的私有路径中显示我们的所有文章。您可以在这里找到有关如何编写此组件的更详细的文章

npm i
npm run start
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

组件/ArticleCard.tsx

用于显示书面文章的卡片组件。

该组件还包含一个链接,该链接将指向文章显示页面和编辑先前撰写的文章的页面。

npx create-next-app myblog
登录后复制
登录后复制
登录后复制

组件/ArticleList.tsx

负责进行 API 调用并显示响应的组件。

在这里,我们将通过我们编写的函数使用两个 API 调用:

  1. getArticles.ts - 返回将在组件中显示的所有文章。
  2. removeArticle - 从我们的列表和服务器中删除特定的文章。

我们将使用之前编写的 Pagination.tsx 组件来跨页面分割文章数量。

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
登录后复制
登录后复制
登录后复制

页数

接下来,我们将按各自的路线划分浏览每个页面。

公共页面

登录

这是我们应用程序的主页。这是一个简单的页面,您可以根据需要对其进行修改。在这个页面中,我们将使用next-auth导航库提供的登录功能。

在文件 src/app/pages/public/login/page.tsx 中。

remark remark-gfm remark-react
登录后复制
登录后复制
登录后复制

文章页

为了创建文章阅读页面,我们将开发一个动态页面。

您访问过的每个博客平台可能都有一个用于阅读文章的专用页面,可通过 URL 访问。其原因是动态页面路由。幸运的是,Next.js 通过其新的 AppRouter 方法使这一切变得简单,使我们的生活变得更加简单。

首先:我们需要通过添加 [id] 文件夹在结构中创建路由。这将产生以下结构:pages/(public)/articles/[id]/pages.tsx.

  • id 对应于我们导航路线的 slug。
  • params 是通过包含导航 slug 的应用程序树传递的属性。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
登录后复制
登录后复制
登录后复制

第二:使用MarkdownIt库,使页面能够显示Markdown格式的文本。

npm i react-icons @types/react-icons
登录后复制
登录后复制
登录后复制

最后,

页面准备好后,例如通过在浏览器中访问 localhost:3000/articles/1,您将能够使用提供的 ID 查看文章。

在我们的例子中,当单击其中一个 ArticleCards.tsx 组件时,ID 将通过导航传递,该组件将呈现在我们的私有路由的主页上。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
登录后复制
登录后复制
登录后复制

私人页面

这是我们的私人页面,只有用户在我们的应用程序中通过身份验证后才能访问。

在我们的app/pages/文件夹中,当在()内声明一个文件时,就意味着该路由对应于/。

在我们的例子中,(Home) 文件夹指的是我们私人路线的主页。这是用户在系统中进行身份验证后看到的第一个页面。此页面将显示我们数据库中的文章列表。

数据将由我们的 ArticlesList.tsx 组件处理。如果您还没有编写此代码,请参阅组件部分。

在应用程序/(页面)/(私人)/(主页)/page.tsx中。

npm i
npm run start
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

新文章

这是我们应用程序中最重要的页面之一,因为它允许我们注册我们的文章。

此页面将使用户能够:

  1. 以 Markdown 格式写一篇文章。
  2. 为文章分配图像。
  3. 在将 Markdown 文本提交到服务器之前预览它。

页面使用了多个钩子

  1. useCallback - 用于记忆函数。
  2. useState - 允许您向我们的组件添加状态变量。
  3. useSession - 让我们检查用户是否经过身份验证并获取身份验证令牌。

为此,我们将使用两个组件:

  1. TextEditor.tsx:我们之前编写的文本编辑器。
  2. Preview.tsx:用于显示 Markdown 格式文件的组件。

在构建此页面时,我们将使用我们的 API:

  1. POST:使用我们的函数 postArticle,我们将把文章发送到服务器。

我们还将使用 next-auth 库提供的 useSession 钩子来获取用户的身份验证令牌,该令牌将用于在服务器上注册文章。

这将涉及三个不同的 API 调用。
在 app/pages/(private)/newArticle/page.tsx.

“使用客户端”;
从“react”导入 React, { ChangeEvent, useCallback, useState };
从“next-auth/react”导入{useSession};
从“下一步/导航”导入{重定向};
从“@/services/postArticle”导入 postArtical;
从“react-icons/ai”导入{AiOutlineFolderOpen};
从“react-icons/ri”导入 { RiImageEditLine };

从“下一个/图像”导入图像;
从“@/components/textEditor”导入文本编辑器;
从“@/components/PreviewText”导入预览;
从“react-icons/ai”导入{AiOutlineSend};
从“react-icons/bs”导入{BsBodyText};

导出默认函数 NewArticle(params:any) {
  const { 数据:会话 }:任何 = useSession({
    要求:真实,
    onUnauthenticated(){
      重定向(“/登录”);
    },
  });
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false);
  const [标题,setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>("# Escreva o seu texto... n");
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);

  if (!session?.user) 返回 null;

  const handleArticleSubmit = async (e:any) =>; {
        e.preventDefault();
    const token: string = session.user.token;
    尝试 {
      const res = 等待 postArtical({
        id: session.user.userId.toString(),
        令牌:令牌,
        图片网址: 图片网址,
        标题:“标题”
        文档: 文档,
      });
      console.log('re--->', res);
      重定向('/成功');
    } 捕获(错误){
      console.error('提交文章时出错:', error);
      // 如果需要,处理错误
      抛出错误;
    }
  };

  const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; {
    if (e.target.files && e.target.files.length > 0) {
      const 文件 = e.target.files[0];
      const url = URL.createObjectURL(文件);
      设置预览图像(网址);
      setImageUrl(文件);
    }
  };

  const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
  };
  返回 (
    <section classname="w-full h-full min-h-screenrelative py-8">
      {预览文本&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>;
      )}

      <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-xl flex flex-col 间隙-2“>
        {“”}
        <div className="flex justify- Between items-center">
          <按钮
            className =“border-b-2 rounded-md border-slate-500 p-2 flex items-center差距-2悬停:border-slate-400悬停:text-slate-800”
            onClick={handleTextPreview}
          >
            <BsBodyText/>>
            预览
          </按钮>{" onclick="{handleArticleSubmit}">
            恩维亚尔·特克斯托
            <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" />
          </按钮>
        ;
        <div className="header-wrapper flex flex-col gap-2">
          <div className="image-box">
            {previewImage.length === 0 && (
              <div className="select-image">
                
                  <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>
                  拖放图像
                </标签>
                



<h4>
  
  
  编辑文章
</h4>

<p>与<em>新文章</em>(newArticle)类似的页面,但有一些差异。</p>

<p>首先,我们定义一条动态路线,在其中接收 id 作为导航参数。这与文章阅读页面上所做的非常相似。 <br>
app/(pages)/(private)/editArticle/[id]/page.tsx<br>
</p>
<pre class="brush:php;toolbar:false">“使用客户端”;
从“react”导入 React, { useState, useEffect, useCallback, useRef, ChangeEvent };
从“next-auth/react”导入{useSession};
从“下一步/导航”导入{重定向};
从“下一个/图像”导入图像;

从“@/interfaces/article.interface”导入{IArticle};
从“react-icons/ai”导入{AiOutlineEdit};
从“react-icons/bs”导入{BsBodyText};
从“react-icons/ai”导入{AiOutlineFolderOpen};
从“react-icons/ri”导入 { RiImageEditLine };

从“@/components/PreviewText”导入预览;
从“@/components/textEditor”导入文本编辑器;
从'@/components/Loading'导入加载;
从“@/services/editArticle”导入 editArtical;

导出默认函数 EditArticle({ params }: { params: any }) {
 const { 数据:会话 }:任何 = useSession({
    要求:真实,
    onUnauthenticated(){
      重定向(“/登录”);
    },
  });
  const id: 数字 = params.id;
  const [文章,setArticle] = useState<iarticle>(空);
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false)
  const [标题,setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>('');
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);
  const inputRef= useRef<htmlinputelement>(null);

  const fetchArticle = async (id: number) =>; {
    尝试 {
      常量响应 = 等待获取(
        `http://localhost:8080/articles/getById/${id}`,
      );
      const jsonData = 等待响应.json();
      setArticle(jsonData);
    } 捕获(错误){
      console.log("出了点问题:", err);
    }
  };
  useEffect(() => {
    if (文章 !== null || 文章 !== 未定义) {
      获取文章(id);
    }
  }, [ID]);

  useEffect(()=>{
    if(文章!= null && 文章.内容){
        setDoc(文章.内容)
    }

    if(文章!=null && 文章.image){
      setPreviewImage(`http://localhost:8080/` 文章.image)
    }
  },[文章])

  const handleArticleSubmit = async (e:any) =>; {
     e.preventDefault();
    const token: string = session.user.token;
    尝试{
      const res = 等待 editArtical({
      身份证号: 身份证号,
      令牌:令牌,
      图片网址:图片网址,
      标题: 标题,
      文档: 文档,
      });
        console.log('re--->',res)
        返回资源;
    } 捕获(错误){
    console.log(“错误:”,错误)
    }
  };
  const handleImageClick = ()=>{
      console.log('hiii')
    if(inputRef.current){
      inputRef.current.click();
    }
  }const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; {
    if (e.target.files && e.target.files.length > 0) {
      const 文件 = e.target.files[0];
      const url = URL.createObjectURL(文件);
      设置预览图像(网址);
      setImageUrl(文件);
    }

  };
   const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
    console.log('预览版你好!')
  };

  if(!article) return >
  if(文章?.内容)
  返回 (
    <section className='w-full h-full min-h-screenrelative py-8'>
      {预览文本&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>
      )}

      <div classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- Shadow-md flex flex-col 间隙-2">
        <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-md flex flex-col 间隙-2 ">
          {“”}
          <div classname="flex justify- Between items-center">;
            
              <bsbodytext></bsbodytext>>
              预览
            </按钮>{" "}
            
                编辑阿蒂戈 
              <aioutlineedit classname="w-5 h-5 group-hover:text-red-500"></aioutlineedit>>
            </按钮>
          </div>;
          <div classname="header-wrapper flex flex-col gap-2">;
            <div classname="image-box">;
              {previewImage.length === 0 && (
                <div classname="select-image">;
                  
                    <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>>
                    拖放图像
                  </标签>
                  



<h2>
  
  
  结论
</h2>

<p>首先,我要感谢您花时间阅读本教程,并且我还要祝贺您完成它。我希望它对您有帮助,并且分步说明很容易遵循。</p>

<p>其次,我想强调一下关于我们刚刚构建的内容的几点。这是博客系统的基础,还有很多东西需要添加,比如显示所有文章的公共页面、用户注册页面,甚至是自定义的 404 错误页面。如果在教程期间您对这些页面感到好奇并错过了它们,请知道这是故意的。本教程为您提供了足够的经验来自行创建这些新页面、添加许多其他页面以及实现新功能。</p>

<p>非常感谢,下次再见。哦/</p>


          </div>

            
        </div>
</div>
</form>
</div></htmlinputelement></htmlinputelement></string></string></boolean></string></object></iarticle>
登录后复制

以上是使用 Next.js 构建动态博客仪表板的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
JavaScript引擎:比较实施 JavaScript引擎:比较实施 Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

Python vs. JavaScript:学习曲线和易用性 Python vs. JavaScript:学习曲线和易用性 Apr 16, 2025 am 12:12 AM

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

从C/C到JavaScript:所有工作方式 从C/C到JavaScript:所有工作方式 Apr 14, 2025 am 12:05 AM

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。

JavaScript和Web:核心功能和用例 JavaScript和Web:核心功能和用例 Apr 18, 2025 am 12:19 AM

JavaScript在Web开发中的主要用途包括客户端交互、表单验证和异步通信。1)通过DOM操作实现动态内容更新和用户交互;2)在用户提交数据前进行客户端验证,提高用户体验;3)通过AJAX技术实现与服务器的无刷新通信。

JavaScript在行动中:现实世界中的示例和项目 JavaScript在行动中:现实世界中的示例和项目 Apr 19, 2025 am 12:13 AM

JavaScript在现实世界中的应用包括前端和后端开发。1)通过构建TODO列表应用展示前端应用,涉及DOM操作和事件处理。2)通过Node.js和Express构建RESTfulAPI展示后端应用。

了解JavaScript引擎:实施详细信息 了解JavaScript引擎:实施详细信息 Apr 17, 2025 am 12:05 AM

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

Python vs. JavaScript:社区,图书馆和资源 Python vs. JavaScript:社区,图书馆和资源 Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

Python vs. JavaScript:开发环境和工具 Python vs. JavaScript:开发环境和工具 Apr 26, 2025 am 12:09 AM

Python和JavaScript在开发环境上的选择都很重要。1)Python的开发环境包括PyCharm、JupyterNotebook和Anaconda,适合数据科学和快速原型开发。2)JavaScript的开发环境包括Node.js、VSCode和Webpack,适用于前端和后端开发。根据项目需求选择合适的工具可以提高开发效率和项目成功率。

See all articles