├── static ├── .gitkeep └── project-structure.png ├── code ├── server │ ├── index.js │ ├── models │ │ ├── label.js │ │ ├── user.js │ │ ├── blog.js │ │ └── message.js │ ├── app.js │ ├── middleware │ │ ├── func │ │ │ ├── get_info.js │ │ │ ├── index.js │ │ │ └── file.js │ │ ├── log │ │ │ ├── index.js │ │ │ ├── access.js │ │ │ └── log.js │ │ ├── send │ │ │ └── index.js │ │ ├── auth │ │ │ └── index.js │ │ ├── rule │ │ │ └── index.js │ │ └── index.js │ ├── mongodb.js │ ├── config.js │ ├── controller │ │ ├── client │ │ │ ├── label.js │ │ │ ├── message.js │ │ │ └── blog.js │ │ └── admin │ │ │ ├── upload.js │ │ │ ├── message.js │ │ │ ├── label.js │ │ │ └── blog.js │ └── router │ │ └── index.js ├── client │ ├── src │ │ ├── images │ │ │ ├── bg4.jpg │ │ │ ├── logo.png │ │ │ ├── avatar.png │ │ │ ├── qrcode.jpg │ │ │ ├── wall.jpeg │ │ │ ├── back-top.png │ │ │ ├── blueprint.png │ │ │ ├── favicon.ico │ │ │ ├── loading.gif │ │ │ ├── none-data.jpg │ │ │ ├── cloud-left.png │ │ │ ├── cloud-right.png │ │ │ ├── load-error.jpeg │ │ │ ├── grass-confetti.png │ │ │ └── emoji │ │ │ │ ├── 2764.svg │ │ │ │ ├── 1f603.svg │ │ │ │ ├── 1f446.svg │ │ │ │ ├── 1f447.svg │ │ │ │ ├── icon.svg │ │ │ │ ├── 1f449.svg │ │ │ │ ├── 1f448.svg │ │ │ │ ├── 1f4aa.svg │ │ │ │ ├── 1f494.svg │ │ │ │ ├── 1f436.svg │ │ │ │ ├── 1f47b.svg │ │ │ │ ├── 1f632.svg │ │ │ │ ├── 1f613.svg │ │ │ │ ├── 1f604.svg │ │ │ │ ├── 1f637.svg │ │ │ │ ├── 1f60d.svg │ │ │ │ ├── 1f633.svg │ │ │ │ ├── 1f61c.svg │ │ │ │ ├── 1f625.svg │ │ │ │ ├── 1f60a.svg │ │ │ │ ├── 1f622.svg │ │ │ │ ├── 1f620.svg │ │ │ │ ├── 1f621.svg │ │ │ │ ├── 1f469.svg │ │ │ │ ├── 1f614.svg │ │ │ │ ├── 1f61e.svg │ │ │ │ ├── 1f601.svg │ │ │ │ ├── 1f466.svg │ │ │ │ ├── 1f628.svg │ │ │ │ ├── 1f609.svg │ │ │ │ ├── 1f4a9.svg │ │ │ │ ├── 1f630.svg │ │ │ │ ├── 1f612.svg │ │ │ │ ├── 1f61d.svg │ │ │ │ ├── 1f62d.svg │ │ │ │ ├── 1f44a.svg │ │ │ │ ├── 1f60c.svg │ │ │ │ ├── 1f60f.svg │ │ │ │ ├── 1f62a.svg │ │ │ │ ├── 1f623.svg │ │ │ │ ├── 1f385.svg │ │ │ │ ├── 1f468.svg │ │ │ │ ├── 1f631.svg │ │ │ │ ├── 1f44c.svg │ │ │ │ ├── 1f44e.svg │ │ │ │ ├── 1f44d.svg │ │ │ │ ├── 1f602.svg │ │ │ │ ├── 1f467.svg │ │ │ │ ├── 1f616.svg │ │ │ │ ├── 1f61a.svg │ │ │ │ ├── 1f618.svg │ │ │ │ ├── 270a.svg │ │ │ │ ├── 270c.svg │ │ │ │ ├── 1f44f.svg │ │ │ │ └── 1f431.svg │ │ ├── constant │ │ │ ├── headerColor.js │ │ │ └── side.js │ │ ├── api │ │ │ ├── label.js │ │ │ ├── blog.js │ │ │ └── message.js │ │ ├── style │ │ │ ├── index.less │ │ │ └── less │ │ │ │ ├── function.less │ │ │ │ ├── animation.less │ │ │ │ └── init.less │ │ ├── store │ │ │ ├── modules │ │ │ │ ├── search.js │ │ │ │ └── label.js │ │ │ └── index.js │ │ ├── components │ │ │ ├── common │ │ │ │ ├── index.js │ │ │ │ ├── noneData.vue │ │ │ │ ├── svgIcon.vue │ │ │ │ ├── breadCrumbs.vue │ │ │ │ ├── loading.vue │ │ │ │ └── tag.vue │ │ │ ├── copyRight.vue │ │ │ ├── top.vue │ │ │ ├── labelClassify.vue │ │ │ ├── myself.vue │ │ │ ├── github.vue │ │ │ ├── search.vue │ │ │ └── nav.vue │ │ ├── mixins │ │ │ ├── label.js │ │ │ └── like.js │ │ ├── App.vue │ │ ├── main.js │ │ ├── views │ │ │ ├── home │ │ │ │ ├── label.vue │ │ │ │ ├── index.vue │ │ │ │ └── components │ │ │ │ │ └── intro.vue │ │ │ ├── article │ │ │ │ └── detail.vue │ │ │ └── message │ │ │ │ └── components │ │ │ │ ├── inputBox.vue │ │ │ │ └── replyItem.vue │ │ ├── filters │ │ │ └── index.js │ │ ├── utils │ │ │ └── request.js │ │ └── router │ │ │ └── index.js │ └── index.html └── admin │ ├── src │ ├── styles │ │ ├── index.less │ │ └── less │ │ │ ├── init.less │ │ │ └── element-ui.less │ ├── components │ │ ├── markdown │ │ │ └── blueprint.png │ │ ├── common │ │ │ ├── index.js │ │ │ ├── tool │ │ │ │ ├── svg-icon.vue │ │ │ │ └── zp-dialog.vue │ │ │ └── page │ │ │ │ ├── zp-page-filter.vue │ │ │ │ ├── zp-page-edit.vue │ │ │ │ ├── zp-page-header.vue │ │ │ │ ├── zp-page.vue │ │ │ │ └── zp-page-list.vue │ │ ├── github.vue │ │ └── hamburger.vue │ ├── constant │ │ └── classify.js │ ├── views │ │ ├── Layout │ │ │ ├── index.js │ │ │ ├── content.vue │ │ │ ├── index.vue │ │ │ ├── levelbar.vue │ │ │ ├── account.vue │ │ │ ├── navBar.vue │ │ │ ├── tabsView.vue │ │ │ └── slideBar.vue │ │ ├── Home │ │ │ └── index.vue │ │ ├── Permission │ │ │ └── add.vue │ │ └── Label │ │ │ └── add.vue │ ├── App.vue │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ └── permission.js │ ├── utils │ │ ├── auth.js │ │ ├── config │ │ │ └── styleConfig.js │ │ ├── request.js │ │ └── storage.js │ ├── api │ │ ├── upload.js │ │ ├── message.js │ │ ├── label.js │ │ └── blog.js │ ├── main.js │ ├── mixins │ │ └── pageInfo.js │ ├── filters │ │ └── index.js │ └── router │ │ └── permission.js │ └── index.html ├── public └── images │ └── 88b4903e27008.jpeg ├── postcss.config.js ├── .gitignore ├── .babelrc ├── styles └── theme.less └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') // babel编译 2 | module.exports = require('./app.js') -------------------------------------------------------------------------------- /static/project-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/static/project-structure.png -------------------------------------------------------------------------------- /code/client/src/images/bg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/bg4.jpg -------------------------------------------------------------------------------- /code/client/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/logo.png -------------------------------------------------------------------------------- /code/client/src/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/avatar.png -------------------------------------------------------------------------------- /code/client/src/images/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/qrcode.jpg -------------------------------------------------------------------------------- /code/client/src/images/wall.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/wall.jpeg -------------------------------------------------------------------------------- /public/images/88b4903e27008.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/public/images/88b4903e27008.jpeg -------------------------------------------------------------------------------- /code/admin/src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "./less/reset.less"; 2 | @import "./less/init.less"; 3 | @import "./less/element-ui.less"; 4 | -------------------------------------------------------------------------------- /code/client/src/images/back-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/back-top.png -------------------------------------------------------------------------------- /code/client/src/images/blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/blueprint.png -------------------------------------------------------------------------------- /code/client/src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/favicon.ico -------------------------------------------------------------------------------- /code/client/src/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/loading.gif -------------------------------------------------------------------------------- /code/client/src/images/none-data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/none-data.jpg -------------------------------------------------------------------------------- /code/client/src/images/cloud-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/cloud-left.png -------------------------------------------------------------------------------- /code/client/src/images/cloud-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/cloud-right.png -------------------------------------------------------------------------------- /code/client/src/images/load-error.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/load-error.jpeg -------------------------------------------------------------------------------- /code/client/src/images/grass-confetti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/client/src/images/grass-confetti.png -------------------------------------------------------------------------------- /code/admin/src/components/markdown/blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue-node-mongodb-blog/HEAD/code/admin/src/components/markdown/blueprint.png -------------------------------------------------------------------------------- /code/admin/src/constant/classify.js: -------------------------------------------------------------------------------- 1 | // 来源 2 | export const sources = [ 3 | { name: "原创", id: 1 }, 4 | { name: "转载", id: 2 }, 5 | { name: "翻译", id: 3 }, 6 | ]; 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("autoprefixer")({ 4 | browsers: ["iOS >= 7", "Android >= 4.1", 'last 5 versions'] 5 | }) 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | logs/ 5 | 6 | 7 | # Editor directories and files 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/index.js: -------------------------------------------------------------------------------- 1 | export { default as NavBar } from './navBar' 2 | export { default as SlideBar } from './slideBar' 3 | export { default as ContentView } from './content' 4 | -------------------------------------------------------------------------------- /code/admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/content.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /code/admin/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | userName: (state) => state.user.username, 3 | userList: (state) => state.user.list, 4 | userTotal: (state) => state.user.total, 5 | }; 6 | export default getters; 7 | -------------------------------------------------------------------------------- /code/client/src/constant/headerColor.js: -------------------------------------------------------------------------------- 1 | export const colorList = [ 2 | "#EB6841", 3 | "#3FB8AF", 4 | "#464646", 5 | "#FC9D9A", 6 | "#EDC951", 7 | "#C8C8A9", 8 | "#83AF9B", 9 | "#036564", 10 | ]; 11 | -------------------------------------------------------------------------------- /code/server/models/label.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let labelSchema = db.Schema({ 3 | label: String, 4 | bgColor: String, 5 | createTime: { type: Date, default: Date.now }, 6 | }); 7 | export default db.model("label", labelSchema); 8 | -------------------------------------------------------------------------------- /code/client/src/api/label.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取标签列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetLabelList(params) { 9 | return axios.get("/label/list", params); 10 | } 11 | -------------------------------------------------------------------------------- /code/client/src/style/index.less: -------------------------------------------------------------------------------- 1 | @import "./less/function.less"; 2 | @import "./less/animation.less"; 3 | @import "./less/markdown.less"; 4 | @import "./less/reset.less"; 5 | @import "./less/init.less"; 6 | @import "../../../../node_modules/highlight.js/styles/tomorrow-night-eighties.css"; 7 | -------------------------------------------------------------------------------- /code/server/models/user.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let userSchema = db.Schema({ 3 | username: String, 4 | pwd: String, 5 | avatar: String, 6 | roles: Array, 7 | createTime: { type: Date, default: Date.now }, 8 | loginTime: Date, 9 | }); 10 | export default db.model("user", userSchema); 11 | -------------------------------------------------------------------------------- /code/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /code/admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { Cookie } from "./storage"; 2 | const TokenKey = "Token-Auth"; 3 | 4 | export function getToken() { 5 | return Cookie.get(TokenKey); 6 | } 7 | 8 | export function setToken(token) { 9 | return Cookie.set(TokenKey, token); 10 | } 11 | 12 | export function removeToken() { 13 | return Cookie.remove(TokenKey); 14 | } 15 | -------------------------------------------------------------------------------- /code/client/src/store/modules/search.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | keyword: "", 5 | }, 6 | mutations: { 7 | setKeyword(state, data) { 8 | state.keyword = data; 9 | }, 10 | }, 11 | actions: {}, 12 | getters: { 13 | keyword(state) { 14 | return state.keyword; 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /code/admin/src/styles/less/init.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | width: 1em; 3 | height: 1em; 4 | vertical-align: -0.15em; 5 | fill: currentColor; 6 | overflow: hidden; 7 | } 8 | 9 | ::-webkit-scrollbar { 10 | width: 4px; 11 | height: 4px; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | opacity: 0.8; 16 | background: #ddd; 17 | border-radius: 4px; 18 | transition: all 0.5s; 19 | } 20 | -------------------------------------------------------------------------------- /code/client/src/constant/side.js: -------------------------------------------------------------------------------- 1 | // 侧边栏文章类别 2 | const BROWSE_STATUS = 1; // 热门点击 3 | const RECOMMEND_STATUS = 2; // 最新推荐 4 | const sideType = { 5 | [BROWSE_STATUS]: '热门点击', 6 | [RECOMMEND_STATUS]: '最新推荐', 7 | }; 8 | const sortType = { 9 | [BROWSE_STATUS]: 'pv', 10 | [RECOMMEND_STATUS]: 'releaseTime', 11 | }; 12 | 13 | export { BROWSE_STATUS, RECOMMEND_STATUS, sideType, sortType }; 14 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/2764.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import label from "./modules/label"; 4 | import search from "./modules/search"; 5 | 6 | Vue.use(Vuex); 7 | 8 | const store = new Vuex.Store({ 9 | state: {}, 10 | mutations: {}, 11 | actions: {}, 12 | modules: { 13 | label, 14 | search, 15 | }, 16 | }); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /code/server/app.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import ip from 'ip' 3 | import conf from './config' 4 | import router from './router' 5 | import middleware from './middleware' 6 | import './mongodb' 7 | 8 | const app = new Koa() 9 | middleware(app) 10 | router(app) 11 | app.listen(conf.port, '0.0.0.0', () => { 12 | console.log(`server is running at http://${ip.address()}:${conf.port}`) 13 | }) -------------------------------------------------------------------------------- /code/server/middleware/func/get_info.js: -------------------------------------------------------------------------------- 1 | export const get_client_ip = (ctx) => { 2 | return ( 3 | ctx.request.headers["x-forwarded-for"] || 4 | (ctx.request.connection && ctx.request.connection.remoteAddress) || 5 | ctx.request.socket.remoteAddress || 6 | (ctx.request.connection.socket && 7 | ctx.request.connection.socket.remoteAddress) || 8 | null 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /code/admin/src/views/Home/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | [ 5 | "env", 6 | { 7 | "modules": false, 8 | "targets": { 9 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 10 | } 11 | } 12 | ], 13 | "stage-2" 14 | ], 15 | "plugins": ["transform-runtime"], 16 | "env": { 17 | "test": { 18 | "presets": ["env", "stage-2"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/admin/src/components/common/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 注入公共组件 3 | */ 4 | import Vue from "vue"; 5 | // 检索当前目录的vue文件,便检索子文件夹 6 | const componentsContext = require.context("./", true, /.vue$/); 7 | componentsContext.keys().forEach((component) => { 8 | // 获取文件中的 default 模块 9 | const componentConfig = componentsContext(component).default; 10 | componentConfig.name && Vue.component(componentConfig.name, componentConfig); 11 | }); 12 | -------------------------------------------------------------------------------- /code/client/src/components/common/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 注入公共组件 3 | */ 4 | import Vue from "vue"; 5 | // 检索当前目录的vue文件,便检索子文件夹 6 | const componentsContext = require.context("./", true, /.vue$/); 7 | componentsContext.keys().forEach((component) => { 8 | // 获取文件中的 default 模块 9 | const componentConfig = componentsContext(component).default; 10 | componentConfig.name && Vue.component(componentConfig.name, componentConfig); 11 | }); 12 | -------------------------------------------------------------------------------- /code/admin/src/api/upload.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 上传图片 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiUploadImg(params) { 9 | return axios.postFile("/uploadImage", params); 10 | } 11 | /** 12 | * 删除图片 13 | * @param data 14 | * @returns {AxiosPromise} 15 | */ 16 | export function apiDelUploadImg(params) { 17 | return axios.post("/delUploadImage", params); 18 | } 19 | -------------------------------------------------------------------------------- /code/server/middleware/func/index.js: -------------------------------------------------------------------------------- 1 | import * as get_Info_func from "./get_info"; 2 | import * as db_func from "./db"; 3 | import * as file_func from "./file"; 4 | 5 | export default () => { 6 | const func = Object.assign({}, get_Info_func, db_func, file_func); 7 | return async (ctx, next) => { 8 | for (let v in func) { 9 | if (func.hasOwnProperty(v)) ctx[v] = func[v]; 10 | } 11 | await next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /code/admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import getters from "./getters"; 4 | import app from "./modules/app"; 5 | import user from "./modules/user"; 6 | import permission from "./modules/permission"; 7 | 8 | Vue.use(Vuex); 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | user, 14 | permission, 15 | }, 16 | getters, 17 | }); 18 | 19 | export default store; 20 | -------------------------------------------------------------------------------- /code/client/src/components/common/noneData.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /code/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | wallBlog 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /code/client/src/components/common/svgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /code/admin/src/components/common/tool/svg-icon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /code/server/mongodb.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import conf from "./config"; 3 | const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆 4 | mongoose.Promise = global.Promise; 5 | mongoose.connect(DB_URL, { useMongoClient: true }, (err) => { 6 | if (err) { 7 | console.log("数据库连接失败!"); 8 | } else { 9 | console.log("数据库连接成功!"); 10 | } 11 | }); 12 | export default mongoose; 13 | -------------------------------------------------------------------------------- /code/server/middleware/log/index.js: -------------------------------------------------------------------------------- 1 | import logger from './log' 2 | 3 | export default opts => { 4 | let loggerMiddleware = logger(opts); 5 | return async (ctx, next) => { 6 | return loggerMiddleware(ctx, next) 7 | .catch( e => { 8 | if (ctx.status < 500) { 9 | ctx.status = 500; 10 | } 11 | ctx.log.error(e.stack); 12 | ctx.state.logged = true; 13 | ctx.throw(e); 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f603.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/models/blog.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let blogSchema = db.Schema({ 3 | type: Array, 4 | title: String, 5 | desc: String, 6 | fileCoverImgUrl: String, 7 | html: String, 8 | markdown: String, 9 | level: Number, 10 | github: String, 11 | auth: String, 12 | source: Number, 13 | isVisible: Boolean, 14 | releaseTime: String, 15 | pv: { type: Number, default: 0 }, 16 | likes: { type: Number, default: 0 }, 17 | comments: { type: Number, default: 0 }, 18 | }); 19 | export default db.model("blog", blogSchema); 20 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f446.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f447.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f449.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/middleware/log/access.js: -------------------------------------------------------------------------------- 1 | export default (ctx, msg, commonInfo) => { 2 | const { 3 | method, // 请求方法 get post或其他 4 | url, // 请求链接 5 | host, // 发送请求的客户端的host 6 | headers // 请求中的headers 7 | } = ctx.request; 8 | const client = { 9 | method, 10 | url, 11 | host, 12 | msg, 13 | ip: ctx.get_client_ip(ctx), 14 | referer: headers['referer'], // 请求的源地址 15 | userAgent: headers['user-agent'] // 客户端信息 设备及浏览器信息 16 | } 17 | return JSON.stringify(Object.assign(commonInfo, client)); 18 | } -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f448.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/api/message.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取留言列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetMessageList(params) { 9 | return axios.get("/message/list", params); 10 | } 11 | 12 | /** 13 | * 删除留言 14 | * @param data 15 | * @returns {AxiosPromise} 16 | */ 17 | export function apiDelMessage(params) { 18 | return axios.get("/message/del", params); 19 | } 20 | 21 | /** 22 | * 删除回复 23 | * @param data 24 | * @returns {AxiosPromise} 25 | */ 26 | export function apiDelReply(params) { 27 | return axios.postFile("/message/delReply", params); 28 | } 29 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f4aa.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f494.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/mixins/label.js: -------------------------------------------------------------------------------- 1 | // 获取标签的背景色 2 | import { mapGetters } from "vuex"; 3 | export default { 4 | computed: { 5 | ...mapGetters({ 6 | labelList: "label/labelList", 7 | }), 8 | getLabelColor({ labelList }) { 9 | return (labelName) => { 10 | if (labelList && labelList.length) { 11 | let labelIndex = 0; 12 | labelList.forEach((item, index) => { 13 | if (labelName === item.label) { 14 | labelIndex = index; 15 | } 16 | }); 17 | return labelList[labelIndex].bgColor; 18 | } 19 | return ""; 20 | }; 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /code/server/models/message.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let messageSchema = db.Schema({ 3 | content: String, 4 | headerColor: { type: String, default: "#ff6c1a" }, 5 | nickname: { type: String, default: "匿名网友" }, 6 | createTime: String, 7 | likes: { type: Number, default: 0 }, 8 | comments: { type: Number, default: 0 }, 9 | replyList: [ 10 | { 11 | replyHeaderColor: { type: String, default: "#009688" }, 12 | replyContent: String, 13 | replyUser: { type: String, default: "匿名网友" }, 14 | byReplyUser: String, 15 | replyTime: String, 16 | }, 17 | ], 18 | }); 19 | export default db.model("message", messageSchema); 20 | -------------------------------------------------------------------------------- /code/client/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f436.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f47b.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router/permission"; 4 | import store from "./store"; 5 | import ElementUI from "element-ui"; 6 | import "element-ui/lib/theme-chalk/index.css"; 7 | import "./styles/index.less"; 8 | import pageInfoMixin from "src/mixins/pageInfo"; 9 | 10 | import "./utils/config/iconfont"; 11 | import "src/components/common/index.js"; 12 | 13 | import * as filters from "./filters"; 14 | Object.keys(filters).forEach((key) => { 15 | Vue.filter(key, filters[key]); 16 | }); 17 | 18 | Vue.mixin(pageInfoMixin); 19 | Vue.use(ElementUI); 20 | 21 | new Vue({ 22 | el: "#app", 23 | router, 24 | store, 25 | template: "", 26 | components: { App }, 27 | }); 28 | -------------------------------------------------------------------------------- /code/server/middleware/send/index.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | let render = (ctx) => { 3 | return (json, msg) => { 4 | ctx.set("Content-Type", "application/json"); 5 | ctx.body = JSON.stringify({ 6 | code: 1, 7 | data: json || {}, 8 | msg: msg || "success", 9 | }); 10 | }; 11 | }; 12 | let renderError = (ctx) => { 13 | return (msg) => { 14 | ctx.set("Content-Type", "application/json"); 15 | ctx.body = JSON.stringify({ 16 | code: 0, 17 | data: {}, 18 | msg: msg.toString(), 19 | }); 20 | }; 21 | }; 22 | return async (ctx, next) => { 23 | ctx.send = render(ctx); 24 | ctx.sendError = renderError(ctx); 25 | await next(); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /code/server/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | const auth = { 3 | admin_secret: 'admin-token', 4 | tokenKey: 'Token-Auth', 5 | whiteList: ['login', 'client_api', '/images/'], 6 | blackList: ['admin_api'], 7 | }; 8 | 9 | const log = { 10 | logLevel: 'debug', // 指定记录的日志级别 11 | dir: path.resolve(__dirname, '../../logs'), // 指定日志存放的目录名 12 | projectName: 'blog', // 项目名,记录在日志中的项目信息 13 | ip: '0.0.0.0', // 默认情况下服务器 ip 地址 14 | }; 15 | const port = process.env.NODE_ENV === 'production' ? '80' : '3000'; 16 | 17 | export default { 18 | env: process.env.NODE_ENV, 19 | port, 20 | auth, 21 | log, 22 | mongodb: { 23 | username: 'wall', 24 | pwd: 123456, 25 | address: '127.0.0.1:27017', 26 | db: 'wallBlog', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /code/client/src/components/copyRight.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /code/server/middleware/auth/index.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import conf from '../../config'; 3 | 4 | export default () => { 5 | return async (ctx, next) => { 6 | // 白名单就不需要走 jwt 鉴权 7 | if (!conf.auth.whiteList.some((v) => ctx.path.includes(v))) { 8 | let token = ctx.cookies.get(conf.auth.tokenKey); 9 | try { 10 | jwt.verify(token, conf.auth.admin_secret); 11 | } catch (e) { 12 | if ('TokenExpiredError' === e.name) { 13 | ctx.sendError('token已过期, 请重新登录!'); 14 | ctx.throw(401, 'token已过期, 请重新登录!'); 15 | } 16 | ctx.sendError('token验证失败, 请重新登录!'); 17 | ctx.throw(401, 'token验证失败, 请重新登录!'); 18 | } 19 | console.log('鉴权成功'); 20 | } 21 | await next(); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /code/admin/src/api/label.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取标签列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetLabelList(params) { 9 | return axios.get("/label/list", params); 10 | } 11 | /** 12 | * 新增标签 13 | * @param data 14 | * @returns {AxiosPromise} 15 | */ 16 | export function apiAddLabel(params) { 17 | return axios.postFile("/label/add", params); 18 | } 19 | /** 20 | * 修改标签 21 | * @param data 22 | * @returns {AxiosPromise} 23 | */ 24 | export function apiUpdateLabel(params) { 25 | return axios.post("/label/update", params); 26 | } 27 | /** 28 | * 删除标签 29 | * @param data 30 | * @returns {AxiosPromise} 31 | */ 32 | export function apiDelLabel(params) { 33 | return axios.get("/label/del", params); 34 | } 35 | -------------------------------------------------------------------------------- /code/server/controller/client/label.js: -------------------------------------------------------------------------------- 1 | import labelModel from "../../models/label"; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | "----------------获取标签列表 label/list-----------------------" 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query; 9 | try { 10 | let reg = new RegExp(keyword, "i"); 11 | let data = await ctx.findPage( 12 | labelModel, 13 | { 14 | $or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }], 15 | }, 16 | null, 17 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 18 | ); 19 | ctx.send(data); 20 | } catch (e) { 21 | console.log(e); 22 | ctx.sendError(e); 23 | } 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /code/client/src/api/blog.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取博客列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetBlogList(params) { 9 | return axios.get("/blog/list", params); 10 | } 11 | /** 12 | * 获取博客详情 13 | * @param data 14 | * @returns {AxiosPromise} 15 | */ 16 | export function apiGetBlogDetail(params) { 17 | return axios.get("/blog/info", params); 18 | } 19 | /** 20 | * 点赞 21 | * @param data 22 | * @returns {AxiosPromise} 23 | */ 24 | export function apiUpdateLikes(params) { 25 | return axios.post("/blog/updateLikes", params); 26 | } 27 | /** 28 | * 浏览量 29 | * @param data 30 | * @returns {AxiosPromise} 31 | */ 32 | export function apiUpdatePV(params) { 33 | return axios.post("/blog/updatePV", params); 34 | } 35 | -------------------------------------------------------------------------------- /code/client/src/components/common/breadCrumbs.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /code/admin/src/mixins/pageInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 1、先引入 pageInfo 的mixin文件,注入mixin 3 | * 2、给 requestPageData 方法赋值页面翻页查询方法 4 | */ 5 | 6 | export default { 7 | data() { 8 | return { 9 | requestPageData: null, // 每个table组建的数据源获取方法 10 | pageInfo: { 11 | total: 0, 12 | pageNum: 1, 13 | pageSize: 10, 14 | }, 15 | }; 16 | }, 17 | watch: { 18 | "pageInfo.pageNum"() { 19 | this.requestPageData && this.requestPageData(); 20 | }, 21 | "pageInfo.pageSize"() { 22 | this.requestPageData && this.requestPageData(); 23 | }, 24 | }, 25 | methods: { 26 | handleCurrentChange(page) { 27 | this.pageInfo.pageNum = page; 28 | }, 29 | handleSizeChange(val) { 30 | this.pageInfo.pageSize = val; 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /code/client/src/store/modules/label.js: -------------------------------------------------------------------------------- 1 | import { apiGetLabelList } from "src/api/label"; 2 | export default { 3 | namespaced: true, 4 | state: { 5 | labelList: null, 6 | }, 7 | mutations: { 8 | setLabelList(state, data) { 9 | state.labelList = data; 10 | }, 11 | }, 12 | actions: { 13 | getLabelList(context) { 14 | let params = { 15 | pageindex: 1, 16 | pagesize: 50, 17 | }; 18 | return apiGetLabelList(params) 19 | .then((res) => { 20 | let { list } = res.data; 21 | context.commit("setLabelList", list); 22 | }) 23 | .catch((err) => { 24 | console.log(err); 25 | }); 26 | }, 27 | }, 28 | getters: { 29 | labelList(state) { 30 | return state.labelList; 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f632.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/theme.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 变量样式 3 | */ 4 | @mainColor: #333; // 主颜色 5 | @thinColor: #555; // 描述色 6 | @assistColor: #7b7e86; // 辅助色 7 | @warningColor: #ff6c1a; // 提醒色 8 | @errorColor: #fe3438; //错误、禁用色 9 | @highlightColor: #009688; // 高亮色 10 | @thinHighlightColor: #20b2aa; // 高亮浅色 11 | @borderColor: #ededed; // 边框色 12 | @borderBoldColor: #ccc; // 边框辅色 13 | @cuttingLineColor: #f0f0f0; // 分割线色 14 | @backgroundColor: rgba(233, 234, 237, 0.7); // 背景色 15 | @thinBgColor: #f6f6f6; // 背景色 16 | @shadowColor: gainsboro; // 阴影色 17 | 18 | @baseSpH: 24px; // 基础左右间距 19 | @baseSp: 20px; // 基础间距 20 | 21 | @min-body-height: calc(~'100vh - 50px'); // 页面最小高度,去除顶部的高度 22 | 23 | .ellipsis-line-clamp (@line: 1) { 24 | display: -webkit-box; 25 | text-overflow: ellipsis; 26 | -webkit-line-clamp: @line; 27 | -webkit-box-orient: vertical; 28 | overflow: hidden; 29 | } 30 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f613.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/controller/admin/upload.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | async uploadImage(ctx, next) { 5 | console.log("----------------添加图片 uploadImage-----------------------"); 6 | try { 7 | let opts = { 8 | path: path.resolve(__dirname, "../../../../public"), 9 | }; 10 | let result = await ctx.uploadFile(ctx, opts); 11 | ctx.send(result); 12 | } catch (e) { 13 | ctx.sendError(e); 14 | } 15 | }, 16 | async delUploadImage(ctx, next) { 17 | console.log( 18 | "----------------删除图片 delUploadImage-----------------------" 19 | ); 20 | let fileName = ctx.request.body.fileName; 21 | let fileCoverImgUrl = `public/images/${fileName}`; 22 | try { 23 | ctx.removeFile(fileCoverImgUrl); 24 | ctx.send(); 25 | } catch (e) { 26 | ctx.sendError(e); 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /code/client/src/components/common/loading.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 23 | -------------------------------------------------------------------------------- /code/client/src/mixins/like.js: -------------------------------------------------------------------------------- 1 | // 点赞操作 2 | 3 | import { apiUpdateLikes } from 'api/blog'; 4 | export default { 5 | computed: { 6 | getLikesNumber({ likeList }) { 7 | return (id, likes) => (likeList.includes(id) ? likes + 1 : likes); 8 | }, 9 | getLikesColor({ likeList }) { 10 | return (id) => likeList.includes(id); 11 | }, 12 | }, 13 | data() { 14 | return { 15 | likeList: [], 16 | }; 17 | }, 18 | methods: { 19 | // 点赞 20 | handleLikes(id) { 21 | return apiUpdateLikes({ _id: id, isLike: this.likeList.includes(id) }) 22 | .then(() => { 23 | this.likeList.includes(id) 24 | ? this.likeList.splice(this.likeList.indexOf(id), 1) 25 | : this.likeList.push(id); 26 | }) 27 | .catch((err) => { 28 | console.log(err); 29 | }) 30 | .finally(() => {}); 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f604.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f637.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import "element-ui/lib/theme-chalk/index.css"; 6 | import "./style/index.less"; 7 | 8 | import * as filters from "./filters"; 9 | Object.keys(filters).forEach((key) => { 10 | Vue.filter(key, filters[key]); 11 | }); 12 | import "./utils/iconfont"; 13 | import "./components/common/index"; 14 | 15 | import { Loading } from "element-ui"; 16 | Vue.use(Loading.directive); 17 | 18 | import VueLazyload from "vue-lazyload"; 19 | const loadimage = require("./images/loading.gif"); 20 | const errorimage = require("./images/load-error.jpeg"); 21 | 22 | Vue.use(VueLazyload, { 23 | preLoad: 1.3, 24 | error: errorimage, 25 | loading: loadimage, 26 | attempt: 1, 27 | }); 28 | 29 | new Vue({ 30 | router, 31 | store, 32 | render: (h) => h(App), 33 | created() {}, 34 | }).$mount("#app"); 35 | -------------------------------------------------------------------------------- /code/client/src/style/less/function.less: -------------------------------------------------------------------------------- 1 | .home-wrapper { 2 | padding-top: 50px; 3 | min-height: @min-body-height; 4 | .home-body { 5 | width: 1200px; 6 | margin: 0 auto 20px; 7 | display: flex; 8 | } 9 | } 10 | .side-wrapper { 11 | flex: 1; 12 | margin-left: 24px; 13 | } 14 | 15 | .side-title { 16 | padding: 0 10px; 17 | line-height: 50px; 18 | height: 50px; 19 | font-size: 18px; 20 | border-bottom: 1px solid #e5e5e5; 21 | color: @mainColor; 22 | font-weight: bold; 23 | position: relative; 24 | display: flex; 25 | align-items: center; 26 | svg { 27 | margin-right: 6px; 28 | } 29 | &::after { 30 | content: " "; 31 | position: absolute; 32 | height: 2px; 33 | width: 10%; 34 | left: 0; 35 | bottom: -1px; 36 | background: @mainColor; 37 | transition: 2s ease all; 38 | } 39 | &:hover { 40 | &::after { 41 | width: 100%; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f60d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f633.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/api/blog.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取博客列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetBlogList(params) { 9 | return axios.get("/blog/list", params); 10 | } 11 | 12 | /** 13 | * 获取博客详情 14 | * @param data 15 | * @returns {AxiosPromise} 16 | */ 17 | export function apiGetBlogDetail(params) { 18 | return axios.get("/blog/info", params); 19 | } 20 | 21 | /** 22 | * 新增博客 23 | * @param data 24 | * @returns {AxiosPromise} 25 | */ 26 | export function apiAddBlog(params) { 27 | return axios.postFile("/blog/add", params); 28 | } 29 | /** 30 | * 修改博客 31 | * @param data 32 | * @returns {AxiosPromise} 33 | */ 34 | export function apiUpdateBlog(params) { 35 | return axios.postFile("/blog/update", params); 36 | } 37 | /** 38 | * 删除博客 39 | * @param data 40 | * @returns {AxiosPromise} 41 | */ 42 | export function apiDelBlog(params) { 43 | return axios.get("/blog/del", params); 44 | } 45 | -------------------------------------------------------------------------------- /code/admin/src/utils/config/styleConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | 列表表格里 固定宽度 栏 3 | */ 4 | const TABLE_FIX_WIDTH_PHONE = 'phone'; // 手机号 5 | const TABLE_FIX_WIDTH_USERNAME = 'username'; // 名字 6 | const TABLE_FIX_WIDTH_ID_CARD = 'idCard'; // 身份证 7 | const TABLE_FIX_WIDTH_BANK_CARD = 'bankCard'; // 银行卡号码 8 | const TABLE_FIX_WIDTH_LABEL = 'label'; // 标签 9 | const TABLE_FIX_WIDTH_TIME = 'datetime'; // 时间 10 | const TABLE_FIX_WIDTH_DATE = 'date'; // 日期 11 | 12 | const tableFixWidths = { 13 | [TABLE_FIX_WIDTH_PHONE]: 120, 14 | [TABLE_FIX_WIDTH_USERNAME]: 88, 15 | [TABLE_FIX_WIDTH_LABEL]: 88, 16 | [TABLE_FIX_WIDTH_TIME]: 168, 17 | [TABLE_FIX_WIDTH_DATE]: 120, 18 | [TABLE_FIX_WIDTH_ID_CARD]: 180, 19 | [TABLE_FIX_WIDTH_BANK_CARD]: 190, 20 | }; 21 | 22 | export { 23 | TABLE_FIX_WIDTH_PHONE, 24 | TABLE_FIX_WIDTH_USERNAME, 25 | TABLE_FIX_WIDTH_LABEL, 26 | TABLE_FIX_WIDTH_TIME, 27 | TABLE_FIX_WIDTH_DATE, 28 | TABLE_FIX_WIDTH_ID_CARD, 29 | TABLE_FIX_WIDTH_BANK_CARD, 30 | tableFixWidths, 31 | }; 32 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f61c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/api/message.js: -------------------------------------------------------------------------------- 1 | import axios from "utils/request"; 2 | 3 | /** 4 | * 获取留言列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export function apiGetMessageList(params) { 9 | return axios.get("/message/list", params); 10 | } 11 | 12 | /** 13 | * 获取回复数量 14 | * @param data 15 | * @returns {AxiosPromise} 16 | */ 17 | export function apiGetReplyCount(params) { 18 | return axios.get("/message/replyCount", params); 19 | } 20 | 21 | /** 22 | * 添加留言 23 | * @param data 24 | * @returns {AxiosPromise} 25 | */ 26 | export function apiAddMessage(params) { 27 | return axios.post("/message/add", params); 28 | } 29 | 30 | /** 31 | * 点赞 32 | * @param data 33 | * @returns {AxiosPromise} 34 | */ 35 | export function apiUpdateLikes(params) { 36 | return axios.post("/message/updateLikes", params); 37 | } 38 | 39 | /** 40 | * 回复 41 | * @param data 42 | * @returns {AxiosPromise} 43 | */ 44 | export function apiUpdateReplys(params) { 45 | return axios.post("/message/updateReplys", params); 46 | } 47 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f625.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/filters/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 时间日期格式化 3 | * 用法 formatTime(new Date(), 'yyyy-MM-dd hh:mm:ss') 4 | * @param time 5 | * @param fmt 6 | */ 7 | export function formatTime(time, fmt) { 8 | time = parseInt(time); 9 | if (!time) { 10 | return ""; 11 | } 12 | const date = new Date(time); 13 | let o = { 14 | "M+": date.getMonth() + 1, // 月份 15 | "d+": date.getDate(), // 日 16 | "h+": date.getHours(), // 小时 17 | "m+": date.getMinutes(), // 分 18 | "s+": date.getSeconds(), // 秒 19 | "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 20 | S: date.getMilliseconds(), // 毫秒 21 | }; 22 | if (/(y+)/.test(fmt)) { 23 | fmt = fmt.replace( 24 | RegExp.$1, 25 | (date.getFullYear() + "").substr(4 - RegExp.$1.length) 26 | ); 27 | } 28 | for (let k in o) { 29 | if (new RegExp("(" + k + ")").test(fmt)) { 30 | fmt = fmt.replace( 31 | RegExp.$1, 32 | RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length) 33 | ); 34 | } 35 | } 36 | return fmt; 37 | } 38 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f60a.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f622.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/views/home/label.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | 41 | 43 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f620.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f621.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/components/common/page/zp-page-filter.vue: -------------------------------------------------------------------------------- 1 | 19 | 40 | 41 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f469.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f614.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/middleware/rule/index.js: -------------------------------------------------------------------------------- 1 | import Path from "path"; 2 | import fs from "fs"; 3 | 4 | export default (opts) => { 5 | let { app, rules = [] } = opts; 6 | if (!app) { 7 | throw new Error("the app params is necessary!"); 8 | } 9 | 10 | app.router = {}; 11 | const appKeys = Object.keys(app); 12 | rules.forEach((item) => { 13 | let { path, name } = item; 14 | if (appKeys.includes(name)) { 15 | throw new Error(`the name of ${name} already exists!`); 16 | } 17 | let content = {}; 18 | //readdirSync: 方法将返回一个包含“指定目录下所有文件名称”的数组对象。 19 | //extname: 返回path路径文件扩展名,如果path以 ‘.' 为结尾,将返回 ‘.',如果无扩展名 又 不以'.'结尾,将返回空值。 20 | //basename: path.basename(p, [ext]) p->要处理的path ext->要过滤的字符 21 | fs.readdirSync(path).forEach((filename) => { 22 | let extname = Path.extname(filename); 23 | if (extname === ".js") { 24 | let name = Path.basename(filename, extname); 25 | content[name] = require(Path.join(path, filename)); 26 | content[name].filename = name; 27 | } 28 | }); 29 | app[name] = content; 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /code/admin/src/components/common/tool/zp-dialog.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | 40 | 51 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f61e.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f601.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f466.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/router/permission.js: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | import { getToken } from "src/utils/auth"; 3 | import { router } from "./index"; 4 | import NProgress from "nprogress"; // Progress 进度条 5 | import "nprogress/nprogress.css"; // Progress 进度条样式 6 | 7 | router.beforeEach((to, from, next) => { 8 | NProgress.start(); 9 | if (getToken()) { 10 | if (!store.state.user.roles) { 11 | // 重新拉取用户信息 12 | store.dispatch("getUserInfo").then((res) => { 13 | // 如果token过期,则需重新登录 14 | if (res.code === 401) { 15 | next("/login"); 16 | } else { 17 | let roles = res.data.roles; 18 | store.dispatch("setRoutes", { roles }).then(() => { 19 | // 根据权限动态添加路由 20 | router.addRoutes(store.state.permission.addRouters); 21 | next({ ...to }); // hash模式 确保路由加载完成 22 | }); 23 | } 24 | }); 25 | } else { 26 | next(); 27 | } 28 | } else { 29 | to.path === "/login" ? next() : next("/login"); 30 | } 31 | }); 32 | router.afterEach((to, from) => { 33 | document.title = to.name; 34 | NProgress.done(); 35 | }); 36 | 37 | export default router; 38 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f628.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f609.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f4a9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f630.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f612.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f61d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 25 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f62d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/filters/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 时间日期格式化 3 | * 用法 formatTime(new Date(), 'yyyy-MM-dd hh:mm:ss') 4 | * @param time 5 | * @param fmt 6 | */ 7 | export function formatTime(time, fmt) { 8 | time = parseInt(time); 9 | if (!time) { 10 | return ""; 11 | } 12 | const date = new Date(time); 13 | let o = { 14 | "M+": date.getMonth() + 1, // 月份 15 | "d+": date.getDate(), // 日 16 | "h+": date.getHours(), // 小时 17 | "m+": date.getMinutes(), // 分 18 | "s+": date.getSeconds(), // 秒 19 | "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 20 | S: date.getMilliseconds(), // 毫秒 21 | }; 22 | if (/(y+)/.test(fmt)) { 23 | fmt = fmt.replace( 24 | RegExp.$1, 25 | (date.getFullYear() + "").substr(4 - RegExp.$1.length) 26 | ); 27 | } 28 | for (let k in o) { 29 | if (new RegExp("(" + k + ")").test(fmt)) { 30 | fmt = fmt.replace( 31 | RegExp.$1, 32 | RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length) 33 | ); 34 | } 35 | } 36 | return fmt; 37 | } 38 | 39 | /** 40 | * 数字转成 k、w 方式 41 | * @param num 42 | */ 43 | export function formatNumber(num) { 44 | return num >= 1e3 && num < 1e4 45 | ? (num / 1e3).toFixed(1) + "k" 46 | : num >= 1e4 47 | ? (num / 1e4).toFixed(1) + "w" 48 | : num; 49 | } 50 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f44a.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f60c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f60f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f62a.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f623.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/levelbar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | 43 | -------------------------------------------------------------------------------- /code/client/src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 49 | -------------------------------------------------------------------------------- /code/client/src/components/top.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 46 | 47 | 60 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f385.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/admin/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { Local } from "src/utils/storage"; 2 | const app = { 3 | state: { 4 | slideBar: { 5 | opened: Local.get("slideBarStatus"), 6 | }, 7 | tagViews: JSON.parse(Local.get("tagViews")) || [], 8 | is_add_router: false, 9 | }, 10 | mutations: { 11 | TOGGLE_SIDEBAR(state) { 12 | if (state.slideBar.opened) { 13 | Local.set("slideBarStatus", false); 14 | } else { 15 | Local.set("slideBarStatus", true); 16 | } 17 | state.slideBar.opened = !state.slideBar.opened; 18 | }, 19 | ADD_TAGVIEW(state, tag) { 20 | if (state.tagViews.some((v) => v.name === tag.name)) return; 21 | state.tagViews.push({ name: tag.name, path: tag.path }); 22 | Local.set("tagViews", JSON.stringify(state.tagViews)); 23 | }, 24 | DEL_TAGVIEW(state, tag) { 25 | let index; 26 | for (let [i, v] of state.tagViews.entries()) { 27 | if (v.name === tag.name) index = i; 28 | } 29 | state.tagViews.splice(index, 1); 30 | Local.set("tagViews", JSON.stringify(state.tagViews)); 31 | }, 32 | }, 33 | actions: { 34 | toggleSideBar({ commit }) { 35 | commit("TOGGLE_SIDEBAR"); 36 | }, 37 | addTagView({ commit }, tag) { 38 | commit("ADD_TAGVIEW", tag); 39 | }, 40 | delTagView({ commit }, tag) { 41 | commit("DEL_TAGVIEW", tag); 42 | }, 43 | }, 44 | }; 45 | export default app; 46 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/account.vue: -------------------------------------------------------------------------------- 1 | 14 | 30 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/navBar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /code/admin/src/components/common/page/zp-page-edit.vue: -------------------------------------------------------------------------------- 1 | 21 | 45 | 68 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f468.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f631.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/components/common/tag.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f44c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/middleware/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import bodyParser from "koa-bodyparser"; 3 | import staticFiles from "koa-static"; 4 | import Rule from "./rule"; 5 | import Send from "./send"; 6 | import Auth from "./auth"; 7 | import Log from "./log"; 8 | import Func from "./func"; 9 | 10 | export default (app) => { 11 | //缓存拦截器 12 | app.use(async (ctx, next) => { 13 | if (ctx.url == "/favicon.ico") return; 14 | 15 | await next(); 16 | ctx.status = 200; 17 | ctx.set("Cache-Control", "must-revalidation"); 18 | if (ctx.fresh) { 19 | ctx.status = 304; 20 | return; 21 | } 22 | }); 23 | 24 | // 日志中间件 25 | app.use(Log()); 26 | 27 | // 数据返回的封装 28 | app.use(Send()); 29 | 30 | // 方法封装 31 | app.use(Func()); 32 | 33 | //权限中间件 34 | app.use(Auth()); 35 | 36 | //post请求中间件 37 | app.use(bodyParser()); 38 | 39 | //静态文件中间件 40 | app.use(staticFiles(path.resolve(__dirname, "../../../public"))); 41 | 42 | // 规则中间件 43 | Rule({ 44 | app, 45 | rules: [ 46 | { 47 | path: path.join(__dirname, "../controller/admin"), 48 | name: "admin", 49 | }, 50 | { 51 | path: path.join(__dirname, "../controller/client"), 52 | name: "client", 53 | }, 54 | ], 55 | }); 56 | 57 | // 增加错误的监听处理 58 | app.on("error", (err, ctx) => { 59 | if (ctx && !ctx.headerSent && ctx.status < 500) { 60 | ctx.status = 500; 61 | } 62 | if (ctx && ctx.log && ctx.log.error) { 63 | if (!ctx.state.logged) { 64 | ctx.log.error(err.stack); 65 | } 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/tabsView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /code/admin/src/components/common/page/zp-page-header.vue: -------------------------------------------------------------------------------- 1 | 7 | 23 | 24 | 43 | 44 | 76 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f44e.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | import { Message } from 'element-ui'; 4 | 5 | axios.defaults.withCredentials = true; 6 | 7 | // 发送时 8 | axios.interceptors.request.use( 9 | (config) => config, 10 | (err) => Promise.reject(err) 11 | ); 12 | 13 | // 响应时 14 | axios.interceptors.response.use( 15 | (response) => response, 16 | (err) => Promise.resolve(err.response) 17 | ); 18 | 19 | // 检查状态码 20 | function checkStatus(res) { 21 | // console.log("log status =>", res); 22 | if (res.status === 200 || res.status === 304) { 23 | return res.data; 24 | } 25 | return { 26 | code: 0, 27 | msg: res.data.msg || res.statusText, 28 | data: res.statusText, 29 | }; 30 | } 31 | 32 | // 检查CODE值 33 | function checkCode(res) { 34 | // console.log("log code =>", res); 35 | if (res.code === 0) { 36 | Message({ 37 | message: res.msg, 38 | type: 'error', 39 | duration: 2 * 1000, 40 | }); 41 | throw new Error(res.msg); 42 | } 43 | return res; 44 | } 45 | 46 | const prefix = '/client_api'; 47 | export default { 48 | get(url, params) { 49 | if (!url) return; 50 | return axios({ 51 | method: 'get', 52 | url: prefix + url, 53 | params, 54 | timeout: 30000, 55 | }) 56 | .then(checkStatus) 57 | .then(checkCode); 58 | }, 59 | post(url, data) { 60 | if (!url) return; 61 | return axios({ 62 | method: 'post', 63 | url: prefix + url, 64 | data: qs.stringify(data), 65 | timeout: 30000, 66 | }) 67 | .then(checkStatus) 68 | .then(checkCode); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f44d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f602.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f467.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f616.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/controller/admin/message.js: -------------------------------------------------------------------------------- 1 | import messageModel from '../../models/message'; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | '----------------获取留言列表 admin_api/message/list-----------------------' 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query; 9 | 10 | let reg = new RegExp(keyword, 'i'); 11 | 12 | let conditions = { 13 | $or: [{ nickname: { $regex: reg } }, { content: { $regex: reg } }], 14 | }; 15 | 16 | // 排序参数 17 | let sortParams = { 18 | createTime: -1, 19 | }; 20 | 21 | let options = { 22 | limit: pagesize * 1, 23 | skip: (pageindex - 1) * pagesize, 24 | sort: sortParams, 25 | }; 26 | 27 | try { 28 | let data = await ctx.find(messageModel, conditions, null, options); 29 | return ctx.send(data); 30 | } catch (e) { 31 | console.log(e); 32 | return ctx.sendError(e); 33 | } 34 | }, 35 | 36 | async del(ctx, next) { 37 | console.log( 38 | '----------------删除留言 admin_api/message/del-----------------------' 39 | ); 40 | let id = ctx.request.query.id; 41 | try { 42 | ctx.remove(messageModel, { _id: id }); 43 | ctx.send(); 44 | } catch (e) { 45 | ctx.sendError(e); 46 | } 47 | }, 48 | 49 | async delReply(ctx, next) { 50 | console.log( 51 | '----------------删除回复 admin_api/message/delReply-----------------------' 52 | ); 53 | let { _id } = ctx.request.body; 54 | let options = { 55 | $pull: { replyList: { _id } }, 56 | }; 57 | try { 58 | let data = await ctx.update(messageModel, { _id }, options); 59 | ctx.send(); 60 | } catch (e) { 61 | ctx.sendError(e); 62 | } 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /code/admin/src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { constantRouterMap, asyncRouterMap } from "src/router"; 2 | 3 | /** 4 | * 通过meta.role判断是否与当前用户权限匹配 5 | * @param role 6 | * @param route 7 | */ 8 | const hasPermission = (roles, route) => { 9 | if (route.meta && route.meta.role) { 10 | return roles.some((role) => route.meta.role.indexOf(role) >= 0); 11 | } else { 12 | return true; 13 | } 14 | }; 15 | 16 | /** 17 | * 递归过滤异步路由表,返回符合用户角色权限的路由表 18 | * @param asyncRouterMap 19 | * @param role 20 | */ 21 | const filterAsyncRouter = (asyncRouterMap, roles) => { 22 | const accessedRouters = asyncRouterMap.filter((route) => { 23 | if (hasPermission(roles, route)) { 24 | if (route.children && route.children.length) { 25 | route.children = filterAsyncRouter(route.children, roles); 26 | } 27 | return true; 28 | } 29 | return false; 30 | }); 31 | return accessedRouters; 32 | }; 33 | 34 | const permission = { 35 | state: { 36 | routes: constantRouterMap.concat(asyncRouterMap), 37 | addRouters: [], 38 | }, 39 | mutations: { 40 | SETROUTES(state, routers) { 41 | state.addRouters = routers; 42 | state.routes = constantRouterMap.concat(routers); 43 | }, 44 | }, 45 | actions: { 46 | setRoutes({ commit }, info) { 47 | return new Promise((resolve, reject) => { 48 | let { roles } = info; 49 | let accessedRouters = []; 50 | if (roles.indexOf("admin") >= 0) { 51 | accessedRouters = asyncRouterMap; 52 | } else { 53 | accessedRouters = filterAsyncRouter(asyncRouterMap, roles); 54 | } 55 | 56 | commit("SETROUTES", accessedRouters); 57 | resolve(); 58 | }); 59 | }, 60 | }, 61 | }; 62 | export default permission; 63 | -------------------------------------------------------------------------------- /code/client/src/components/labelClassify.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | 42 | 77 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f61a.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/views/article/detail.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 59 | 60 | 66 | -------------------------------------------------------------------------------- /code/admin/src/styles/less/element-ui.less: -------------------------------------------------------------------------------- 1 | @bg-color: #324157; 2 | @font-color: #bfcbd9; 3 | @font-hover-color: #48576a; 4 | @active-font-color: #409EFF; 5 | @submenu-color: #1f2d3d; 6 | .el-menu-vertical { 7 | background-color: @bg-color; 8 | .el-submenu__title { 9 | color: @font-color; 10 | background-color: @bg-color; 11 | } 12 | .el-submenu .el-menu { 13 | background-color: @submenu-color; 14 | .el-menu-item { 15 | background: transparent; 16 | } 17 | } 18 | .el-menu-item { 19 | color: @font-color; 20 | background: @bg-color; 21 | } 22 | .el-menu-item:hover, 23 | .el-submenu__title:hover { 24 | background-color: @font-hover-color; 25 | } 26 | .el-menu-item.is-active { 27 | color: @active-font-color; 28 | background-color: transparent; 29 | } 30 | } 31 | 32 | .music-cover-uploader { 33 | .el-upload { 34 | border: 1px dashed #d9d9d9; 35 | border-radius: 6px; 36 | cursor: pointer; 37 | position: relative; 38 | overflow: hidden; 39 | } 40 | .el-upload:hover { 41 | border-color: #409EFF; 42 | } 43 | .avatar-uploader-icon { 44 | font-size: 28px; 45 | color: #8c939d; 46 | width: 178px; 47 | height: 178px; 48 | line-height: 178px; 49 | text-align: center; 50 | } 51 | .avatar { 52 | width: 178px; 53 | height: 178px; 54 | display: block; 55 | } 56 | } 57 | .el-breadcrumb__inner, .el-breadcrumb__inner a { 58 | color: #48576a; 59 | font-weight: 400; 60 | } 61 | 62 | .el-breadcrumb__item:last-child .el-breadcrumb__inner, .el-breadcrumb__item:last-child .el-breadcrumb__inner a, .el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover, .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover { 63 | color: #97a8be; 64 | cursor: text; 65 | font-weight: 400; 66 | } -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f618.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/views/message/components/inputBox.vue: -------------------------------------------------------------------------------- 1 | 15 | 43 | 44 | -------------------------------------------------------------------------------- /code/server/controller/admin/label.js: -------------------------------------------------------------------------------- 1 | import labelModel from "../../models/label"; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | "----------------获取标签列表 label/list-----------------------" 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query; 9 | try { 10 | let reg = new RegExp(keyword, "i"); 11 | let data = await ctx.findPage( 12 | labelModel, 13 | { 14 | $or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }], 15 | }, 16 | null, 17 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 18 | ); 19 | ctx.send(data); 20 | } catch (e) { 21 | console.log(e); 22 | ctx.sendError(e); 23 | } 24 | }, 25 | 26 | async add(ctx, next) { 27 | console.log("----------------添加标签 label/add-----------------------"); 28 | let paramsData = ctx.request.body; 29 | try { 30 | let data = await ctx.findOne(labelModel, { label: paramsData.label }); 31 | if (data) { 32 | ctx.sendError("数据已经存在, 请重新添加!"); 33 | } else { 34 | let result = await ctx.add(labelModel, paramsData); 35 | ctx.send(result); 36 | } 37 | } catch (e) { 38 | ctx.sendError(e); 39 | } 40 | }, 41 | 42 | async update(ctx, next) { 43 | console.log("----------------更新标签 label/update-----------------------"); 44 | let paramsData = ctx.request.body; 45 | try { 46 | let data = await ctx.update( 47 | labelModel, 48 | { _id: paramsData._id }, 49 | paramsData 50 | ); 51 | ctx.send(data); 52 | } catch (e) { 53 | if (e === "暂无数据") { 54 | ctx.sendError(e); 55 | } 56 | } 57 | }, 58 | 59 | async del(ctx, next) { 60 | console.log("----------------删除标签 label/del-----------------------"); 61 | let id = ctx.request.query.id; 62 | try { 63 | let data = await ctx.remove(labelModel, { _id: id }); 64 | ctx.send(data); 65 | } catch (e) { 66 | ctx.sendError(e); 67 | } 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /code/client/src/components/myself.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 87 | -------------------------------------------------------------------------------- /code/admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | import { Message } from 'element-ui'; 4 | import { removeToken } from './auth'; 5 | 6 | axios.defaults.withCredentials = true; 7 | 8 | // 发送时 9 | axios.interceptors.request.use( 10 | (config) => config, 11 | (err) => Promise.reject(err) 12 | ); 13 | 14 | // 响应时 15 | axios.interceptors.response.use( 16 | (response) => response, 17 | (err) => Promise.resolve(err.response) 18 | ); 19 | 20 | // 检查状态码 21 | function checkStatus(res) { 22 | if (res.status === 200 || res.status === 304) { 23 | return res.data; 24 | } 25 | // token过期清掉 26 | if (res.status === 401) { 27 | removeToken(); 28 | Message({ 29 | message: res.data, 30 | type: 'error', 31 | duration: 2 * 1000, 32 | }); 33 | return { 34 | code: 401, 35 | msg: res.data || res.statusText, 36 | data: res.data, 37 | }; 38 | } 39 | return { 40 | code: 0, 41 | msg: res.data.msg || res.statusText, 42 | data: res.statusText, 43 | }; 44 | } 45 | 46 | // 检查CODE值 47 | function checkCode(res) { 48 | if (res.code === 0) { 49 | Message({ 50 | message: res.msg, 51 | type: 'error', 52 | duration: 2 * 1000, 53 | }); 54 | throw new Error(res.msg); 55 | } 56 | return res; 57 | } 58 | 59 | const prefix = '/admin_api'; 60 | export default { 61 | get(url, params) { 62 | if (!url) return; 63 | return axios({ 64 | method: 'get', 65 | url: prefix + url, 66 | params, 67 | timeout: 30000, 68 | }) 69 | .then(checkStatus) 70 | .then(checkCode); 71 | }, 72 | post(url, data) { 73 | if (!url) return; 74 | return axios({ 75 | method: 'post', 76 | url: prefix + url, 77 | data: qs.stringify(data), 78 | timeout: 30000, 79 | }) 80 | .then(checkStatus) 81 | .then(checkCode); 82 | }, 83 | postFile(url, data) { 84 | if (!url) return; 85 | return axios({ 86 | method: 'post', 87 | url: prefix + url, 88 | data, 89 | }) 90 | .then(checkStatus) 91 | .then(checkCode); 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /code/client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | Vue.use(Router); 4 | 5 | // 解决同一页面,参数不同的路由报错 6 | const VueRouterPush = Router.prototype.push; 7 | Router.prototype.push = function push(to) { 8 | return VueRouterPush.call(this, to).catch((err) => err); 9 | }; 10 | 11 | /** 12 | * 加载模块 13 | * @param {string | Component} component 路径或组件 14 | * @param {boolean} lazy 是否懒加载 15 | * @returns {Function | object} 懒加载方法或组件对象 16 | */ 17 | const loadComponent = (component, lazy = true) => 18 | lazy ? () => import(`views/${component}.vue`) : component; 19 | 20 | const router = new Router({ 21 | // mode: 'history', 22 | routes: [ 23 | { 24 | path: "/index", 25 | component: loadComponent("home/index"), 26 | meta: { 27 | title: "首页", 28 | }, 29 | }, 30 | { 31 | path: "/label", 32 | component: loadComponent("home/label"), 33 | children: [ 34 | { 35 | path: ":keyword", 36 | component: loadComponent("home/components/blogClassify"), 37 | meta: { 38 | title: "文章分类", 39 | }, 40 | }, 41 | ], 42 | }, 43 | { 44 | path: "/article/detail", 45 | component: loadComponent("article/detail"), 46 | children: [ 47 | { 48 | path: ":id", 49 | component: loadComponent("article/components/detailContent"), 50 | meta: { 51 | title: "文章详情", 52 | }, 53 | }, 54 | ], 55 | }, 56 | { 57 | path: "/message", 58 | component: loadComponent("message/index"), 59 | meta: { 60 | title: "留言板", 61 | }, 62 | }, 63 | { 64 | path: "/myself", 65 | component: loadComponent("myself/index"), 66 | meta: { 67 | title: "关于我", 68 | }, 69 | }, 70 | { 71 | path: "*", 72 | redirect: "/index", 73 | }, 74 | ], 75 | }); 76 | 77 | router.beforeEach((to, from, next) => { 78 | /* 路由发生变化修改页面title */ 79 | if (to.meta && to.meta.title) { 80 | document.title = to.meta.title; 81 | } 82 | next(); 83 | }); 84 | 85 | export default router; 86 | -------------------------------------------------------------------------------- /code/admin/src/components/github.vue: -------------------------------------------------------------------------------- 1 | 30 | 41 | 42 | 71 | 72 | -------------------------------------------------------------------------------- /code/client/src/components/github.vue: -------------------------------------------------------------------------------- 1 | 30 | 41 | 42 | 71 | 72 | -------------------------------------------------------------------------------- /code/admin/src/utils/storage.js: -------------------------------------------------------------------------------- 1 | const ls = window.localStorage; 2 | const ss = window.sessionStorage; 3 | 4 | export const Cookie = { 5 | get(key) { 6 | let arr = document.cookie.split('; '); 7 | for (let i = 0; i < arr.length; i++) { 8 | let arr2 = arr[i].trim().split('='); 9 | if (arr2[0] == key) { 10 | return arr2[1]; 11 | } 12 | } 13 | return ''; 14 | }, 15 | set(key, value, day) { 16 | let setting = arguments[0]; 17 | if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') { 18 | for (let i in setting) { 19 | let oDate = new Date(); 20 | oDate.setDate(oDate.getDate() + day); 21 | document.cookie = i + '=' + setting[i] + ';expires=' + oDate; 22 | } 23 | } else { 24 | let oDate = new Date(); 25 | oDate.setDate(oDate.getDate() + day); 26 | document.cookie = key + '=' + value + ';expires=' + oDate; 27 | } 28 | }, 29 | remove(key) { 30 | this.set(key, 1, -1); 31 | }, 32 | }; 33 | 34 | export const Local = { 35 | get(key) { 36 | if (key) return JSON.parse(ls.getItem(key)); 37 | return null; 38 | }, 39 | set(key, val) { 40 | const setting = arguments[0]; 41 | if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') { 42 | for (const i in setting) { 43 | ls.setItem(i, JSON.stringify(setting[i])); 44 | } 45 | } else { 46 | ls.setItem(key, JSON.stringify(val)); 47 | } 48 | }, 49 | remove(key) { 50 | ls.removeItem(key); 51 | }, 52 | clear() { 53 | ls.clear(); 54 | }, 55 | }; 56 | 57 | export const Session = { 58 | get(key) { 59 | if (key) return JSON.parse(ss.getItem(key)); 60 | return null; 61 | }, 62 | set(key, val) { 63 | const setting = arguments[0]; 64 | if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') { 65 | for (const i in setting) { 66 | ss.setItem(i, JSON.stringify(setting[i])); 67 | } 68 | } else { 69 | ss.setItem(key, JSON.stringify(val)); 70 | } 71 | }, 72 | remove(key) { 73 | ss.removeItem(key); 74 | }, 75 | clear() { 76 | ss.clear(); 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /code/admin/src/components/hamburger.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | 49 | 64 | -------------------------------------------------------------------------------- /code/admin/src/views/Layout/slideBar.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 60 | 61 | -------------------------------------------------------------------------------- /code/admin/src/components/common/page/zp-page.vue: -------------------------------------------------------------------------------- 1 | 7 | 20 | 27 | 65 | -------------------------------------------------------------------------------- /code/client/src/style/less/animation.less: -------------------------------------------------------------------------------- 1 | @keyframes easeBarrageMove { 2 | 100% {left: -50%;} 3 | } 4 | 5 | @keyframes btnGroups { 6 | 0% {transform: scale(1.2, 0.8);} 7 | 1% {transform: scale(1.18, 0.82);} 8 | 2% {transform: scale(1.16, 0.84);} 9 | 3% {transform: scale(1.13, 0.87);} 10 | 4% {transform: scale(1.1, 0.9);} 11 | 5% {transform: scale(1.07, 0.93);} 12 | 6% {transform: scale(1.04, 0.96);} 13 | 7% {transform: scale(1.01, 0.99);} 14 | 8% {transform: scale(0.99, 1.01);} 15 | 9% {transform: scale(0.97, 1.03);} 16 | 10% {transform: scale(0.95, 1.05);} 17 | 11% {transform: scale(0.94, 1.06);} 18 | 12% {transform: scale(0.93, 1.07);} 19 | 13% {transform: scale(0.93, 1.07);} 20 | 14% {transform: scale(0.93, 1.07);} 21 | 15% {transform: scale(0.93, 1.07);} 22 | 16% {transform: scale(0.94, 1.06);} 23 | 17% {transform: scale(0.94, 1.06);} 24 | 18% {transform: scale(0.95, 1.05);} 25 | 19% {transform: scale(0.96, 1.04);} 26 | 20% {transform: scale(0.98, 1.02);} 27 | 21% {transform: scale(0.99, 1.01);} 28 | 22% {transform: scale(1, 1);} 29 | 23% {transform: scale(1, 1);} 30 | 24% {transform: scale(1.01, 0.99);} 31 | 25% {transform: scale(1.02, 0.98);} 32 | 26% {transform: scale(1.02, 0.98);} 33 | 27% {transform: scale(1.02, 0.98);} 34 | 28% {transform: scale(1.03, 0.97);} 35 | 29% {transform: scale(1.03, 0.97);} 36 | 30% {transform: scale(1.02, 0.98);} 37 | 31% {transform: scale(1.02, 0.98);} 38 | 32% {transform: scale(1.02, 0.98);} 39 | 33% {transform: scale(1.02, 0.98);} 40 | 34% {transform: scale(1.01, 0.99);} 41 | 35% {transform: scale(1.01, 0.99);} 42 | 36% {transform: scale(1.01, 0.99);} 43 | 37% {transform: scale(1, 1);} 44 | 38% {transform: scale(1, 1);} 45 | 39% {transform: scale(1, 1);} 46 | 40% {transform: scale(0.99, 1.01);} 47 | 41% {transform: scale(0.99, 1.01);} 48 | 42% {transform: scale(0.99, 1.01);} 49 | 43% {transform: scale(0.99, 1.01);} 50 | 44% {transform: scale(0.99, 1.01);} 51 | 45% {transform: scale(0.99, 1.01);} 52 | 46% {transform: scale(0.99, 1.01);} 53 | 47% {transform: scale(0.99, 1.01);} 54 | 48% {transform: scale(0.99, 1.01);} 55 | 49% {transform: scale(1, 1);} 56 | } -------------------------------------------------------------------------------- /code/client/src/views/home/components/intro.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 87 | -------------------------------------------------------------------------------- /code/client/src/style/less/init.less: -------------------------------------------------------------------------------- 1 | body { 2 | background: @backgroundColor; 3 | border-top: 1px solid transparent; 4 | margin-top: -1px; 5 | font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", sans-serif; 6 | } 7 | 8 | button { 9 | border: none; 10 | background: none; 11 | outline: none; 12 | } 13 | input, 14 | textarea { 15 | -webkit-appearance: none; 16 | } 17 | 18 | .icon { 19 | width: 1em; 20 | height: 1em; 21 | vertical-align: -0.15em; 22 | fill: currentColor; 23 | overflow: hidden; 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | width: 4px; 28 | height: 4px; 29 | } 30 | 31 | ::-webkit-scrollbar-thumb { 32 | opacity: 0.8; 33 | background: #ddd; 34 | border-radius: 4px; 35 | transition: all 0.5s; 36 | } 37 | 38 | /* nprogress styles */ 39 | /* Make clicks pass-through */ 40 | 41 | #nprogress { 42 | pointer-events: none; 43 | } 44 | #nprogress .bar { 45 | background: #dfa400; 46 | position: fixed; 47 | z-index: 1031; 48 | top: 0; 49 | left: 0; 50 | width: 100%; 51 | height: 2px; 52 | } 53 | 54 | /* Fancy blur effect */ 55 | 56 | #nprogress .peg { 57 | display: block; 58 | position: absolute; 59 | right: 0px; 60 | width: 100px; 61 | height: 100%; 62 | box-shadow: 0 0 10px #dfa400, 0 0 5px #dfa400; 63 | opacity: 1; 64 | transform: rotate(3deg) translate(0px, -4px); 65 | } 66 | 67 | /* Remove these to get rid of the spinner */ 68 | 69 | #nprogress .spinner { 70 | display: block; 71 | position: fixed; 72 | z-index: 1031; 73 | top: 15px; 74 | right: 15px; 75 | } 76 | 77 | #nprogress .spinner-icon { 78 | width: 18px; 79 | height: 18px; 80 | box-sizing: border-box; 81 | border: solid 2px transparent; 82 | border-top-color: #dfa400; 83 | border-left-color: #dfa400; 84 | border-radius: 50%; 85 | animation: nprogress-spinner 400ms linear infinite; 86 | } 87 | 88 | .nprogress-custom-parent { 89 | overflow: hidden; 90 | position: relative; 91 | } 92 | 93 | .nprogress-custom-parent #nprogress .spinner, 94 | .nprogress-custom-parent #nprogress .bar { 95 | position: absolute; 96 | } 97 | 98 | @keyframes nprogress-spinner { 99 | 0% { 100 | transform: rotate(0deg); 101 | } 102 | 100% { 103 | transform: rotate(360deg); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /code/admin/src/components/common/page/zp-page-list.vue: -------------------------------------------------------------------------------- 1 | 7 | 32 | 60 | 104 | -------------------------------------------------------------------------------- /code/server/middleware/func/file.js: -------------------------------------------------------------------------------- 1 | import Busboy from "busboy"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | //检测文件并创建文件 6 | const mkdirSync = (dirname) => { 7 | if (fs.existsSync(dirname)) { 8 | return true; 9 | } else { 10 | if (mkdirSync(path.dirname(dirname))) { 11 | fs.mkdirSync(dirname); 12 | return true; 13 | } 14 | } 15 | }; 16 | // 删除本地图片 17 | export const removeFile = (filePath) => { 18 | fs.unlink(filePath, function(err) { 19 | if (err) { 20 | throw err; 21 | } 22 | console.log("文件:" + filePath + "删除成功!"); 23 | }); 24 | }; 25 | 26 | export const uploadFile = (ctx, opts) => { 27 | //重命名 28 | function rename(fileName) { 29 | return ( 30 | Math.random() 31 | .toString(16) 32 | .substr(2) + 33 | "." + 34 | fileName.split(".").pop() 35 | ); 36 | } 37 | let busboy = new Busboy({ headers: ctx.req.headers }); 38 | console.log("start uploading..."); 39 | /* 40 | filename: 字段名, 41 | file: 文件流, 42 | filename: 文件名 43 | */ 44 | return new Promise((resolve, reject) => { 45 | var fileObj = {}; 46 | busboy.on("file", async (fieldname, file, filename, encoding, mimetype) => { 47 | let filePath = "", 48 | imgPrefix = ""; 49 | 50 | filePath = path.join(opts.path, mimetype.split("/")[0] + "s"); 51 | // 现网图片路径不一样 52 | imgPrefix = `${ctx.protocol}://${ctx.host}/${mimetype.split("/")[0]}s`; 53 | 54 | if (!mkdirSync(filePath)) { 55 | throw new Error("没找到目录"); 56 | } 57 | let fName = rename(filename), 58 | fPath = path.join(path.join(filePath, fName)); 59 | file.pipe(fs.createWriteStream(fPath)); 60 | 61 | console.log("fName =>", fName); 62 | console.log("fPath =>", fPath); 63 | 64 | file.on("end", () => { 65 | fileObj[fieldname] = `${imgPrefix}/${fName}`; 66 | }); 67 | }); 68 | 69 | busboy.on( 70 | "field", 71 | ( 72 | fieldname, 73 | val, 74 | fieldnameTruncated, 75 | valTruncated, 76 | encoding, 77 | mimetype 78 | ) => { 79 | fileObj[fieldname] = val; 80 | } 81 | ); 82 | 83 | busboy.on("finish", async () => { 84 | resolve(fileObj); 85 | console.log("finished...", fileObj); 86 | }); 87 | busboy.on("error", function(err) { 88 | console.log("err:" + err); 89 | reject(err); 90 | }); 91 | 92 | ctx.req.pipe(busboy); 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /code/server/router/index.js: -------------------------------------------------------------------------------- 1 | import koaRouter from "koa-router"; 2 | const router = koaRouter(); 3 | 4 | export default (app) => { 5 | /*----------------------admin-------------------------------*/ 6 | // 用户请求 7 | router.post("/admin_api/user/login", app.admin.user.login); 8 | router.get("/admin_api/user/info", app.admin.user.info); 9 | router.get("/admin_api/user/list", app.admin.user.list); 10 | router.post("/admin_api/user/add", app.admin.user.add); 11 | router.post("/admin_api/user/update", app.admin.user.update); 12 | router.get("/admin_api/user/del", app.admin.user.del); 13 | 14 | // 文章请求 15 | router.get("/admin_api/blog/list", app.admin.blog.list); 16 | router.post("/admin_api/blog/add", app.admin.blog.add); 17 | router.post("/admin_api/blog/update", app.admin.blog.update); 18 | router.get("/admin_api/blog/del", app.admin.blog.del); 19 | router.get("/admin_api/blog/info", app.admin.blog.info); 20 | 21 | // 标签请求 22 | router.get("/admin_api/label/list", app.admin.label.list); 23 | router.post("/admin_api/label/add", app.admin.label.add); 24 | router.post("/admin_api/label/update", app.admin.label.update); 25 | router.get("/admin_api/label/del", app.admin.label.del); 26 | 27 | // 留言请求 28 | router.get("/admin_api/message/list", app.admin.message.list); 29 | router.get("/admin_api/message/del", app.admin.message.del); 30 | router.post("/admin_api/message/delReply", app.admin.message.delReply); 31 | 32 | // 图片请求 33 | router.post("/admin_api/uploadImage", app.admin.upload.uploadImage); 34 | router.post("/admin_api/delUploadImage", app.admin.upload.delUploadImage); 35 | 36 | /*----------------------client-------------------------------*/ 37 | // 文章请求 38 | router.get("/client_api/blog/list", app.client.blog.list); 39 | router.get("/client_api/blog/info", app.client.blog.info); 40 | router.post("/client_api/blog/updateLikes", app.client.blog.updateLikes); 41 | router.post("/client_api/blog/updatePV", app.client.blog.updatePV); 42 | 43 | // 标签请求 44 | router.get("/client_api/label/list", app.client.label.list); 45 | 46 | // 留言请求 47 | router.post("/client_api/message/add", app.client.message.add); 48 | router.get("/client_api/message/list", app.client.message.list); 49 | router.get("/client_api/message/replyCount", app.client.message.replyCount); 50 | router.post( 51 | "/client_api/message/updateLikes", 52 | app.client.message.updateLikes 53 | ); 54 | router.post( 55 | "/client_api/message/updateReplys", 56 | app.client.message.updateReplys 57 | ); 58 | 59 | app.use(router.routes()).use(router.allowedMethods()); 60 | }; 61 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/270a.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/components/search.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 66 | 67 | 114 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/270c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/server/middleware/log/log.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js' 2 | import access from './access' // 引入日志输出信息的封装文件 3 | import config from '../../config' 4 | const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]; 5 | 6 | // 提取默认公用参数对象 7 | const baseInfo = config.log 8 | export default (options = {}) => { 9 | let contextLogger = {}, //错误日志等级对象,最后会赋值给ctx上,用于打印各种日志 10 | appenders = {}, //日志配置 11 | opts = Object.assign({}, baseInfo, options), //系统配置 12 | { 13 | logLevel, 14 | dir, 15 | ip, 16 | projectName 17 | } = opts, 18 | commonInfo = { 19 | projectName, 20 | ip 21 | }; //存储公用的日志信息 22 | 23 | //指定要记录的日志分类 24 | appenders.all = { 25 | type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符 26 | filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径 27 | pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面 28 | alwaysIncludePattern: true //是否总是有后缀名 29 | } 30 | 31 | // 环境变量为dev local development 认为是开发环境 32 | if (config.env === "dev" || config.env === "local" || config.env === "development") { 33 | appenders.out = { 34 | type: "console" 35 | } 36 | } 37 | 38 | let logConfig = { 39 | appenders, 40 | 41 | /** 42 | * 指定日志的默认配置项 43 | * 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项 44 | */ 45 | categories: { 46 | default: { 47 | appenders: Object.keys(appenders), 48 | level: logLevel 49 | } 50 | } 51 | } 52 | 53 | let logger = log4js.getLogger('cheese'); 54 | return async (ctx, next) => { 55 | const start = Date.now() // 记录请求开始的时间 56 | 57 | // 循环methods将所有方法挂载到ctx 上 58 | methods.forEach((method, i) => { 59 | contextLogger[method] = message => { 60 | logConfig.appenders.cheese = { 61 | type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符 62 | filename: `${dir}/${method}/`, 63 | pattern: `${method}-yyyy-MM-dd.log`, 64 | alwaysIncludePattern: true //是否总是有后缀名 65 | } 66 | log4js.configure(logConfig) 67 | logger[method](access(ctx, message, commonInfo)) 68 | } 69 | }) 70 | ctx.log = contextLogger 71 | await next() 72 | // 记录完成的时间 作差 计算响应时间 73 | const responseTime = Date.now() - start 74 | 75 | ctx.log.info(access(ctx, { 76 | responseTime: `响应时间为${responseTime/1000}s` 77 | }, commonInfo)) 78 | 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f44f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/images/emoji/1f431.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/client/src/views/message/components/replyItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 60 | 61 | 111 | -------------------------------------------------------------------------------- /code/server/controller/client/message.js: -------------------------------------------------------------------------------- 1 | import messageModel from '../../models/message'; 2 | 3 | module.exports = { 4 | async add(ctx, next) { 5 | console.log('----------------添加留言 message/add-----------------------'); 6 | let paramsData = ctx.request.body; 7 | if (!paramsData.nickname) { 8 | paramsData.nickname = '匿名网友'; 9 | } 10 | try { 11 | await ctx.add(messageModel, paramsData); 12 | ctx.send(paramsData); 13 | } catch (e) { 14 | ctx.sendError(e); 15 | } 16 | }, 17 | 18 | async list(ctx, next) { 19 | console.log( 20 | '----------------获取评论列表 client_api/message/list-----------------------' 21 | ); 22 | let { pageindex = 1, pagesize = 10 } = ctx.request.query; 23 | 24 | // 排序参数 25 | let sortParams = { 26 | createTime: -1, 27 | }; 28 | 29 | let options = { 30 | limit: pagesize * 1, 31 | skip: (pageindex - 1) * pagesize, 32 | sort: sortParams, 33 | }; 34 | 35 | try { 36 | let data = await ctx.find(messageModel, null, null, options); 37 | return ctx.send(data); 38 | } catch (e) { 39 | console.log(e); 40 | return ctx.sendError(e); 41 | } 42 | }, 43 | 44 | async replyCount(ctx, next) { 45 | console.log( 46 | '----------------获取回复列表 client_api/message/replyCount-----------------------' 47 | ); 48 | let conditions = [ 49 | { $project: { replyCount: { $size: '$replyList' } } }, 50 | { $group: { _id: null, replyCount: { $sum: '$replyCount' } } }, 51 | ]; 52 | try { 53 | let data = await ctx.aggregate(messageModel, conditions); 54 | return ctx.send(data); 55 | } catch (e) { 56 | console.log(e); 57 | return ctx.sendError(e); 58 | } 59 | }, 60 | 61 | async updateLikes(ctx, next) { 62 | console.log( 63 | '----------------点赞留言 client_api/message/updateLikes------------------' 64 | ); 65 | let paramsData = ctx.request.body; 66 | let num = JSON.parse(paramsData.isLike) ? -1 : 1; 67 | let options = { $inc: { likes: num } }; 68 | try { 69 | let data = await ctx.update( 70 | messageModel, 71 | { _id: paramsData._id }, 72 | options 73 | ); 74 | ctx.send(); 75 | } catch (e) { 76 | ctx.sendError(e); 77 | } 78 | }, 79 | 80 | async updateReplys(ctx, next) { 81 | console.log( 82 | '----------------回复留言 client_api/message/updateReplys------------------' 83 | ); 84 | let paramsData = ctx.request.body; 85 | if (!paramsData.replyUser) { 86 | paramsData.replyUser = '匿名网友'; 87 | } 88 | let options = { 89 | $push: { replyList: { $each: [paramsData], $position: 0 } }, 90 | }; 91 | try { 92 | let data = await ctx.update( 93 | messageModel, 94 | { _id: paramsData._id }, 95 | options 96 | ); 97 | ctx.send(); 98 | } catch (e) { 99 | ctx.sendError(e); 100 | } 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /code/server/controller/client/blog.js: -------------------------------------------------------------------------------- 1 | import blogModel from '../../models/blog'; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | '----------------获取博客列表 client_api/blog/list-----------------------' 7 | ); 8 | let { 9 | keyword = null, 10 | isQuery = null, 11 | pageindex = 1, 12 | pagesize = 9, 13 | sortBy = null, 14 | isMobile = false, 15 | type = null, 16 | } = ctx.request.query; 17 | 18 | // 条件参数 19 | let conditions = { isVisible: true }; 20 | // 用isMobile来区分移动端和pc端 21 | let reg = new RegExp(keyword, 'i'); 22 | if (isMobile) { 23 | if (type) { 24 | conditions.type = type; 25 | } 26 | if (keyword) { 27 | let searchObj = [{ title: { $regex: reg } }, { desc: { $regex: reg } }]; 28 | conditions['$or'] = [...searchObj]; 29 | } 30 | } else { 31 | if (keyword) { 32 | // 区分搜索框、标签场景 33 | let searchObj = isQuery 34 | ? [{ title: { $regex: reg } }, { desc: { $regex: reg } }] 35 | : [{ type: { $regex: reg } }]; 36 | conditions['$or'] = [...searchObj]; 37 | } 38 | } 39 | // 排序参数 40 | let sortParams = {}; 41 | if (sortBy) { 42 | sortParams[sortBy] = -1; 43 | } else { 44 | sortParams['level'] = -1; 45 | sortParams['releaseTime'] = -1; 46 | } 47 | 48 | let options = { 49 | limit: pagesize * 1, 50 | skip: (pageindex - 1) * pagesize, 51 | sort: sortParams, 52 | }; 53 | 54 | try { 55 | let data = await ctx.find(blogModel, conditions, null, options); 56 | return ctx.send(data); 57 | } catch (e) { 58 | console.log(e); 59 | return ctx.sendError(e); 60 | } 61 | }, 62 | 63 | async info(ctx, next) { 64 | console.log( 65 | '----------------获取博客信息 client_api/blog/info-----------------------' 66 | ); 67 | let _id = ctx.request.query._id; 68 | try { 69 | let data = await ctx.findOne(blogModel, { _id }); 70 | return ctx.send(data); 71 | } catch (e) { 72 | return ctx.sendError(e); 73 | } 74 | }, 75 | 76 | async updateLikes(ctx, next) { 77 | console.log( 78 | '----------------点赞文章 client_api/blog/updateLikes------------------' 79 | ); 80 | let paramsData = ctx.request.body; 81 | let num = JSON.parse(paramsData.isLike) ? -1 : 1; 82 | let options = { $inc: { likes: num } }; 83 | try { 84 | let data = await ctx.update(blogModel, { _id: paramsData._id }, options); 85 | ctx.send(); 86 | } catch (e) { 87 | ctx.sendError(e); 88 | } 89 | }, 90 | 91 | async updatePV(ctx, next) { 92 | console.log( 93 | '----------------文章浏览量 client_api/blog/updatePV------------------' 94 | ); 95 | let paramsData = ctx.request.body; 96 | let options = { $inc: { pv: 1 } }; 97 | try { 98 | let data = await ctx.update(blogModel, { _id: paramsData._id }, options); 99 | ctx.send(); 100 | } catch (e) { 101 | ctx.sendError(e); 102 | } 103 | }, 104 | }; 105 | -------------------------------------------------------------------------------- /code/client/src/components/nav.vue: -------------------------------------------------------------------------------- 1 | 18 | 57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wall-blog", 3 | "version": "1.0.0", 4 | "author": "wall", 5 | "description": "A blog website sharing front-end technology", 6 | "keywords": [ 7 | "vue", 8 | "node", 9 | "koa", 10 | "mongodb" 11 | ], 12 | "main": "index.js", 13 | "scripts": { 14 | "dev:admin": "cross-env NODE_ENV_TYPE=admin webpack-dev-server --config build/webpack.dev.conf.js", 15 | "dev:client": "cross-env NODE_ENV_TYPE=client webpack-dev-server --config build/webpack.dev.conf.js", 16 | "build:admin": "cross-env NODE_ENV_TYPE=admin NODE_ENV=prod node build/build.js", 17 | "build:client": "cross-env NODE_ENV_TYPE=client NODE_ENV=prod node build/build.js", 18 | "analyz:admin": "cross-env analyz_npm_config_report=true npm run build:admin", 19 | "analyz:client": "cross-env analyz_npm_config_report=true npm run build:client", 20 | "server": "cross-env NODE_ENV=development nodemon code/server/index.js", 21 | "start": "pm2 start code/server/index.js", 22 | "stop": "pm2 stop code/server/index.js", 23 | "restart": "pm2 restart code/server/index.js" 24 | }, 25 | "license": "ISC", 26 | "devDependencies": { 27 | "babel-core": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-polyfill": "^6.26.0", 31 | "babel-preset-env": "^1.6.1", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-stage-2": "^6.24.1", 34 | "babel-register": "^6.26.0", 35 | "chalk": "^2.3.0", 36 | "compression-webpack-plugin": "^8.0.1", 37 | "cross-env": "^5.1.3", 38 | "css-loader": "^0.28.8", 39 | "css-minimizer-webpack-plugin": "^3.0.2", 40 | "file-loader": "^1.1.6", 41 | "friendly-errors-webpack-plugin": "^1.6.1", 42 | "html-webpack-plugin": "^5.3.2", 43 | "less": "^2.7.3", 44 | "less-loader": "^4.0.5", 45 | "mini-css-extract-plugin": "^2.2.0", 46 | "node-notifier": "^5.1.2", 47 | "nodemon": "^1.12.1", 48 | "ora": "^1.3.0", 49 | "postcss-loader": "^2.0.10", 50 | "rimraf": "^2.6.2", 51 | "style-resources-loader": "^1.4.1", 52 | "terser-webpack-plugin": "^5.1.4", 53 | "url-loader": "^0.6.2", 54 | "vue-loader": "^15.7.0", 55 | "vue-style-loader": "^4.1.3", 56 | "vue-template-compiler": "^2.5.13", 57 | "webpack": "^5.51.1", 58 | "webpack-bundle-analyzer": "^4.4.2", 59 | "webpack-cli": "^4.8.0", 60 | "webpack-dev-server": "^4.0.0", 61 | "webpack-merge": "^5.8.0" 62 | }, 63 | "dependencies": { 64 | "axios": "^0.17.0", 65 | "babel-polyfill": "^6.26.0", 66 | "busboy": "^0.2.14", 67 | "clipboard": "^2.0.8", 68 | "element-ui": "^2.0.11", 69 | "highlight.js": "^10.7.0", 70 | "ip": "^1.1.5", 71 | "js-md5": "^0.7.3", 72 | "jsonwebtoken": "^8.1.0", 73 | "koa": "^2.4.1", 74 | "koa-bodyparser": "^4.2.0", 75 | "koa-router": "^7.3.0", 76 | "koa-static": "^4.0.2", 77 | "log4js": "^2.4.1", 78 | "marked": "^0.3.12", 79 | "mongoose": "^4.13.9", 80 | "nprogress": "^0.2.0", 81 | "qs": "^6.5.1", 82 | "vue": "^2.5.13", 83 | "vue-color": "^2.8.1", 84 | "vue-lazyload": "^1.3.3", 85 | "vue-router": "^3.0.1", 86 | "vuex": "^3.0.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /code/admin/src/views/Permission/add.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /code/admin/src/views/Label/add.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 103 | -------------------------------------------------------------------------------- /code/server/controller/admin/blog.js: -------------------------------------------------------------------------------- 1 | import blogModel from "../../models/blog"; 2 | import marked from "marked"; 3 | 4 | marked.setOptions({ 5 | renderer: new marked.Renderer(), 6 | gfm: true, //允许 Git Hub标准的markdown. 7 | tables: true, //允许支持表格语法。该选项要求 gfm 为true。 8 | breaks: true, //允许回车换行。该选项要求 gfm 为true。 9 | pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。 10 | sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签) 11 | smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉. 12 | smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。 13 | highlight: function(code) { 14 | return require("highlight.js").highlightAuto(code).value; 15 | }, 16 | }); 17 | 18 | module.exports = { 19 | async list(ctx, next) { 20 | console.log( 21 | "----------------获取博客列表 blog/list-----------------------" 22 | ); 23 | let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query; 24 | console.log("ctx.request =>", ctx.request); 25 | console.log( 26 | "keyword:" + 27 | keyword + 28 | "," + 29 | "pageindex:" + 30 | pageindex + 31 | "," + 32 | "pagesize:" + 33 | pagesize 34 | ); 35 | try { 36 | let reg = new RegExp(keyword, "i"); 37 | let data = await ctx.findPage( 38 | blogModel, 39 | { 40 | $or: [{ type: { $regex: reg } }, { title: { $regex: reg } }], 41 | }, 42 | null, 43 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 44 | ); 45 | ctx.send(data); 46 | } catch (e) { 47 | console.log(e); 48 | ctx.sendError(e); 49 | } 50 | }, 51 | 52 | async add(ctx, next) { 53 | console.log("----------------添加博客 blog/add-----------------------"); 54 | let paramsData = ctx.request.body; 55 | try { 56 | let data = await ctx.findOne(blogModel, { title: paramsData.title }); 57 | if (data) { 58 | ctx.sendError("数据已经存在, 请重新添加!"); 59 | } else { 60 | paramsData.html = marked(paramsData.html); 61 | let data = await ctx.add(blogModel, paramsData); 62 | ctx.send(paramsData); 63 | } 64 | } catch (e) { 65 | ctx.sendError(e); 66 | } 67 | }, 68 | 69 | async update(ctx, next) { 70 | console.log("----------------更新博客 blog/update-----------------------"); 71 | let paramsData = ctx.request.body; 72 | try { 73 | paramsData.html = marked(paramsData.html); 74 | let data = await ctx.update( 75 | blogModel, 76 | { _id: paramsData._id }, 77 | paramsData 78 | ); 79 | ctx.send(); 80 | } catch (e) { 81 | if (e === "暂无数据") { 82 | ctx.sendError(e); 83 | } 84 | } 85 | }, 86 | 87 | async del(ctx, next) { 88 | console.log("----------------删除博客 blog/del-----------------------"); 89 | let id = ctx.request.query.id; 90 | try { 91 | ctx.remove(blogModel, { _id: id }); 92 | ctx.send(); 93 | } catch (e) { 94 | ctx.sendError(e); 95 | } 96 | }, 97 | 98 | async info(ctx, next) { 99 | console.log( 100 | "----------------获取博客信息 blog/info-----------------------" 101 | ); 102 | let _id = ctx.request.query._id; 103 | try { 104 | let data = await ctx.findOne(blogModel, { _id }); 105 | return ctx.send(data); 106 | } catch (e) { 107 | return ctx.sendError(e); 108 | } 109 | }, 110 | }; 111 | --------------------------------------------------------------------------------