目录
成分
科莫美国
服务器
首页 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

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

热工具

记事本++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教程
1653
14
CakePHP 教程
1413
52
Laravel 教程
1305
25
PHP教程
1251
29
C# 教程
1224
24
前端热敏纸小票打印遇到乱码问题怎么办? 前端热敏纸小票打印遇到乱码问题怎么办? Apr 04, 2025 pm 02:42 PM

前端热敏纸小票打印的常见问题与解决方案在前端开发中,小票打印是一个常见的需求。然而,很多开发者在实...

神秘的JavaScript:它的作用以及为什么重要 神秘的JavaScript:它的作用以及为什么重要 Apr 09, 2025 am 12:07 AM

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

谁得到更多的Python或JavaScript? 谁得到更多的Python或JavaScript? Apr 04, 2025 am 12:09 AM

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

如何实现视差滚动和元素动画效果,像资生堂官网那样?
或者:
怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? 如何实现视差滚动和元素动画效果,像资生堂官网那样? 或者: 怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? Apr 04, 2025 pm 05:36 PM

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript难以学习吗? JavaScript难以学习吗? Apr 03, 2025 am 12:20 AM

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

JavaScript的演变:当前的趋势和未来前景 JavaScript的演变:当前的趋势和未来前景 Apr 10, 2025 am 09:33 AM

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? 如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? Apr 04, 2025 pm 05:09 PM

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

前端开发中如何实现类似 VSCode 的面板拖拽调整功能? 前端开发中如何实现类似 VSCode 的面板拖拽调整功能? Apr 04, 2025 pm 02:06 PM

探索前端中类似VSCode的面板拖拽调整功能的实现在前端开发中,如何实现类似于VSCode...

See all articles