六、使用 PrimeVue UI 框架构建度假预订应用

第 5 章使用 Ionic构建多用途计算器移动应用中,我们使用 Ionic 移动应用框架构建了一个基于 Vue.js 的移动应用。然而,到目前为止,在本书中,我们还没有使用基于 Vue.js 的 UI 库或框架构建任何 web 应用。此外,我们还没有构建任何有自己后端的东西。在大多数系统中,后端绝对是必需的,因为我们需要一个地方来存储数据、验证用户、运行后台任务等等。在本章中,我们将使用 PrimeVue UI 框架构建一个假期预订应用。

我们将使用 Vue 3 前端进行管理,另一个前端用于用户添加预订。我们还将包括一个简单的后端,用于在管理员执行只能由他们完成的任务之前对其进行身份验证。为了使项目尽可能简单,面向公众的前端不需要身份验证。

在本章中,我们将重点关注以下主题:

  • 使用 PrimeVue UI 框架构建前端
  • 使用 Express 构建用于身份验证的简单后端
  • 使用 SQLite 在后端持久化数据
  • 在前端使用 Vue 路由进行身份验证
  • 使用 Vee Validate 和 Yup 进行表单验证

技术要求

本章代码位于https://github.com/PacktPublishing/-Vue.js-3-By-Example/tree/master/Chapter06

PrimeVue是基于 Vue 3 的 UI 框架。这意味着我们可以将其用于 Vue 3 应用。基于 Vue 2 的框架不能由 Vue 3 应用使用,因为 API 已经历重大更改。Vue 3 的底层代码也不同于 Vue 2。PrimeVue 包括许多我们在 web 应用中使用的常用组件,如文本输入、按钮、菜单栏、表格等。就所包括的内容而言,它是非常全面的。此外,它还提供了主题形式的项目样式。这意味着我们可以立即使用内置组件。由于 PrimeVue 是为 Vue 3 设计的,因此我们可以简单地注册组件、导入 CSS 并在代码中使用组件。我们还可以根据给定组件所需的组件,在本地或全局注册它们。

了解 PrimeVue

PrimeVue 提供了输入样式和各种文本,如验证错误和按钮。它还附带 flexbox 助手,我们可以使用它轻松设置组件的间距。这非常有用,因为我们可以简单地使用 PrimeVue 附带的 CSS 类来设置组件的位置和间距。

到目前为止,在本书中,我们还没有使用任何库来使表单验证更加方便。表单验证是我们必须为大多数 web 应用做的很多事情。

理解 Vee 验证和 Yup

Vee Validate 4是一个与 Vue 3 兼容的表单验证库。有了它,我们可以在前端添加组件,为表单添加验证。我们将与 Yup 数据验证库一起使用它来创建表单验证模式,Vee Validate 可以使用该模式进行验证。通过Form组件,我们可以添加一个表单,使我们能够使用 Vee Validate 4 进行表单验证。然后,Field组件可用于验证 PrimeVue 附带的表单控制组件。它通过将[T2]组件附带的插槽道具作为道具传递给输入组件来实现这一点。

Yup 库将与 Vee Validate 一起使用,使我们能够轻松地验证表单值,而无需从头编写所有代码。它允许我们创建表单验证模式对象,我们可以将这些对象传递到使用 Vee Validate 创建的表单中,以添加表单验证。

理解快车

为了创建一个简单的后端来存储数据,我们使用Express framework。这是一个非常简单的 Node.js 后端框架,它允许我们快速创建一个简单的后端。为了存储数据,我们将使用 SQLite 数据库使项目保持简单。我们将使用 Express 框架创建一个 API,使前端能够向其发出 HTTP 请求。我们让他们通过公开 API 端点发出请求,前端可以通过向其添加路由来使用该端点。每个路由都有一个处理函数,用于处理前端提交的数据。我们从前端发出的 HTTP 请求中获取请求数据,其中包括头和正文,并在路由处理程序中使用它们以我们想要的方式获取和存储数据。

连接前端和后端

要使前端应用与后端应用通信,我们需要在后端启用跨域通信,以便前端的流量可以通过后端。这可以通过跨源资源共享CORS中间件轻松实现,我们将添加到我们的 Express app 中。

为了使用 SQLite 数据库,我们使用sqlite3库,它允许我们在 Node.js 应用中操作 SQLite 数据库。我们可以进行查询并运行 SQL 命令来插入或删除数据库中的数据。

此外,我们将为管理员前端提供简单的身份验证。我们将检查管理员登录的用户名和密码,如果有效,我们可以发出令牌并将其发送到前端。然后,前端将使用存储在报头中的令牌来检查是否可以从前端发出请求。我们只为admin only路由添加身份验证,因此我们只需要在加载之前检查需要身份验证的路由的令牌。

为了创建和检查令牌,我们使用jsonwebtoken库。这允许我们创建一个令牌,并用一个秘密字符串对其进行签名。它还使我们能够检查带有密码的令牌,以查看其是否有效。我们将jsonwebtoken库放在一个中间件中,该中间件在路由处理程序执行检查之前运行。

如果令牌是有效的,那么我们调用一个函数继续路由处理程序。否则,我们会将401状态发送回客户端。

现在,我们要建造这个项目。

创建假期预订项目

要创建假期预订应用,我们需要为前端、管理前端和后端创建子项目。为了创建frontendadmin frontend项目支架,我们使用 Vue CLI。要创建backend文件夹,我们使用Express Generator全局包。

按照以下步骤设置项目:

  1. 首先,创建travel-booking-app文件夹以容纳所有项目。
  2. 接下来,在主文件夹中创建admin-frontendfrontendbackend文件夹。
  3. Go into the admin-frontend folder and run the following command:

    js npx vue create

    这将在admin-frontend文件夹中添加 Vue 项目的脚手架代码。

  4. If you are asked to create the project in the current folder, select Y. Then, when you're asked to choose the Vue version of the project, choose Vue 3.

    同样,对[T0]文件夹以相同的方式运行 Vue CLI。

  5. To create an Express project, run the Express application generator app. To do this, go into the backend folder and run the following command:

    js npx express-generator

    前面的命令将在backend文件夹中添加项目所需的所有文件。如果您出现错误,请尝试将express-generator作为administrator运行。

