TinyBlog 前端开发 Vue+Vuetify

1,578 阅读4分钟

0 项目概述

TinyBlog 是一个博客系统,正如其名 Tiny,它是一个极其微小小小小小小👌的博客系统,定位是一个用于学习各种技术的 Demo 应用😀。

本文主要是使用 Vue + Vuetify 实现前端页面。后台接口部分可以参考之前的文章

TinyBlog 后端开发 SpringBoot+JdbcTemplate 版本

先看下最终实现的效果。

查看所有 Blog 列表。(博客数据是在掘金随机复制的,感兴趣查请搜索查看原文,这里仅作为测试数据演示)

新建 Blog。

编辑 Blog。

1 项目搭建

1.1 Vue CLI 创建工程

打开 Terminal,通过命令 vue create tinyblog-vue-vuetify 创建工程。

1.2 安装 Vuetify

然后我们切换到工程目录 cd tinyblog-vue-vuetify/,通过 vue add vuetify 命令,添加 Vuetify。

1.3 安装 Vue Router

通过命令 npm install vue-router 添加 Vue Router。

1.4 安装 Axios

通过 npm install axios 命令,添加 Axios。

最后,通过执行 code . 使用 VS Code 打开工程。我们可以通过 npm run serve 命令,启动应用。

浏览器访问应用的网址,正常应该可以看到下面的页面。

1.5 技术栈

  • Vue, Vuetify

1.6 开发工具

  • Vue CLI, VS Code

2 编写代码

2.1 封装后台接口服务

2.1.1 封装 axios

新建文件 src/http.js,封装 axios,定义后台接口地址。

2.1.2 封装后台接口

将我们之前后台提供的接口,封装成对应的方法,方便后续使用。(后台接口参考之前文章:TinyBlog 后端开发 SpringBoot+JdbcTemplate 版本

请求URL接口说明
POST/api/blog创建一篇博客
GET/api/blog获取所有博客
GET/api/blog/:id获取 id 为 :id 的博客
PUT/api/blog/:id更新 id 为 :id 的博客
DELETE/api/blog/:id删除 id 为 :id 的博客

创建 src/services/BlogService.js 文件,封装后台接口。

2.2 规划页面及路由

2.2.1 规划页面

一个博客系统最基础的功能至少包括:查看博客列表,新建博客,编辑博客。所以我们至少定义 3 个路由地址:

  • /blogs:查看所有博客。
  • /edit/${id}:编辑 id 为 ${id} 的博客。
  • /add:新建一篇博客。

2.2.2 定义路由

新建 src/router.js 文件,定义好我们的路由信息。

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);


export default new Router({
  mode: "history",
  routes: [
    {
      path: "/",
      alias: "/blogs",
      name: "blogs",
      component: () => import("./components/BlogList")
    },
    {
      path: "/edit/:id",
      name: "edit",
      component: () => import("./components/EditBlog")
    },
    {
      path: "/add",
      name: "add",
      component: () => import("./components/AddBlog")
    }
  ]
});

2.2.3 引入路由

创建好 router.js 后,还需要在 main.js 中引入路由才能生效。(如果你正在运行,记得把 router.js 中的路由暂时注释掉,不然会报错找不到 Vue 组件)

可以看到 3 个路由对应了 3 个 Vue 的组件,3 个组件目前还没有编写,不过没关系,我们先定义好,后面慢慢实现。

2.3 编写 Vue 组件

根据前面的规划,我们需要 3 个 Vue 组件。但是不要着急编写,因为我们首先需要创建网页的导航栏。

2.3.1 编写导航栏

我们创建导航栏组件 src/components/Navbar.vue

<template>
  <v-app-bar app color="white" class="d-flex justify-center">
    <div class="d-flex" style="width: 960px">
      <!-- LOGO -->
      <v-toolbar-title class="mr-2 headline grey--text text--darken-1">
        <span class="font-weight-light">Tiny</span>
        <span>Blog</span>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <!-- 搜索框 -->
      <v-text-field
        v-model="searchParam"
        class="mr-4"
        style="max-width: 450px"
        dense
        flat
        hide-no-data
        hide-details
        label="搜索博客"
        append-icon="mdi-magnify"
        @click:append="searchByParam"
        solo-inverted
      ></v-text-field>

      <!-- 导航链接 -->
      <v-btn to="/blogs" text>
        <span class="subtitle-1 grey--text text--darken-2">首页</span>
      </v-btn>
      <v-btn to="/add" text>
        <span class="subtitle-1 grey--text text--darken-2">写文章</span>
      </v-btn>
      <v-btn @click="goToGitHub" text>
        <v-icon class="grey--text text--darken-2"> mdi-github </v-icon>
      </v-btn>
    </div>
  </v-app-bar>
