init
This commit is contained in:
commit
560d986995
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
12584
package-lock.json
generated
Normal file
12584
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "doubao_community_frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"buefy": "^0.9.4",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"darkreader": "^4.9.27",
|
||||||
|
"date-fns": "^2.17.0",
|
||||||
|
"dayjs": "^1.10.4",
|
||||||
|
"element-ui": "^2.15.0",
|
||||||
|
"js-cookie": "^2.2.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"vditor": "^3.8.1",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.2.0",
|
||||||
|
"vuex": "^3.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
31
src/App.vue
Normal file
31
src/App.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<Header></Header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container context">
|
||||||
|
<router-view :key="this.$route.fullPath"></router-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Header from "@/components/Layout/Header";
|
||||||
|
import Footer from "@/components/Layout/Footer";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
components: {Header, Footer},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
32
src/api/auth/auth.js
Normal file
32
src/api/auth/auth.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
export function userRegister(userDTO) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/register',
|
||||||
|
method: 'post',
|
||||||
|
data: userDTO
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前台用户登录
|
||||||
|
export function login(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/login',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 登录后获取前台用户信息
|
||||||
|
export function getUserInfo() {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/info',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 前台用户注销
|
||||||
|
export function logout() {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/logout'
|
||||||
|
})
|
||||||
|
}
|
8
src/api/billboard.js
Normal file
8
src/api/billboard.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getBillboard() {
|
||||||
|
return request({
|
||||||
|
url: '/billboard/show',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
20
src/api/comment.js
Normal file
20
src/api/comment.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function fetchCommentsByTopicId(topic_Id) {
|
||||||
|
return request({
|
||||||
|
url: '/comment/get_comments',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
topicid: topic_Id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushComment(data) {
|
||||||
|
return request({
|
||||||
|
url: '/comment/add_comment',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
25
src/api/follow.js
Normal file
25
src/api/follow.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 关注
|
||||||
|
export function follow(id) {
|
||||||
|
return request(({
|
||||||
|
url: `/relationship/subscribe/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关注
|
||||||
|
export function unFollow(id) {
|
||||||
|
return request(({
|
||||||
|
url: `/relationship/unsubscribe/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否关注
|
||||||
|
export function hasFollow(topicUserId) {
|
||||||
|
return request(({
|
||||||
|
url: `/relationship/validate/${topicUserId}`,
|
||||||
|
method: 'get'
|
||||||
|
}))
|
||||||
|
}
|
56
src/api/post.js
Normal file
56
src/api/post.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
export function getList(pageNo, size, tab) {
|
||||||
|
return request(({
|
||||||
|
url: '/post/list',
|
||||||
|
method: 'get',
|
||||||
|
params: { pageNo: pageNo, size: size, tab: tab }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布
|
||||||
|
export function post(topic) {
|
||||||
|
return request({
|
||||||
|
url: '/post/create',
|
||||||
|
method: 'post',
|
||||||
|
data: topic
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浏览
|
||||||
|
export function getTopic(id) {
|
||||||
|
return request({
|
||||||
|
url: `/post`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
id: id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取详情页推荐
|
||||||
|
export function getRecommendTopics(id) {
|
||||||
|
return request({
|
||||||
|
url: '/post/recommend',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
topicId: id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update(topic) {
|
||||||
|
return request({
|
||||||
|
url: '/post/update',
|
||||||
|
method: 'post',
|
||||||
|
data: topic
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTopic(id) {
|
||||||
|
return request({
|
||||||
|
url: `/post/delete/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
9
src/api/promote.js
Normal file
9
src/api/promote.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取推广
|
||||||
|
export function getList() {
|
||||||
|
return request(({
|
||||||
|
url: '/promotion/all',
|
||||||
|
method: 'get'
|
||||||
|
}))
|
||||||
|
}
|
14
src/api/search.js
Normal file
14
src/api/search.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 关键词检索
|
||||||
|
export function searchByKeyword(query) {
|
||||||
|
return request({
|
||||||
|
url: `/search`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
keyword: query.keyword,
|
||||||
|
pageNum: query.pageNum,
|
||||||
|
pageSize: query.pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
12
src/api/tag.js
Normal file
12
src/api/tag.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getTopicsByTag(paramMap) {
|
||||||
|
return request({
|
||||||
|
url: '/tag/' + paramMap.name,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
page: paramMap.page,
|
||||||
|
size: paramMap.size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
8
src/api/tip.js
Normal file
8
src/api/tip.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getTodayTip() {
|
||||||
|
return request({
|
||||||
|
url: '/tip/today',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
30
src/api/user.js
Normal file
30
src/api/user.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 用户主页
|
||||||
|
export function getInfoByName(username, page, size) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/' + username,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
pageNo: page,
|
||||||
|
size: size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 用户主页
|
||||||
|
export function getInfo() {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/info',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 更新
|
||||||
|
export function update(user) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/update',
|
||||||
|
method: 'post',
|
||||||
|
data: user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
150
src/assets/app.css
Normal file
150
src/assets/app.css
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
color: black;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC,
|
||||||
|
Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei,
|
||||||
|
sans-serif, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji,
|
||||||
|
Segoe UI Symbol, Android Emoji, EmojiSymbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*背景图*/
|
||||||
|
/*body {*/
|
||||||
|
/* background-image: url('https://api.mz-moe.cn/img.php');*/
|
||||||
|
/* background-repeat: round;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
width: 760px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.container {
|
||||||
|
width: 980px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
width: 1080px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*滚动条*/
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
/**/
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgb(239, 239, 239);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #bfbfbf;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #179a16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 89;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1032px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 0px rgba(26, 26, 26, 0.1);
|
||||||
|
height: 53px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1d1d1d;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #f60;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-1 {
|
||||||
|
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1),
|
||||||
|
0 0 0 1px rgba(10, 10, 10, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dropdown {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*统一卡片样式*/
|
||||||
|
.el-card {
|
||||||
|
/*border-radius: 3px !important;*/
|
||||||
|
margin-bottom: 16px;
|
||||||
|
/*border: none;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-card:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: rgba(67, 135, 244, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框 */
|
||||||
|
.search-bar input {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*按钮居中*/
|
||||||
|
.button-center {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
display: block;
|
||||||
|
display: -webkit-box;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-ellipsis-1 {
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-ellipsis-2 {
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-ellipsis-3 {
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
BIN
src/assets/image/doubao.png
Normal file
BIN
src/assets/image/doubao.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
2337
src/assets/plugins/font-awesome-4.7.0/css/font-awesome.css
vendored
Normal file
2337
src/assets/plugins/font-awesome-4.7.0/css/font-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
src/assets/plugins/font-awesome-4.7.0/css/font-awesome.min.css
vendored
Normal file
4
src/assets/plugins/font-awesome-4.7.0/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/assets/plugins/font-awesome-4.7.0/fonts/FontAwesome.otf
Normal file
BIN
src/assets/plugins/font-awesome-4.7.0/fonts/FontAwesome.otf
Normal file
Binary file not shown.
Binary file not shown.
2671
src/assets/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
Normal file
2671
src/assets/plugins/font-awesome-4.7.0/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
src/components/Backtop/BackTop.vue
Normal file
28
src/components/Backtop/BackTop.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<el-backtop :bottom="60" :right="60">
|
||||||
|
<div title="回到顶部"
|
||||||
|
style="{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f2f5f6;
|
||||||
|
box-shadow: 0 1px 0 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
color: #167df0;
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<i class="fa fa-arrow-up"></i>
|
||||||
|
</div>
|
||||||
|
</el-backtop>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "BackTop"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
57
src/components/Comment/Comments.vue
Normal file
57
src/components/Comment/Comments.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<section class="box comments">
|
||||||
|
<hr />
|
||||||
|
<h3 class="title is-5">Comments</h3>
|
||||||
|
<lv-comments-form :slug="slug" v-if="token" @loadComments="fetchComments"/>
|
||||||
|
|
||||||
|
<lv-comments-item
|
||||||
|
v-for="comment in comments"
|
||||||
|
:key="`comment-${comment.id}`"
|
||||||
|
:comment="comment"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { fetchCommentsByTopicId } from '@/api/comment'
|
||||||
|
import LvCommentsForm from './CommentsForm'
|
||||||
|
import LvCommentsItem from './CommentsItem'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LvComments',
|
||||||
|
components: {
|
||||||
|
LvCommentsForm,
|
||||||
|
LvCommentsItem
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
comments: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
slug: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'token'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.fetchComments(this.slug)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 初始化
|
||||||
|
async fetchComments(topic_id) {
|
||||||
|
console.log(topic_id)
|
||||||
|
fetchCommentsByTopicId(topic_id).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
this.comments = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
70
src/components/Comment/CommentsForm.vue
Normal file
70
src/components/Comment/CommentsForm.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<article class="media">
|
||||||
|
<div class="media-content">
|
||||||
|
<form @submit.prevent="onSubmit">
|
||||||
|
<b-field>
|
||||||
|
<b-input
|
||||||
|
v-model.lazy="commentText"
|
||||||
|
type="textarea"
|
||||||
|
maxlength="400"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
:disabled="isLoading"
|
||||||
|
></b-input>
|
||||||
|
</b-field>
|
||||||
|
<nav class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<b-button
|
||||||
|
type="is-primary"
|
||||||
|
native-type="submit"
|
||||||
|
class="level-item"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Comment
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { pushComment } from '@/api/comment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LvCommentsForm',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
commentText: '',
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
slug: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onSubmit() {
|
||||||
|
this.isLoading = true
|
||||||
|
try {
|
||||||
|
let postData = {}
|
||||||
|
console.log(this.commentText)
|
||||||
|
postData['content'] = this.commentText
|
||||||
|
postData['topic_id'] = this.slug
|
||||||
|
await pushComment(postData)
|
||||||
|
this.$emit('loadComments', this.slug)
|
||||||
|
this.$message.success('留言成功')
|
||||||
|
} catch (e) {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: `Cannot comment this story. ${e}`,
|
||||||
|
type: 'is-danger'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
32
src/components/Comment/CommentsItem.vue
Normal file
32
src/components/Comment/CommentsItem.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left image is-48x48">
|
||||||
|
<img :src="`https://cn.gravatar.com/avatar/${comment.userId}?s=164&d=monsterid`">
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<strong>{{ comment.username }}</strong>
|
||||||
|
<small class="ml-2">{{ comment.createTime | date }}</small>
|
||||||
|
<br />
|
||||||
|
{{ comment.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LvCommentsItem',
|
||||||
|
props: {
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
57
src/components/Layout/Footer.vue
Normal file
57
src/components/Layout/Footer.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="footer has-text-grey-light has-background-grey-darker">
|
||||||
|
<div class="container">
|
||||||
|
<div class="">
|
||||||
|
<span>简洁、实用、美观</span>
|
||||||
|
|
||||||
|
<span style="float: right">
|
||||||
|
<a href="/?lang=zh_CN">中文</a> |
|
||||||
|
<a href="/?lang=en_US">English</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>{{ title }} ALL RIGHTS RESERVED</span>
|
||||||
|
<div style="float: right">
|
||||||
|
<template>
|
||||||
|
<b-taglist attached>
|
||||||
|
<b-tag size="is-normal" type="is-dark">Design</b-tag>
|
||||||
|
<b-tag size="is-normal" type="is-info">{{ author }}</b-tag>
|
||||||
|
</b-taglist>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<back-top></back-top>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BackTop from "@/components/Backtop/BackTop";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Footer",
|
||||||
|
components: {
|
||||||
|
BackTop
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: "© " + new Date().getFullYear() + ' yovinchen',
|
||||||
|
author: 'yovinchen',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 120px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
183
src/components/Layout/Header.vue
Normal file
183
src/components/Layout/Header.vue
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header has-background-white has-text-black">
|
||||||
|
<b-navbar
|
||||||
|
:fixed-top="true"
|
||||||
|
class="container is-white"
|
||||||
|
>
|
||||||
|
<template slot="brand">
|
||||||
|
<b-navbar-item
|
||||||
|
:to="{ path: '/' }"
|
||||||
|
tag="router-link">
|
||||||
|
<img :src="doubaoImg" alt="logo">
|
||||||
|
</b-navbar-item>
|
||||||
|
|
||||||
|
<b-navbar-item
|
||||||
|
:to="{ path: '/' }"
|
||||||
|
class="is-hidden-desktop"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
主页
|
||||||
|
</b-navbar-item>
|
||||||
|
</template>
|
||||||
|
<template slot="start">
|
||||||
|
<b-navbar-item
|
||||||
|
:to="{ path: '/' }"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
🌐 主页
|
||||||
|
</b-navbar-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="end">
|
||||||
|
<b-navbar-item tag="div">
|
||||||
|
<b-field position="is-centered">
|
||||||
|
<b-input
|
||||||
|
v-model="searchKey"
|
||||||
|
class="s_input"
|
||||||
|
clearable
|
||||||
|
placeholder="搜索帖子、标签和用户"
|
||||||
|
rounded
|
||||||
|
width="80%"
|
||||||
|
@keyup.enter.native="search()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="control">
|
||||||
|
<b-button
|
||||||
|
class="is-info"
|
||||||
|
@click="search()"
|
||||||
|
>检索
|
||||||
|
</b-button>
|
||||||
|
</p>
|
||||||
|
</b-field>
|
||||||
|
</b-navbar-item>
|
||||||
|
|
||||||
|
<b-navbar-item tag="div">
|
||||||
|
<b-switch
|
||||||
|
v-model="darkMode"
|
||||||
|
passive-type="is-warning"
|
||||||
|
type="is-dark"
|
||||||
|
>
|
||||||
|
{{ darkMode ? "夜" : "日" }}
|
||||||
|
</b-switch>
|
||||||
|
</b-navbar-item>
|
||||||
|
|
||||||
|
<b-navbar-item
|
||||||
|
v-if="token == null || token === ''"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button
|
||||||
|
:to="{ path: '/register' }"
|
||||||
|
class="is-light"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
:to="{ path: '/login' }"
|
||||||
|
class="is-light"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-navbar-item>
|
||||||
|
|
||||||
|
<b-navbar-dropdown
|
||||||
|
v-else
|
||||||
|
:label="user.alias"
|
||||||
|
>
|
||||||
|
<b-navbar-item
|
||||||
|
:to="{ path: `/member/${user.username}/home` }"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
🧘 个人中心
|
||||||
|
</b-navbar-item>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<b-navbar-item
|
||||||
|
:to="{ path: `/member/${user.username}/setting` }"
|
||||||
|
tag="router-link"
|
||||||
|
>
|
||||||
|
⚙ 设置中心
|
||||||
|
</b-navbar-item>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<b-navbar-item
|
||||||
|
tag="a"
|
||||||
|
@click="logout"
|
||||||
|
> 👋 退出登录
|
||||||
|
</b-navbar-item>
|
||||||
|
</b-navbar-dropdown>
|
||||||
|
</template>
|
||||||
|
</b-navbar>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {disable as disableDarkMode, enable as enableDarkMode} from 'darkreader'
|
||||||
|
import {getDarkMode, setDarkMode} from '@/utils/auth'
|
||||||
|
import {mapGetters} from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Header',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logoUrl: require('@/assets/logo.png'),
|
||||||
|
doubaoImg: require('@/assets/image/doubao.png'),
|
||||||
|
searchKey: '',
|
||||||
|
darkMode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['token', 'user'])
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听Theme模式
|
||||||
|
darkMode(val) {
|
||||||
|
if (val) {
|
||||||
|
enableDarkMode({})
|
||||||
|
} else {
|
||||||
|
disableDarkMode()
|
||||||
|
}
|
||||||
|
setDarkMode(this.darkMode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 获取cookie中的夜间还是白天模式
|
||||||
|
this.darkMode = getDarkMode()
|
||||||
|
if (this.darkMode) {
|
||||||
|
enableDarkMode({})
|
||||||
|
} else {
|
||||||
|
disableDarkMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async logout() {
|
||||||
|
this.$store.dispatch('user/logout').then(() => {
|
||||||
|
this.$message.info('退出登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({path: this.redirect || '/'})
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
search() {
|
||||||
|
console.log(this.token)
|
||||||
|
if (this.searchKey.trim() === null || this.searchKey.trim() === '') {
|
||||||
|
this.$message.info({
|
||||||
|
showClose: true,
|
||||||
|
message: '请输入关键字搜索!',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.$router.push({path: '/search?key=' + this.searchKey})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
input {
|
||||||
|
width: 80%;
|
||||||
|
height: 86%;
|
||||||
|
}
|
||||||
|
</style>
|
103
src/components/Pagination/index.vue
Normal file
103
src/components/Pagination/index.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{ hidden: hidden }" class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
:background="background"
|
||||||
|
:current-page.sync="currentPage"
|
||||||
|
:page-size.sync="pageSize"
|
||||||
|
:layout="layout"
|
||||||
|
:page-sizes="pageSizes"
|
||||||
|
:total="total"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {scrollTo} from "@/utils/scroll-to";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Pagination",
|
||||||
|
props: {
|
||||||
|
total: {
|
||||||
|
required: true,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
pageSizes: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [5, 10, 20, 30, 50];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: "total, sizes, prev, pager, next, jumper",
|
||||||
|
// default: 'sizes, prev, pager, next, jumper'
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentPage: {
|
||||||
|
get() {
|
||||||
|
return this.page;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit("update:page", val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
get() {
|
||||||
|
return this.limit;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit("update:limit", val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.$emit("pagination", { page: this.currentPage, limit: val });
|
||||||
|
if (this.autoScroll) {
|
||||||
|
scrollTo(0, 800);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.$emit("pagination", { page: val, limit: this.pageSize });
|
||||||
|
if (this.autoScroll) {
|
||||||
|
scrollTo(0, 800);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination-container {
|
||||||
|
/* background: #fff; */
|
||||||
|
padding: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
42
src/main.js
Normal file
42
src/main.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
// Buefy
|
||||||
|
import Buefy from 'buefy'
|
||||||
|
import 'buefy/dist/buefy.css'
|
||||||
|
// ElementUI
|
||||||
|
import ElementUI from 'element-ui';
|
||||||
|
import 'element-ui/lib/theme-chalk/index.css';
|
||||||
|
import '@/assets/app.css'
|
||||||
|
import './assets/plugins/font-awesome-4.7.0/css/font-awesome.min.css'
|
||||||
|
import format from 'date-fns/format'
|
||||||
|
import '@/permission'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
// 国际化
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
const dayjs = require('dayjs');
|
||||||
|
|
||||||
|
// 相对时间插件
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn') // use locale globally
|
||||||
|
dayjs().locale('zh-cn').format() // use locale in a specific instance
|
||||||
|
|
||||||
|
Vue.prototype.dayjs = dayjs;//可以全局使用dayjs
|
||||||
|
|
||||||
|
Vue.filter('date', (date) => {
|
||||||
|
return format(new Date(date), 'yyyy-MM-dd')
|
||||||
|
})
|
||||||
|
|
||||||
|
Vue.use(Buefy)
|
||||||
|
Vue.use(ElementUI);
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
41
src/permission.js
Normal file
41
src/permission.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
import getPageTitle from '@/utils/get-page-title'
|
||||||
|
|
||||||
|
import NProgress from 'nprogress' // progress bar
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
import {getToken} from "@/utils/auth"; // progress bar style
|
||||||
|
|
||||||
|
NProgress.configure({showSpinner: false}) // NProgress Configuration
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
// start progress bar
|
||||||
|
NProgress.start()
|
||||||
|
// set page title
|
||||||
|
document.title = getPageTitle(to.meta.title)
|
||||||
|
// determine whether the user has logged in
|
||||||
|
const hasToken = getToken();
|
||||||
|
|
||||||
|
if (hasToken) {
|
||||||
|
if (to.path === '/login') {
|
||||||
|
// 登录,跳转首页
|
||||||
|
next({path: '/'})
|
||||||
|
NProgress.done()
|
||||||
|
} else {
|
||||||
|
// 获取用户信息
|
||||||
|
await store.dispatch('user/getInfo')
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
} else if (!to.meta.requireAuth)
|
||||||
|
{
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
// finish progress bar
|
||||||
|
NProgress.done()
|
||||||
|
})
|
98
src/router/index.js
Normal file
98
src/router/index.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import Vue from "vue";
|
||||||
|
import VueRouter from "vue-router";
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Home",
|
||||||
|
component: () => import("@/views/Home"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
name: "register",
|
||||||
|
component: () => import("@/views/auth/Register"),
|
||||||
|
meta: { title: "注册" },
|
||||||
|
},
|
||||||
|
// 登录
|
||||||
|
{
|
||||||
|
name: "login",
|
||||||
|
path: "/login",
|
||||||
|
component: () => import("@/views/auth/Login"),
|
||||||
|
meta: { title: "登录" },
|
||||||
|
},
|
||||||
|
// 发布
|
||||||
|
{
|
||||||
|
name: "post-create",
|
||||||
|
path: "/post/create",
|
||||||
|
component: () => import("@/views/post/Create"),
|
||||||
|
meta: { title: "信息发布", requireAuth: true },
|
||||||
|
},
|
||||||
|
// 编辑
|
||||||
|
{
|
||||||
|
name: 'topic-edit',
|
||||||
|
path: '/topic/edit/:id',
|
||||||
|
component: () => import('@/views/post/Edit'),
|
||||||
|
meta: {
|
||||||
|
title: '编辑',
|
||||||
|
requireAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 详情
|
||||||
|
{
|
||||||
|
name: "post-detail",
|
||||||
|
path: "/post/:id",
|
||||||
|
component: () => import("@/views/post/Detail"),
|
||||||
|
meta: { title: "详情" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tag',
|
||||||
|
path: '/tag/:name',
|
||||||
|
component: () => import('@/views/tag/Tag'),
|
||||||
|
meta: { title: '主题列表' }
|
||||||
|
},
|
||||||
|
// 检索
|
||||||
|
{
|
||||||
|
name: 'search',
|
||||||
|
path: '/search',
|
||||||
|
component: () => import('@/views/Search'),
|
||||||
|
meta: { title: '检索' }
|
||||||
|
},
|
||||||
|
// 用户主页
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
path: '/member/:username/home',
|
||||||
|
component: () => import('@/views/user/Profile'),
|
||||||
|
meta: { title: '用户主页' }
|
||||||
|
},
|
||||||
|
// 用户设置
|
||||||
|
{
|
||||||
|
name: 'user-setting',
|
||||||
|
path: '/member/:username/setting',
|
||||||
|
component: () => import('@/views/user/Setting'),
|
||||||
|
meta: { title: '设置', requireAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/404",
|
||||||
|
name: "404",
|
||||||
|
component: () => import("@/views/error/404"),
|
||||||
|
meta: { title: "404-NotFound" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
redirect: "/404",
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const originalPush = VueRouter.prototype.push;
|
||||||
|
VueRouter.prototype.push = function push(location) {
|
||||||
|
return originalPush.call(this, location).catch((err) => err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
5
src/store/getters.js
Normal file
5
src/store/getters.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const getters = {
|
||||||
|
token: state => state.user.token, // token
|
||||||
|
user: state => state.user.user, // 用户对象
|
||||||
|
}
|
||||||
|
export default getters
|
15
src/store/index.js
Normal file
15
src/store/index.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import getters from './getters'
|
||||||
|
import user from './modules/user'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
user
|
||||||
|
},
|
||||||
|
getters
|
||||||
|
})
|
||||||
|
|
||||||
|
export default store
|
80
src/store/modules/user.js
Normal file
80
src/store/modules/user.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { getUserInfo, login, logout } from "@/api/auth/auth";
|
||||||
|
import { getToken, setToken, removeToken } from "@/utils/auth";
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
token: getToken(), // token
|
||||||
|
user: "", // 用户对象
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
SET_TOKEN_STATE: (state, token) => {
|
||||||
|
state.token = token;
|
||||||
|
},
|
||||||
|
SET_USER_STATE: (state, user) => {
|
||||||
|
state.user = user;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
// 用户登录
|
||||||
|
login({ commit }, userInfo) {
|
||||||
|
console.log(userInfo);
|
||||||
|
const { name, pass, rememberMe } = userInfo;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
login({ username: name.trim(), password: pass, rememberMe: rememberMe })
|
||||||
|
.then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
commit("SET_TOKEN_STATE", data.token);
|
||||||
|
setToken(data.token);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 获取用户信息
|
||||||
|
getInfo({ commit, state }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getUserInfo()
|
||||||
|
.then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) {
|
||||||
|
commit("SET_TOKEN_STATE", "");
|
||||||
|
commit("SET_USER_STATE", "");
|
||||||
|
removeToken();
|
||||||
|
resolve();
|
||||||
|
reject("Verification failed, please Login again.");
|
||||||
|
}
|
||||||
|
commit("SET_USER_STATE", data);
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 注销
|
||||||
|
logout({ commit, state }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logout(state.token)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
commit("SET_TOKEN_STATE", "");
|
||||||
|
commit("SET_USER_STATE", "");
|
||||||
|
removeToken();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
mutations,
|
||||||
|
actions,
|
||||||
|
};
|
30
src/user.js
Normal file
30
src/user.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 用户主页
|
||||||
|
export function getInfoByName(username, page, size) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/' + username,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
pageNo: page,
|
||||||
|
size: size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户主页
|
||||||
|
export function getInfo() {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/info',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
export function update(user) {
|
||||||
|
return request({
|
||||||
|
url: '/ums/user/update',
|
||||||
|
method: 'post',
|
||||||
|
data: user
|
||||||
|
})
|
||||||
|
}
|
31
src/utils/auth.js
Normal file
31
src/utils/auth.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
const uToken = 'u_token'
|
||||||
|
const darkMode = 'dark_mode';
|
||||||
|
|
||||||
|
// 获取Token
|
||||||
|
export function getToken() {
|
||||||
|
return Cookies.get(uToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Token,1天,与后端同步
|
||||||
|
export function setToken(token) {
|
||||||
|
return Cookies.set(uToken, token, {expires: 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除Token
|
||||||
|
export function removeToken() {
|
||||||
|
return Cookies.remove(uToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAll() {
|
||||||
|
return Cookies.Cookies.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDarkMode(mode) {
|
||||||
|
return Cookies.set(darkMode, mode, {expires: 365})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDarkMode() {
|
||||||
|
return !(undefined === Cookies.get(darkMode) || 'false' === Cookies.get(darkMode));
|
||||||
|
}
|
8
src/utils/get-page-title.js
Normal file
8
src/utils/get-page-title.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const title = '小而美的智慧社区系统'
|
||||||
|
|
||||||
|
export default function getPageTitle(pageTitle) {
|
||||||
|
if (pageTitle) {
|
||||||
|
return `${pageTitle} - ${title}`
|
||||||
|
}
|
||||||
|
return `${title}`
|
||||||
|
}
|
81
src/utils/request.js
Normal file
81
src/utils/request.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { Message, MessageBox } from 'element-ui'
|
||||||
|
import store from '@/store'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
|
// 1.创建axios实例
|
||||||
|
const service = axios.create({
|
||||||
|
// 公共接口--这里注意后面会讲,url = base url + request url
|
||||||
|
baseURL: process.env.VUE_APP_SERVER_URL,
|
||||||
|
|
||||||
|
// baseURL: 'https://api.example.com',
|
||||||
|
// 超时时间 单位是ms,这里设置了5s的超时时间
|
||||||
|
timeout: 5 * 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2.请求拦截器request interceptor
|
||||||
|
service.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
// 发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求去添加
|
||||||
|
// 注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
|
||||||
|
if (store.getters.token) {
|
||||||
|
// config.params = {'token': token} // 如果要求携带在参数中
|
||||||
|
// config.headers.token = token; // 如果要求携带在请求头中
|
||||||
|
// bearer:w3c规范
|
||||||
|
config.headers['Authorization'] = 'Bearer ' + getToken()
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// do something with request error
|
||||||
|
// console.log(error) // for debug
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 设置cross跨域 并设置访问权限 允许跨域携带cookie信息,使用JWT可关闭
|
||||||
|
service.defaults.withCredentials = false
|
||||||
|
|
||||||
|
service.interceptors.response.use(
|
||||||
|
// 接收到响应数据并成功后的一些共有的处理,关闭loading等
|
||||||
|
response => {
|
||||||
|
const res = response.data
|
||||||
|
// 如果自定义代码不是200,则将其判断为错误。
|
||||||
|
if (res.code !== 200) {
|
||||||
|
// 50008: 非法Token; 50012: 异地登录; 50014: Token失效;
|
||||||
|
if (res.code === 401 || res.code === 50012 || res.code === 50014) {
|
||||||
|
// 重新登录
|
||||||
|
MessageBox.confirm('会话失效,您可以留在当前页面,或重新登录', '权限不足', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
center: true
|
||||||
|
}).then(() => {
|
||||||
|
window.location.href = '#/login'
|
||||||
|
})
|
||||||
|
} else { // 其他异常直接提示
|
||||||
|
Message({
|
||||||
|
showClose: true,
|
||||||
|
message: '⚠' + res.message || 'Error',
|
||||||
|
type: 'error',
|
||||||
|
duration: 3 * 1000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message || 'Error'))
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
/** *** 接收到异常响应的处理开始 *****/
|
||||||
|
// console.log('err' + error) // for debug
|
||||||
|
Message({
|
||||||
|
showClose: true,
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
duration: 5 * 1000
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export default service
|
58
src/utils/scroll-to.js
Normal file
58
src/utils/scroll-to.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
Math.easeInOutQuad = function(t, b, c, d) {
|
||||||
|
t /= d / 2
|
||||||
|
if (t < 1) {
|
||||||
|
return c / 2 * t * t + b
|
||||||
|
}
|
||||||
|
t--
|
||||||
|
return -c / 2 * (t * (t - 2) - 1) + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
|
||||||
|
var requestAnimFrame = (function() {
|
||||||
|
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
|
||||||
|
})()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because it's so fucking difficult to detect the scrolling element, just move them all
|
||||||
|
* @param {number} amount
|
||||||
|
*/
|
||||||
|
function move(amount) {
|
||||||
|
document.documentElement.scrollTop = amount
|
||||||
|
document.body.parentNode.scrollTop = amount
|
||||||
|
document.body.scrollTop = amount
|
||||||
|
}
|
||||||
|
|
||||||
|
function position() {
|
||||||
|
return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} to
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
export function scrollTo(to, duration, callback) {
|
||||||
|
const start = position()
|
||||||
|
const change = to - start
|
||||||
|
const increment = 20
|
||||||
|
let currentTime = 0
|
||||||
|
duration = (typeof (duration) === 'undefined') ? 500 : duration
|
||||||
|
var animateScroll = function() {
|
||||||
|
// increment the time
|
||||||
|
currentTime += increment
|
||||||
|
// find the value with the quadratic in-out easing function
|
||||||
|
var val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||||
|
// move the document.body
|
||||||
|
move(val)
|
||||||
|
// do the animation unless its over
|
||||||
|
if (currentTime < duration) {
|
||||||
|
requestAnimFrame(animateScroll)
|
||||||
|
} else {
|
||||||
|
if (callback && typeof (callback) === 'function') {
|
||||||
|
// the animation is done so lets callback
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateScroll()
|
||||||
|
}
|
42
src/views/Home.vue
Normal file
42
src/views/Home.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="box">🔔 {{ billboard.content }}</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-three-quarters">
|
||||||
|
<TopicList></TopicList>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<CardBar></CardBar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getBillboard } from "@/api/billboard";
|
||||||
|
import CardBar from "@/views/card/CardBar"
|
||||||
|
import PostList from '@/views/post/Index'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Home",
|
||||||
|
components: {CardBar, TopicList: PostList},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
billboard: {
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchBillboard();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchBillboard() {
|
||||||
|
getBillboard().then((value) => {
|
||||||
|
const { data } = value;
|
||||||
|
this.billboard = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
104
src/views/Search.vue
Normal file
104
src/views/Search.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
检索到 <code>{{ list.length }}</code>
|
||||||
|
条关于 <code class="has-text-info">{{ query.keyword }}</code> 的记录
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<article v-for="(item, index) in list" :key="index" class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="`https://cn.gravatar.com/avatar/${item.userId}?s=164&d=monsterid`">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="">
|
||||||
|
<p class="ellipsis is-ellipsis-1">
|
||||||
|
<el-tooltip class="item" effect="dark" :content="item.title" placement="top">
|
||||||
|
<router-link :to="{name:'post-detail',params:{id:item.id}}">
|
||||||
|
<span class="is-size-6">{{ item.title }}</span>
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<nav class="level has-text-grey is-mobile is-size-7 mt-2">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-left">
|
||||||
|
<router-link class="level-item" :to="{ path: `/member/${item.username}/home` }">
|
||||||
|
{{ item.alias }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<span class="mr-1">
|
||||||
|
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-for="(tag, index) in item.tags"
|
||||||
|
:key="index"
|
||||||
|
class="tag is-hidden-mobile is-success is-light mr-1"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'tag', params: { name: tag.name } }">
|
||||||
|
{{ "#" + tag.name }}
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="is-hidden-mobile">浏览:{{ item.view }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="media-right" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--分页-->
|
||||||
|
<pagination
|
||||||
|
v-show="query.total > 0"
|
||||||
|
:total="query.total"
|
||||||
|
:page.sync="query.pageNum"
|
||||||
|
:limit.sync="query.pageSize"
|
||||||
|
@pagination="fetchList"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { searchByKeyword } from '@/api/search'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Search',
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
query: {
|
||||||
|
keyword: this.$route.query.key,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchList() {
|
||||||
|
searchByKeyword(this.query).then(value => {
|
||||||
|
const { data } = value
|
||||||
|
this.list = data.records
|
||||||
|
this.query.total = data.total
|
||||||
|
this.query.pageSize = data.size
|
||||||
|
this.query.pageNum = data.current
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
116
src/views/auth/Login.vue
Normal file
116
src/views/auth/Login.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns py-6">
|
||||||
|
<div class="column is-half is-offset-one-quarter">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header" class="has-text-centered has-text-weight-bold">
|
||||||
|
用户登录
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-form
|
||||||
|
v-loading="loading"
|
||||||
|
:model="ruleForm"
|
||||||
|
status-icon
|
||||||
|
:rules="rules"
|
||||||
|
ref="ruleForm"
|
||||||
|
label-width="100px"
|
||||||
|
class="demo-ruleForm"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号" prop="name">
|
||||||
|
<el-input v-model="ruleForm.name"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="pass">
|
||||||
|
<el-input
|
||||||
|
type="password"
|
||||||
|
v-model="ruleForm.pass"
|
||||||
|
autocomplete="off"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="记住" prop="delivery">
|
||||||
|
<el-switch v-model="ruleForm.rememberMe"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('ruleForm')"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
<el-button @click="resetForm('ruleForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Login",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
redirect: undefined,
|
||||||
|
loading: false,
|
||||||
|
ruleForm: {
|
||||||
|
name: "",
|
||||||
|
pass: "",
|
||||||
|
rememberMe: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||||
|
{
|
||||||
|
min: 2,
|
||||||
|
max: 15,
|
||||||
|
message: "长度在 2 到 15 个字符",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pass: [
|
||||||
|
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
min: 6,
|
||||||
|
max: 20,
|
||||||
|
message: "长度在 6 到 20 个字符",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
this.loading = true;
|
||||||
|
this.$store
|
||||||
|
.dispatch("user/login", this.ruleForm)
|
||||||
|
.then(() => {
|
||||||
|
this.$message({
|
||||||
|
message: "恭喜你,登录成功",
|
||||||
|
type: "success",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.$router.push({ path: this.redirect || "/" });
|
||||||
|
}, 0.1 * 1000);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
150
src/views/auth/Register.vue
Normal file
150
src/views/auth/Register.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns py-6">
|
||||||
|
<div class="column is-half is-offset-one-quarter">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header" class="has-text-centered has-text-weight-bold">
|
||||||
|
新用户入驻
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-form
|
||||||
|
ref="ruleForm"
|
||||||
|
v-loading="loading"
|
||||||
|
:model="ruleForm"
|
||||||
|
status-icon
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
class="demo-ruleForm"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号" prop="name">
|
||||||
|
<el-input v-model="ruleForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="pass">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.pass"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认密码" prop="checkPass">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.checkPass"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="ruleForm.email" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submitForm('ruleForm')"
|
||||||
|
>立即注册</el-button>
|
||||||
|
<el-button @click="resetForm('ruleForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { userRegister } from '@/api/auth/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Register',
|
||||||
|
data() {
|
||||||
|
const validatePass = (rule, value, callback) => {
|
||||||
|
if (value === '') {
|
||||||
|
callback(new Error('请再次输入密码'))
|
||||||
|
} else if (value !== this.ruleForm.pass) {
|
||||||
|
callback(new Error('两次输入密码不一致!'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
ruleForm: {
|
||||||
|
name: '',
|
||||||
|
pass: '',
|
||||||
|
checkPass: '',
|
||||||
|
email: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 2,
|
||||||
|
max: 10,
|
||||||
|
message: '长度在 2 到 10 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pass: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 6,
|
||||||
|
max: 20,
|
||||||
|
message: '长度在 6 到 20 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
checkPass: [
|
||||||
|
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||||
|
{ validator: validatePass, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'email',
|
||||||
|
message: '请输入正确的邮箱地址',
|
||||||
|
trigger: ['blur', 'change']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
this.loading = true
|
||||||
|
userRegister(this.ruleForm)
|
||||||
|
.then((value) => {
|
||||||
|
const { code, message } = value
|
||||||
|
if (code === 200) {
|
||||||
|
this.$message({
|
||||||
|
message: '账号注册成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.$router.push({ path: this.redirect || '/login' })
|
||||||
|
}, 0.1 * 1000)
|
||||||
|
} else {
|
||||||
|
this.$message.error('注册失败,' + message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
26
src/views/card/CardBar.vue
Normal file
26
src/views/card/CardBar.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<!--是否登录-->
|
||||||
|
<login-welcome />
|
||||||
|
|
||||||
|
<!--今日赠言-->
|
||||||
|
<tip-card />
|
||||||
|
|
||||||
|
<!--资源推介-->
|
||||||
|
<PromotionCard />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TipCard from '@/views/card/Tip'
|
||||||
|
import PromotionCard from '@/views/card/Promotion'
|
||||||
|
import LoginWelcome from '@/views/card/LoginWelcome'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CardBar',
|
||||||
|
components: { LoginWelcome, PromotionCard, TipCard }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
32
src/views/card/LoginWelcome.vue
Normal file
32
src/views/card/LoginWelcome.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="box-card" shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span>💐 发帖</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="token != null && token !== ''" class="has-text-centered">
|
||||||
|
<b-button type="is-danger" tag="router-link" :to="{path:'/post/create'}" outlined>✍ 发表想法</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="has-text-centered">
|
||||||
|
<b-button type="is-primary" tag="router-link" :to="{path:'/register'}" outlined>马上入驻</b-button>
|
||||||
|
<b-button type="is-danger" tag="router-link" :to="{path:'/login'}" outlined class="ml-2"> 社区登入</b-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoginWelcome',
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'token'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
39
src/views/card/Promotion.vue
Normal file
39
src/views/card/Promotion.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="box-card" shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span>🥂 推广</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-for="(item, index) in list" :key="index" class="block">
|
||||||
|
<a :href="item.link" target="_blank">{{ item.title }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getList } from '@/api/promote'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Promotion',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchList() {
|
||||||
|
getList().then((response) => {
|
||||||
|
const { data } = response
|
||||||
|
this.list = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
43
src/views/card/Tip.vue
Normal file
43
src/views/card/Tip.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="box-card" shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span>🥳 每日一句</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="has-text-left block">
|
||||||
|
{{ tip.content }}
|
||||||
|
</div>
|
||||||
|
<div class="has-text-right mt-5 block">
|
||||||
|
——{{ tip.author }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {getTodayTip} from '@/api/tip'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Tip',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tip: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchTodayTip()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchTodayTip() {
|
||||||
|
getTodayTip().then(response => {
|
||||||
|
const { data } = response
|
||||||
|
this.tip = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
40
src/views/error/404.vue
Normal file
40
src/views/error/404.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns mt-6">
|
||||||
|
<div class="column mt-6">
|
||||||
|
<div class="mt-6">
|
||||||
|
<p class="content">UH OH! 页面丢失</p>
|
||||||
|
<p class="content subtitle mt-6">
|
||||||
|
您所寻找的页面不存在, {{ times }} 秒后,将返回首页!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "404",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
times: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.goHome();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goHome: function () {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.times--
|
||||||
|
if (this.times === 0) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.$router.push({path: '/'});
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
103
src/views/post/Author.vue
Normal file
103
src/views/post/Author.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<section id="author">
|
||||||
|
<el-card class="" shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span class="has-text-weight-bold">👨💻 关于作者</span>
|
||||||
|
</div>
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<p class="is-size-5 mb-5">
|
||||||
|
<router-link :to="{ path: `/member/${user.username}/home` }">
|
||||||
|
{{ user.alias }} <span class="is-size-7 has-text-grey">{{ '@' + user.username }}</span>
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-half">
|
||||||
|
<code>{{ user.topicCount }}</code>
|
||||||
|
<p>文章</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<code>{{ user.followerCount }}</code>
|
||||||
|
<p>粉丝</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="hasFollow"
|
||||||
|
class="button is-success button-center is-fullwidth"
|
||||||
|
@click="handleUnFollow(user.id)"
|
||||||
|
>
|
||||||
|
已关注
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-else class="button is-link button-center is-fullwidth" @click="handleFollow(user.id)">
|
||||||
|
关注
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { follow, hasFollow, unFollow } from '@/api/follow'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
export default {
|
||||||
|
name: 'Author',
|
||||||
|
props: {
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasFollow: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchInfo()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'token'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchInfo() {
|
||||||
|
if(this.token != null && this.token !== '')
|
||||||
|
{
|
||||||
|
hasFollow(this.user.id).then(value => {
|
||||||
|
const { data } = value
|
||||||
|
this.hasFollow = data.hasFollow
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleFollow: function(id) {
|
||||||
|
if(this.token != null && this.token !== '')
|
||||||
|
{
|
||||||
|
follow(id).then(response => {
|
||||||
|
const { message } = response
|
||||||
|
this.$message.success(message)
|
||||||
|
this.hasFollow = !this.hasFollow
|
||||||
|
this.user.followerCount = parseInt(this.user.followerCount) + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.$message.success('请先登录')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleUnFollow: function(id) {
|
||||||
|
unFollow(id).then(response => {
|
||||||
|
const { message } = response
|
||||||
|
this.$message.success(message)
|
||||||
|
this.hasFollow = !this.hasFollow
|
||||||
|
this.user.followerCount = parseInt(this.user.followerCount) - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
153
src/views/post/Create.vue
Normal file
153
src/views/post/Create.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-full">
|
||||||
|
<el-card
|
||||||
|
class="box-card"
|
||||||
|
shadow="never"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="header"
|
||||||
|
class="clearfix"
|
||||||
|
>
|
||||||
|
<span><i class="fa fa fa-book"> 主题 / 发布主题</i></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-form
|
||||||
|
ref="ruleForm"
|
||||||
|
:model="ruleForm"
|
||||||
|
:rules="rules"
|
||||||
|
class="demo-ruleForm"
|
||||||
|
>
|
||||||
|
<el-form-item prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.title"
|
||||||
|
placeholder="输入主题名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!--Markdown-->
|
||||||
|
<div id="vditor" />
|
||||||
|
|
||||||
|
<b-taginput
|
||||||
|
v-model="ruleForm.tags"
|
||||||
|
class="my-3"
|
||||||
|
maxlength="15"
|
||||||
|
maxtags="3"
|
||||||
|
ellipsis
|
||||||
|
placeholder="请输入主题标签,限制为 15 个字符和 3 个标签"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submitForm('ruleForm')"
|
||||||
|
>立即创建
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetForm('ruleForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { post } from '@/api/post'
|
||||||
|
import Vditor from 'vditor'
|
||||||
|
import 'vditor/dist/index.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TopicPost',
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
contentEditor: {},
|
||||||
|
ruleForm: {
|
||||||
|
title: '', // 标题
|
||||||
|
tags: [], // 标签
|
||||||
|
content: '' // 内容
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: '请输入话题名称', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
min: 1,
|
||||||
|
max: 25,
|
||||||
|
message: '长度在 1 到 25 个字符',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.contentEditor = new Vditor('vditor', {
|
||||||
|
height: 500,
|
||||||
|
placeholder: '此处为话题内容……',
|
||||||
|
theme: 'classic',
|
||||||
|
counter: {
|
||||||
|
enable: true,
|
||||||
|
type: 'markdown'
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
delay: 0,
|
||||||
|
hljs: {
|
||||||
|
style: 'monokai',
|
||||||
|
lineNumber: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tab: '\t',
|
||||||
|
typewriterMode: true,
|
||||||
|
toolbarConfig: {
|
||||||
|
pin: true
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
enable: false
|
||||||
|
},
|
||||||
|
mode: 'sv'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (
|
||||||
|
this.contentEditor.getValue().length === 1 ||
|
||||||
|
this.contentEditor.getValue() == null ||
|
||||||
|
this.contentEditor.getValue() === ''
|
||||||
|
) {
|
||||||
|
alert('话题内容不可为空')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.ruleForm.tags == null || this.ruleForm.tags.length === 0) {
|
||||||
|
alert('标签不可以为空')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.ruleForm.content = this.contentEditor.getValue()
|
||||||
|
post(this.ruleForm).then((response) => {
|
||||||
|
const { data } = response
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'post-detail',
|
||||||
|
params: { id: data.id }
|
||||||
|
})
|
||||||
|
}, 800)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('error submit!!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields()
|
||||||
|
this.contentEditor.setValue('')
|
||||||
|
this.ruleForm.tags = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
153
src/views/post/Detail.vue
Normal file
153
src/views/post/Detail.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns">
|
||||||
|
<!--文章详情-->
|
||||||
|
<div class="column is-three-quarters">
|
||||||
|
<!--主题-->
|
||||||
|
<el-card
|
||||||
|
class="box-card"
|
||||||
|
shadow="never"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="header"
|
||||||
|
class="has-text-centered"
|
||||||
|
>
|
||||||
|
<p class="is-size-5 has-text-weight-bold">{{ topic.title }}</p>
|
||||||
|
<div class="has-text-grey is-size-7 mt-3">
|
||||||
|
<span>{{ dayjs(topic.createTime).format('YYYY/MM/DD HH:mm:ss') }}</span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span>发布者:{{ topicUser.alias }}</span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span>查看:{{ topic.view }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--Markdown-->
|
||||||
|
<div id="preview" />
|
||||||
|
|
||||||
|
<!--标签-->
|
||||||
|
<nav class="level has-text-grey is-size-7 mt-6">
|
||||||
|
<div class="level-left">
|
||||||
|
<p class="level-item">
|
||||||
|
<b-taglist>
|
||||||
|
<router-link
|
||||||
|
v-for="(tag, index) in tags"
|
||||||
|
:key="index"
|
||||||
|
:to="{ name: 'tag', params: { name: tag.name } }"
|
||||||
|
>
|
||||||
|
<b-tag type="is-info is-light mr-1">
|
||||||
|
{{ "#" + tag.name }}
|
||||||
|
</b-tag>
|
||||||
|
</router-link>
|
||||||
|
</b-taglist>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="token && user.id === topicUser.id"
|
||||||
|
class="level-right"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="level-item"
|
||||||
|
:to="{name:'topic-edit',params: {id:topic.id}}"
|
||||||
|
>
|
||||||
|
<span class="tag">编辑</span>
|
||||||
|
</router-link>
|
||||||
|
<a class="level-item">
|
||||||
|
<span
|
||||||
|
class="tag"
|
||||||
|
@click="handleDelete(topic.id)"
|
||||||
|
>删除</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<lv-comments :slug="topic.id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<!--作者-->
|
||||||
|
<Author
|
||||||
|
v-if="flag"
|
||||||
|
:user="topicUser"
|
||||||
|
/>
|
||||||
|
<!--推荐-->
|
||||||
|
<recommend
|
||||||
|
v-if="flag"
|
||||||
|
:topic-id="topic.id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { deleteTopic, getTopic } from '@/api/post'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import Author from '@/views/post/Author'
|
||||||
|
import Recommend from '@/views/post/Recommend'
|
||||||
|
import LvComments from '@/components/Comment/Comments'
|
||||||
|
import Vditor from 'vditor'
|
||||||
|
import 'vditor/dist/index.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TopicDetail',
|
||||||
|
components: { Author, Recommend, LvComments },
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'token','user'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
flag: false,
|
||||||
|
topic: {
|
||||||
|
content: '',
|
||||||
|
id: this.$route.params.id
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
topicUser: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchTopic()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderMarkdown(md) {
|
||||||
|
Vditor.preview(document.getElementById('preview'), md, {
|
||||||
|
hljs: { style: 'github' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 初始化
|
||||||
|
async fetchTopic() {
|
||||||
|
getTopic(this.$route.params.id).then(response => {
|
||||||
|
const { data } = response
|
||||||
|
document.title = data.topic.title
|
||||||
|
|
||||||
|
this.topic = data.topic
|
||||||
|
this.tags = data.tags
|
||||||
|
this.topicUser = data.user
|
||||||
|
// this.comments = data.comments
|
||||||
|
this.renderMarkdown(this.topic.content)
|
||||||
|
this.flag = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(id) {
|
||||||
|
deleteTopic(id).then(value => {
|
||||||
|
const { code, message } = value
|
||||||
|
alert(message)
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({ path: '/' })
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#preview {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
108
src/views/post/Edit.vue
Normal file
108
src/views/post/Edit.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-full">
|
||||||
|
<el-card class="box-card" shadow="never">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span><i class="fa fa fa-book"> 主题 / 更新主题</i></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-form :model="topic" ref="topic" class="demo-topic">
|
||||||
|
<el-form-item prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="topic.title"
|
||||||
|
placeholder="输入新的主题名称"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!--Markdown-->
|
||||||
|
<div id="vditor"></div>
|
||||||
|
|
||||||
|
<b-taginput
|
||||||
|
v-model="tags"
|
||||||
|
class="my-3"
|
||||||
|
maxlength="15"
|
||||||
|
maxtags="3"
|
||||||
|
ellipsis
|
||||||
|
placeholder="请输入主题标签,限制为 15 个字符和 3 个标签"
|
||||||
|
/>
|
||||||
|
<el-form-item class="mt-3">
|
||||||
|
<el-button type="primary" @click="handleUpdate()"
|
||||||
|
>更新
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetForm('topic')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getTopic, update } from "@/api/post";
|
||||||
|
import Vditor from "vditor";
|
||||||
|
import "vditor/dist/index.css";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TopicEdit",
|
||||||
|
components: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
topic: {},
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchTopic();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderMarkdown(md) {
|
||||||
|
this.contentEditor = new Vditor("vditor", {
|
||||||
|
height: 460,
|
||||||
|
placeholder: "输入要更新的内容",
|
||||||
|
preview: {
|
||||||
|
hljs: { style: "monokai" },
|
||||||
|
},
|
||||||
|
mode: "sv",
|
||||||
|
after: () => {
|
||||||
|
this.contentEditor.setValue(md);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchTopic() {
|
||||||
|
getTopic(this.$route.params.id).then((value) => {
|
||||||
|
this.topic = value.data.topic;
|
||||||
|
this.tags = value.data.tags.map(tag => tag.name);
|
||||||
|
this.renderMarkdown(this.topic.content);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleUpdate: function () {
|
||||||
|
this.topic.content = this.contentEditor.getValue();
|
||||||
|
update(this.topic).then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
console.log(data);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({
|
||||||
|
name: "post-detail",
|
||||||
|
params: { id: data.id },
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
this.contentEditor.setValue("");
|
||||||
|
this.tags = "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vditor-reset pre > code {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
157
src/views/post/Index.vue
Normal file
157
src/views/post/Index.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||||
|
<el-tab-pane label="最新主题" name="latest">
|
||||||
|
<article v-for="(item, index) in articleList" :key="index" class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="`https://cn.gravatar.com/avatar/${item.userId}?s=164&d=monsterid`"
|
||||||
|
style="border-radius: 5px;">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="">
|
||||||
|
<p class="ellipsis is-ellipsis-1">
|
||||||
|
<el-tooltip :content="item.title" class="item" effect="dark" placement="top">
|
||||||
|
<router-link :to="{name:'post-detail',params:{id:item.id}}">
|
||||||
|
<span class="is-size-6">{{ item.title }}</span>
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<nav class="level has-text-grey is-mobile is-size-7 mt-2">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-left">
|
||||||
|
<router-link :to="{ path: `/member/${item.username}/home` }" class="level-item">
|
||||||
|
{{ item.alias }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<span class="mr-1">
|
||||||
|
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-for="(tag, index) in item.tags"
|
||||||
|
:key="index"
|
||||||
|
class="tag is-hidden-mobile is-success is-light mr-1"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'tag', params: { name: tag.name } }">
|
||||||
|
{{ "#" + tag.name }}
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="is-hidden-mobile">浏览:{{ item.view }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="media-right"/>
|
||||||
|
</article>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="热门主题" name="hot">
|
||||||
|
<article v-for="(item, index) in articleList" :key="index" class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="`https://cn.gravatar.com/avatar/${item.userId}?s=164&d=monsterid`"
|
||||||
|
style="border-radius: 5px;">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="">
|
||||||
|
<p class="ellipsis is-ellipsis-1">
|
||||||
|
<el-tooltip :content="item.title" class="item" effect="dark" placement="top">
|
||||||
|
<router-link :to="{name:'post-detail',params:{id:item.id}}">
|
||||||
|
<span class="is-size-6">{{ item.title }}</span>
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<nav class="level has-text-grey is-mobile is-size-7 mt-2">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-left">
|
||||||
|
<router-link :to="{ path: `/member/${item.username}/home` }" class="level-item">
|
||||||
|
{{ item.alias }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<span class="mr-1">
|
||||||
|
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-for="(tag, index) in item.tags"
|
||||||
|
:key="index"
|
||||||
|
class="tag is-hidden-mobile is-success is-light mr-1"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'tag', params: { name: tag.name } }">
|
||||||
|
{{ "#" + tag.name }}
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="is-hidden-mobile">浏览:{{ item.view }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="media-right"/>
|
||||||
|
</article>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--分页-->
|
||||||
|
<pagination
|
||||||
|
v-show="page.total > 0"
|
||||||
|
:limit.sync="page.size"
|
||||||
|
:page.sync="page.current"
|
||||||
|
:total="page.total"
|
||||||
|
@pagination="init"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {getList} from '@/api/post'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TopicList',
|
||||||
|
components: {Pagination},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeName: 'latest',
|
||||||
|
articleList: [],
|
||||||
|
page: {
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
tab: 'latest'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.init(this.tab)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init(tab) {
|
||||||
|
getList(this.page.current, this.page.size, tab).then((response) => {
|
||||||
|
const {data} = response
|
||||||
|
this.page.current = data.current
|
||||||
|
this.page.total = data.total
|
||||||
|
this.page.size = data.size
|
||||||
|
this.articleList = data.records
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleClick(tab) {
|
||||||
|
this.page.current = 1
|
||||||
|
this.init(tab.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
54
src/views/post/Recommend.vue
Normal file
54
src/views/post/Recommend.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="" shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span class="has-text-weight-bold">🧐 随便看看</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p v-for="(item,index) in recommend" :key="index" :title="item.title" class="block ellipsis is-ellipsis-1">
|
||||||
|
<router-link :to="{name:'post-detail',params: { id: item.id }}">
|
||||||
|
<span v-if="index<9" class="tag">
|
||||||
|
0{{ parseInt(index) + 1 }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="tag">
|
||||||
|
{{ parseInt(index) + 1 }}
|
||||||
|
</span>
|
||||||
|
{{ item.title }}
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getRecommendTopics } from '@/api/post'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Recommend',
|
||||||
|
props: {
|
||||||
|
topicId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recommend: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchRecommendTopics()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchRecommendTopics() {
|
||||||
|
getRecommendTopics(this.topicId).then(value => {
|
||||||
|
const { data } = value
|
||||||
|
this.recommend = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
109
src/views/tag/Tag.vue
Normal file
109
src/views/tag/Tag.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div id="tag" class="columns">
|
||||||
|
<div class="column is-three-quarters">
|
||||||
|
<el-card class="box-card" shadow="never">
|
||||||
|
<div slot="header" class="">
|
||||||
|
🔍 检索到 <span class="has-text-info">{{ topics.length }}</span> 篇有关
|
||||||
|
<span class="has-text-info">{{ this.$route.params.name }}</span>
|
||||||
|
的话题
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text item">
|
||||||
|
<article v-for="(item, index) in topics" :key="index" class="media mt-3">
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<el-tooltip class="item" effect="dark" :content="item.title" placement="top">
|
||||||
|
<router-link :to="{ name: 'post-detail',params:{id: item.id } }">
|
||||||
|
{{ item.title }}
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="level has-text-grey is-size-7">
|
||||||
|
<div class="level-left">
|
||||||
|
<span>发布于:{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||||
|
|
||||||
|
<span class="mx-3">浏览:{{ item.view }}</span>
|
||||||
|
|
||||||
|
<span>评论:{{ item.comments }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<el-card class="box-card" shadow="hover">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
🤙 关于标签
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li class="content">标签由平台用户发布使用</li>
|
||||||
|
<li class="content">系统每周会定时清理无用标签</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div slot="header">
|
||||||
|
🏷 热门标签
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(tag,index) in tags" :key="index" style="padding: 6px 0">
|
||||||
|
<router-link :to="{name:'tag',params:{name:tag.name}}">
|
||||||
|
<span v-if="index<9" class="tag">
|
||||||
|
0{{ parseInt(index) + 1 }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="tag">
|
||||||
|
{{ parseInt(index) + 1 }}
|
||||||
|
</span>
|
||||||
|
{{ tag.name }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getTopicsByTag } from '@/api/tag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Tag',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
topics: [],
|
||||||
|
tags: [],
|
||||||
|
paramMap: {
|
||||||
|
name: this.$route.params.name,
|
||||||
|
page: 1,
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchList: function() {
|
||||||
|
getTopicsByTag(this.paramMap).then(response => {
|
||||||
|
console.log(response)
|
||||||
|
this.topics = response.data.topics.records
|
||||||
|
this.tags = response.data.hotTags.records
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#tag {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
134
src/views/user/Profile.vue
Normal file
134
src/views/user/Profile.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="member">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-quarter">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header" class="has-text-centered">
|
||||||
|
<el-avatar :size="64" :src="`https://cn.gravatar.com/avatar/${topicUser.id}?s=164&d=monsterid`" />
|
||||||
|
<p class="mt-3">{{ topicUser.alias || topicUser.username }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="content">积分:<code>{{ topicUser.score }}</code></p>
|
||||||
|
<p class="content">入驻:{{ dayjs(topicUser.createTime).format("YYYY/MM/DD HH:MM:ss") }}</p>
|
||||||
|
<p class="content">简介:{{ topicUser.bio }}</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<!--用户发布的话题-->
|
||||||
|
<el-card class="box-card content" shadow="never">
|
||||||
|
<div slot="header" class="has-text-weight-bold">
|
||||||
|
<span>话题</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="topics.length===0">
|
||||||
|
暂无话题
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="topicUser-info">
|
||||||
|
<article v-for="(item, index) in topics" :key="index" class="media">
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content ellipsis is-ellipsis-1">
|
||||||
|
<el-tooltip class="item" effect="dark" :content="item.title" placement="top">
|
||||||
|
<router-link :to="{ name: 'post-detail', params: { id: item.id } }">
|
||||||
|
{{ item.title }}
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<nav class="level has-text-grey is-size-7">
|
||||||
|
<div class="level-left">
|
||||||
|
<span class="mr-1">
|
||||||
|
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD HH:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div v-if="token" class="media-right">
|
||||||
|
<div v-if="topicUser.username === user.username" class="level">
|
||||||
|
<div class="level-item mr-1">
|
||||||
|
<router-link :to="{name:'topic-edit',params: {id:item.id}}">
|
||||||
|
<span class="tag is-warning">编辑</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<a @click="handleDelete(item.id)">
|
||||||
|
<span class="tag is-danger">删除</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--分页-->
|
||||||
|
<pagination
|
||||||
|
v-show="page.total > 0"
|
||||||
|
class="mt-5"
|
||||||
|
:total="page.total"
|
||||||
|
:page.sync="page.current"
|
||||||
|
:limit.sync="page.size"
|
||||||
|
@pagination="fetchUserById"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getInfoByName } from '@/api/user'
|
||||||
|
import pagination from '@/components/Pagination/index'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { deleteTopic } from '@/api/post'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Profile',
|
||||||
|
components: { pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
topicUser: {},
|
||||||
|
topics: {},
|
||||||
|
page: {
|
||||||
|
current: 1,
|
||||||
|
size: 5,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['token', 'user'])
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchUserById()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchUserById() {
|
||||||
|
getInfoByName(this.$route.params.username, this.page.current, this.page.size).then((res) => {
|
||||||
|
const { data } = res
|
||||||
|
this.topicUser = data.user
|
||||||
|
this.page.current = data.topics.current
|
||||||
|
this.page.size = data.topics.size
|
||||||
|
this.page.total = data.topics.total
|
||||||
|
this.topics = data.topics.records
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(id) {
|
||||||
|
deleteTopic(id).then(value => {
|
||||||
|
const { code, message } = value
|
||||||
|
alert(message)
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({ path: '/' })
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
120
src/views/user/Setting.vue
Normal file
120
src/views/user/Setting.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
个人设置
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-full">
|
||||||
|
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||||
|
<el-tab-pane label="基础信息" name="first">
|
||||||
|
<el-form :label-position="labelPosition" label-width="100px" :model="user">
|
||||||
|
<el-form-item label="账号">
|
||||||
|
<el-input v-model="user.username" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称">
|
||||||
|
<el-input v-model="user.alias" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="简介">
|
||||||
|
<el-input v-model="user.bio" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
|
||||||
|
<el-button @click="resetForm('ruleForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="头像" name="second">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="`https://cn.gravatar.com/avatar/${this.user.id}?s=164&d=monsterid`">
|
||||||
|
</figure>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="电子邮箱" name="third">
|
||||||
|
<el-form ref="dynamicValidateForm" :model="user" label-width="100px" class="demo-dynamic">
|
||||||
|
<el-form-item
|
||||||
|
prop="email"
|
||||||
|
label="邮箱"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="user.email" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('dynamicValidateForm')">提交</el-button>
|
||||||
|
<el-button @click="resetForm('dynamicValidateForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="手机号" name="fourth">
|
||||||
|
<el-form ref="dynamicValidateForm" :model="user" label-width="100px" class="demo-dynamic">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="user.mobile" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('dynamicValidateForm')">提交</el-button>
|
||||||
|
<el-button @click="resetForm('dynamicValidateForm')">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {getInfo, update} from '@/api/user'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Setting',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeName: 'first',
|
||||||
|
labelPosition: 'right',
|
||||||
|
user: {
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
alias: '',
|
||||||
|
bio: '',
|
||||||
|
email: '',
|
||||||
|
mobile: '',
|
||||||
|
avatar: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchInfo()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchInfo() {
|
||||||
|
getInfo(this.$route.params.username).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
const { data } = res
|
||||||
|
this.user = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleClick(tab, event) {
|
||||||
|
console.log(tab, event)
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
console.log(this.user)
|
||||||
|
update(this.user).then(res => {
|
||||||
|
this.$message.success('信息修改成功')
|
||||||
|
this.fetchInfo()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
8079
yarn-error.log
Normal file
8079
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user