现在我们已经创建了项目 scaffold 文件和文件夹,我们已经准备好开始后端工作了。

创建后端

现在我们已经用 scaffolding 代码创建了项目文件夹,我们可以开始处理项目代码了。我们将从后端开始,因为两个前端都需要它。

首先,让我们添加一些库,这些库是操作 SQLite 数据库和向我们的应用添加身份验证所需的。此外,我们还需要库将 CORS 添加到我们的应用中。

要安装所有这些组件,请运行以下命令:

npm i cors jsonwebtoken sqlite3

安装包之后,我们就可以开始编写代码了。

添加身份验证中间件

首先,我们添加中间件,用于检查令牌。我们可以通过jsonwebtoken库轻松实现这一点。这有verify方法来检查令牌。

要添加中间件,请在后端文件夹中创建middlewares文件夹,然后添加verify-token.js文件。接下来,为中间件添加以下代码:

const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
  const token = req.get('x-token')
  try {
    jwt.verify(token, 'secret');
    next()
  } catch (err) {
    res.status(401)
  }
}

这里,我们使用req.get方法获得x-token请求头。然后,我们使用返回的tokensecret调用jwt.verify,以验证令牌是否有效。然后,如果有效,我们调用next。如果无效,将抛出一个错误,catch块将运行。运行带有401res.status以将401响应返回到前端,因为令牌在此场景中无效。

module.exports属性被设置为middleware函数作为我们正在导出的值。导出该函数可在我们后端应用的其他模块中使用。

添加路由以处理请求

接下来,我们将添加带有路由的router模块。首先,添加路线以操纵预订。为此,请将bookings.js文件添加到routes文件夹中。在文件中,编写以下代码:

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const router = express.Router();
const verifyToken = require('../middlewares/verify-token')
router.get('/', (req, res, next) => {
  const db = new sqlite3.Database('./db.sqlite');
  db.serialize(() => {
    db.all(`
      SELECT
        bookings.*,
        catalog_items.name AS catalog_item_name,
        catalog_items.description AS catalog_item_description
      FROM bookings
      INNER JOIN catalog_items ON catalog_items.id = 
        bookings.catalog_item_id
    `,
      [],
      (err, rows = []) => {
        res.json(rows)
      });
  })
  db.close();
});
...

在这里,我们导入所需的模块,包括我们刚刚创建的verify-token middleware文件。

router.get方法允许我们创建 GET 请求 API 端点。路径在第一个参数中。它是路由的路径,并且与路由的路径相关。因此,路由的路由路径是第一段,router.get的第一个参数中的路径构成 URL 的其余部分。

router.get方法的第二个参数是路由处理程序。req参数是具有请求数据的对象。res参数是一个对象,允许我们向前端发送各种响应。我们使用带有数据库文件路径的sqlite3.Database构造函数获取数据库。

接下来,我们调用db.serialize函数,以便可以在回调中按顺序运行代码。

db.all方法获取查询返回的所有结果。字符串是从 bookings 表中检索所有数据的 SQL 命令,我们将使用自己的 SQL 代码创建该表。bookings表与catalog_items表联接,使vacation套餐数据与预订关联。

db.all的第二个参数是我们想要传入的额外参数,我们通过传入一个空数组来忽略这些参数。然后,在最后一个参数中,我们有一个带有err参数的函数,它的对象形式有错误。rows参数具有来自查询的结果。在回调中,我们调用res.json返回带有rows参数数据的 JSON 响应。

然后,一旦完成所需的操作,我们调用db.close关闭数据库连接。

接下来,我们将创建一个 POST 路由。这将允许我们运行一个INSERTSQL 命令,在 bookings 表中插入一个条目。添加以下代码:

...
router.post('/', (req, res) => {
  const db = new sqlite3.Database('./db.sqlite');
  const { catalogItemId, name, address, startDate, endDate } = 
   req.body
  db.serialize(() => {
    const stmt = db.prepare(`
      INSERT INTO bookings (
        catalog_item_id,
        name,
        address,
        start_date,
        end_date
      ) VALUES (?, ?, ?, ?, ?)
    `);
    stmt.run(catalogItemId, name, address, startDate, endDate)
    stmt.finalize();
    res.json({ catalogItemId, name, address, startDate,
      endDate })
  })
  db.close();
});
...

在这里,我们用req.body属性得到身体属性。我们得到了要插入到条目中的所有属性。接下来,我们用INSERT语句创建一个准备好的语句。末尾的值是问号;这意味着它们是占位符,当我们运行stmt.run方法时,我们可以在其中放置自己的值。

准备好的语句很有用,因为它们使我们能够安全地将值传递给 SQL 命令。这些值都已清理,因此恶意代码无法在代码内部运行。我们运行stmt.run以使用我们想要替换占位符的值运行准备好的语句。然后我们调用stmt.finalize通过写入数据来完成操作。接下来,我们调用res.json将 JSON 响应作为响应返回到前端。然后,我们调用db.close再次关闭数据库连接。

接下来,我们将使用router.delete方法创建一个DELETE端点。为此,请编写以下代码:

...
router.delete('/:id', verifyToken, (req, res) => {
  const db = new sqlite3.Database('./db.sqlite');
  const { id } = req.params
  db.serialize(() => {
    const stmt = db.prepare("DELETE FROM bookings WHERE id = (?)");
    stmt.run(id)
    stmt.finalize();
    res.json({ status: 'success' })
  })
  db.close();
});
...

这里,我们有/:id路径。:id是路由的 URL 参数占位符。我们还在booking.js文件顶部导入了verifyToken中间件。在继续运行路由处理程序的代码之前,我们可以使用它来验证令牌。要使此路由中的令牌通过身份验证,则意味着此路由标头中的令牌调用需要成功。