</template>

<script>
export default {
  name: "Navbar",

  data() {
    return {
      searchParam: "",
    };
  },

  methods: {
    searchByParam() {
    },
    goToGitHub() {
        window.location.href ="https://gitee.com/noissues/tinyblog-vue-vuetify.git";
    },
  },
};
</script>

<style>
.navitem {
  width: 960px;
}
</style>

接着,我们要到 App.vue 中添加我们封装的导航栏。(目前它里面应该还是 HelloWorld 的内容)

修改之后,npm run serve 启动后,便可以看到我们修改的内容生效了。

2.3.2 编写 BlogList 组件

新增 src/components/BlogList.vue 组件。

<template>
  <v-card class="mx-auto mt-3" style="max-width: 960px" tile>
    <v-card-title>博客列表</v-card-title>

    <!-- 列表展示 -->
    <v-col v-for="(item, i) in blogs" :key="i" cols="12" class="py-1">
      <v-card outlined @click="editBlog(item.id)">
        <div class="d-flex flex-no-wrap justify-space-between">
          <div>
            <!-- 标题 -->
            <v-card-title
              class="subtitle-1 font-weight-black"
              v-text="item.title"
            ></v-card-title>

            <!-- 描述 -->
            <v-card-subtitle
              class="grey--text"
              v-text="item.description"
            ></v-card-subtitle>
          </div>
        </div>
      </v-card>
    </v-col>
  </v-card>
</template>

<script>
import BlogService from "../services/BlogService";
export default {
  props: ["searchParam"],
  name: "BlogList",
  data() {
    return {
      blogs: [],
    };
  },
  methods: {
    getAllBlogs() {
      BlogService.getAllBlogs()
        .then((response) => {
          this.blogs = response.data.map(this.getDisplayBlog);
          console.log(response.data);
        })
        .catch((e) => {
          console.log(e);
        });
    },

    editBlog(id) {
      this.$router.push({ path: `/edit/${id}` });
    },

    // 界面展示转换
    getDisplayBlog(blog) {
      return {
        id: blog.id,
        title:
          blog.title.length > 30
            ? blog.title.substr(0, 30) + "..."
            : blog.title,
        description:
          blog.description.length > 30
            ? blog.description.substr(0, 30) + "..."
            : blog.description,
      };
    },
  },
  mounted() {
    this.getAllBlogs();
  },
};
</script>

<style>
.list {
  max-width: 960px;
}
</style>

2.3.3 编写 BlogEditor 组件

注意到新增和编辑 Blog 的页面可以复用,我们将其单独拆出成一个组件。

新增 src/components/BlogList.vue 组件。

<template>
  <v-form ref="form" lazy-validation>
    <v-card class="mx-auto" tile>
      <v-card-title v-if="currentBlog">编辑博客</v-card-title>
      <v-card-title v-else>新建博客</v-card-title>

      <v-card-text>
        <v-text-field
          outlined
          v-model="blog.title"
          :rules="[(v) => !!v || '请填写标题']"
          label="标题"
          required
        ></v-text-field>
        <v-text-field
          outlined
          v-model="blog.description"
          :rules="[(v) => !!v || '请填写简介']"
          label="简介"
          required
        ></v-text-field>

        <v-textarea
          outlined
          label="内容"
          v-model="blog.content"
          :rules="[(v) => !!v || '请填写内容']"
          required
        ></v-textarea>
      </v-card-text>

      <v-divider class="mt-2"></v-divider>

      <v-card-actions>
        <div v-if="currentBlog">
          <v-btn @click="updateBlog" color="primary" small class="mr-2">
            更新
          </v-btn>

          <v-btn @click="deleteBlog" color="error" small class="mr-2">
            删除
          </v-btn>
          <v-btn small @click="back">取消</v-btn>
        </div>
        <div v-else>
          <v-btn @click="addBlog" color="primary" small class="mr-3">
            提交
          </v-btn>
        </div>
      </v-card-actions>
    </v-card>
  </v-form>
</template>


<script>
import BlogService from "../services/BlogService";

