VUE.JS和NODE.JS构建一个简易的前后端分离静态博客系统(二)
后台管理页面,需要配合NODE.JS搭建的EXPRESS服务器使用。
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {
Button,
Input,
Form,
Link,
Divider,
Upload,
Dialog,
Card,
Popover,
MessageBox,
Message,
Loading,
Breadcrumb,
BreadcrumbItem,
Select,
Option,
Table,
TableColumn,
Avatar,
Pagination,
Checkbox,
CheckboxGroup,
} from 'element-ui';
// 局部引入必须这么做才能正常使用
Vue.prototype.$message = Message
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$loading = Loading.service
Vue.use(Button)
Vue.use(Input)
Vue.use(Form)
Vue.use(Link)
Vue.use(Divider)
Vue.use(Upload)
Vue.use(Dialog)
Vue.use(Card)
Vue.use(Popover)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Select)
Vue.use(Option)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Avatar)
Vue.use(Pagination)
Vue.use(Checkbox)
Vue.use(CheckboxGroup)
Vue.config.productionTip = false
Vue.prototype.$url_posts = "http://localhost:8081/posts"
Vue.prototype.$url_categories = "http://localhost:8081/categories"
new Vue({
render: h => h(App),
router,
}).$mount('#app')
这段代码主要执行了以下操作:
-
导入所需的库和组件:
- 导入 Vue 核心库。
- 导入根组件 App.vue。
- 导入路由配置文件。
- 从 Element UI 库中导入了一系列 UI 组件,如 Button、Input、Form 等。
-
配置 Vue 全局属性:
- 将 Message、MessageBox 和 Loading 服务挂载到 Vue 原型对象上,这样可以在整个应用中直接使用它们,例如
this.$message
、this.$confirm
和this.$loading
。
- 将 Message、MessageBox 和 Loading 服务挂载到 Vue 原型对象上,这样可以在整个应用中直接使用它们,例如
-
注册 UI 组件:
- 使用 Vue.use() 方法注册导入的 Element UI 组件,使其可以在 Vue 应用中全局使用。
-
关闭 Vue 生产环境的提示信息:
- 设置
Vue.config.productionTip = false
,以关闭生产环境下的提示信息。
- 设置
-
定义全局 API 地址:
- 在 Vue 原型对象上定义了
$url_posts
和$url_categories
两个属性,分别用于存储 post 和 category 相关 API 的 URL。这样在整个应用中都可以方便地访问这两个 URL。
- 在 Vue 原型对象上定义了
-
创建 Vue 实例并挂载:
- 创建一个新的 Vue 实例,指定渲染函数以将根组件 App.vue 渲染到页面上,并传入路由配置。最后将 Vue 实例挂载到页面的 '#app' 元素上。
简言之,这段代码的主要作用是:
- 导入所需的库和组件。
- 配置 Vue 全局属性和 UI 组件。
- 定义全局 API 地址。
- 创建 Vue 实例并将其挂载到页面上。
App.vue
<template>
<div id="app">
<header>
<div class="header-left">
<ul>
<li
:class="{ active: activeRoute === 'Management' }"
class="nav-btn clickable"
@click="jump2('Management')"
>管理
</li>
<li
:class="{ active: activeRoute === 'Edit' }"
class="nav-btn clickable"
@click="jump2('Edit')"
>新随笔
</li>
<li
:class="{ active: activeRoute === 'Edit2' }"
class="nav-btn auto"
>编辑
</li>
<li
:class="{ active: activeRoute === 'Category' }"
class="nav-btn clickable"
@click="jump2('Category')"
>分类
</li>
</ul>
</div>
<div class="header-right">
<ul>
<li>
<div id="user" slot="reference">
<el-avatar
icon="el-icon-user-solid"
style="display: inline-block"
></el-avatar>
</div>
</li>
</ul>
</div>
</header>
<main>
<router-view></router-view>
</main>
</div>
</template>
<script>
export default {
name: "App",
computed: {
activeRoute() {
return this.$route.name
}
},
methods: {
jump2(router_name) {
if (this.activeRoute !== router_name) {
this.$router.push({ name: router_name });
}
},
switch2management() {
// 弃用,待修改
if (this.activeRoute !== 'Management') {
if (this.activeRoute === 'Edit2') {
this.$confirm('跳转页面将丢失未保存的修改内容,确认跳转?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.$router.push({ name: "Management" });
})
} else {
this.$router.push({ name: "Management" });
}
}
},
},
};
</script>
<style>
/* 全局样式在这里设置,其它一律scoped */
body {
margin: 0;
}
#app {
/* font-family: Avenir, Helvetica, Arial, sans-serif; */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* text-align: center; */
/* color: #2c3e50; */
/* margin-top: 60px; */
}
</style>
<style scoped>
#app {
background: white;
/* 设定最大宽度,并且居中 */
max-width: 1380px;
min-width: 450px;
margin: 0 auto;
padding: 0 15px;
/* 用下面的grid配置在打开有element表格的页面有一个神奇的无限往右延长的BUG */
/* display: grid;
grid-template: 65px 1fr / 25px 1fr 25px; */
display: flex;
flex-direction: column;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
}
header {
/* grid-column: 2 / 3; */
margin: 10px 0;
display: flex;
justify-content: space-between;
align-items: center;
/* 下面这个线画到在main上画 */
/* border-bottom: 1px solid #dcdfe6; */
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
display: inline-block;
box-sizing: border-box;
/* li之间互相应该有一定间隙 */
margin: 5px;
user-select: none;
}
.nav-btn {
/* 按钮静默状态样式 */
/* 上下10px 左右22px */
padding: 10px 22px;
/* 字体影响非常非常大................ */
font-size: 18px;
font-family: 'Courier New', Courier, monospace;
/* 特效之静默状态 */
opacity: 0.5;
border: 1.3px solid #292b2d56;
border-radius: 5%;
}
.nav-btn.clickable {
cursor: pointer;
/* 保持与element一致 */
color: #409EFF;
transition: 0.3s ease-in-out;
}
.nav-btn.clickable:hover, .nav-btn.clickable.active {
/* 鼠标悬浮时亮起,0字体改变,1是变成不透明,2是边框颜色改变,4悬浮 */
color: #409EFF;
opacity: 1;
border: 1.3px solid #409EFF;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);
transition: 0.3s ease-in-out;
}
.nav-btn.auto {
color: #67C23A;
}
.nav-btn.auto.active {
color: #67C23A;
opacity: 1;
border: 1.3px solid #67C23A;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);
transition: 0.3s ease-in-out;
}
div#user {
cursor: pointer;
}
main {
border-top: 1px solid #dcdfe6;
}
</style>
这段代码是一个Vue.js的单文件组件,包括了一个template、一个script和两个style块。这个组件表示了一个页面的布局和导航,其中包括一个头部和一个主体。头部包括了一个左侧的导航栏和一个右侧的用户头像。左侧导航栏包括了四个导航按钮,分别是“管理”、“新随笔”、“编辑”和“分类”。主体部分是一个router-view组件,用于显示路由对应的内容。
在script部分,该组件的名字是“App”,定义了两个方法:jump2和switch2management,以及一个computed属性activeRoute。activeRoute返回当前路由的名称,jump2方法根据传入的路由名称进行跳转,而switch2management方法目前被弃用了。
在style部分,第一个style块是全局样式的设置,第二个style块是局部样式,它们都是通过scoped属性限定了作用域。该组件的整体样式是一个白色背景,最大宽度为1380像素,居中显示。头部采用了flex布局,左侧导航栏使用了inline-block布局,右侧用户头像使用了slot插槽。导航按钮有静默状态和悬浮状态,点击后会有对应的路由跳转。主体部分有一个上边框,用于分隔头部和主体。
vue-my-cnblog\src\router\index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Management from '@/page/Management.vue'
import Edit from '@/page/Edit.vue'
import Edit2 from '@/page/Edit2.vue'
import PostInfo from '@/page/PostInfo.vue'
import Category from '@/page/Category.vue'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{
path: '/',
name: 'Management',
component: Management,
},
{
path: '/edit',
name: 'Edit',
component: Edit,
// 这个新随笔页面,用户可以主动进入
},
{
path: '/edit2',
name: 'Edit2',
component: Edit2,
// 真 · 编辑页面,用户不能主动进入
},
{
path: '/postinfo',
name: 'PostInfo',
component: PostInfo,
},
{
path: '/category',
name: 'Category',
component: Category,
},
],
})
export default router
这段代码使用了 Vue.js 和 Vue Router 插件,定义了一个路由器实例 router,并指定了多个路由对象作为它的配置项,每个路由对象包含了路径、名称和组件。其中:
'/'
表示默认路径,对应的组件是 Management.vue。'/edit'
路径表示进入编辑页面,对应的组件是 Edit.vue。'/edit2'
路径表示真正的编辑页面,对应的组件是 Edit2.vue。'/postinfo'
路径表示文章信息页面,对应的组件是 PostInfo.vue。'/category'
路径表示文章分类页面,对应的组件是 Category.vue。
路由器实例通过 Vue.use(VueRouter) 进行初始化,并使用 new VueRouter(options) 来创建,其中 options 包含了多个路由配置。这个路由器实例最终会被导出并在其他模块中使用。
需要注意的是,Edit2 路径对应的组件是不可以直接通过 URL 访问的,只能在代码中被调用。
Management.vue
<template>
<div id="management">
<main>
<table>
<thead>
<tr>
<th>标题</th>
<th>发布时间</th>
<th>发布状态</th>
<th>操作1</th>
<th>操作2</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id">
<td><div class="post_title" @click="postInfo(post)">{{ post.title }}</div></td>
<td>{{ post.pubDate }}</td>
<td>{{ post.state }}</td>
<td><div class="btn" @click="editPost(post)">编辑</div></td>
<td><div class="btn" @click="removePost(post)">删除</div></td>
</tr>
</tbody>
</table>
</main>
<footer>
<el-pagination
background
layout="prev, pager, next"
:current-page.sync="currentPage"
:page-size="5"
:total="total">
</el-pagination>
</footer>
</div>
</template>
<script>
import axios from 'axios'
// import qs from 'qs'
const PAGE_SIZE = 5
export default {
name: 'Management',
data() {
return {
users: [],
title: '',
content: '',
currentPage: 1,
allPosts: [],
}
},
computed: {
total() {
return this.allPosts.length;
},
start() {
return (this.currentPage - 1) * PAGE_SIZE
},
end() {
return this.currentPage * PAGE_SIZE
},
posts() {
// 变化,例如说,删除一条,那么allposts变化导致,total和posts变化
return this.allPosts.slice(this.start, this.end)
}
},
created() {
console.log('Management created')
this.reloadPosts()
},
methods: {
reloadPosts() {
axios.get(this.$url_posts)
.then(resp => {
// console.log(resp.data)
// 接受到数据后进行格式化,每当数据更新应该调用该方法
if (resp.data) {
this.allPosts = resp.data.map(post => {
return {
id: post.id,
title: post.title,
createTime: new Date(post.createTime).toLocaleString(),
category: [],
state: post.state,
pubDate: new Date(post.pubDate).toLocaleString(),
}
})
}
})
.catch(err => {
console.log(err)
})
},
postInfo(post) {
const POST_ID = post.id
if (this.$route.name !== 'PostInfo') {
this.$router.push({
name: "PostInfo",
query: {
post_id: POST_ID,
},
});
}
},
removePost(post) {
const POST_ID = post.id
console.log('removePost: ' + POST_ID)
// 确认一下
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 把屏幕锁了防止乱点
const LOADING = this.$loading({
lock: true,
text: '正在删除',
// spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
// 发送删除文件的请求
axios.delete(this.$url_posts + `/${POST_ID}`)
.then(() => {
setTimeout(() => {
// 刷新
this.reloadPosts()
// 至少锁1秒才解除
LOADING.close();
this.$message({
type: 'success',
message: '删除成功!',
});
}, 1000);
})
.catch(err => {
console.log(err)
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
editPost(post) {
const POST_ID = post.id
// 跳转到对应的文件详情页
if (this.$route.name !== 'Edit2') {
this.$router.push({
name: "Edit2",
query: {
post_id: POST_ID,
},
});
}
},
editContent(id) {
axios.get(`http://localhost:8081/users/${id}`)
.then(resp => {
this.content = resp.data
})
.catch(err => {
console.log(err)
})
},
},
}
</script>
<style scoped>
table {
width: 100%;
table-layout: fixed;
/* fixed之后默认平均分配位置,不会按内容分配 */
border-collapse: collapse;
/* collapse只有设置border1时会把那个空余的地方变成线,其它时候好像没啥用 */
}
thead, tbody {
/* font-size: 14px; */
font-family:'Courier New', Courier, monospace;
text-align: center;
}
th, td {
padding: 10px;
/* 看上去宽松点 */
border: 1px solid black;
}
th {
letter-spacing: 2px;
/* 和td区分一下 */
}
thead th:nth-child(1) {
width: 50%;
}
div.post_title {
height: 50px;
overflow-y: hidden;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
}
div.post_title:hover {
overflow-y: visible;
color: #409EFF;
text-decoration: underline;
}
div.btn {
cursor: pointer;
border: 1px solid black;
/* 调整按钮大小 */
padding: 5px;
border-radius: 10px;
}
div.btn:hover {
color: #409EFF;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
footer {
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
这段代码是一个Vue.js组件,包括一个HTML模板和一个JavaScript脚本以及一个局部的CSS样式。
该组件创建了一个管理界面,包括一个带有标题的表格和一个分页组件。表格包含了标题、发布时间、发布状态以及两个操作按钮。分页组件用于分页显示所有的文章。
数据部分包括一个名为“allPosts”的数组,其中存储了所有文章的信息。在组件创建后,它使用Axios库从服务器中获取文章列表,并将其格式化为所需的格式。在点击“编辑”或“删除”按钮时,将使用Axios库向服务器发送请求来执行相应的操作。
HTML模板包含一个带有唯一ID“management”的div元素,其中包含一个主区域和一个页脚区域。主区域包含一个带有标题的表格,表格头包含标题、发布时间、发布状态和两个操作按钮。表格主体使用Vue的v-for指令将文章列表中的每一篇文章都渲染为一个表格行。页脚区域包含一个分页组件,可以用于分页显示所有文章。
JavaScript脚本定义了一个Vue.js组件,名称为“Management”。它包含了一些数据属性、计算属性和方法。其中数据属性包括一个名为“users”的空数组、一个名为“title”的空字符串、一个名为“content”的空字符串、一个名为“currentPage”的数字1和一个名为“allPosts”的空数组。计算属性包括一个名为“total”的方法,该方法返回文章列表的总数;一个名为“start”的方法,该方法计算当前页的起始索引;一个名为“end”的方法,该方法计算当前页的结束索引;一个名为“posts”的方法,该方法使用数组切片从“allPosts”中提取当前页的文章列表。组件生命周期钩子函数中的“created”钩子调用了“reloadPosts”方法,该方法使用Axios库从服务器中获取文章列表并将其格式化。
方法包括“reloadPosts”方法,该方法使用Axios库从服务器中获取文章列表,并将其格式化为所需的格式。在点击“编辑”或“删除”按钮时,将使用Axios库向服务器发送请求来执行相应的操作。其他方法包括“postInfo”方法,该方法在单击文章标题时用于打开文章详细信息页面;“removePost”方法,该方法在单击“删除”按钮时用于删除文章;“editPost”方法,该方法在单击“编辑”按钮时用于打开文章编辑页面;以及“editContent”方法,该方法使用Axios库从服务器中获取文章内容。
CSS样式定义了一些用于布局和样式化表格、标题、按钮和分页组件的样式规则。其中一些规则使用了Vue.js的“scoped”属性,以确保它们只应用于该组件的元素。