├── 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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/code/admin/src/views/Layout/content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 | home
3 |
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 |
2 |
3 |

4 |
5 |
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 |
2 |
5 |
6 |
7 |
23 |
--------------------------------------------------------------------------------
/code/admin/src/components/common/tool/svg-icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
Copyright © www.rasblog.com All rights reserved.
5 | 备案号:粤ICP备2021121326号
9 |
10 |
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 |
3 | {{ title }}
4 |
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 |
3 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
3 |
4 |
9 |
10 |
15 |
16 |
17 |
18 |
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 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
9 |
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 |
2 |
3 |
4 | {{ item.name }}
5 |
6 |
7 |
8 |
9 |
42 |
43 |
--------------------------------------------------------------------------------
/code/client/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
49 |
--------------------------------------------------------------------------------
/code/client/src/components/top.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ userName }}
9 | 退出
10 |
11 |
12 |
13 |
14 |
30 |
--------------------------------------------------------------------------------
/code/admin/src/views/Layout/navBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
38 |
39 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/code/admin/src/components/common/page/zp-page-edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ header }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 | {{ text }}
6 |
7 |
8 |
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 |
2 |
3 |
9 |
15 | {{ tag.name }}
16 |
17 |
18 |
19 |
20 |
21 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/code/admin/src/components/common/page/zp-page-header.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
22 |
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 |
2 |
3 |
4 |
5 | 标签分类
6 |
7 |
8 |
14 |
15 | {{ item.label }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
41 |
42 |
77 |
--------------------------------------------------------------------------------
/code/client/src/images/emoji/1f61a.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/code/client/src/views/article/detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
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 |
2 |
14 |
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 |
2 |
3 |
7 |
8 |
wall | 苏s哈
9 |
Web前端工程师
10 |
11 | 一个热爱篮球与前端技术的95后!20年入行,
12 | 热衷于研究Web前端技术,一边工作一边积累经验,分享一些自己整理的笔记和优选文章。
13 |
14 |
15 |
16 |
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 |
2 |
8 |
28 |
29 |
30 |
41 |
42 |
71 |
72 |
--------------------------------------------------------------------------------
/code/client/src/components/github.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
28 |
29 |
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 |
2 |
3 |
30 |
31 |
32 |
33 |
48 |
49 |
64 |
--------------------------------------------------------------------------------
/code/admin/src/views/Layout/slideBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
42 |
43 |
44 |
60 |
61 |
--------------------------------------------------------------------------------
/code/admin/src/components/common/page/zp-page.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
2 |
3 |
4 |
WALL BLOG
5 |
6 | 享受编程和技术所带来的快乐 – Coding Your Ambition
7 |
8 |
9 |

10 |

11 |

12 |
13 |
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 |
8 |
9 |
10 |
11 |
12 | {{ header }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
2 |
3 |
4 |
11 |
12 |
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 |
2 |
3 |
8 |
9 |
10 |
11 | {{ replyUser }}
12 | @ {{ byReplyUser }}:
13 |
14 |
15 |
16 |
17 | {{ replyTime | formatTime('yyyy-MM-dd hh:mm') }}
18 |
19 |
20 |
21 |
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 |
2 |
3 |
16 |
17 |
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 |
2 |
3 |
4 |
5 | {{ header }}
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
33 |
34 |
35 |
36 |
37 | 立即创建
43 |
44 |
45 |
46 |
47 |
48 |
49 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/code/admin/src/views/Label/add.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ header }}
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 | {{ info.label }}
29 |
30 |
31 |
32 | 立即创建
38 |
39 |
40 |
41 |
42 |
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 |
--------------------------------------------------------------------------------