export default {
  name: "BlogEditor",

  props: ["currentBlog"],

  data() {
    return {
      blog: {
        id: null,
        title: "",
        description: "",
        content: "",
      },

      message: "",
    };
  },
  methods: {
    back() {
      this.$router.back();
    },

    updateBlog() {
      BlogService.updateBlog(this.blog.id, this.blog)
        .then((response) => {
          console.log(response.data);
          this.$emit("updated", "🎉博客更新成功!");
        })
        .catch((e) => {
          console.log(e);
        });
    },

    addBlog() {
      var data = {
        title: this.blog.title,
        description: this.blog.description,
        content: this.blog.content,
      };

      BlogService.addBlog(data)
        .then((response) => {
          console.log(response.data);

          let data = {};
          data.id = response.data.id;
          data.message = "🎉博客创建成功!";
          this.$emit("added", data);
        })
        .catch((e) => {
          console.log(e);
        });
    },

    deleteBlog() {
      BlogService.deleteBlog(this.blog.id)
        .then((response) => {
          console.log(response.data);
          this.$router.push({ name: "blogs" });
        })
        .catch((e) => {
          console.log(e);
        });
    },
  },
  mounted() {
    if (this.currentBlog) {
      this.blog.id = this.currentBlog.id;
      this.blog.title = this.currentBlog.title;
      this.blog.description = this.currentBlog.description;
      this.blog.content = this.currentBlog.content;
    }
  },
};
</script>

2.3.4 编写 AddBlog 组件

新增 src/components/AddBlog.vue 组件。使用我们刚刚创建的 BlogEditor 组件。

<template>
  <div class="submit-form mt-3 mx-auto">
    <div v-if="!submitted">
      <BlogEditor @added="addBlogHandler" />
    </div>
    <div v-else>
      <v-card class="mx-auto">
        <v-card-title> 🎉博客创建成功! </v-card-title>

        <v-card-subtitle>
          请选择查看已提交的博客还是继续创建博客
        </v-card-subtitle>

        <v-card-actions>
          <v-btn color="primary" @click="editBlog">查看已提交博客</v-btn>
          <v-btn color="success" @click="addBlog">继续创建</v-btn>
        </v-card-actions>
      </v-card>
    </div>
  </div>
</template>

<script>
import BlogEditor from "./BlogEditor";

export default {
  name: "AddBlog",

  components: {
    BlogEditor,
  },

  data() {
    return {
      submitted: false,
    };
  },
  methods: {
    addBlogHandler(data) {
      this.submitted = true;
      this.id = data.id; // 新创建的博客 id
    },

    addBlog() {
      this.submitted = false;
    },

    editBlog() {
      this.$router.push({ path: `/edit/${this.id}` });
    },
  },
};
</script>

<style>
.submit-form {
  max-width: 960px;
  margin: auto;
}
</style>

2.3.5 编写 EditBlog 组件

新增 src/components/EditBlog.vue 组件。

<template>
  <div v-if="currentBlog" class="edit-form mt-3 mx-auto">
    <BlogEditor v-bind:currentBlog="currentBlog" @updated="updateBlogHandler" />

    <v-snackbar v-model="snackbar" :timeout="timeout" top>
      {{ message }}

      <template v-slot:action="{ attrs }">
        <v-btn color="white" text v-bind="attrs" @click="snackbar = false">
          关闭
        </v-btn>
      </template>
    </v-snackbar>
  </div>
</template>

<script>
import BlogService from "../services/BlogService";
import BlogEditor from "./BlogEditor";

export default {
  name: "EditBlog",

  components: {
    BlogEditor,
  },
  data() {
    return {
      snackbar: false,
      timeout: 2000,
      currentBlog: null,
      message: "",
    };
  },
  methods: {
    updateBlogHandler(message) {
      this.snackbar = true;
      this.message = message;
    },
    getBlog(id) {
      BlogService.getBlogById(id)
        .then((response) => {
          this.currentBlog = response.data;
          console.log(response.data);
        })
        .catch((e) => {
          console.log(e);
        });
    },
  },
  mounted() {
    this.getBlog(this.$route.params.id);
  },
};
</script>

<style>
.edit-form {
  max-width: 960px;
  margin: auto;
}
</style>

3 运行测试

首先启动后端服务。

注意在测试前端代码的时候,发现了后端接口的几个 Bug,已经修复,请更新最新代码测试。

然后 npm run serve 启动前台应用。

新建 Blog。

编辑 Blog。

4 项目总结

本文主要介绍了 TinyBlog 的前端开发过程,主要使用了 Vue + Vuetify 实现的。代码已经上传到我的 Gitee 仓库

因为平时工作主要还是后台开发,TinyBlog 的前端部分也是现学现卖,所以如果大家有什么想法或者建议,欢迎评论区留言。