在路由处理程序中,我们从req.params属性获取idURL 参数。然后,我们调用db.serialize,就像我们之前的路线一样。在回调中,我们有准备好的语句,因此我们可以使用在stmt.run方法中设置的id值发出一个DELETESQL 命令。然后,我们像在其他路线上一样,叫stmt.finalizeres.jsondb.close

最后,在booking.js文件的末尾,我们添加以下内容:

module.exports= = router;

添加前面的语句允许我们将其导入另一个文件以注册路由。注册路由将使其可访问。

接下来,我们将在routes文件夹中创建catalog.js文件。该文件是一个带有 API 端点的router模块,用于添加我们的度假包。首先,我们从以下方面着手:

const express = require('express');
const router = express.Router();
const sqlite3 = require('sqlite3').verbose();
const verifyToken = require('../middlewares/verify-token')
router.get('/', (req, res,) => {
  const db = new sqlite3.Database('./db.sqlite');
  db.serialize(() => {
    db.all("SELECT * FROM catalog_items", [], (err, rows = []) 
      => {
      res.json(rows)
    });
  })
  db.close();
});
...

这与bookings.js中的路线GET路线几乎相同;然而,在这里,我们从catalog_items表中检索所有项目。

接下来,让我们添加一个POST路由,将一个条目添加到catalog_items表中。编写以下代码:

...
router.post('/', verifyToken, (req, res,) => {
  const { name, description, imageUrl } = req.body
  const db = new sqlite3.Database('./db.sqlite');
  db.serialize(() => {
    const stmt = db.prepare(`
    INSERT INTO catalog_items (
      name,
      description,
      image_url
    ) VALUES (?, ?, ?)
  `
    );
    stmt.run(name, description, imageUrl)
    stmt.finalize();
    res.json({ status: 'success' })
  })
  db.close();
});
...

这里,我们在第二个参数中有verifyToken在第三个参数中运行路由处理程序之前检查该路由中的令牌。

接下来,我们添加一个路由,使我们能够删除catalog_items条目。我们使用以下代码执行此操作:

