Repository: songboriceman/doubao_community_frontend
Branch: master
Commit: d36b5d06a013
Files: 56
Total size: 112.1 KB
Directory structure:
gitextract_czw2jpqd/
├── .browserslistrc
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public/
│ └── index.html
└── src/
├── App.vue
├── api/
│ ├── auth/
│ │ └── auth.js
│ ├── billboard.js
│ ├── comment.js
│ ├── follow.js
│ ├── post.js
│ ├── promote.js
│ ├── search.js
│ ├── tag.js
│ ├── tip.js
│ └── user.js
├── assets/
│ ├── app.css
│ └── plugins/
│ └── font-awesome-4.7.0/
│ ├── css/
│ │ └── font-awesome.css
│ └── fonts/
│ └── FontAwesome.otf
├── components/
│ ├── Backtop/
│ │ └── BackTop.vue
│ ├── Comment/
│ │ ├── Comments.vue
│ │ ├── CommentsForm.vue
│ │ └── CommentsItem.vue
│ ├── Layout/
│ │ ├── Footer.vue
│ │ └── Header.vue
│ └── Pagination/
│ └── index.vue
├── main.js
├── permission.js
├── router/
│ └── index.js
├── store/
│ ├── getters.js
│ ├── index.js
│ └── modules/
│ └── user.js
├── user.js
├── utils/
│ ├── auth.js
│ ├── get-page-title.js
│ ├── request.js
│ └── scroll-to.js
└── views/
├── Home.vue
├── Search.vue
├── auth/
│ ├── Login.vue
│ └── Register.vue
├── card/
│ ├── CardBar.vue
│ ├── LoginWelcome.vue
│ ├── Promotion.vue
│ └── Tip.vue
├── error/
│ └── 404.vue
├── post/
│ ├── Author.vue
│ ├── Create.vue
│ ├── Detail.vue
│ ├── Edit.vue
│ ├── Index.vue
│ └── Recommend.vue
├── tag/
│ └── Tag.vue
└── user/
├── Profile.vue
└── Setting.vue
================================================
FILE CONTENTS
================================================
================================================
FILE: .browserslistrc
================================================
> 1%
last 2 versions
not dead
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: README.md
================================================
### 豆宝社区项目实战视频教程简介
本项目实战视频教程全部免费,配套代码完全开源。手把手从零开始搭建一个目前应用最广泛的Springboot+Vue前后端分离多用户社区项目。本项目难度适中,为便于大家学习,每一集视频教程对应在Github上的每一次提交。
### 致谢
本项目大量借鉴了[极光社区项目](https://github.com/haoyu21/aurora),在此感谢原作者的无私开源。本项目在其基础上做了一些增删,删除了一些未完成的模块(活动,旅游),新增了评论功能,简化了后端认证与授权功能。最主要的工作是将原项目从零开始开始搭建,各个功能的实现分解成几十步来完成,便于大家更好的学习。
### 在线体验
http://kamiba.gitee.io/doubao_deploy_frontend/
### 代码开源地址
[前端](https://github.com/songboriceman/doubao_community_frontend)
[后端](https://github.com/songboriceman/doubao_community_backend)
### 视频教程地址
[视频教程](https://www.bilibili.com/video/BV1Wz4y1U7vC)
### 项目主要业务及实现的功能
本项目类似一个简版的掘金这样的技术社区,实现了多个用户注册,登录,发帖,回帖,评论,关注,用户中心等功能。
### 前端技术栈
Vue
Vuex
Vue Router
Axios
Bulma
Buefy
Element
Vditor
DarkReader
### 后端技术栈
Spring Boot
Mysql
Mybatis
MyBatis-Plus
Spring Security
JWT
Lombok
### 项目实战大纲:
01.豆宝社区项目介绍
02.豆宝社区项目所需的基础知识
03.前端项目搭建
04.前端公告板功能实现
05.初始化springboot后端项目
06.初始化后端数据库,springboot配置mybatis连接
07.后端项目目录结构初始化
08.后端公告板接口功能实现01
09.后端公告板接口功能实现02
10.前端端公告板接口功能实现
11.实现跨域,前后端接口联调
12.每日一句功能前端界面实现01
13.每日一句功能前端界面实现02
14.每日一句功能前端接口实现
15.每日一句功能后端接口实现
16.(非常重要)善用github提交记录进行项目学习
17.推广链接功能 前后端实现
18.用户注册前端实现
19.用户注册后端实现
20.jwt以及web通信流程
21.用户登录后端实现
22.vuex简介
23.js-cookie介绍
24.用户登录前端实现
25.前端侧边栏,马上入驻,社区登入功能
26.前端在axios请求拦截器中在请求头中加入jwt
27.后端设置请求拦截器检查用户请求头中是否包含jwt01
28.后端设置请求拦截器检查用户请求头中是否包含jwt02
29.前端header实现01
30.前端header实现02
31.退出登录
32.前端页脚功能实现
33.帖子列表功能前端
34.帖子列表功能后端
35.帖子分页功能实现
36.前端实现发表帖子功能
37.后端实现发表帖子功能
38.前端实现帖子详情功能
39.后端实现帖子详情功能
40.帖子详情右侧边栏帖子作者详情功能实现(前端)
41.帖子详情右侧边栏帖子作者详情用户关注功能实现(后端)
42.随便看看模块前端实现
43.随便看看模块后端实现
44.评论列表功能前端实现
45.评论列表功能后端实现
46.添加评论功能前端实现
47.添加评论功能后端实现
48.帖子更新与删除功能前后端实现
49.显示某个标签的全部文章功能前端
50.显示某个标签的全部文章功能后端实现
51.帖子搜索功能前端实现
52.帖子搜索功能前端实现
53.用户中心功能前端实现
54.用户中心功能前端实现
55.用户个人信息修改
56.前端发帖,留言等页面登录权限验证
57.后端发帖,留言等需要登录页面的权限验证
58.项目总结及遗留问题说明
59.(重要的说明)如何利用github上开源的项目代码提交记录更有效的学习本项目
### 豆约翰团队:
一群热爱分享技术,拥有多年开发经验及培训经验的老司机组成
### 擅长的领域:
java,python,前端,c++,.net
### 项目部分截图
#### PC
#### 首页

#### 发表文章

#### 文章详情及评论页面

#### 个人中心

#### 用户设置

#### 移动端
#### 首页

#### 用户中心

#### 详情页

### 技术讨论群
为方便同学们讨论项目中的技术,建了一个QQ群:

================================================
FILE: babel.config.js
================================================
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
================================================
FILE: package.json
================================================
{
"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"
}
}
================================================
FILE: public/index.html
================================================
<!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>
================================================
FILE: src/App.vue
================================================
<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>
================================================
FILE: src/api/auth/auth.js
================================================
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'
})
}
================================================
FILE: src/api/billboard.js
================================================
import request from '@/utils/request'
export function getBillboard() {
return request({
url: '/billboard/show',
method: 'get'
})
}
================================================
FILE: src/api/comment.js
================================================
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
})
}
================================================
FILE: src/api/follow.js
================================================
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'
}))
}
================================================
FILE: src/api/post.js
================================================
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'
})
}
================================================
FILE: src/api/promote.js
================================================
import request from '@/utils/request'
// 获取推广
export function getList() {
return request(({
url: '/promotion/all',
method: 'get'
}))
}
================================================
FILE: src/api/search.js
================================================
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
}
})
}
================================================
FILE: src/api/tag.js
================================================
import request from '@/utils/request'
export function getTopicsByTag(paramMap) {
return request({
url: '/tag/' + paramMap.name,
method: 'get',
params: {
page: paramMap.page,
size: paramMap.size
}
})
}
================================================
FILE: src/api/tip.js
================================================
import request from '@/utils/request'
export function getTodayTip() {
return request({
url: '/tip/today',
method: 'get'
})
}
================================================
FILE: src/api/user.js
================================================
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
})
}
================================================
FILE: src/assets/app.css
================================================
* {
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;
}
================================================
FILE: src/assets/plugins/font-awesome-4.7.0/css/font-awesome.css
================================================
/*!
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
.fa {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* makes the font 33% larger relative to the icon container */
.fa-lg {
font-size: 1.33333333em;
line-height: 0.75em;
vertical-align: -15%;
}
.fa-2x {
font-size: 2em;
}
.fa-3x {
font-size: 3em;
}
.fa-4x {
font-size: 4em;
}
.fa-5x {
font-size: 5em;
}
.fa-fw {
width: 1.28571429em;
text-align: center;
}
.fa-ul {
padding-left: 0;
margin-left: 2.14285714em;
list-style-type: none;
}
.fa-ul > li {
position: relative;
}
.fa-li {
position: absolute;
left: -2.14285714em;
width: 2.14285714em;
top: 0.14285714em;
text-align: center;
}
.fa-li.fa-lg {
left: -1.85714286em;
}
.fa-border {
padding: .2em .25em .15em;
border: solid 0.08em #eeeeee;
border-radius: .1em;
}
.fa-pull-left {
float: left;
}
.fa-pull-right {
float: right;
}
.fa.fa-pull-left {
margin-right: .3em;
}
.fa.fa-pull-right {
margin-left: .3em;
}
/* Deprecated as of 4.4.0 */
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.fa.pull-left {
margin-right: .3em;
}
.fa.pull-right {
margin-left: .3em;
}
.fa-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
.fa-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.fa-rotate-90 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.fa-rotate-180 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.fa-rotate-270 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.fa-flip-horizontal {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
-ms-transform: scale(1, -1);
transform: scale(1, -1);
}
:root .fa-rotate-90,
:root .fa-rotate-180,
:root .fa-rotate-270,
:root .fa-flip-horizontal,
:root .fa-flip-vertical {
filter: none;
}
.fa-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: middle;
}
.fa-stack-1x,
.fa-stack-2x {
position: absolute;
left: 0;
width: 100%;
text-align: center;
}
.fa-stack-1x {
line-height: inherit;
}
.fa-stack-2x {
font-size: 2em;
}
.fa-inverse {
color: #ffffff;
}
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */
.fa-glass:before {
content: "\f000";
}
.fa-music:before {
content: "\f001";
}
.fa-search:before {
content: "\f002";
}
.fa-envelope-o:before {
content: "\f003";
}
.fa-heart:before {
content: "\f004";
}
.fa-star:before {
content: "\f005";
}
.fa-star-o:before {
content: "\f006";
}
.fa-user:before {
content: "\f007";
}
.fa-film:before {
content: "\f008";
}
.fa-th-large:before {
content: "\f009";
}
.fa-th:before {
content: "\f00a";
}
.fa-th-list:before {
content: "\f00b";
}
.fa-check:before {
content: "\f00c";
}
.fa-remove:before,
.fa-close:before,
.fa-times:before {
content: "\f00d";
}
.fa-search-plus:before {
content: "\f00e";
}
.fa-search-minus:before {
content: "\f010";
}
.fa-power-off:before {
content: "\f011";
}
.fa-signal:before {
content: "\f012";
}
.fa-gear:before,
.fa-cog:before {
content: "\f013";
}
.fa-trash-o:before {
content: "\f014";
}
.fa-home:before {
content: "\f015";
}
.fa-file-o:before {
content: "\f016";
}
.fa-clock-o:before {
content: "\f017";
}
.fa-road:before {
content: "\f018";
}
.fa-download:before {
content: "\f019";
}
.fa-arrow-circle-o-down:before {
content: "\f01a";
}
.fa-arrow-circle-o-up:before {
content: "\f01b";
}
.fa-inbox:before {
content: "\f01c";
}
.fa-play-circle-o:before {
content: "\f01d";
}
.fa-rotate-right:before,
.fa-repeat:before {
content: "\f01e";
}
.fa-refresh:before {
content: "\f021";
}
.fa-list-alt:before {
content: "\f022";
}
.fa-lock:before {
content: "\f023";
}
.fa-flag:before {
content: "\f024";
}
.fa-headphones:before {
content: "\f025";
}
.fa-volume-off:before {
content: "\f026";
}
.fa-volume-down:before {
content: "\f027";
}
.fa-volume-up:before {
content: "\f028";
}
.fa-qrcode:before {
content: "\f029";
}
.fa-barcode:before {
content: "\f02a";
}
.fa-tag:before {
content: "\f02b";
}
.fa-tags:before {
content: "\f02c";
}
.fa-book:before {
content: "\f02d";
}
.fa-bookmark:before {
content: "\f02e";
}
.fa-print:before {
content: "\f02f";
}
.fa-camera:before {
content: "\f030";
}
.fa-font:before {
content: "\f031";
}
.fa-bold:before {
content: "\f032";
}
.fa-italic:before {
content: "\f033";
}
.fa-text-height:before {
content: "\f034";
}
.fa-text-width:before {
content: "\f035";
}
.fa-align-left:before {
content: "\f036";
}
.fa-align-center:before {
content: "\f037";
}
.fa-align-right:before {
content: "\f038";
}
.fa-align-justify:before {
content: "\f039";
}
.fa-list:before {
content: "\f03a";
}
.fa-dedent:before,
.fa-outdent:before {
content: "\f03b";
}
.fa-indent:before {
content: "\f03c";
}
.fa-video-camera:before {
content: "\f03d";
}
.fa-photo:before,
.fa-image:before,
.fa-picture-o:before {
content: "\f03e";
}
.fa-pencil:before {
content: "\f040";
}
.fa-map-marker:before {
content: "\f041";
}
.fa-adjust:before {
content: "\f042";
}
.fa-tint:before {
content: "\f043";
}
.fa-edit:before,
.fa-pencil-square-o:before {
content: "\f044";
}
.fa-share-square-o:before {
content: "\f045";
}
.fa-check-square-o:before {
content: "\f046";
}
.fa-arrows:before {
content: "\f047";
}
.fa-step-backward:before {
content: "\f048";
}
.fa-fast-backward:before {
content: "\f049";
}
.fa-backward:before {
content: "\f04a";
}
.fa-play:before {
content: "\f04b";
}
.fa-pause:before {
content: "\f04c";
}
.fa-stop:before {
content: "\f04d";
}
.fa-forward:before {
content: "\f04e";
}
.fa-fast-forward:before {
content: "\f050";
}
.fa-step-forward:before {
content: "\f051";
}
.fa-eject:before {
content: "\f052";
}
.fa-chevron-left:before {
content: "\f053";
}
.fa-chevron-right:before {
content: "\f054";
}
.fa-plus-circle:before {
content: "\f055";
}
.fa-minus-circle:before {
content: "\f056";
}
.fa-times-circle:before {
content: "\f057";
}
.fa-check-circle:before {
content: "\f058";
}
.fa-question-circle:before {
content: "\f059";
}
.fa-info-circle:before {
content: "\f05a";
}
.fa-crosshairs:before {
content: "\f05b";
}
.fa-times-circle-o:before {
content: "\f05c";
}
.fa-check-circle-o:before {
content: "\f05d";
}
.fa-ban:before {
content: "\f05e";
}
.fa-arrow-left:before {
content: "\f060";
}
.fa-arrow-right:before {
content: "\f061";
}
.fa-arrow-up:before {
content: "\f062";
}
.fa-arrow-down:before {
content: "\f063";
}
.fa-mail-forward:before,
.fa-share:before {
content: "\f064";
}
.fa-expand:before {
content: "\f065";
}
.fa-compress:before {
content: "\f066";
}
.fa-plus:before {
content: "\f067";
}
.fa-minus:before {
content: "\f068";
}
.fa-asterisk:before {
content: "\f069";
}
.fa-exclamation-circle:before {
content: "\f06a";
}
.fa-gift:before {
content: "\f06b";
}
.fa-leaf:before {
content: "\f06c";
}
.fa-fire:before {
content: "\f06d";
}
.fa-eye:before {
content: "\f06e";
}
.fa-eye-slash:before {
content: "\f070";
}
.fa-warning:before,
.fa-exclamation-triangle:before {
content: "\f071";
}
.fa-plane:before {
content: "\f072";
}
.fa-calendar:before {
content: "\f073";
}
.fa-random:before {
content: "\f074";
}
.fa-comment:before {
content: "\f075";
}
.fa-magnet:before {
content: "\f076";
}
.fa-chevron-up:before {
content: "\f077";
}
.fa-chevron-down:before {
content: "\f078";
}
.fa-retweet:before {
content: "\f079";
}
.fa-shopping-cart:before {
content: "\f07a";
}
.fa-folder:before {
content: "\f07b";
}
.fa-folder-open:before {
content: "\f07c";
}
.fa-arrows-v:before {
content: "\f07d";
}
.fa-arrows-h:before {
content: "\f07e";
}
.fa-bar-chart-o:before,
.fa-bar-chart:before {
content: "\f080";
}
.fa-twitter-square:before {
content: "\f081";
}
.fa-facebook-square:before {
content: "\f082";
}
.fa-camera-retro:before {
content: "\f083";
}
.fa-key:before {
content: "\f084";
}
.fa-gears:before,
.fa-cogs:before {
content: "\f085";
}
.fa-comments:before {
content: "\f086";
}
.fa-thumbs-o-up:before {
content: "\f087";
}
.fa-thumbs-o-down:before {
content: "\f088";
}
.fa-star-half:before {
content: "\f089";
}
.fa-heart-o:before {
content: "\f08a";
}
.fa-sign-out:before {
content: "\f08b";
}
.fa-linkedin-square:before {
content: "\f08c";
}
.fa-thumb-tack:before {
content: "\f08d";
}
.fa-external-link:before {
content: "\f08e";
}
.fa-sign-in:before {
content: "\f090";
}
.fa-trophy:before {
content: "\f091";
}
.fa-github-square:before {
content: "\f092";
}
.fa-upload:before {
content: "\f093";
}
.fa-lemon-o:before {
content: "\f094";
}
.fa-phone:before {
content: "\f095";
}
.fa-square-o:before {
content: "\f096";
}
.fa-bookmark-o:before {
content: "\f097";
}
.fa-phone-square:before {
content: "\f098";
}
.fa-twitter:before {
content: "\f099";
}
.fa-facebook-f:before,
.fa-facebook:before {
content: "\f09a";
}
.fa-github:before {
content: "\f09b";
}
.fa-unlock:before {
content: "\f09c";
}
.fa-credit-card:before {
content: "\f09d";
}
.fa-feed:before,
.fa-rss:before {
content: "\f09e";
}
.fa-hdd-o:before {
content: "\f0a0";
}
.fa-bullhorn:before {
content: "\f0a1";
}
.fa-bell:before {
content: "\f0f3";
}
.fa-certificate:before {
content: "\f0a3";
}
.fa-hand-o-right:before {
content: "\f0a4";
}
.fa-hand-o-left:before {
content: "\f0a5";
}
.fa-hand-o-up:before {
content: "\f0a6";
}
.fa-hand-o-down:before {
content: "\f0a7";
}
.fa-arrow-circle-left:before {
content: "\f0a8";
}
.fa-arrow-circle-right:before {
content: "\f0a9";
}
.fa-arrow-circle-up:before {
content: "\f0aa";
}
.fa-arrow-circle-down:before {
content: "\f0ab";
}
.fa-globe:before {
content: "\f0ac";
}
.fa-wrench:before {
content: "\f0ad";
}
.fa-tasks:before {
content: "\f0ae";
}
.fa-filter:before {
content: "\f0b0";
}
.fa-briefcase:before {
content: "\f0b1";
}
.fa-arrows-alt:before {
content: "\f0b2";
}
.fa-group:before,
.fa-users:before {
content: "\f0c0";
}
.fa-chain:before,
.fa-link:before {
content: "\f0c1";
}
.fa-cloud:before {
content: "\f0c2";
}
.fa-flask:before {
content: "\f0c3";
}
.fa-cut:before,
.fa-scissors:before {
content: "\f0c4";
}
.fa-copy:before,
.fa-files-o:before {
content: "\f0c5";
}
.fa-paperclip:before {
content: "\f0c6";
}
.fa-save:before,
.fa-floppy-o:before {
content: "\f0c7";
}
.fa-square:before {
content: "\f0c8";
}
.fa-navicon:before,
.fa-reorder:before,
.fa-bars:before {
content: "\f0c9";
}
.fa-list-ul:before {
content: "\f0ca";
}
.fa-list-ol:before {
content: "\f0cb";
}
.fa-strikethrough:before {
content: "\f0cc";
}
.fa-underline:before {
content: "\f0cd";
}
.fa-table:before {
content: "\f0ce";
}
.fa-magic:before {
content: "\f0d0";
}
.fa-truck:before {
content: "\f0d1";
}
.fa-pinterest:before {
content: "\f0d2";
}
.fa-pinterest-square:before {
content: "\f0d3";
}
.fa-google-plus-square:before {
content: "\f0d4";
}
.fa-google-plus:before {
content: "\f0d5";
}
.fa-money:before {
content: "\f0d6";
}
.fa-caret-down:before {
content: "\f0d7";
}
.fa-caret-up:before {
content: "\f0d8";
}
.fa-caret-left:before {
content: "\f0d9";
}
.fa-caret-right:before {
content: "\f0da";
}
.fa-columns:before {
content: "\f0db";
}
.fa-unsorted:before,
.fa-sort:before {
content: "\f0dc";
}
.fa-sort-down:before,
.fa-sort-desc:before {
content: "\f0dd";
}
.fa-sort-up:before,
.fa-sort-asc:before {
content: "\f0de";
}
.fa-envelope:before {
content: "\f0e0";
}
.fa-linkedin:before {
content: "\f0e1";
}
.fa-rotate-left:before,
.fa-undo:before {
content: "\f0e2";
}
.fa-legal:before,
.fa-gavel:before {
content: "\f0e3";
}
.fa-dashboard:before,
.fa-tachometer:before {
content: "\f0e4";
}
.fa-comment-o:before {
content: "\f0e5";
}
.fa-comments-o:before {
content: "\f0e6";
}
.fa-flash:before,
.fa-bolt:before {
content: "\f0e7";
}
.fa-sitemap:before {
content: "\f0e8";
}
.fa-umbrella:before {
content: "\f0e9";
}
.fa-paste:before,
.fa-clipboard:before {
content: "\f0ea";
}
.fa-lightbulb-o:before {
content: "\f0eb";
}
.fa-exchange:before {
content: "\f0ec";
}
.fa-cloud-download:before {
content: "\f0ed";
}
.fa-cloud-upload:before {
content: "\f0ee";
}
.fa-user-md:before {
content: "\f0f0";
}
.fa-stethoscope:before {
content: "\f0f1";
}
.fa-suitcase:before {
content: "\f0f2";
}
.fa-bell-o:before {
content: "\f0a2";
}
.fa-coffee:before {
content: "\f0f4";
}
.fa-cutlery:before {
content: "\f0f5";
}
.fa-file-text-o:before {
content: "\f0f6";
}
.fa-building-o:before {
content: "\f0f7";
}
.fa-hospital-o:before {
content: "\f0f8";
}
.fa-ambulance:before {
content: "\f0f9";
}
.fa-medkit:before {
content: "\f0fa";
}
.fa-fighter-jet:before {
content: "\f0fb";
}
.fa-beer:before {
content: "\f0fc";
}
.fa-h-square:before {
content: "\f0fd";
}
.fa-plus-square:before {
content: "\f0fe";
}
.fa-angle-double-left:before {
content: "\f100";
}
.fa-angle-double-right:before {
content: "\f101";
}
.fa-angle-double-up:before {
content: "\f102";
}
.fa-angle-double-down:before {
content: "\f103";
}
.fa-angle-left:before {
content: "\f104";
}
.fa-angle-right:before {
content: "\f105";
}
.fa-angle-up:before {
content: "\f106";
}
.fa-angle-down:before {
content: "\f107";
}
.fa-desktop:before {
content: "\f108";
}
.fa-laptop:before {
content: "\f109";
}
.fa-tablet:before {
content: "\f10a";
}
.fa-mobile-phone:before,
.fa-mobile:before {
content: "\f10b";
}
.fa-circle-o:before {
content: "\f10c";
}
.fa-quote-left:before {
content: "\f10d";
}
.fa-quote-right:before {
content: "\f10e";
}
.fa-spinner:before {
content: "\f110";
}
.fa-circle:before {
content: "\f111";
}
.fa-mail-reply:before,
.fa-reply:before {
content: "\f112";
}
.fa-github-alt:before {
content: "\f113";
}
.fa-folder-o:before {
content: "\f114";
}
.fa-folder-open-o:before {
content: "\f115";
}
.fa-smile-o:before {
content: "\f118";
}
.fa-frown-o:before {
content: "\f119";
}
.fa-meh-o:before {
content: "\f11a";
}
.fa-gamepad:before {
content: "\f11b";
}
.fa-keyboard-o:before {
content: "\f11c";
}
.fa-flag-o:before {
content: "\f11d";
}
.fa-flag-checkered:before {
content: "\f11e";
}
.fa-terminal:before {
content: "\f120";
}
.fa-code:before {
content: "\f121";
}
.fa-mail-reply-all:before,
.fa-reply-all:before {
content: "\f122";
}
.fa-star-half-empty:before,
.fa-star-half-full:before,
.fa-star-half-o:before {
content: "\f123";
}
.fa-location-arrow:before {
content: "\f124";
}
.fa-crop:before {
content: "\f125";
}
.fa-code-fork:before {
content: "\f126";
}
.fa-unlink:before,
.fa-chain-broken:before {
content: "\f127";
}
.fa-question:before {
content: "\f128";
}
.fa-info:before {
content: "\f129";
}
.fa-exclamation:before {
content: "\f12a";
}
.fa-superscript:before {
content: "\f12b";
}
.fa-subscript:before {
content: "\f12c";
}
.fa-eraser:before {
content: "\f12d";
}
.fa-puzzle-piece:before {
content: "\f12e";
}
.fa-microphone:before {
content: "\f130";
}
.fa-microphone-slash:before {
content: "\f131";
}
.fa-shield:before {
content: "\f132";
}
.fa-calendar-o:before {
content: "\f133";
}
.fa-fire-extinguisher:before {
content: "\f134";
}
.fa-rocket:before {
content: "\f135";
}
.fa-maxcdn:before {
content: "\f136";
}
.fa-chevron-circle-left:before {
content: "\f137";
}
.fa-chevron-circle-right:before {
content: "\f138";
}
.fa-chevron-circle-up:before {
content: "\f139";
}
.fa-chevron-circle-down:before {
content: "\f13a";
}
.fa-html5:before {
content: "\f13b";
}
.fa-css3:before {
content: "\f13c";
}
.fa-anchor:before {
content: "\f13d";
}
.fa-unlock-alt:before {
content: "\f13e";
}
.fa-bullseye:before {
content: "\f140";
}
.fa-ellipsis-h:before {
content: "\f141";
}
.fa-ellipsis-v:before {
content: "\f142";
}
.fa-rss-square:before {
content: "\f143";
}
.fa-play-circle:before {
content: "\f144";
}
.fa-ticket:before {
content: "\f145";
}
.fa-minus-square:before {
content: "\f146";
}
.fa-minus-square-o:before {
content: "\f147";
}
.fa-level-up:before {
content: "\f148";
}
.fa-level-down:before {
content: "\f149";
}
.fa-check-square:before {
content: "\f14a";
}
.fa-pencil-square:before {
content: "\f14b";
}
.fa-external-link-square:before {
content: "\f14c";
}
.fa-share-square:before {
content: "\f14d";
}
.fa-compass:before {
content: "\f14e";
}
.fa-toggle-down:before,
.fa-caret-square-o-down:before {
content: "\f150";
}
.fa-toggle-up:before,
.fa-caret-square-o-up:before {
content: "\f151";
}
.fa-toggle-right:before,
.fa-caret-square-o-right:before {
content: "\f152";
}
.fa-euro:before,
.fa-eur:before {
content: "\f153";
}
.fa-gbp:before {
content: "\f154";
}
.fa-dollar:before,
.fa-usd:before {
content: "\f155";
}
.fa-rupee:before,
.fa-inr:before {
content: "\f156";
}
.fa-cny:before,
.fa-rmb:before,
.fa-yen:before,
.fa-jpy:before {
content: "\f157";
}
.fa-ruble:before,
.fa-rouble:before,
.fa-rub:before {
content: "\f158";
}
.fa-won:before,
.fa-krw:before {
content: "\f159";
}
.fa-bitcoin:before,
.fa-btc:before {
content: "\f15a";
}
.fa-file:before {
content: "\f15b";
}
.fa-file-text:before {
content: "\f15c";
}
.fa-sort-alpha-asc:before {
content: "\f15d";
}
.fa-sort-alpha-desc:before {
content: "\f15e";
}
.fa-sort-amount-asc:before {
content: "\f160";
}
.fa-sort-amount-desc:before {
content: "\f161";
}
.fa-sort-numeric-asc:before {
content: "\f162";
}
.fa-sort-numeric-desc:before {
content: "\f163";
}
.fa-thumbs-up:before {
content: "\f164";
}
.fa-thumbs-down:before {
content: "\f165";
}
.fa-youtube-square:before {
content: "\f166";
}
.fa-youtube:before {
content: "\f167";
}
.fa-xing:before {
content: "\f168";
}
.fa-xing-square:before {
content: "\f169";
}
.fa-youtube-play:before {
content: "\f16a";
}
.fa-dropbox:before {
content: "\f16b";
}
.fa-stack-overflow:before {
content: "\f16c";
}
.fa-instagram:before {
content: "\f16d";
}
.fa-flickr:before {
content: "\f16e";
}
.fa-adn:before {
content: "\f170";
}
.fa-bitbucket:before {
content: "\f171";
}
.fa-bitbucket-square:before {
content: "\f172";
}
.fa-tumblr:before {
content: "\f173";
}
.fa-tumblr-square:before {
content: "\f174";
}
.fa-long-arrow-down:before {
content: "\f175";
}
.fa-long-arrow-up:before {
content: "\f176";
}
.fa-long-arrow-left:before {
content: "\f177";
}
.fa-long-arrow-right:before {
content: "\f178";
}
.fa-apple:before {
content: "\f179";
}
.fa-windows:before {
content: "\f17a";
}
.fa-android:before {
content: "\f17b";
}
.fa-linux:before {
content: "\f17c";
}
.fa-dribbble:before {
content: "\f17d";
}
.fa-skype:before {
content: "\f17e";
}
.fa-foursquare:before {
content: "\f180";
}
.fa-trello:before {
content: "\f181";
}
.fa-female:before {
content: "\f182";
}
.fa-male:before {
content: "\f183";
}
.fa-gittip:before,
.fa-gratipay:before {
content: "\f184";
}
.fa-sun-o:before {
content: "\f185";
}
.fa-moon-o:before {
content: "\f186";
}
.fa-archive:before {
content: "\f187";
}
.fa-bug:before {
content: "\f188";
}
.fa-vk:before {
content: "\f189";
}
.fa-weibo:before {
content: "\f18a";
}
.fa-renren:before {
content: "\f18b";
}
.fa-pagelines:before {
content: "\f18c";
}
.fa-stack-exchange:before {
content: "\f18d";
}
.fa-arrow-circle-o-right:before {
content: "\f18e";
}
.fa-arrow-circle-o-left:before {
content: "\f190";
}
.fa-toggle-left:before,
.fa-caret-square-o-left:before {
content: "\f191";
}
.fa-dot-circle-o:before {
content: "\f192";
}
.fa-wheelchair:before {
content: "\f193";
}
.fa-vimeo-square:before {
content: "\f194";
}
.fa-turkish-lira:before,
.fa-try:before {
content: "\f195";
}
.fa-plus-square-o:before {
content: "\f196";
}
.fa-space-shuttle:before {
content: "\f197";
}
.fa-slack:before {
content: "\f198";
}
.fa-envelope-square:before {
content: "\f199";
}
.fa-wordpress:before {
content: "\f19a";
}
.fa-openid:before {
content: "\f19b";
}
.fa-institution:before,
.fa-bank:before,
.fa-university:before {
content: "\f19c";
}
.fa-mortar-board:before,
.fa-graduation-cap:before {
content: "\f19d";
}
.fa-yahoo:before {
content: "\f19e";
}
.fa-google:before {
content: "\f1a0";
}
.fa-reddit:before {
content: "\f1a1";
}
.fa-reddit-square:before {
content: "\f1a2";
}
.fa-stumbleupon-circle:before {
content: "\f1a3";
}
.fa-stumbleupon:before {
content: "\f1a4";
}
.fa-delicious:before {
content: "\f1a5";
}
.fa-digg:before {
content: "\f1a6";
}
.fa-pied-piper-pp:before {
content: "\f1a7";
}
.fa-pied-piper-alt:before {
content: "\f1a8";
}
.fa-drupal:before {
content: "\f1a9";
}
.fa-joomla:before {
content: "\f1aa";
}
.fa-language:before {
content: "\f1ab";
}
.fa-fax:before {
content: "\f1ac";
}
.fa-building:before {
content: "\f1ad";
}
.fa-child:before {
content: "\f1ae";
}
.fa-paw:before {
content: "\f1b0";
}
.fa-spoon:before {
content: "\f1b1";
}
.fa-cube:before {
content: "\f1b2";
}
.fa-cubes:before {
content: "\f1b3";
}
.fa-behance:before {
content: "\f1b4";
}
.fa-behance-square:before {
content: "\f1b5";
}
.fa-steam:before {
content: "\f1b6";
}
.fa-steam-square:before {
content: "\f1b7";
}
.fa-recycle:before {
content: "\f1b8";
}
.fa-automobile:before,
.fa-car:before {
content: "\f1b9";
}
.fa-cab:before,
.fa-taxi:before {
content: "\f1ba";
}
.fa-tree:before {
content: "\f1bb";
}
.fa-spotify:before {
content: "\f1bc";
}
.fa-deviantart:before {
content: "\f1bd";
}
.fa-soundcloud:before {
content: "\f1be";
}
.fa-database:before {
content: "\f1c0";
}
.fa-file-pdf-o:before {
content: "\f1c1";
}
.fa-file-word-o:before {
content: "\f1c2";
}
.fa-file-excel-o:before {
content: "\f1c3";
}
.fa-file-powerpoint-o:before {
content: "\f1c4";
}
.fa-file-photo-o:before,
.fa-file-picture-o:before,
.fa-file-image-o:before {
content: "\f1c5";
}
.fa-file-zip-o:before,
.fa-file-archive-o:before {
content: "\f1c6";
}
.fa-file-sound-o:before,
.fa-file-audio-o:before {
content: "\f1c7";
}
.fa-file-movie-o:before,
.fa-file-video-o:before {
content: "\f1c8";
}
.fa-file-code-o:before {
content: "\f1c9";
}
.fa-vine:before {
content: "\f1ca";
}
.fa-codepen:before {
content: "\f1cb";
}
.fa-jsfiddle:before {
content: "\f1cc";
}
.fa-life-bouy:before,
.fa-life-buoy:before,
.fa-life-saver:before,
.fa-support:before,
.fa-life-ring:before {
content: "\f1cd";
}
.fa-circle-o-notch:before {
content: "\f1ce";
}
.fa-ra:before,
.fa-resistance:before,
.fa-rebel:before {
content: "\f1d0";
}
.fa-ge:before,
.fa-empire:before {
content: "\f1d1";
}
.fa-git-square:before {
content: "\f1d2";
}
.fa-git:before {
content: "\f1d3";
}
.fa-y-combinator-square:before,
.fa-yc-square:before,
.fa-hacker-news:before {
content: "\f1d4";
}
.fa-tencent-weibo:before {
content: "\f1d5";
}
.fa-qq:before {
content: "\f1d6";
}
.fa-wechat:before,
.fa-weixin:before {
content: "\f1d7";
}
.fa-send:before,
.fa-paper-plane:before {
content: "\f1d8";
}
.fa-send-o:before,
.fa-paper-plane-o:before {
content: "\f1d9";
}
.fa-history:before {
content: "\f1da";
}
.fa-circle-thin:before {
content: "\f1db";
}
.fa-header:before {
content: "\f1dc";
}
.fa-paragraph:before {
content: "\f1dd";
}
.fa-sliders:before {
content: "\f1de";
}
.fa-share-alt:before {
content: "\f1e0";
}
.fa-share-alt-square:before {
content: "\f1e1";
}
.fa-bomb:before {
content: "\f1e2";
}
.fa-soccer-ball-o:before,
.fa-futbol-o:before {
content: "\f1e3";
}
.fa-tty:before {
content: "\f1e4";
}
.fa-binoculars:before {
content: "\f1e5";
}
.fa-plug:before {
content: "\f1e6";
}
.fa-slideshare:before {
content: "\f1e7";
}
.fa-twitch:before {
content: "\f1e8";
}
.fa-yelp:before {
content: "\f1e9";
}
.fa-newspaper-o:before {
content: "\f1ea";
}
.fa-wifi:before {
content: "\f1eb";
}
.fa-calculator:before {
content: "\f1ec";
}
.fa-paypal:before {
content: "\f1ed";
}
.fa-google-wallet:before {
content: "\f1ee";
}
.fa-cc-visa:before {
content: "\f1f0";
}
.fa-cc-mastercard:before {
content: "\f1f1";
}
.fa-cc-discover:before {
content: "\f1f2";
}
.fa-cc-amex:before {
content: "\f1f3";
}
.fa-cc-paypal:before {
content: "\f1f4";
}
.fa-cc-stripe:before {
content: "\f1f5";
}
.fa-bell-slash:before {
content: "\f1f6";
}
.fa-bell-slash-o:before {
content: "\f1f7";
}
.fa-trash:before {
content: "\f1f8";
}
.fa-copyright:before {
content: "\f1f9";
}
.fa-at:before {
content: "\f1fa";
}
.fa-eyedropper:before {
content: "\f1fb";
}
.fa-paint-brush:before {
content: "\f1fc";
}
.fa-birthday-cake:before {
content: "\f1fd";
}
.fa-area-chart:before {
content: "\f1fe";
}
.fa-pie-chart:before {
content: "\f200";
}
.fa-line-chart:before {
content: "\f201";
}
.fa-lastfm:before {
content: "\f202";
}
.fa-lastfm-square:before {
content: "\f203";
}
.fa-toggle-off:before {
content: "\f204";
}
.fa-toggle-on:before {
content: "\f205";
}
.fa-bicycle:before {
content: "\f206";
}
.fa-bus:before {
content: "\f207";
}
.fa-ioxhost:before {
content: "\f208";
}
.fa-angellist:before {
content: "\f209";
}
.fa-cc:before {
content: "\f20a";
}
.fa-shekel:before,
.fa-sheqel:before,
.fa-ils:before {
content: "\f20b";
}
.fa-meanpath:before {
content: "\f20c";
}
.fa-buysellads:before {
content: "\f20d";
}
.fa-connectdevelop:before {
content: "\f20e";
}
.fa-dashcube:before {
content: "\f210";
}
.fa-forumbee:before {
content: "\f211";
}
.fa-leanpub:before {
content: "\f212";
}
.fa-sellsy:before {
content: "\f213";
}
.fa-shirtsinbulk:before {
content: "\f214";
}
.fa-simplybuilt:before {
content: "\f215";
}
.fa-skyatlas:before {
content: "\f216";
}
.fa-cart-plus:before {
content: "\f217";
}
.fa-cart-arrow-down:before {
content: "\f218";
}
.fa-diamond:before {
content: "\f219";
}
.fa-ship:before {
content: "\f21a";
}
.fa-user-secret:before {
content: "\f21b";
}
.fa-motorcycle:before {
content: "\f21c";
}
.fa-street-view:before {
content: "\f21d";
}
.fa-heartbeat:before {
content: "\f21e";
}
.fa-venus:before {
content: "\f221";
}
.fa-mars:before {
content: "\f222";
}
.fa-mercury:before {
content: "\f223";
}
.fa-intersex:before,
.fa-transgender:before {
content: "\f224";
}
.fa-transgender-alt:before {
content: "\f225";
}
.fa-venus-double:before {
content: "\f226";
}
.fa-mars-double:before {
content: "\f227";
}
.fa-venus-mars:before {
content: "\f228";
}
.fa-mars-stroke:before {
content: "\f229";
}
.fa-mars-stroke-v:before {
content: "\f22a";
}
.fa-mars-stroke-h:before {
content: "\f22b";
}
.fa-neuter:before {
content: "\f22c";
}
.fa-genderless:before {
content: "\f22d";
}
.fa-facebook-official:before {
content: "\f230";
}
.fa-pinterest-p:before {
content: "\f231";
}
.fa-whatsapp:before {
content: "\f232";
}
.fa-server:before {
content: "\f233";
}
.fa-user-plus:before {
content: "\f234";
}
.fa-user-times:before {
content: "\f235";
}
.fa-hotel:before,
.fa-bed:before {
content: "\f236";
}
.fa-viacoin:before {
content: "\f237";
}
.fa-train:before {
content: "\f238";
}
.fa-subway:before {
content: "\f239";
}
.fa-medium:before {
content: "\f23a";
}
.fa-yc:before,
.fa-y-combinator:before {
content: "\f23b";
}
.fa-optin-monster:before {
content: "\f23c";
}
.fa-opencart:before {
content: "\f23d";
}
.fa-expeditedssl:before {
content: "\f23e";
}
.fa-battery-4:before,
.fa-battery:before,
.fa-battery-full:before {
content: "\f240";
}
.fa-battery-3:before,
.fa-battery-three-quarters:before {
content: "\f241";
}
.fa-battery-2:before,
.fa-battery-half:before {
content: "\f242";
}
.fa-battery-1:before,
.fa-battery-quarter:before {
content: "\f243";
}
.fa-battery-0:before,
.fa-battery-empty:before {
content: "\f244";
}
.fa-mouse-pointer:before {
content: "\f245";
}
.fa-i-cursor:before {
content: "\f246";
}
.fa-object-group:before {
content: "\f247";
}
.fa-object-ungroup:before {
content: "\f248";
}
.fa-sticky-note:before {
content: "\f249";
}
.fa-sticky-note-o:before {
content: "\f24a";
}
.fa-cc-jcb:before {
content: "\f24b";
}
.fa-cc-diners-club:before {
content: "\f24c";
}
.fa-clone:before {
content: "\f24d";
}
.fa-balance-scale:before {
content: "\f24e";
}
.fa-hourglass-o:before {
content: "\f250";
}
.fa-hourglass-1:before,
.fa-hourglass-start:before {
content: "\f251";
}
.fa-hourglass-2:before,
.fa-hourglass-half:before {
content: "\f252";
}
.fa-hourglass-3:before,
.fa-hourglass-end:before {
content: "\f253";
}
.fa-hourglass:before {
content: "\f254";
}
.fa-hand-grab-o:before,
.fa-hand-rock-o:before {
content: "\f255";
}
.fa-hand-stop-o:before,
.fa-hand-paper-o:before {
content: "\f256";
}
.fa-hand-scissors-o:before {
content: "\f257";
}
.fa-hand-lizard-o:before {
content: "\f258";
}
.fa-hand-spock-o:before {
content: "\f259";
}
.fa-hand-pointer-o:before {
content: "\f25a";
}
.fa-hand-peace-o:before {
content: "\f25b";
}
.fa-trademark:before {
content: "\f25c";
}
.fa-registered:before {
content: "\f25d";
}
.fa-creative-commons:before {
content: "\f25e";
}
.fa-gg:before {
content: "\f260";
}
.fa-gg-circle:before {
content: "\f261";
}
.fa-tripadvisor:before {
content: "\f262";
}
.fa-odnoklassniki:before {
content: "\f263";
}
.fa-odnoklassniki-square:before {
content: "\f264";
}
.fa-get-pocket:before {
content: "\f265";
}
.fa-wikipedia-w:before {
content: "\f266";
}
.fa-safari:before {
content: "\f267";
}
.fa-chrome:before {
content: "\f268";
}
.fa-firefox:before {
content: "\f269";
}
.fa-opera:before {
content: "\f26a";
}
.fa-internet-explorer:before {
content: "\f26b";
}
.fa-tv:before,
.fa-television:before {
content: "\f26c";
}
.fa-contao:before {
content: "\f26d";
}
.fa-500px:before {
content: "\f26e";
}
.fa-amazon:before {
content: "\f270";
}
.fa-calendar-plus-o:before {
content: "\f271";
}
.fa-calendar-minus-o:before {
content: "\f272";
}
.fa-calendar-times-o:before {
content: "\f273";
}
.fa-calendar-check-o:before {
content: "\f274";
}
.fa-industry:before {
content: "\f275";
}
.fa-map-pin:before {
content: "\f276";
}
.fa-map-signs:before {
content: "\f277";
}
.fa-map-o:before {
content: "\f278";
}
.fa-map:before {
content: "\f279";
}
.fa-commenting:before {
content: "\f27a";
}
.fa-commenting-o:before {
content: "\f27b";
}
.fa-houzz:before {
content: "\f27c";
}
.fa-vimeo:before {
content: "\f27d";
}
.fa-black-tie:before {
content: "\f27e";
}
.fa-fonticons:before {
content: "\f280";
}
.fa-reddit-alien:before {
content: "\f281";
}
.fa-edge:before {
content: "\f282";
}
.fa-credit-card-alt:before {
content: "\f283";
}
.fa-codiepie:before {
content: "\f284";
}
.fa-modx:before {
content: "\f285";
}
.fa-fort-awesome:before {
content: "\f286";
}
.fa-usb:before {
content: "\f287";
}
.fa-product-hunt:before {
content: "\f288";
}
.fa-mixcloud:before {
content: "\f289";
}
.fa-scribd:before {
content: "\f28a";
}
.fa-pause-circle:before {
content: "\f28b";
}
.fa-pause-circle-o:before {
content: "\f28c";
}
.fa-stop-circle:before {
content: "\f28d";
}
.fa-stop-circle-o:before {
content: "\f28e";
}
.fa-shopping-bag:before {
content: "\f290";
}
.fa-shopping-basket:before {
content: "\f291";
}
.fa-hashtag:before {
content: "\f292";
}
.fa-bluetooth:before {
content: "\f293";
}
.fa-bluetooth-b:before {
content: "\f294";
}
.fa-percent:before {
content: "\f295";
}
.fa-gitlab:before {
content: "\f296";
}
.fa-wpbeginner:before {
content: "\f297";
}
.fa-wpforms:before {
content: "\f298";
}
.fa-envira:before {
content: "\f299";
}
.fa-universal-access:before {
content: "\f29a";
}
.fa-wheelchair-alt:before {
content: "\f29b";
}
.fa-question-circle-o:before {
content: "\f29c";
}
.fa-blind:before {
content: "\f29d";
}
.fa-audio-description:before {
content: "\f29e";
}
.fa-volume-control-phone:before {
content: "\f2a0";
}
.fa-braille:before {
content: "\f2a1";
}
.fa-assistive-listening-systems:before {
content: "\f2a2";
}
.fa-asl-interpreting:before,
.fa-american-sign-language-interpreting:before {
content: "\f2a3";
}
.fa-deafness:before,
.fa-hard-of-hearing:before,
.fa-deaf:before {
content: "\f2a4";
}
.fa-glide:before {
content: "\f2a5";
}
.fa-glide-g:before {
content: "\f2a6";
}
.fa-signing:before,
.fa-sign-language:before {
content: "\f2a7";
}
.fa-low-vision:before {
content: "\f2a8";
}
.fa-viadeo:before {
content: "\f2a9";
}
.fa-viadeo-square:before {
content: "\f2aa";
}
.fa-snapchat:before {
content: "\f2ab";
}
.fa-snapchat-ghost:before {
content: "\f2ac";
}
.fa-snapchat-square:before {
content: "\f2ad";
}
.fa-pied-piper:before {
content: "\f2ae";
}
.fa-first-order:before {
content: "\f2b0";
}
.fa-yoast:before {
content: "\f2b1";
}
.fa-themeisle:before {
content: "\f2b2";
}
.fa-google-plus-circle:before,
.fa-google-plus-official:before {
content: "\f2b3";
}
.fa-fa:before,
.fa-font-awesome:before {
content: "\f2b4";
}
.fa-handshake-o:before {
content: "\f2b5";
}
.fa-envelope-open:before {
content: "\f2b6";
}
.fa-envelope-open-o:before {
content: "\f2b7";
}
.fa-linode:before {
content: "\f2b8";
}
.fa-address-book:before {
content: "\f2b9";
}
.fa-address-book-o:before {
content: "\f2ba";
}
.fa-vcard:before,
.fa-address-card:before {
content: "\f2bb";
}
.fa-vcard-o:before,
.fa-address-card-o:before {
content: "\f2bc";
}
.fa-user-circle:before {
content: "\f2bd";
}
.fa-user-circle-o:before {
content: "\f2be";
}
.fa-user-o:before {
content: "\f2c0";
}
.fa-id-badge:before {
content: "\f2c1";
}
.fa-drivers-license:before,
.fa-id-card:before {
content: "\f2c2";
}
.fa-drivers-license-o:before,
.fa-id-card-o:before {
content: "\f2c3";
}
.fa-quora:before {
content: "\f2c4";
}
.fa-free-code-camp:before {
content: "\f2c5";
}
.fa-telegram:before {
content: "\f2c6";
}
.fa-thermometer-4:before,
.fa-thermometer:before,
.fa-thermometer-full:before {
content: "\f2c7";
}
.fa-thermometer-3:before,
.fa-thermometer-three-quarters:before {
content: "\f2c8";
}
.fa-thermometer-2:before,
.fa-thermometer-half:before {
content: "\f2c9";
}
.fa-thermometer-1:before,
.fa-thermometer-quarter:before {
content: "\f2ca";
}
.fa-thermometer-0:before,
.fa-thermometer-empty:before {
content: "\f2cb";
}
.fa-shower:before {
content: "\f2cc";
}
.fa-bathtub:before,
.fa-s15:before,
.fa-bath:before {
content: "\f2cd";
}
.fa-podcast:before {
content: "\f2ce";
}
.fa-window-maximize:before {
content: "\f2d0";
}
.fa-window-minimize:before {
content: "\f2d1";
}
.fa-window-restore:before {
content: "\f2d2";
}
.fa-times-rectangle:before,
.fa-window-close:before {
content: "\f2d3";
}
.fa-times-rectangle-o:before,
.fa-window-close-o:before {
content: "\f2d4";
}
.fa-bandcamp:before {
content: "\f2d5";
}
.fa-grav:before {
content: "\f2d6";
}
.fa-etsy:before {
content: "\f2d7";
}
.fa-imdb:before {
content: "\f2d8";
}
.fa-ravelry:before {
content: "\f2d9";
}
.fa-eercast:before {
content: "\f2da";
}
.fa-microchip:before {
content: "\f2db";
}
.fa-snowflake-o:before {
content: "\f2dc";
}
.fa-superpowers:before {
content: "\f2dd";
}
.fa-wpexplorer:before {
content: "\f2de";
}
.fa-meetup:before {
content: "\f2e0";
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
================================================
FILE: src/components/Backtop/BackTop.vue
================================================
<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>
================================================
FILE: src/components/Comment/Comments.vue
================================================
<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>
================================================
FILE: src/components/Comment/CommentsForm.vue
================================================
<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>
================================================
FILE: src/components/Comment/CommentsItem.vue
================================================
<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>
================================================
FILE: src/components/Layout/Footer.vue
================================================
<template>
<footer class="footer has-text-grey-light has-background-grey-darker">
<div class="container">
<div class="">
<span>简洁、实用、美观</span>
<span style="float: right">
<router-link :to="{path:'/admin/login'}">
管理员登录
</router-link>
|
<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 type="is-dark" size="is-normal">Design</b-tag>
<b-tag type="is-info" size="is-normal">{{ 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() + ' 豆约翰',
author: '豆约翰',
};
},
};
</script>
<style scoped>
footer {
margin-top: 120px;
height: 150px;
}
footer a{
color: #bfbfbf;
}
</style>
================================================
FILE: src/components/Layout/Header.vue
================================================
<template>
<header class="header has-background-white has-text-black">
<b-navbar
class="container is-white"
:fixed-top="true"
>
<template slot="brand">
<b-navbar-item tag="div">
<img :src="doubaoImg" alt="logo">
</b-navbar-item>
<b-navbar-item
class="is-hidden-desktop"
tag="router-link"
:to="{ path: '/' }"
>
主页
</b-navbar-item>
</template>
<template slot="start">
<b-navbar-item
tag="router-link"
:to="{ path: '/' }"
>
🌐 主页
</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"
width="80%"
placeholder="搜索帖子、标签和用户"
rounded
clearable
@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
class="is-light"
tag="router-link"
:to="{ path: '/register' }"
>
注册
</b-button>
<b-button
class="is-light"
tag="router-link"
:to="{ path: '/login' }"
>
登录
</b-button>
</div>
</b-navbar-item>
<b-navbar-dropdown
v-else
:label="user.alias"
>
<b-navbar-item
tag="router-link"
:to="{ path: `/member/${user.username}/home` }"
>
🧘 个人中心
</b-navbar-item>
<hr class="dropdown-divider">
<b-navbar-item
tag="router-link"
:to="{ path: `/member/${user.username}/setting` }"
>
⚙ 设置中心
</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>
================================================
FILE: src/components/Pagination/index.vue
================================================
<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>
================================================
FILE: src/main.js
================================================
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')
================================================
FILE: src/permission.js
================================================
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()
})
================================================
FILE: src/router/index.js
================================================
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;
================================================
FILE: src/store/getters.js
================================================
const getters = {
token: state => state.user.token, // token
user: state => state.user.user, // 用户对象
}
export default getters
================================================
FILE: src/store/index.js
================================================
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
================================================
FILE: src/store/modules/user.js
================================================
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,
};
================================================
FILE: src/user.js
================================================
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
})
}
================================================
FILE: src/utils/auth.js
================================================
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));
}
================================================
FILE: src/utils/get-page-title.js
================================================
const title = '小而美的智慧社区系统'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}
================================================
FILE: src/utils/request.js
================================================
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
================================================
FILE: src/utils/scroll-to.js
================================================
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()
}
================================================
FILE: src/views/Home.vue
================================================
<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>
================================================
FILE: src/views/Search.vue
================================================
<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>
================================================
FILE: src/views/auth/Login.vue
================================================
<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>
================================================
FILE: src/views/auth/Register.vue
================================================
<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>
================================================
FILE: src/views/card/CardBar.vue
================================================
<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>
================================================
FILE: src/views/card/LoginWelcome.vue
================================================
<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>
================================================
FILE: src/views/card/Promotion.vue
================================================
<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>
================================================
FILE: src/views/card/Tip.vue
================================================
<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>
================================================
FILE: src/views/error/404.vue
================================================
<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>
================================================
FILE: src/views/post/Author.vue
================================================
<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>
================================================
FILE: src/views/post/Create.vue
================================================
<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>
================================================
FILE: src/views/post/Detail.vue
================================================
<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>
================================================
FILE: src/views/post/Edit.vue
================================================
<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>
================================================
FILE: src/views/post/Index.vue
================================================
<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 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>
</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 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>
</el-tab-pane>
</el-tabs>
</div>
<!--分页-->
<pagination
v-show="page.total > 0"
:total="page.total"
:page.sync="page.current"
:limit.sync="page.size"
@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>
================================================
FILE: src/views/post/Recommend.vue
================================================
<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>
================================================
FILE: src/views/tag/Tag.vue
================================================
<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>
================================================
FILE: src/views/user/Profile.vue
================================================
<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>
================================================
FILE: src/views/user/Setting.vue
================================================
<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>
gitextract_czw2jpqd/
├── .browserslistrc
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public/
│ └── index.html
└── src/
├── App.vue
├── api/
│ ├── auth/
│ │ └── auth.js
│ ├── billboard.js
│ ├── comment.js
│ ├── follow.js
│ ├── post.js
│ ├── promote.js
│ ├── search.js
│ ├── tag.js
│ ├── tip.js
│ └── user.js
├── assets/
│ ├── app.css
│ └── plugins/
│ └── font-awesome-4.7.0/
│ ├── css/
│ │ └── font-awesome.css
│ └── fonts/
│ └── FontAwesome.otf
├── components/
│ ├── Backtop/
│ │ └── BackTop.vue
│ ├── Comment/
│ │ ├── Comments.vue
│ │ ├── CommentsForm.vue
│ │ └── CommentsItem.vue
│ ├── Layout/
│ │ ├── Footer.vue
│ │ └── Header.vue
│ └── Pagination/
│ └── index.vue
├── main.js
├── permission.js
├── router/
│ └── index.js
├── store/
│ ├── getters.js
│ ├── index.js
│ └── modules/
│ └── user.js
├── user.js
├── utils/
│ ├── auth.js
│ ├── get-page-title.js
│ ├── request.js
│ └── scroll-to.js
└── views/
├── Home.vue
├── Search.vue
├── auth/
│ ├── Login.vue
│ └── Register.vue
├── card/
│ ├── CardBar.vue
│ ├── LoginWelcome.vue
│ ├── Promotion.vue
│ └── Tip.vue
├── error/
│ └── 404.vue
├── post/
│ ├── Author.vue
│ ├── Create.vue
│ ├── Detail.vue
│ ├── Edit.vue
│ ├── Index.vue
│ └── Recommend.vue
├── tag/
│ └── Tag.vue
└── user/
├── Profile.vue
└── Setting.vue
SYMBOL INDEX (39 symbols across 15 files)
FILE: src/api/auth/auth.js
function userRegister (line 4) | function userRegister(userDTO) {
function login (line 13) | function login(data) {
function getUserInfo (line 21) | function getUserInfo() {
function logout (line 28) | function logout() {
FILE: src/api/billboard.js
function getBillboard (line 3) | function getBillboard() {
FILE: src/api/comment.js
function fetchCommentsByTopicId (line 3) | function fetchCommentsByTopicId(topic_Id) {
function pushComment (line 13) | function pushComment(data) {
FILE: src/api/follow.js
function follow (line 4) | function follow(id) {
function unFollow (line 12) | function unFollow(id) {
function hasFollow (line 20) | function hasFollow(topicUserId) {
FILE: src/api/post.js
function getList (line 4) | function getList(pageNo, size, tab) {
function post (line 13) | function post(topic) {
function getTopic (line 22) | function getTopic(id) {
function getRecommendTopics (line 32) | function getRecommendTopics(id) {
function update (line 42) | function update(topic) {
function deleteTopic (line 50) | function deleteTopic(id) {
FILE: src/api/promote.js
function getList (line 4) | function getList() {
FILE: src/api/search.js
function searchByKeyword (line 4) | function searchByKeyword(query) {
FILE: src/api/tag.js
function getTopicsByTag (line 3) | function getTopicsByTag(paramMap) {
FILE: src/api/tip.js
function getTodayTip (line 3) | function getTodayTip() {
FILE: src/api/user.js
function getInfoByName (line 4) | function getInfoByName(username, page, size) {
function getInfo (line 15) | function getInfo() {
function update (line 22) | function update(user) {
FILE: src/store/modules/user.js
method login (line 20) | login({ commit }, userInfo) {
method getInfo (line 37) | getInfo({ commit, state }) {
method logout (line 58) | logout({ commit, state }) {
FILE: src/user.js
function getInfoByName (line 4) | function getInfoByName(username, page, size) {
function getInfo (line 16) | function getInfo() {
function update (line 24) | function update(user) {
FILE: src/utils/auth.js
function getToken (line 7) | function getToken() {
function setToken (line 12) | function setToken(token) {
function removeToken (line 17) | function removeToken() {
function removeAll (line 21) | function removeAll() {
function setDarkMode (line 25) | function setDarkMode(mode) {
function getDarkMode (line 29) | function getDarkMode() {
FILE: src/utils/get-page-title.js
function getPageTitle (line 3) | function getPageTitle(pageTitle) {
FILE: src/utils/scroll-to.js
function move (line 19) | function move(amount) {
function position (line 25) | function position() {
function scrollTo (line 34) | function scrollTo(to, duration, callback) {
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (131K chars).
[
{
"path": ".browserslistrc",
"chars": 30,
"preview": "> 1%\nlast 2 versions\nnot dead\n"
},
{
"path": ".gitignore",
"chars": 231,
"preview": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyar"
},
{
"path": "README.md",
"chars": 3195,
"preview": "### 豆宝社区项目实战视频教程简介\n本项目实战视频教程全部免费,配套代码完全开源。手把手从零开始搭建一个目前应用最广泛的Springboot+Vue前后端分离多用户社区项目。本项目难度适中,为便于大家学习,每一集视频教程对应在Github"
},
{
"path": "babel.config.js",
"chars": 73,
"preview": "module.exports = {\n presets: [\n '@vue/cli-plugin-babel/preset'\n ]\n}\n"
},
{
"path": "package.json",
"chars": 751,
"preview": "{\n \"name\": \"doubao_community_frontend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"serve\": \"vue-cli-s"
},
{
"path": "public/index.html",
"chars": 611,
"preview": "<!DOCTYPE html>\n<html lang=\"\">\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
},
{
"path": "src/App.vue",
"chars": 505,
"preview": "<template>\n <div>\n <div class=\"mb-5\">\n <Header></Header>\n </div>\n\n <div class=\"container context\">\n "
},
{
"path": "src/api/auth/auth.js",
"chars": 512,
"preview": "import request from '@/utils/request'\n\n// 注册\nexport function userRegister(userDTO) {\n return request({\n url: '/ums/u"
},
{
"path": "src/api/billboard.js",
"chars": 143,
"preview": "import request from '@/utils/request'\n\nexport function getBillboard() {\n return request({\n url: '/billboard/show',\n "
},
{
"path": "src/api/comment.js",
"chars": 345,
"preview": "import request from '@/utils/request'\n\nexport function fetchCommentsByTopicId(topic_Id) {\n return request({\n url: '/"
},
{
"path": "src/api/follow.js",
"chars": 438,
"preview": "import request from '@/utils/request'\n\n// 关注\nexport function follow(id) {\n return request(({\n url: `/relationship/su"
},
{
"path": "src/api/post.js",
"chars": 868,
"preview": "import request from '@/utils/request'\n\n// 列表\nexport function getList(pageNo, size, tab) {\n return request(({\n url: '"
},
{
"path": "src/api/promote.js",
"chars": 148,
"preview": "import request from '@/utils/request'\n\n// 获取推广\nexport function getList() {\n return request(({\n url: '/promotion/all'"
},
{
"path": "src/api/search.js",
"chars": 265,
"preview": "import request from '@/utils/request'\n\n// 关键词检索\nexport function searchByKeyword(query) {\n return request({\n url: `/s"
},
{
"path": "src/api/tag.js",
"chars": 234,
"preview": "import request from '@/utils/request'\n\nexport function getTopicsByTag(paramMap) {\n return request({\n url: '/tag/' + "
},
{
"path": "src/api/tip.js",
"chars": 137,
"preview": "import request from '@/utils/request'\n\nexport function getTodayTip() {\n return request({\n url: '/tip/today',\n met"
},
{
"path": "src/api/user.js",
"chars": 473,
"preview": "import request from '@/utils/request'\n\n// 用户主页\nexport function getInfoByName(username, page, size) {\n return request({\n"
},
{
"path": "src/assets/app.css",
"chars": 2296,
"preview": "* {\n margin: 0;\n padding: 0;\n}\n\nbody,\nhtml {\n background-color: #f6f6f6;\n color: black;\n width: 100%;\n font-size: "
},
{
"path": "src/assets/plugins/font-awesome-4.7.0/css/font-awesome.css",
"chars": 37414,
"preview": "/*!\n * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome\n * License - http://fontawesome.io/lice"
},
{
"path": "src/components/Backtop/BackTop.vue",
"chars": 480,
"preview": "<template>\n <el-backtop :bottom=\"60\" :right=\"60\">\n <div title=\"回到顶部\"\n style=\"{\n height: 100%;\n "
},
{
"path": "src/components/Comment/Comments.vue",
"chars": 1107,
"preview": "<template>\n <section class=\"box comments\">\n <hr />\n <h3 class=\"title is-5\">Comments</h3>\n <lv-comments-form :s"
},
{
"path": "src/components/Comment/CommentsForm.vue",
"chars": 1548,
"preview": "<template>\n <article class=\"media\">\n <div class=\"media-content\">\n <form @submit.prevent=\"onSubmit\">\n <b-"
},
{
"path": "src/components/Comment/CommentsItem.vue",
"chars": 634,
"preview": "<template>\n <article class=\"media\">\n <figure class=\"media-left image is-48x48\">\n <img :src=\"`https://cn.gravata"
},
{
"path": "src/components/Layout/Footer.vue",
"chars": 1217,
"preview": "<template>\n <footer class=\"footer has-text-grey-light has-background-grey-darker\">\n <div class=\"container\">\n <d"
},
{
"path": "src/components/Layout/Header.vue",
"chars": 4225,
"preview": "<template>\n <header class=\"header has-background-white has-text-black\">\n <b-navbar\n class=\"container is-white\"\n"
},
{
"path": "src/components/Pagination/index.vue",
"chars": 1967,
"preview": "<template>\n <div :class=\"{ hidden: hidden }\" class=\"pagination-container\">\n <el-pagination\n :background=\"backgr"
},
{
"path": "src/main.js",
"chars": 964,
"preview": "import Vue from 'vue'\nimport App from './App.vue'\nimport router from './router'\nimport store from './store'\n// Buefy\nimp"
},
{
"path": "src/permission.js",
"chars": 1010,
"preview": "import router from './router'\nimport store from './store'\nimport getPageTitle from '@/utils/get-page-title'\n\nimport NPro"
},
{
"path": "src/router/index.js",
"chars": 1976,
"preview": "import Vue from \"vue\";\nimport VueRouter from \"vue-router\";\n\nVue.use(VueRouter);\n\nconst routes = [\n {\n path: \"/\",\n "
},
{
"path": "src/store/getters.js",
"chars": 135,
"preview": "const getters = {\n token: state => state.user.token, // token\n user: state => state.user.user, // 用户对象\n}\nexport "
},
{
"path": "src/store/index.js",
"chars": 230,
"preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport getters from './getters'\nimport user from './modules/user'\n\nVue.use"
},
{
"path": "src/store/modules/user.js",
"chars": 1906,
"preview": "import { getUserInfo, login, logout } from \"@/api/auth/auth\";\nimport { getToken, setToken, removeToken } from \"@/utils/a"
},
{
"path": "src/user.js",
"chars": 473,
"preview": "import request from '@/utils/request'\n\n// 用户主页\nexport function getInfoByName(username, page, size) {\n return request({\n"
},
{
"path": "src/utils/auth.js",
"chars": 641,
"preview": "import Cookies from 'js-cookie'\n\nconst uToken = 'u_token'\nconst darkMode = 'dark_mode';\n\n// 获取Token\nexport function getT"
},
{
"path": "src/utils/get-page-title.js",
"chars": 170,
"preview": "const title = '小而美的智慧社区系统'\n\nexport default function getPageTitle(pageTitle) {\n if (pageTitle) {\n return `${pag"
},
{
"path": "src/utils/request.js",
"chars": 2205,
"preview": "import axios from 'axios'\nimport { Message, MessageBox } from 'element-ui'\nimport store from '@/store'\nimport { getToken"
},
{
"path": "src/utils/scroll-to.js",
"chars": 1714,
"preview": "Math.easeInOutQuad = function(t, b, c, d) {\n t /= d / 2\n if (t < 1) {\n return c / 2 * t * t + b\n }\n t--\n return "
},
{
"path": "src/views/Home.vue",
"chars": 821,
"preview": "<template>\n <div>\n <div class=\"box\">🔔 {{ billboard.content }}</div>\n <div class=\"columns\">\n <div class=\"colu"
},
{
"path": "src/views/Search.vue",
"chars": 3006,
"preview": "<template>\n <div>\n <el-card shadow=\"never\">\n <div slot=\"header\" class=\"clearfix\">\n 检索到 <code>{{ list.len"
},
{
"path": "src/views/auth/Login.vue",
"chars": 2871,
"preview": "<template>\n <div class=\"columns py-6\">\n <div class=\"column is-half is-offset-one-quarter\">\n <el-card shadow=\"ne"
},
{
"path": "src/views/auth/Register.vue",
"chars": 3890,
"preview": "<template>\n <div class=\"columns py-6\">\n <div class=\"column is-half is-offset-one-quarter\">\n <el-card shadow=\"ne"
},
{
"path": "src/views/card/CardBar.vue",
"chars": 441,
"preview": "<template>\n <section>\n <!--是否登录-->\n <login-welcome />\n\n <!--今日赠言-->\n <tip-card />\n\n <!--资源推介-->\n <Pro"
},
{
"path": "src/views/card/LoginWelcome.vue",
"chars": 780,
"preview": "<template>\n <el-card class=\"box-card\" shadow=\"never\">\n <div slot=\"header\">\n <span>💐 发帖</span>\n </div>\n <d"
},
{
"path": "src/views/card/Promotion.vue",
"chars": 666,
"preview": "<template>\n <el-card class=\"box-card\" shadow=\"never\">\n <div slot=\"header\">\n <span>🥂 推广</span>\n </div>\n <d"
},
{
"path": "src/views/card/Tip.vue",
"chars": 690,
"preview": "<template>\n <el-card class=\"box-card\" shadow=\"never\">\n <div slot=\"header\">\n <span>🥳 每日一句</span>\n </div>\n "
},
{
"path": "src/views/error/404.vue",
"chars": 693,
"preview": "<template>\n <div class=\"columns mt-6\">\n <div class=\"column mt-6\">\n <div class=\"mt-6\">\n <p class=\"content"
},
{
"path": "src/views/post/Author.vue",
"chars": 2529,
"preview": "<template>\n <section id=\"author\">\n <el-card class=\"\" shadow=\"never\">\n <div slot=\"header\">\n <span class=\""
},
{
"path": "src/views/post/Create.vue",
"chars": 3498,
"preview": "<template>\n <div class=\"columns\">\n <div class=\"column is-full\">\n <el-card\n class=\"box-card\"\n shad"
},
{
"path": "src/views/post/Detail.vue",
"chars": 3707,
"preview": "<template>\n <div class=\"columns\">\n <!--文章详情-->\n <div class=\"column is-three-quarters\">\n <!--主题-->\n <el-"
},
{
"path": "src/views/post/Edit.vue",
"chars": 2677,
"preview": "<template>\n <section>\n <div class=\"columns\">\n <div class=\"column is-full\">\n <el-card class=\"box-card\" sh"
},
{
"path": "src/views/post/Index.vue",
"chars": 5411,
"preview": "<template>\n <div>\n <el-card shadow=\"never\">\n <div slot=\"header\" class=\"clearfix\">\n <el-tabs v-model=\"act"
},
{
"path": "src/views/post/Recommend.vue",
"chars": 1119,
"preview": "<template>\n <el-card class=\"\" shadow=\"never\">\n <div slot=\"header\">\n <span class=\"has-text-weight-bold\">🧐 随便看看</"
},
{
"path": "src/views/tag/Tag.vue",
"chars": 2877,
"preview": "<template>\n <div id=\"tag\" class=\"columns\">\n <div class=\"column is-three-quarters\">\n <el-card class=\"box-card\" s"
},
{
"path": "src/views/user/Profile.vue",
"chars": 4057,
"preview": "<template>\n <div class=\"member\">\n <div class=\"columns\">\n <div class=\"column is-one-quarter\">\n <el-card s"
},
{
"path": "src/views/user/Setting.vue",
"chars": 3741,
"preview": "<template>\n <section>\n <el-card shadow=\"never\">\n <div slot=\"header\">\n 个人设置\n </div>\n <div class"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the songboriceman/doubao_community_frontend GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (112.1 KB), approximately 34.6k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.