项目演示地址:https://hujiyi.github.io/acme-world-web/


网站前台的规划与路由

要显示论坛的话题列表,以及显示话题的详细内容、添加及显示用户的评论,需要添加相应的视图

添加前台视图文件

src/views/home 文件夹添加一个名为 pages 的文件夹,然后在该文件夹中添加用于显示话题列表的视图文件TopicList.vue 以及 显示话题详情的视图文件 Detail.vue

src/views/ 文件夹的最终组成结构如以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
|-- account
| |-- pages
| | |-- Login.vue
| | |-- PasswordReset.vue
| | `-- SignUp.vue
| `-- Index.vue
|-- dashboard
| |-- forum
| | |-- CommentManager.vue
| | `-- TopicManager.vue
| |-- layout
| | |-- Aside.vue
| | |-- Header.vue
| | `-- MenuTree.vue
| |-- pages
| | `-- MainIndex.vue
| `-- Index.vue
`-- home
|-- pages
| |-- Detail.vue
| `-- ToplicList.vue
`-- Index.vue

编辑路由

打开文件: src/router/index.js, 在原来的 name: 'Home' 的路由项添加前面两个视图对应的路由项,最终完成的代码如以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import Vue from 'vue';
import VueRouter from 'vue-router';


Vue.use(VueRouter);

const routes = [
{
path: '/',
// name: 'Home',
component: () => import('../views/home/Index.vue'),
children: [
{
path: '',
name: 'ToplicList', // 论坛话题列表
component: () => import('../views/home/pages/ToplicList.vue'),
},
{
path: '/detail/:id', // 动态路由, id 用于接收参数
name: 'Detail', // 话题详情
component: () => import('../views/home/pages/Detail.vue'),
},

]
},
{
path: '/account',
redirect: '/login',
component: () => import('../views/account/Index.vue'),
children: [
{
path: '/login',
name: 'Login',
component: () => import('../views/account/pages/Login.vue'),
},
{
path: '/signup',
name: 'SignUp',
component: () => import('../views/account/pages/SignUp.vue'),
},
{
path: '/password_reset',
name: 'PasswordReset',
component: () => import('../views/account/pages/PasswordReset.vue'),
},
]
},
{
path: '/dashboard',
component: () => import('../views/dashboard/Index.vue'),
// 添加路由元信息 meta: { requiresAuth: true, } 用于标识进入该路由必须登录
meta: { requiresAuth: true },
children: [
{
path: '',
alias: 'index',
name: 'Dashboard',
component: () => import('@/views/dashboard/pages/MainIndex.vue'),
},
{
path: 'topic_manager',
name: 'TopicManager',
component: () => import('@/views/dashboard/forum/TopicManager.vue'),
},
{
path: 'comment_manager',
name: 'CommentManager',
component: () => import('@/views/dashboard/forum/CommentManager.vue'),
},

],
}
];

const router = new VueRouter({
routes
});



// 使用路由守卫,具体内容见官方文档:https://router.vuejs.org/zh/guide/advanced/meta.html
// 特别注意事项:确保 router.beforeEach 中只有一个 next() 会被执行
// 如果有多个 next() 被执行,将会出现重复路由的异常
router.beforeEach((to, from, next) => {
let login_router = ['/account', '/login'];
// 如果 跳转的目标 path 是登录页(看上面的路由设置 )
if (to.path in login_router) {
// 保存跳转到登录页之前的地址, 登录成功后跳转回该页
// 回跳功能在 登录页 "src/views/account/pages/Login.vue" 实现
localStorage.setItem("preRoute", router.currentRoute.fullPath);
}

// 检查跳转的目标路由是否需要登录(包含 meta: {requiresAuth: true, },)
if (to.matched.some(record => record.meta.requiresAuth)) {
// 检查用户是否登录
if (!localStorage.getItem('token')) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next();
}
} else {
next();
}
});


export default router;

网站首页的修改

打开文件: src/views/home/Index.vue, 将原来 <el-main> 标签中的文字删掉,并替换为 <router-view /> 用于显示匹配的子路由项对应的视图, 最终完成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<template>
<el-container>
<el-header class="horiz-container">
<div class="logo">
<!-- 添加返回首页的链接 -->
<router-link to="/">Acme World APP</router-link>
</div>
<div class="spacer"></div>
<router-link to="/dashboard" class="m-2">
<el-button type="text">后台管理</el-button>
</router-link>
<router-link to="/login" class="m-2">
<el-button type="text">登录</el-button>
</router-link>
</el-header>
<el-main>
<!-- 响应并显示匹配子路由的组件 -->
<router-view />
</el-main>
</el-container>
</template>

<script>
export default {
name: 'Home',
}
</script>

<style>
.el-container {
/* 让最外层组件占满整个可见的高度 */
height: 100vh;
}
.el-header {
background-color: #b3c0d1;
color: #333;
line-height: 60px;
/* 交叉轴对齐方式:居中 */
align-items: center;
}
.el-main {
background-color: #e6e6e6;
}

.logo a{
text-decoration: none;
font-size: 24px;
font-weight: bold;
padding-left: 20px;
}
</style>

显示论坛话题列表

src/views/home/pages/ToplicList.vue 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
<template>
<div>
<el-row type="flex" justify="center" align="middle">
<el-col :lg="18" :md="20" :sm="24">
<div class="shadow">
<el-row type="flex" align="middle" class="topic-title">
<el-col style="text-align:center">标题</el-col>
<el-col style="width:100px;" class="hidden-md-and-down">查看</el-col>
<el-col style="width:100px;" class="hidden-md-and-down">评论</el-col>
<el-col
style="width:290px; text-align:center;"
class="hidden-sm-and-down"
>最后评论</el-col>
</el-row>
<div v-for="(item,index) in topics" :key="index" id="topic" class="topic-item">
<el-row type="flex" justify="center" align="middle" class="spacer">
<el-col :class="{ top: item.level > 0 }">
<span v-if="item.level > 0">[置顶]</span>
<span v-else>[话题]</span>
<router-link
:to="{ name: 'Detail', params: { id: item.id } }"
>{{ item.title }}</router-link>
</el-col>
<el-col style="width:100px;" class="hidden-md-and-down">{{ item.views }}</el-col>
<el-col style="width:100px;" class="hidden-md-and-down">{{ item.reply }}</el-col>
<el-col style="width:290px;" class="hidden-sm-and-down">
<span>{{ item.lastEditor.username }}</span>
<br />
<span
style="color:#a0a0a0;font-size:14px;"
>{{ item.lastRepliedAt | datetimeFormat }}</span>
</el-col>
</el-row>
</div>
<div class="horiz-container pagination">
<div class="spacer"></div>
<el-pagination
background
@size-change="getTopics"
@current-change="getTopics"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
:page-sizes="[5, 10, 20, 30]"
:page-size.sync="pageSize"
:current-page.sync="currentPage"
></el-pagination>
</div>
</div>
</el-col>
</el-row>
</div>
</template>

<script>
import TopicService from '../../../api/service/topic_service';
import moment from 'moment';
export default {
name: 'TopicList',
data: () => ({
topics: [], // 从服务器取回的一页数据
pageSize: 10, // 每页大小
currentPage: 1, // 当前页
totalCount: 0, // 总记录数量
}),
mounted() {
this.getTopics(); // 获取论坛话题
},
methods: {
async getTopics() {
this.loading = true;
this.topics = []; // 清空数据
let skip = (this.currentPage - 1) * this.pageSize;
// 查询当前页的数据,
let response = await TopicService.fetchAll(this.pageSize, skip, ["createdBy", "lastEditor"], ['-level', '-lastRepliedAt']);
if (response.status_code === 'ok') {
console.log(response);
this.totalCount = response.totalCount; // 总记录数
// 返回当前页所有数据的结果
this.topics = response.reslut.map(item => { return this.toJson(item) });
}
this.loading = false;
},

// 获取用户中各个字段的值, 可以根据需要添加
getUser(user) {
// console.log(user);
return {
id: user.id,
username: user.get("username"),
email: user.get("email"),
}
},

// 将从服务器端取到的数据不能直接使用,转为对象格式的数据
toJson(item) {
return {
id: item.id, // leancloud Class 自带属性,对应 leanclud Class 中的 "ObjectId", 可以用".id"直接引用
title: item.get('title'),
content: item.get('content'),
views: item.get('views'),
reply: item.get('reply'),
level: item.get('level'),
lastRepliedAt: item.get('lastRepliedAt'),
createdAt: item.createdAt, // leancloud Class 自带属性,创建时间,不能修改值
updatedAt: item.updatedAt, // leancloud Class 自带属性,最后修改时间,只能leancloud 自动修改

/**
* createdBy、lastEditor 指向另一个表的字段, 默认只能得到该字段在关联表中的id 值
* 要得到完整值, 查询时字段名要包含在 include 中,
* 取值时,要先判断是否为空,如果为空,取值会抛出异常,
*/
createdBy: item.get("createdBy")
? this.getUser(item.get("createdBy"))
: "",
lastEditor: item.get("lastEditor")
? this.getUser(item.get("lastEditor"))
: "",
}
},

/**
* 自定义用于局部区域显示 loading 的函数
* @param {*} targetNode 目标区域的选择器,建设使用 id 选择器
* @returns
*/
showLoading(targetNode, message) {
this.loading = Loading.service({
// 锁定屏幕的滚动
lock: true,
// 显示的文本
text: message,
// document.querySelector 用于以获取到对应 DOM 节点
target: document.querySelector(targetNode),
});
},

// 停止显示 loading
endLoading() {
this.loading.close();
}

},

// 过滤器
filters: {
// 使用 moment.js 定义日期时间过滤器函数
datetimeFormat(val) {
let now = new Date();
// 计算两个时间相差的天数
let days = moment(now).diff(moment(val), 'days');
if (days <= 3) {
// 小于三天的返回格式:1小时前
return moment(val).fromNow();
}
// 三天以上的返回日期时间
return moment(val).format("lll");
},
},

}
</script>

<style>
/* 标题行的样式 */
.topic-title {
background-color: #f1f4f8;
padding-top: 20px;
padding-bottom: 20px;
font-size: 14px;
font-weight: bold;
border-top: 2px solid #1985db;
border-bottom: 1px solid #a0a0a0;
}

/* 置顶话题的样式 */
.top {
font-weight: bold;
}

/* 置顶话题超链接的样式 */
.top a {
color: #2897c5 !important ;
}

.topic-item {
display: flex;
align-items: center;
height: 50px;
padding: 5px;
background: #ffffff;
border-bottom: 1px solid #a0a0a0;
}

/* 隔行显示不同的背景颜色 */
.topic-item:nth-child(odd) {
background: #f1f4f8;
}

.topic-item a {
text-decoration: none;
color: #333333;
padding-left: 8px;
}

/* 鼠标指向的行背景色 */
.topic-item:hover {
background: #b0d0fd;
}

.pagination {
padding-top: 16px;
padding-bottom: 16px;
}

/* 阴影 */
.shadow {
box-shadow: 0 2px 12px 2px rgba(0, 0, 0, 0.1);
border-radius: 8px;
border: 1px solid #ebeef5;
background-color: #fff;
color: #303133;
transition: 0.3s;
}
</style>

后继内容:
使用 Element UI 和 Leancloud 的 Vue.js 项目开发 X

===END===