...
router.delete('/:id', verifyToken, (req, res,) => {
  const { id } = req.params
  const db = new sqlite3.Database('./db.sqlite');
  db.serialize(() => {
    const stmt = db.prepare("DELETE FROM catalog_items WHERE 
      id = (?)");
stmt.run(id)
stmt.finalize();
res.json({status:'success'})
db.close();
});
...

最后,我们导出路由:

module.exports = router;

本模块与booking.js没有太大区别。

接下来,我们删除routes/users.js文件的内容,如果它不存在,则创建它。然后,我们添加以下代码:

const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();
router.post('/login', (req, res) => {
  const { username, password } = req.body
  if (username === 'admin' && password === 'password') {
    res.json({ token: jwt.sign({ username }, 'secret') })
  }
  res.status(401)
});
module.exports= = router;

这是我们检查管理员用户的usernamepassword是否有效的地方。我们只有一个用户在这里检查,以保持项目的简单。我们从包含 JSON 请求对象的req.body对象获得usernamepassword。然后,我们用if语句检查usernamepassword,如果if中的表达式返回true,我们调用jwt.sign使用第一个参数中的令牌数据和第二个参数中的secret创建令牌。然后,我们返回带有身份验证令牌的响应,其中包含[T12]。

否则,我们用401调用res.status返回401响应,因为usernamepassword无效。

接下来,我们在app.js中注册我们的router模块和全局中间件。为此,我们编写以下代码:

...
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const catalogRouter = require('./routes/catalog');
const bookingsRouter = require('./routes/bookings');
const app = express();
const cors = require('cors')
...
app.use('/users', usersRouter);
app.use('/catalog', catalogRouter);
app.use('/bookings', bookingsRouter);

我们使用require导入前面导出的router模块和router文件的最后一行。然后我们导入cors模块:

const cors = require('cors')

我们调用app.use来添加cors中间件,然后添加router模块。在最后三行中,我们传入path作为第一个参数,router模块作为第二个参数。这使我们能够访问先前创建的端点。通过cors模块,我们可以在我们的 Express app 中启用跨域通信。

接下来,让我们创建 SQL 脚本,以便可以轻松地删除和创建表。为此,在backend文件夹中创建db.sql文件,并编写以下代码:

DROP TABLE IF EXISTS bookings;
DROP TABLE IF EXISTS catalog_items;
CREATE TABLE catalog_items (
  id INTEGER NOT NULL PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT NOT NULL,
  image_url TEXT NOT NULL
);
CREATE TABLE bookings (
  id INTEGER NOT NULL PRIMARY KEY,
  catalog_item_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  address TEXT NOT NULL,
  start_date TEXT NOT NULL,
  end_date TEXT NOT NULL,
  FOREIGN KEY (catalog_item_id) REFERENCES catalog_items(id)
);

在这里,我们创建了bookingscatalog_items表。每个表都有不同的字段。TEXT创建一个文本列。NOT NULL使列不可为空。PRIMARY KEY表示该列为主键列。FOREIGN KEY表示一列是另一列的外键。

我们可以通过安装 SQLite 程序的 DB 浏览器来运行 SQL 代码,该程序可以在下载 https://sqlitebrowser.org/ ,然后在backend文件夹中创建db.sqlite。然后,我们可以进入执行 SQL选项卡,将代码粘贴到文本输入中。接下来,我们可以选择所有文本并按F5运行代码。这将删除任何现有的bookingscatalog_items表,并再次创建它们。要将数据库的更改写入磁盘,必须保存这些更改。要执行此操作,请单击文件菜单,然后单击写入更改。我们也可以按[T17]Ctrl+S[T18]组合键保存更改。

最后,为了让我们的应用在更改代码时自动运行和重新启动,我们可以在全局范围内安装nodemon包。要执行此操作,请运行以下命令:

npm i -g nodemon

然后,在package.json文件中,将script.start属性的值更改为以下代码:

{
  ...
  "scripts": {
    "start": "nodemon ./bin/www"
  },
  ...
}

我们可以使用nodemon来运行npm start,而不是常规的节点可执行文件,这意味着当我们更改任何代码文件并保存它时,应用将自动重启。

现在我们已经为前端用户创建了一个基本的后端,我们可以继续使用 PrimeVue 和 Vue 3 创建前端应用。

创建管理前端

既然后端应用已经完成,我们就可以开始管理前端的工作了。我们之前已经在admin-frontend文件夹中为管理员前端创建了 Vue 3 项目,所以我们只需要安装所需的软件包并编写代码。我们需要 PrimeVue 软件包,即 Vee Validate、Vue 路由、Axios 和 Yup 软件包。

要安装它们,请在admin-frontend文件夹中运行以下命令:

npm i axios primeflex primeicons primevue@^3.1.1 vee-validate@next vue-router@4 yup

Axios 允许我们向后端发出 HTTP 请求。Vue 路由允许我们将 URL 映射到page组件。Vee Validate 和 Yup 允许我们轻松地将表单验证添加到表单中,其余的包是 PrimeVue 包。

创建管理前端页面

安装完软件包后,我们可以开始代码工作。首先,我们将处理组件。在components文件夹中添加CatalogForm.vue文件,并编写以下代码:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <Field v-slot="{ field, errors }" v-model="name" 
      name="name">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Name"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Name is invalid.
        </small>
      </div>
    </Field>
    ...
  </Form>
</template>

这里,我们有来自 Vee Validate 包的Form组件来添加一个带有表单验证的表单。只有当所有表单值都有效时才会发出submit事件。稍后我们将注册Form组件。validation-schema属性设置为 Yup 创建的验证模式对象。

Form组件中,我们有一个Field组件,它也是由 Vee 验证包提供的。稍后,我们还将全局注册此组件,以便使用它。在Field组件中,我们有InputText组件向我们的应用添加一个输入字段。为了启用InputText组件的表单验证,我们将field对象传递给slot道具,并将整个内容作为v-bind指令的值传递。v-bind指令允许 Vee Validate 处理表单值,并将验证添加到form字段。[T10]数组为我们提供了可能发生的任何验证错误。

p-col-12类由 PrimeVue 的 PrimeFlex 包提供。它允许我们将div标记的宽度设置为全宽,这意味着它从页面上的 12 列中取出 12 列。通过p-inputgroup类,我们可以创建一个输入组。p-error类将文本颜色设置为红色,这样我们就可以以用户容易看到的方式显示表单验证消息。p-invalid类使输入的边缘变为红色。如果误差长度大于0,我们仅将其更改为红色,因为这意味着存在验证误差,并且当误差长度大于0时,我们仅显示较小的元素。

Field组件具有v-model指令,用于将输入值绑定到相应的反应属性。我们还有一个name属性,该属性也用作submit事件处理程序的value参数的属性名,该参数具有输入值。这些值始终有效,因为提交处理程序仅在所有表单值都有效时运行。

通过name字段,我们可以输入vacation包的名称。

接下来,我们需要添加一个文本区域,允许用户输入假期包的描述。为此,请编写以下代码:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="description" 
      name="description">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <Textarea
            placeholder="Description"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Description is invalid
        </small>
      </div>
    </Field>
    ...
  </Form>
</template>

这与name字段几乎相同;然而,在这个场景中,我们将InputText组件切换为Textarea组件。我们还更改了v-modelname值。Textarea组件来自 PrimeVue 软件包,该软件包渲染成具有自己风格的textarea元素。

接下来,我们添加图像 URL 字段,以便为假期包添加图像 URL。我们只是让用户输入图像 URL,以简化我们的项目。要将字段添加到Form组件,请编写以下代码:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="imageUrl" 
      name="imageUrl">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Image URL"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Image URL is invalid.
        </small>
      </div>
    </Field>
    ...
  </Form>
</template>

这只是另一个具有不同名称和v-model值的文本输入。最后,让我们使用以下代码在表单中添加一个submit按钮:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <div class="p-col-12">
      <Button label="Add" type="submit" />
    </div>
  </Form>
</template>

Button组件来自 PrimeVue 软件包,我们稍后将在全球注册该软件包,使其在任何地方都可用。

接下来,我们添加component选项对象。我们使用component选项 API 创建组件。首先,我们导入所有内容并使用 Yup 库创建表单验证模式。要添加代码,请在components/CatalogForm.vue中写入以下内容:

<script>
import * as yup from "yup";
import axios from "axios";
import { APIURL } from "@/constants";
const schema = yup.object().shape({
  name: yup.string().required(),
  description: yup.string().required(),
  imageUrl: yup.string().url().required(),
});
export default {
  name: "BookingForm",
  data() {
    return {
      name: "",
      description: "",
      imageUrl: "",
      schema,
    };
  },
  ...
};
</script>

在这里,我们使用yup.object方法创建模式对象,它允许我们验证具有某些属性的对象。validation模式与v-model绑定是分开的。传递到shape方法的对象属性必须与Field组件的name属性值匹配。

为了验证将name设置为name的字段的值,我们将name属性设置为yup.string().required(),以确保name字段是一个字符串并且有一个值。我们为description设置了相同的值。将imageUrl值设置为yup.string().url().required(),以确保输入的值是一个 URL 且已填写。

data方法返回模式,以便我们可以使用Form组件的validation-schema属性。

为了完成组件,我们添加了onSubmit方法,当Form组件发出submit事件时调用该方法:

<script>
...
export default {
  ...
  methods: {
    async onSubmit(value) {
      const { name, description, imageUrl } = value;
      await axios.post(`${APIURL}/catalog`, {
        name,
        description,
        imageUrl,
      });
      this.$emit("catalog-form-close");
    },
  },
};
</script>

这里,我们只需从具有有效表单字段值的value参数中获取property值。然后,我们向目录端点发出 POST 请求,并将 JSON 有效负载传递到第二个参数中。接下来,我们调用this.$emit方法发出catalog-form-close事件,向对话框组件发出信号,表示该表单将被封装在其中以关闭。

添加顶部栏和菜单栏

接下来,我们将在应用中添加一个top bar组件。为此,请在src/components文件夹中创建TopBar.vue。然后,将以下模板代码添加到文件中:

<template>
  <Menubar :model="items">
    <template #start>
      <b>Admin Frontend</b>
    </template>
  </Menubar>
</template>

Menubar组件由 PrimeVue 组件提供。我们可以使用它添加一个菜单栏,其中包含一些项目,我们可以单击这些项目导航到不同的页面。model属性被设置为items被动属性,这是一个我们将很快添加的菜单项对象数组。start插槽允许我们将项目添加到菜单栏的左侧。我们可以将一些粗体文本放入插槽,它将显示在左侧。

接下来,我们可以为组件添加一个component对象。要添加它,请编写以下代码:

<script>
export default {
  name: "TopBar",
  props: {
    title: String,
  },
  data() {
    return {
      items: [
        {
          label: "Manage Bookings",
          command: () => {
            this.$router.push("/bookings");
          },
        },
...
  methods: {
    logOut() {
      localStorage.clear();
      this.$router.push("/");
    },
  },
  beforeMount() {
    document.title= = this.title;
  },
};
</script>

这里,我们注册title道具,用于设置document.title值。document.title属性在顶部栏上设置标题。在data方法中,我们返回一个具有该项被动属性的对象。这被设置为具有labelcommand属性的对象。label属性显示在用户的菜单栏项中。该项目显示为链接。当我们点击项目时,command方法运行。

通过this.$router.push方法,我们可以导航到映射到给定 URL 的页面。logOut方法导航回映射到/路径的页面,这是登录页面,我们将在后面讨论。此外,我们清除本地存储,以便可以清除身份验证令牌。

beforeMount钩子中,我们将document.title属性设置为title属性的值。

添加共享代码以处理请求

接下来,让我们为 Axios 请求拦截器编写代码,让我们将身份验证令牌添加到除向/login端点发出请求之外的所有请求的x-token请求头中。为此,创建src/plugins文件夹并将axios.js添加到其中。然后,在此文件中,编写以下代码:

import axios from 'axios'
import { APIURL } from '@/constants'
axios.interceptors.request.use((config) => {
  if (config.url.includes(APIURL) 
   && !config.url.includes('login')) {
    config.headers['x-token'] = localStorage.getItem('token')
    return config
  }
  return config
}, (error) => {
  return Promise.reject(error)
})

在这里,我们通过检索带有config.url属性的 URL 来检查请求所针对的 URL。然后,如果我们向/login以外的端点发出任何请求,我们将设置x-token请求头:

config.headers['x-token'] = localStorage.getItem('token')

请注意,我们从本地存储获取令牌,并将其设置为值config.headers['x-token']config.headers属性是具有请求头的对象。第二个参数是请求错误处理程序。在这里,我们只需返回一个被拒绝的带有Promise.reject的承诺,以便处理错误。

接下来,我们将 Vue 路由路由添加到路由中。我们留在src/plugins文件夹中并创建一个vue-router.js文件。然后,我们将以下代码添加到该文件中:

import { createWebHashHistory, createRouter } from 'vue-
 router'
import Login from '../views/Login.vue'
import Bookings from '../views/Bookings.vue'
import Catalog from '../views/Catalog.vue'
const beforeEnter = (to, from, next) => {
  try {
    const token = localStorage.getItem('token')
    if (to.fullPath !== '/' && !token) {
      return next({ fullPath: '/' })
    }
    return next()
  } catch (error) {
    return next({ fullPath: '/' })
  }
}
const routes = [
  { path: '/', component: Login },
  { path: '/bookings', component: Bookings, beforeEnter },
  { path: '/catalog', component: Catalog, beforeEnter },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes,
})
export default router

我们添加了beforeEnter功能,如果我们转到除主页之外的任何前端页面,则检查令牌。我们可以使用to.fullPath属性检查用户尝试进入的路径。如果它不是'/'并且本地存储中没有令牌,那么我们用一个对象调用next,并且fullPath属性设置为'/'进入登录页面。否则,我们调用next时没有任何参数转到我们应该去的页面。如果我们有一个错误,那么我们也会转到登录页面,正如您可以从catch块中的代码中看到的那样。

接下来,我们有一个带有路由定义的routes数组。它具有path属性中的route路径,component是路径映射到的组件。beforeEnter属性被添加到最后两个 route 对象中,这样我们只有在登录后才能到达那里。

然后,为了创建router对象,我们使用createWebHashHistory函数返回的对象设置了history属性的对象调用createRouter;这样我们就可以在主机名和 URL 的其余部分之间保留哈希。我们将routes属性设置为routes数组以注册路由。这样,当我们转到路线时,可以看到正确的组件。

最后,我们将router对象导出为默认导出,以便稍后可以使用app.use将路由对象添加到我们的应用中。

接下来,我们在src文件夹中创建views文件夹。这意味着我们可以添加用户可以访问的页面。现在,让我们添加一个页面,允许管理员通过将Bookings.vue文件添加到src/views文件夹来管理任何预订。我们打开文件并将以下模板添加到组件中。这样我们就可以添加我们之前创建的TopBar组件:

<template>
  <TopBar title="Manage Bookings" />
  <div class="p-col-12">
    <h1>Manage Bookings</h1>
  </div>
  <div class="p-col-12">
    <Card v-for="b of bookings" :key="b.id">
      <template #title>{{ b.name }} </template>
      <template #content>
        <p>Address: {{ b.address }}</p>
        <p>Description: {{ b.description }}</p>
        <p>Start Date: {{ b.start_date }}</p>
        <p>End Date: {{ b.end_date }}</p>
      </template>
      <template #footer>
        <Button
          icon="pi pi-times"
          label="Cancel"
          class="p-button-secondary"
          @click="deleteBooking(b.id)"
        />
      </template>
    </Card>
  </div>
</template>

注意,我们使用h1元素为页面添加标题。然后,我们添加Card组件以向管理员显示预订情况。Card组件由 PrimeVue 提供,我们稍后会注册。我们使用v-for指令将 bookings 数组呈现为多个Card组件。key道具设置为唯一 ID,以便 Vue 3 能够正确区分每个道具。

我们用不同的内容填充titlecontentfooter插槽。footer插槽有一个Button组件,当我们点击按钮时,该组件将运行deleteBooking功能。icon道具允许我们设置按钮左侧的图标。label道具的button文本位于图标右侧。通过p-button-secondary类,我们可以设置按钮的颜色。

接下来,我们可以使用getBookingdeleteBooking方法添加component选项对象,分别通过后端 API 检索预订和删除预订。要添加它们,请编写以下代码:

<script>
import axios from "axios";
import { APIURL } from "@/constants";
import TopBar from "@/components/TopBar";
export default {
  name: "Bookings",
  components: {
    TopBar,
  },
  data() {
    return {
      bookings: [],
    };
  },
...
  beforeMount() {
    this.getBookings();
  },
};
</script>

我们还在components属性中注册TopBar组件。getBookings方法调用axios.get发出 GET 请求,并将this.bookingsreactive 属性的值设置为响应对象。

bookings存储在作为返回承诺的解析值返回的对象的data属性中。

同样地,我们在deleteBooking方法中调用axios.delete来发出DELETE请求删除项目。然后,我们调用this.getBookings再次获取数据。我们还在beforeMount钩子中调用this.getBookings,以便在页面加载时获取数据。

接下来,我们添加一个页面,允许管理员管理假期包项目。为此,让我们将Catalog.vue文件添加到src/views文件夹中。然后,在文件中写入以下内容:

<template>
  <TopBar title="Manage Vacation Packages" />
  <div class="p-col-12">
    <h1>Manage Vacation Packages</h1>
  </div>
  <div class="p-col-12">
    <Button label="Add Vacation Package" 
      @click="displayCatalog= = true" />
    <Dialog header="Add Vacation Package" v-
      model:visible="displayCatalog">
      <CatalogForm
        @catalog-form-close="
          displayCatalog= = false;
          getCatalog();
        "
      />
    </Dialog>
  </div>
  ...
</template>

在这里,我们添加TopBar组件来显示顶部栏;h1显示一个标题。接下来,我们添加一个按钮,通过将displayCatalog设置为true来显示对话框。然后,我们通过将带有可见修饰符的v-model指令设置为displayCatalog值来显示Dialog组件。使用此功能,我们可以控制Dialog组件何时显示。Dialog组件显示一个对话框,该组件由 PrimeVue 提供。

header道具设置对话框的标题文本。我们使用CatalogForm作为内容,并监听CatalogForm组件发出的catalog-form-close事件。当它发出时,我们将displayCatalog设置为false并调用getCatalog再次获取数据:

<template>
  ...
  <div class="p-col-12">
    <Card v-for="c of catalog" :key="c.id">
      <template #header>
        <img :alt="c.description" :src="c.image_url" />
      </template>
      <template #title> {{ c.name }} </template>
      <template #content>
        {{ c.description }}
      </template>
      <template #footer>
        <Button
          icon="pi pi-times"
          label="Delete"
          class="p-button-secondary"
          @click="deleteCatalogItem(c.id)"
        />
      </template>
    </Card>
  </div>
</template>

接下来,我们使用[T1]指令添加从 catalog reactive 属性呈现的[T0]组件,以呈现目录条目。剩下的代码与我们在Bookings.vue文件中的代码类似,但是现在渲染属性不同了,当我们单击Button时,它会调用不同的方法。

接下来,我们通过向src/views/Catalog.vue添加以下代码来添加组件对象:

<script>
import axios from "axios";
import { APIURL } from "@/constants";
import TopBar from "@/components/TopBar";
import CatalogForm from "@/components/CatalogForm";
...
  methods: {
    async getCatalog() {
      const{ { data } = await axios.get(`${APIURL}/catalog`);
      this.catalog = data;
    async deleteCatalogItem(id) {
      await axios.delete(`${APIURL}/catalog/${id}`);
      this.getCatalog();
    },
  },
  beforeMount() {
    this.getCatalog();
  },
};
</script>

在这里,代码与我们在src/views/Bookings.vue中的代码类似,只是在这里,我们向目录端点发出请求以获取和删除数据。

然后,我们在管理员前端应用中创建最后一个页面,即登录页面。为了添加登录页面,我们将Login.vue文件添加到src/views文件夹中。然后,在文件中,我们使用以下代码添加formusername字段:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <div class="p-col-12">
      <h1>Admin Log In</h1>
    </div>
    <Field v-slot="{ field, errors }" v-model="username" 
      name="username">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Username"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Username is invalid.
        </small>
      </div>
    </Field>
    ...
  </Form>
</template>

[T0]字段与我们之前添加的所有其他字段类似。接下来,我们使用以下代码添加password输入和按钮:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="password" 
      name="password">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Password"
            type="password"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Password is invalid
        </small>
      </div>
    </Field>
    <div class="p-col-12">
      <Button label="Log In" type="submit" />
    </div>
  </Form>
</template>

我们将type属性设置为password以使该字段成为密码输入。按钮的type属性被设置为submit,这样我们可以在点击它时触发submit事件,并且所有表单值保持有效。

接下来,我们添加Login.vue文件的组件对象部分,该部分使用onSubmit方法进行登录请求:

<script>
import * as yup from "yup";
import axios from "axios";
import { APIURL } from "@/constants";
const schema = yup.object().shape({
  username: yup.string().required(),
  password: yup.string().required(),
});
export default {
  name: "Login",
  data() {
    return {
      username: "",
      password: "",
      schema,
    };
  },
  methods: {
    async onSubmit(values) {
      const { username, password } = values;
      try {
        const {
          data: { token },
        } = await axios.post(`${APIURL}/users/login`, {
          username,
          password,
        });
        localStorage.setItem("token", token);
        this.$router.push("/bookings");
      } catch (error) {
        alert("Login failed");
      }
    },
  },
};
</script>

我们使用验证模式创建schema对象,这与我们之前使用的其他模式类似。然后,我们将其添加到我们在data方法中返回的对象中。onSubmit方法从value参数中获取usernamepassword属性,以便我们可以使用它向/users/login端点发出 POST 请求。

一旦我们这样做了,如果请求与localStorage.setItem方法一起成功,我们将从响应中获得一个令牌。接下来,我们调用this.$router.push方法重定向到/bookingsURL。如果有任何错误,我们将显示带有"Login failed"消息的警报。

接下来,我们将 Vue 路由提供的router-view组件添加到App.vue中。这样我们就可以显示我们在routes对象中创建的页面。要添加它,请编写管理员前端:共享代码,添加以处理请求“以下代码:

<template>
  <router-view></router-view>
</template>
<script>
export default {
  name: "App",
};
</script>
<style>
body {
  background-color: #ffffff;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI,
    Roboto, Helvetica,
    Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, 
      Segoe UI Symbol;
  font-weight: normal;
  color: #495057;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0px;
}
</style>

我们还有一个style标签来设置字体系列,并将正文的边距设置为0px,因此元素和页面边缘之间没有空白。

接下来,我们将constants.js添加到src文件夹中,然后将APIURL添加到其中:

export const APIURL = 'http://localhost:3000'

main.js中,我们注册了所有全局组件以及我们之前创建的路由对象。我们还导入 PrimeVue 提供的全局样式,因此一切看起来都很好:

import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';
import InputText from "primevue/inputtext";
import Button from "primevue/button";
import Card from 'primevue/card';
import Toolbar from 'primevue/toolbar';
import router from './plugins/vue-router'
import Textarea from 'primevue/textarea';
import Dialog from 'primevue/dialog';
import Menubar from 'primevue/menubar';
import { Form, Field } from "vee-validate";
import "primeflex/primeflex.css";
import 'primevue/resources/themes/bootstrap4-light-blue/theme.css'
import "primevue/resources/primevue.min.css";
import "primeicons/primeicons.css";
import './plugins/axios'
...
app.component("Form", Form);
app.component("Field", Field);
app.use(PrimeVue);
app.use(router)
app.mount('#app')

package.json中,我们通过将script.serve属性更改为以下内容来更改开发服务器运行的端口:

{
  ...
  "scripts": {
    "serve": "vue-cli-service serve --port 8082",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  ...
}

现在,当我们运行npm run serve时,我们得到以下屏幕截图:

Figure 6.1 – Admin Frontend

图 6.1–管理前端

现在,我们已经创建了管理前端应用,我们只需要添加用户前端。

创建用户前端

现在我们已经完成了管理前端的,我们将通过创建用户的前端来完成本章的项目。用户前端与管理员前端类似;但是,在这种情况下,使用它不需要身份验证。

我们将首先安装我们为管理前端安装的相同软件包。导航到前端文件夹并运行以下命令:

npm i axios primeflex primeicons primevue@^3.1.1 vee-validate@next vue-router@4 yup

接下来,创建src/components文件夹,如果它不存在。然后,在src/components中创建BookingForm.vue文件,以便用户可以添加他们的预订。

添加formname字段,允许用户输入姓名:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <Field v-slot="{ field, errors }" v-model="name" 
      name="name">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Name"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">Name 
          is invalid </small>
      </div>
    </Field>
    ...
  </Form>
</template>

这与我们前面添加的其他文本输入字段非常相似。然后,使用以下代码添加address字段:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="address" 
      name="address">
      <div class="p-col-12">
        <div class="p-inputgroup">
          <InputText
            placeholder="Address"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0"
          >Address is invalid</small
        >
      </div>
    </Field>
    ...
  </Form>
</template>

现在,让我们添加 PrimeVue 提供的Calendar组件,这是我们在本项目中没有使用过的。Calendar组件允许用户选择日期。我们可以添加Start Date字段,允许用户选择假期的开始日期:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="startDate"         name="startDate">
      <div class="p-col-12">
        <label>Start Date</label>
        <div class="p-inputgroup">
          <Calendar
            inline
            placeholder="Start Date"
            :class="{ 'p-invalid': errors.length > 0 }"
            :minDate="new Date()"
            v-bind="field"
            v-model="startDate"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0">
          Start date is invalid
        </small>
      </div>
    </Field>
    ...
  </Form>
</template>

在这里,我们有minDate道具,它设置了用户可以选择的最早日期。inline道具将使日期选择器显示在表单上而不是弹出窗口中。同样,我们可以使用以下代码添加End Date字段:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <Field v-slot="{ field, errors }" v-model="endDate"         name="endDate">
      <div class="p-col-12">
        <label>End Date</label>
        <div class="p-inputgroup">
          <Calendar
            inline
            placeholder="End Date"
            :class="{ 'p-invalid': errors.length > 0 }"
            v-bind="field"
            v-model="endDate"
            :minDate="new Date(+startDate + 24 * 3600 * 1000)"
          />
        </div>
        <small class="p-error" v-if="errors.length > 0"
          >End date is invalid</small
        >
      </div>
    </Field>
    ...
  </Form>
</template>

这里,我们将minDate道具设置为startDate之后的一天。24 * 3600 * 1000毫秒相当于一天。最后,我们添加了submit按钮,就像我们在其他表单中所做的那样:

<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    ...
    <div class="p-col-12">
      <Button label="Book" type="submit" />
    </div>
    ...
  </Form>
</template>

接下来,我们通过编写以下内容创建schema

<script>
import { Form, Field } from "vee-validate";
import * as yup from "yup";
import axios from "axios";
import { APIURL } from "@/constants";
const schema = yup.object().shape({
  name: yup.string().required(),
  address: yup.string().required(),
  startDate: yup.date().required().min(new Date()),
  endDate: yup
    .date()
    .required()
    .when(
      "startDate",
      (startDate, schema) => startDate && 
        schema.min(startDate)
    ),
});
...
</script>

为了验证endDate,我们使用要检查的field名称调用when方法。然后,我们调用schema.min以确保的endDate晚于startDate

接下来,我们添加component对象来注册selectedCatalogId道具,并添加onSubmit方法。我们编写以下代码:

<script>
...
export default {
  name: "BookingForm",
  components: {
    Form,
    Field,
  },
  props: {
...
  methods: {
    async onSubmit(values) {
      const { name, address, startDate, endDate } = values;
      await axios.post(`${APIURL}/bookings`, {
        name,
        address,
        startDate,
        endDate,
        catalogItemId: this.selectedCatalogId,
      });
      this.$emit("booking-form-close");
    },
  },
};
</script>

onSubmit方法从values参数获取表单字段值,并向预订端点发出 POST 请求以添加预订。我们使用selectedCatalogId添加预订。然后,我们发出booking-form-close事件,向父级发出事件,通知窗体关闭。

接下来,我们通过将vue-router.js添加到src/plugins文件夹,将 Vue 路由添加到我们的应用中:

import { createWebHashHistory, createRouter } from 'vue-router'
import Catalog from '../views/Catalog.vue'
const routes = [
  { path:'/', component: Catalog },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes,
})
export default router

这与我们在管理前端中所做的非常相似。

接下来,我们创建一个页面,通过添加src/views/Catalog.vue文件,然后添加以下模板代码,向用户显示所有假期套餐:

<template>
  <Card v-for="c of catalog" :key="c.id">
    <template #header>
      <img :alt="c.description" :src="c.image_url" />
    </template>
    <template #title> {{ c.name }} </template>
    <template #content>
      {{ c.description }}
    </template>
    <template #footer>
      <Button
        icon="pi pi-check"
        label="Book"
        class="p-button-secondary"
        @click="book(c.id)"
      />
...
          :selectedCatalogId="selectedCatalogId"
        />
      </Dialog>
    </template>
  </Card>
</template>

这里,我们只是从目录数组中呈现一个表单。我们有一个Dialog组件,里面有BookingForm组件。我们监听它发出的booking-form-close事件,通过将displayBookingForm设置为false并调用displayMessage显示警报来关闭Dialog组件。我们传入selectedCatalogId作为同名道具的值。

模板代码的其余部分几乎与我们之前的相同,除了显示的属性名称和在标题槽中添加的图像。

接下来,我们通过编写以下代码将component选项对象添加到同一个文件中:

<script>
import axios from "axios";
import { APIURL } from "@/constants";
import BookingForm from "../components/BookingForm.vue";
export default {
  name: "Catalog",
  components: {
    BookingForm,
  },
  data() {
    return {
      selectedCatalogId: undefined,
      displayBookingForm: false,
...
    displayMessage() {
      alert("Booking successful");
    },
  },
  beforeMount() {
    this.getCatalog();
  },
};
</script>

我们在components属性中注册BookingForm组件。getCatalog函数从 API 获取度假包目录项。booking功能将displayBookingForm设置为true打开Dialog组件,并且selectedCatalogId也设置在那里。beforeMount钩子调用getCatalog来检索目录数据。

添加路由视图和入口点代码

App.vue中,我们为添加router-view编写以下代码,并设置与我们在管理前端相同的样式:

<template>
  <router-view></router-view>
</template>
<script>
export default {
  name: "App",
};
</script>
<style>
body {
  background-color: #ffffff;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI,
    Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, 
     Segoe UI Emoji, Segoe UI Symbol;
  font-weight: normal;
  color: #495057;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0px;
}
</style>

然后,我们在src文件夹中创建constants.js,并添加以下行以添加APIURL

export const APIURL = 'http://localhost:3000'

然后,在main.js中,我们用以下代码替换文件内容,以全局注册组件和路由。我们还导入 PrimeVue 提供的样式,以使我们的应用看起来不错:

import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';
import InputText from "primevue/inputtext";
import Button from "primevue/button";
import Card from 'primevue/card';
import Toolbar from 'primevue/toolbar';
import Calendar from 'primevue/calendar';
import Dialog from 'primevue/dialog';
import router from './plugins/vue-router'
import "primeflex/primeflex.css";
import 'primevue/resources/themes/bootstrap4-light-blue/theme.css'
import "primevue/resources/primevue.min.css";
import "primeicons/primeicons.css";
const app = createApp(App)
app.component("InputText", InputText);
app.component("Button", Button);
app.component("Card", Card);
app.component("Toolbar", Toolbar);
app.component("Calendar", Calendar);
app.component("Dialog", Dialog);
app.use(PrimeVue);
app.use(router)
app.mount('#app')

package.json中,我们通过将script.serve属性更改为以下内容来更改开发服务器运行的端口:

{
  ...
  "scripts": {
    "serve": "vue-cli-service serve --port 8082",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  ...
}

现在,当我们运行npm run serve时,我们得到以下屏幕截图:

Figure 6.2 – The user frontend

图 6.2–用户前端

随着用户前端的创建,假期预订系统现在已经完成。

总结

在本章中,我们学习了如何有效地使用 PrimeVue 构建假期预订应用。使用 PrimeVue,我们可以轻松创建美观的 Vue 3 web 应用。PrimeVue 附带了许多有用的组件,我们可以添加这些组件来创建 web 应用,例如输入、文本区域、表格、对话框、日期选择器等等。它还内置了样式,因此我们不必自己添加任何样式。此外,我们还可以添加 PrimeVue 提供的 PrimeFlex 包;使用 flexbox,我们可以轻松更改元素和组件的间距和位置。

Vee Validate 和 Yup 允许我们将表单验证添加到 Vue 3 应用中。这很容易与 PrimeVue 提供的输入组件集成。这两个库使许多表单验证工作变得简单,因为我们不必自己编写所有表单验证代码。

为了制作一个简单的后端,我们使用 Express 创建了一个简单的 API 来与前端交互。我们还使用sqlite3包在 API 中使用 SQLite 数据库操作数据。Express 附带了许多附加组件,我们可以使用这些附加组件添加许多功能,例如跨域通信。我们还可以通过jsonwebtoken库轻松地将 JSON Web 令牌身份验证添加到我们的 Express 应用中。

在下一章中,我们将学习如何使用 GraphQL 和 Vue 3 构建店面。