├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── README_CN.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public ├── admin.html ├── favicon.ico ├── index.html ├── robots.txt └── static │ ├── images │ ├── avatar.png │ └── topic-1.png │ ├── img │ └── icons │ │ ├── android-chrome-168x168.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-48x48.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-72x72.png │ │ ├── android-chrome-96x96.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── msapplication-icon-144x144.png │ │ └── safari-pinned-tab.svg │ └── manifest.json ├── src ├── App.vue ├── api │ ├── config-client.js │ ├── index-client.js │ └── upload-api.js ├── assets │ ├── css │ │ ├── github-markdown.css │ │ └── hljs │ │ │ ├── github.css │ │ │ ├── googlecode.css │ │ │ └── hljs.css │ ├── fonts │ │ └── Play.woff2 │ ├── images │ │ ├── @1x │ │ │ ├── action-add-blue.png │ │ │ ├── action-articles.png │ │ │ ├── action-comment.png │ │ │ ├── action-fav-active.png │ │ │ ├── action-fav.png │ │ │ ├── action-follow-active.png │ │ │ ├── action-follow-blue.png │ │ │ ├── action-follow.png │ │ │ ├── action-invite-blue.png │ │ │ ├── action-share-blue.png │ │ │ ├── action-share.png │ │ │ ├── action-voteup-active.png │ │ │ ├── action-voteup.png │ │ │ ├── arrow-down.png │ │ │ ├── arrow-right-circle-green.png │ │ │ ├── arrow-right.png │ │ │ ├── articles.png │ │ │ ├── circle-loading.png │ │ │ ├── close-black.png │ │ │ ├── close-white.png │ │ │ ├── entry-people.png │ │ │ ├── nav-explore.png │ │ │ ├── nav-features.png │ │ │ ├── nav-home.png │ │ │ ├── nav-logo.png │ │ │ ├── prev-black.png │ │ │ ├── search-white.png │ │ │ ├── select-active.png │ │ │ └── select-inactive.png │ │ ├── @2x │ │ │ ├── action-add-blue.png │ │ │ ├── action-articles.png │ │ │ ├── action-comment.png │ │ │ ├── action-fav-active.png │ │ │ ├── action-fav.png │ │ │ ├── action-follow-active.png │ │ │ ├── action-follow-blue.png │ │ │ ├── action-follow.png │ │ │ ├── action-invite-blue.png │ │ │ ├── action-share-blue.png │ │ │ ├── action-share.png │ │ │ ├── action-voteup-active.png │ │ │ ├── action-voteup.png │ │ │ ├── arrow-down.png │ │ │ ├── arrow-right-circle-green.png │ │ │ ├── arrow-right.png │ │ │ ├── articles.png │ │ │ ├── circle-loading.png │ │ │ ├── close-black.png │ │ │ ├── close-white.png │ │ │ ├── entry-people.png │ │ │ ├── nav-explore.png │ │ │ ├── nav-features.png │ │ │ ├── nav-home.png │ │ │ ├── nav-logo.png │ │ │ ├── prev-black.png │ │ │ ├── search-white.png │ │ │ ├── select-active.png │ │ │ └── select-inactive.png │ │ ├── avatar.png │ │ ├── back-top-1.png │ │ ├── back-top-2.png │ │ ├── back-top.png │ │ ├── loading │ │ │ ├── loading.png │ │ │ └── loading@2x.png │ │ └── topic-1.png │ ├── scss │ │ ├── _actions.scss │ │ ├── _backend.scss │ │ ├── _btn.scss │ │ ├── _comments.scss │ │ ├── _frontend.scss │ │ ├── _icon.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _reset.scss │ │ └── style.scss │ └── svg │ │ └── sentiment-very-satisfied.svg ├── components │ ├── _input.vue │ ├── affix.vue │ ├── app │ │ ├── ajax-form.vue │ │ └── loading-spinner.vue │ ├── aside-account.vue │ ├── aside-category.vue │ ├── aside-other.vue │ ├── aside-trending.vue │ ├── backend-menu.vue │ ├── backtop.vue │ ├── frontend-comment.vue │ ├── item-actions.vue │ ├── navigation.vue │ ├── progress-bar.vue │ ├── signin.vue │ ├── signup.vue │ ├── topics-item-none.vue │ └── topics-item.vue ├── entry-client.js ├── event-bus.js ├── filters │ └── index.js ├── main.js ├── mixins │ ├── check-admin.js │ ├── check-user.js │ └── index.js ├── pages │ ├── 404.vue │ ├── backend-admin-list.vue │ ├── backend-admin-modify.vue │ ├── backend-article-comment.vue │ ├── backend-article-insert.vue │ ├── backend-article-list.vue │ ├── backend-article-modify.vue │ ├── backend-category-insert.vue │ ├── backend-category-list.vue │ ├── backend-category-modify.vue │ ├── backend-login.vue │ ├── backend-user-list.vue │ ├── backend-user-modify.vue │ ├── frontend-about.vue │ ├── frontend-article.vue │ ├── frontend-index.vue │ ├── frontend-user-account.vue │ └── frontend-user-password.vue ├── polyfill │ └── index.js ├── registerServiceWorker.js ├── router.js ├── router │ ├── admin.js │ └── index.js ├── store │ ├── index.js │ └── modules │ │ ├── app-shell.js │ │ ├── backend-admin.js │ │ ├── backend-article.js │ │ ├── backend-user.js │ │ ├── frontend-article.js │ │ ├── global-category.js │ │ ├── global-comment.js │ │ └── global.js └── utils │ └── index.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | public/static/**/*.js 4 | dist/**/*.js 5 | dist/**/*.css 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["lcy-vue"], 3 | "rules": { 4 | "no-irregular-whitespace": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.jsx text eol=lf 4 | *.json text eol=lf 5 | *.vue text eol=lf 6 | *.css text eol=lf 7 | *.scss text eol=lf 8 | *.less text eol=lf 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules 4 | /dist 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | *-port.txt 24 | 25 | package-lock.json 26 | admin.lock 27 | yarn-error.log 28 | server/config/secret.js 29 | server/config/mpapp.js 30 | server/config/shihua.js 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "semi": false, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "arrowParens": "avoid", 8 | "htmlWhitespaceSensitivity": "css" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lincenying 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmf-blog vuejs 2.0 v2 [中文说明](https://github.com/lincenying/mmf-blog-vue2/blob/master/README_CN.md) 2 | 3 | ### Vue2生命周期将近, 请关注Vue3版本 https://github.com/lincenying/mmf-blog-vite-vue3 , 本仓库不再更新 4 | 5 | --- 6 | 7 | --- 8 | 9 | demo: [http://vue.mmxiaowu.com](http://vue.mmxiaowu.com) 10 | 11 | The main technical stack: vue2, vue2-router, vuex, webpack, babel, eslint 12 | 13 | --- 14 | 15 | #### Other versions 16 | 17 | react(spa): [https://github.com/lincenying/mmf-blog-react-v2](https://github.com/lincenying/mmf-blog-react-v2) 18 | 19 | vue2(spa): [https://github.com/lincenying/mmf-blog-vue2](https://github.com/lincenying/mmf-blog-vue2) 20 | 21 | vue2(pwa ssr): [https://github.com/lincenying/mmf-blog-vue2-pwa-ssr](https://github.com/lincenying/mmf-blog-vue2-pwa-ssr) 22 | 23 | vue3(spa): [https://github.com/lincenying/mmf-blog-vite-vue3](https://github.com/lincenying/mmf-blog-vite-vue3) 24 | 25 | vue3(pwa ssr): [https://github.com/lincenying/mmf-blog-vite-vue3-ssr](https://github.com/lincenying/mmf-blog-vite-vue3-ssr) 26 | 27 | --- 28 | 29 | First installation `api server`: 30 | 31 | koa2: https://github.com/lincenying/mmf-blog-api-koa2-v2 32 | 33 | express: https://github.com/lincenying/mmf-blog-api-v2 34 | 35 | ```bash 36 | # Install dependencies 37 | yarn #or npm install 38 | # Note: do not install with CNPM dependency 39 | 40 | # Product 41 | yarn build 42 | 43 | # Develop 44 | yarn serve 45 | ``` 46 | 47 | Home Site 48 | http://localhost:8081 49 | 50 | Login 51 | http://localhost:8081/backend 52 | 53 | # LICENSE 54 | 55 | MIT 56 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # mmf-blog vuejs 2.0 v2版 2 | 3 | demo: [http://vue.mmxiaowu.com](http://vue.mmxiaowu.com) 4 | 5 | #### 说明 6 | 7 | 本站采用 Vue2, Vue-Router, Vuex 搭建 8 | 9 | 网站分成前台和后台, 采用 SPA 模式构建 10 | 11 | 主要功能包括: 管理员, 用户, 分类, 文章, 评论, 文章点赞 12 | 13 | 主要技术栈: vue2, vue2-router, vuex, webpack, babel, eslint 14 | 15 | --- 16 | 17 | #### 其他版本 18 | 19 | react spa版本: [https://github.com/lincenying/mmf-blog-react-v2](https://github.com/lincenying/mmf-blog-react-v2) 20 | 21 | vue2 spa版本: [https://github.com/lincenying/mmf-blog-vue2](https://github.com/lincenying/mmf-blog-vue2) 22 | 23 | vue2 pwa ssr版本: [https://github.com/lincenying/mmf-blog-vue2-pwa-ssr](https://github.com/lincenying/mmf-blog-vue2-pwa-ssr) 24 | 25 | vue3 spa版本: [https://github.com/lincenying/mmf-blog-vite-vue3](https://github.com/lincenying/mmf-blog-vite-vue3) 26 | 27 | vue3 pwa ssr版本: [https://github.com/lincenying/mmf-blog-vite-vue3-ssr](https://github.com/lincenying/mmf-blog-vite-vue3-ssr) 28 | 29 | --- 30 | 31 | 先安装 api server: 32 | 33 | koa2版: https://github.com/lincenying/mmf-blog-api-koa2-v2 34 | 35 | express版: https://github.com/lincenying/mmf-blog-api-v2 36 | 37 | ```bash 38 | # 安装依赖 39 | yarn #or npm install 40 | # 注意: 不要用 cnpm 安装依赖 41 | 42 | # 生产模式 43 | yarn build 44 | 45 | # 开发模式 46 | yarn serve 47 | ``` 48 | 49 | 首页 50 | http://localhost:8081 51 | 52 | 登录 53 | http://localhost:8081/backend 54 | 55 | # LICENSE 56 | 57 | MIT 58 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator'] 4 | } 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"], 6 | "~/*": ["src/*"], 7 | }, 8 | "target": "ES6", 9 | "module": "commonjs", 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmf-blog-vue2", 3 | "version": "2.0.5", 4 | "description": "mmf-blog vue2.0版 (vue2, vue-router, vuex)", 5 | "author": "lincenying ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.24.0", 15 | "core-js": "^3.20.1", 16 | "fastclick": "^1.0.6", 17 | "js-cookie": "^3.0.1", 18 | "lodash.get": "^4.4.2", 19 | "markdown-it": "^12.3.0", 20 | "markdown-it-toc-and-anchor": "^4.2.0", 21 | "mavon-editor": "^2.10.4", 22 | "md5": "^2.3.0", 23 | "nprogress": "^0.2.0", 24 | "register-service-worker": "^1.7.2", 25 | "store2": "^2.13.1", 26 | "toastr": "^2.1.4", 27 | "typescript": "^4.5.4", 28 | "vue": "^2.6.14", 29 | "vue-meta": "^2.4.0", 30 | "vue-router": "^3.5.3", 31 | "vuex": "^3.6.2", 32 | "vuex-router-sync": "^5.0.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.5", 36 | "@babel/plugin-proposal-optional-chaining": "^7.16.5", 37 | "@vue/cli-plugin-babel": "^4.5.15", 38 | "@vue/cli-plugin-eslint": "^4.5.15", 39 | "@vue/cli-plugin-pwa": "^4.5.15", 40 | "@vue/cli-service": "^4.5.15", 41 | "@vue/eslint-config-prettier": "^7.0.0", 42 | "babel-eslint": "^10.1.0", 43 | "babel-plugin-jsx-v-model": "^2.0.3", 44 | "babel-plugin-vue-jsx-sync": "^0.0.5", 45 | "browserslist": "^4.19.1", 46 | "eslint": "7.32.0", 47 | "eslint-config-lcy-vue": "3.1.3", 48 | "eslint-plugin-babel": "^5.3.1", 49 | "eslint-plugin-import": "^2.25.3", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "eslint-plugin-vue": "^8.2.0", 52 | "lint-staged": "^12.1.3", 53 | "node-sass": "^5.0.0", 54 | "sass-loader": "^10.1.1", 55 | "shelljs": "^0.8.4", 56 | "sw-precache-webpack-plugin": "^1.0.0", 57 | "vue-content-loader": "^0.2.3", 58 | "vue-template-compiler": "^2.6.14" 59 | }, 60 | "gitHooks": { 61 | "pre-commit": "lint-staged" 62 | }, 63 | "lint-staged": { 64 | "*.{js,vue}": [ 65 | "vue-cli-service lint" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | M.M.F 小屋 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | M·M·F 小屋 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /admin 4 | Disallow: /backend 5 | -------------------------------------------------------------------------------- /public/static/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/images/avatar.png -------------------------------------------------------------------------------- /public/static/images/topic-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/images/topic-1.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-168x168.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-168x168.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-48x48.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-72x72.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-96x96.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /public/static/img/icons/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /public/static/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/static/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/static/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/static/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/static/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/favicon.ico -------------------------------------------------------------------------------- /public/static/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/public/static/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/static/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "M.M.F小屋", 3 | "short_name": "M.M.F小屋", 4 | "icons": [ 5 | { 6 | "src": "/static/img/icons/android-chrome-48x48.png", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | }, { 10 | "src": "/static/img/icons/android-chrome-72x72.png", 11 | "sizes": "72x72", 12 | "type": "image/png" 13 | }, { 14 | "src": "/static/img/icons/android-chrome-96x96.png", 15 | "sizes": "96x96", 16 | "type": "image/png" 17 | }, { 18 | "src": "/static/img/icons/msapplication-icon-144x144.png", 19 | "sizes": "144x144", 20 | "type": "image/png" 21 | }, { 22 | "src": "/static/img/icons/android-chrome-168x168.png", 23 | "sizes": "168x168", 24 | "type": "image/png" 25 | }, { 26 | "src": "/static/img/icons/android-chrome-192x192.png", 27 | "sizes": "192x192", 28 | "type": "image/png" 29 | }, { 30 | "src": "/static/img/icons/android-chrome-512x512.png", 31 | "sizes": "512x512", 32 | "type": "image/png" 33 | } 34 | ], 35 | "start_url": "/", 36 | "background_color": "#000000", 37 | "display": "standalone", 38 | "theme_color": "#2874f0" 39 | } 40 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 78 | -------------------------------------------------------------------------------- /src/api/config-client.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | api: '/api/', 3 | timeout: 30000 4 | } 5 | -------------------------------------------------------------------------------- /src/api/index-client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import qs from 'qs' 3 | import config from './config-client' 4 | import { showMsg } from '@/utils' 5 | 6 | axios.interceptors.request.use( 7 | config => { 8 | return config 9 | }, 10 | error => { 11 | return Promise.reject(error) 12 | } 13 | ) 14 | 15 | axios.interceptors.response.use( 16 | response => response, 17 | error => Promise.resolve(error.response) 18 | ) 19 | 20 | function checkStatus(response) { 21 | if (response && (response.status === 200 || response.status === 304)) { 22 | return response 23 | } 24 | return { 25 | data: { 26 | code: -404, 27 | message: response.statusText, 28 | data: '' 29 | } 30 | } 31 | } 32 | 33 | function checkCode(res) { 34 | if (res.data.code === -500) { 35 | window.location.href = '/backend' 36 | } else if (res.data.code === -400) { 37 | window.location.href = '/' 38 | } else if (res.data.code !== 200) { 39 | showMsg(res.data.message) 40 | } 41 | return res && res.data 42 | } 43 | 44 | export default { 45 | file(url, data) { 46 | return axios({ 47 | method: 'post', 48 | url, 49 | data, 50 | headers: { 51 | 'X-Requested-With': 'XMLHttpRequest' 52 | } 53 | }) 54 | .then(checkStatus) 55 | .then(checkCode) 56 | }, 57 | post(url, data) { 58 | return axios({ 59 | method: 'post', 60 | url: config.api + url, 61 | data: qs.stringify(data), 62 | timeout: config.timeout, 63 | headers: { 64 | 'X-Requested-With': 'XMLHttpRequest', 65 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 66 | } 67 | }) 68 | .then(checkStatus) 69 | .then(checkCode) 70 | }, 71 | get(url, params) { 72 | return axios({ 73 | method: 'get', 74 | url: config.api + url, 75 | params, 76 | timeout: config.timeout, 77 | headers: { 78 | 'X-Requested-With': 'XMLHttpRequest' 79 | } 80 | }) 81 | .then(checkStatus) 82 | .then(checkCode) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/api/upload-api.js: -------------------------------------------------------------------------------- 1 | export const uploadApi = 'https://php.mmxiaowu.com' 2 | -------------------------------------------------------------------------------- /src/assets/css/hljs/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/css/hljs/googlecode.css: -------------------------------------------------------------------------------- 1 | .hljs-comment, 2 | .hljs-quote { 3 | color: #800; 4 | } 5 | 6 | .hljs-keyword, 7 | .hljs-selector-tag, 8 | .hljs-section, 9 | .hljs-title, 10 | .hljs-name { 11 | color: #008; 12 | } 13 | 14 | .hljs-variable, 15 | .hljs-template-variable { 16 | color: #660; 17 | } 18 | 19 | .hljs-string, 20 | .hljs-selector-attr, 21 | .hljs-selector-pseudo, 22 | .hljs-regexp { 23 | color: #080; 24 | } 25 | 26 | .hljs-literal, 27 | .hljs-symbol, 28 | .hljs-bullet, 29 | .hljs-meta, 30 | .hljs-number, 31 | .hljs-link { 32 | color: #066; 33 | } 34 | 35 | .hljs-title, 36 | .hljs-doctag, 37 | .hljs-type, 38 | .hljs-attr, 39 | .hljs-built_in, 40 | .hljs-builtin-name, 41 | .hljs-params { 42 | color: #606; 43 | } 44 | 45 | .hljs-attribute, 46 | .hljs-subst { 47 | color: #000; 48 | } 49 | 50 | .hljs-formula { 51 | background-color: #eee; 52 | font-style: italic; 53 | } 54 | 55 | .hljs-selector-id, 56 | .hljs-selector-class { 57 | color: #9b703f; 58 | } 59 | 60 | .hljs-addition { 61 | background-color: #baeeba; 62 | } 63 | 64 | .hljs-deletion { 65 | background-color: #ffc8bd; 66 | } 67 | 68 | .hljs-doctag, 69 | .hljs-strong { 70 | font-weight: bold; 71 | } 72 | 73 | .hljs-emphasis { 74 | font-style: italic; 75 | } 76 | -------------------------------------------------------------------------------- /src/assets/css/hljs/hljs.css: -------------------------------------------------------------------------------- 1 | .hljs { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 0.5em; 5 | background: #f0f0f0; 6 | } 7 | .hljs, 8 | .hljs-subst { 9 | color: #444; 10 | } 11 | .hljs-comment { 12 | color: #888888; 13 | } 14 | .hljs-keyword, 15 | .hljs-attribute, 16 | .hljs-selector-tag, 17 | .hljs-meta-keyword, 18 | .hljs-doctag, 19 | .hljs-name { 20 | font-weight: bold; 21 | } 22 | .hljs-type, 23 | .hljs-string, 24 | .hljs-number, 25 | .hljs-selector-id, 26 | .hljs-selector-class, 27 | .hljs-quote, 28 | .hljs-template-tag, 29 | .hljs-deletion { 30 | color: #880000; 31 | } 32 | .hljs-title, 33 | .hljs-section { 34 | color: #880000; 35 | font-weight: bold; 36 | } 37 | .hljs-regexp, 38 | .hljs-symbol, 39 | .hljs-variable, 40 | .hljs-template-variable, 41 | .hljs-link, 42 | .hljs-selector-attr, 43 | .hljs-selector-pseudo { 44 | color: #bc6060; 45 | } 46 | .hljs-literal { 47 | color: #78a960; 48 | } 49 | .hljs-built_in, 50 | .hljs-bullet, 51 | .hljs-code, 52 | .hljs-addition { 53 | color: #397300; 54 | } 55 | .hljs-meta { 56 | color: #1f7199; 57 | } 58 | .hljs-meta-string { 59 | color: #4d99bf; 60 | } 61 | .hljs-emphasis { 62 | font-style: italic; 63 | } 64 | .hljs-strong { 65 | font-weight: bold; 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/fonts/Play.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/fonts/Play.woff2 -------------------------------------------------------------------------------- /src/assets/images/@1x/action-add-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-add-blue.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-articles.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-comment.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-fav-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-fav-active.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-fav.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-follow-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-follow-active.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-follow-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-follow-blue.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-follow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-follow.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-invite-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-invite-blue.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-share-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-share-blue.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-share.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-voteup-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-voteup-active.png -------------------------------------------------------------------------------- /src/assets/images/@1x/action-voteup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/action-voteup.png -------------------------------------------------------------------------------- /src/assets/images/@1x/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/arrow-down.png -------------------------------------------------------------------------------- /src/assets/images/@1x/arrow-right-circle-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/arrow-right-circle-green.png -------------------------------------------------------------------------------- /src/assets/images/@1x/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/arrow-right.png -------------------------------------------------------------------------------- /src/assets/images/@1x/articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/articles.png -------------------------------------------------------------------------------- /src/assets/images/@1x/circle-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/circle-loading.png -------------------------------------------------------------------------------- /src/assets/images/@1x/close-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/close-black.png -------------------------------------------------------------------------------- /src/assets/images/@1x/close-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/close-white.png -------------------------------------------------------------------------------- /src/assets/images/@1x/entry-people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/entry-people.png -------------------------------------------------------------------------------- /src/assets/images/@1x/nav-explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/nav-explore.png -------------------------------------------------------------------------------- /src/assets/images/@1x/nav-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/nav-features.png -------------------------------------------------------------------------------- /src/assets/images/@1x/nav-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/nav-home.png -------------------------------------------------------------------------------- /src/assets/images/@1x/nav-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/nav-logo.png -------------------------------------------------------------------------------- /src/assets/images/@1x/prev-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/prev-black.png -------------------------------------------------------------------------------- /src/assets/images/@1x/search-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/search-white.png -------------------------------------------------------------------------------- /src/assets/images/@1x/select-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/select-active.png -------------------------------------------------------------------------------- /src/assets/images/@1x/select-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@1x/select-inactive.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-add-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-add-blue.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-articles.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-comment.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-fav-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-fav-active.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-fav.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-follow-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-follow-active.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-follow-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-follow-blue.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-follow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-follow.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-invite-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-invite-blue.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-share-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-share-blue.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-share.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-voteup-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-voteup-active.png -------------------------------------------------------------------------------- /src/assets/images/@2x/action-voteup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/action-voteup.png -------------------------------------------------------------------------------- /src/assets/images/@2x/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/arrow-down.png -------------------------------------------------------------------------------- /src/assets/images/@2x/arrow-right-circle-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/arrow-right-circle-green.png -------------------------------------------------------------------------------- /src/assets/images/@2x/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/arrow-right.png -------------------------------------------------------------------------------- /src/assets/images/@2x/articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/articles.png -------------------------------------------------------------------------------- /src/assets/images/@2x/circle-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/circle-loading.png -------------------------------------------------------------------------------- /src/assets/images/@2x/close-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/close-black.png -------------------------------------------------------------------------------- /src/assets/images/@2x/close-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/close-white.png -------------------------------------------------------------------------------- /src/assets/images/@2x/entry-people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/entry-people.png -------------------------------------------------------------------------------- /src/assets/images/@2x/nav-explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/nav-explore.png -------------------------------------------------------------------------------- /src/assets/images/@2x/nav-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/nav-features.png -------------------------------------------------------------------------------- /src/assets/images/@2x/nav-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/nav-home.png -------------------------------------------------------------------------------- /src/assets/images/@2x/nav-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/nav-logo.png -------------------------------------------------------------------------------- /src/assets/images/@2x/prev-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/prev-black.png -------------------------------------------------------------------------------- /src/assets/images/@2x/search-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/search-white.png -------------------------------------------------------------------------------- /src/assets/images/@2x/select-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/select-active.png -------------------------------------------------------------------------------- /src/assets/images/@2x/select-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/@2x/select-inactive.png -------------------------------------------------------------------------------- /src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/assets/images/back-top-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/back-top-1.png -------------------------------------------------------------------------------- /src/assets/images/back-top-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/back-top-2.png -------------------------------------------------------------------------------- /src/assets/images/back-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/back-top.png -------------------------------------------------------------------------------- /src/assets/images/loading/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/loading/loading.png -------------------------------------------------------------------------------- /src/assets/images/loading/loading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/loading/loading@2x.png -------------------------------------------------------------------------------- /src/assets/images/topic-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincenying/mmf-blog-vue2/ac5c374f97fe4cbe1e0beca4529a671ca27e1fdc/src/assets/images/topic-1.png -------------------------------------------------------------------------------- /src/assets/scss/_actions.scss: -------------------------------------------------------------------------------- 1 | // item-actions.vue 2 | 3 | .actions-wrap { 4 | border-top: 1px solid #eef2f2; 5 | height: 48px; 6 | font-size: 0; 7 | .action-item { 8 | width: 25%; 9 | display: inline-block; 10 | color: #777; 11 | font-size: 14px; 12 | text-align: center; 13 | line-height: 48px; 14 | position: relative; 15 | transition: all 0.2s ease; 16 | &.active { 17 | color: #3aced5; 18 | } 19 | 20 | &:first-child { 21 | border-radius: 0 0 0 4px; 22 | } 23 | 24 | &:last-child { 25 | border-radius: 0 0 4px 0; 26 | } 27 | 28 | & + .action-item { 29 | &:before { 30 | content: ''; 31 | position: absolute; 32 | height: 48px; 33 | top: 0; 34 | left: 0; 35 | width: 1px; 36 | background: -webkit-linear-gradient(rgba(238, 242, 242, 0.4), rgba(238, 242, 242, 0.8)) top; 37 | background: linear-gradient(rgba(238, 242, 242, 0.4), rgba(238, 242, 242, 0.8)) top; 38 | } 39 | } 40 | &:hover { 41 | opacity: 0.8; 42 | background: #fafbfb; 43 | } 44 | .icon { 45 | vertical-align: -2px; 46 | margin-right: 10px; 47 | } 48 | .icon-action-follow, 49 | .icon-action-follow-active, 50 | .icon-action-follow-blue { 51 | vertical-align: -3px; 52 | margin-right: 9px; 53 | } 54 | 55 | .icon-action-fav, 56 | .icon-action-fav-active { 57 | margin-right: 9px; 58 | } 59 | 60 | .icon-action-share, 61 | .icon-action-share-blue { 62 | vertical-align: -3px; 63 | } 64 | } 65 | .action-item-voteup { 66 | .icon-action-voteup-active { 67 | display: none; 68 | } 69 | &.active { 70 | .icon-action-voteup-active { 71 | display: inline-block; 72 | } 73 | .icon-action-voteup { 74 | display: none; 75 | } 76 | } 77 | } 78 | .action-item-fav { 79 | .icon-action-fav-active { 80 | display: none; 81 | } 82 | &.active { 83 | .icon-action-fav-active { 84 | display: inline-block; 85 | } 86 | .icon-action-fav { 87 | display: none; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/scss/_backend.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: all 0.3s ease; 3 | } 4 | .fade-enter { 5 | opacity: 1; 6 | transform: translate3d(0, 100px, 0); 7 | } 8 | .fade-leave-active { 9 | opacity: 0; 10 | transform: translate3d(100px, 0, 0); 11 | } 12 | .beian { 13 | float: right; 14 | } 15 | .beian i { 16 | width: 14px; 17 | height: 14px; 18 | background: url(https://ae01.alicdn.com/kf/HTB1RRHHbGSs3KVjSZPiq6AsiVXaV.jpg); 19 | background-size: cover; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/scss/_btn.scss: -------------------------------------------------------------------------------- 1 | // 全局按钮样式 2 | 3 | .btn { 4 | height: 40px; 5 | font-size: 15px; 6 | line-height: 40px; 7 | text-align: center; 8 | padding: 0 15px; 9 | border-radius: 4px; 10 | & + .btn { 11 | margin-left: 5px; 12 | } 13 | &.block { 14 | width: 100%; 15 | margin-left: 0; 16 | } 17 | &.disabled { 18 | background: #e2e9e9; 19 | color: #818c8c; 20 | &:hover { 21 | background: #e2e9e9; 22 | color: #818c8c; 23 | } 24 | } 25 | .icon { 26 | vertical-align: -2px; 27 | margin-right: 10px; 28 | } 29 | } 30 | 31 | .btn-yellow { 32 | background: #eded63; 33 | color: #7e7e10; 34 | display: inline-block; 35 | &:hover { 36 | background: #f1f188; 37 | } 38 | } 39 | 40 | .btn-white { 41 | background: white; 42 | color: #3e4545; 43 | &:hover { 44 | opacity: 0.8; 45 | } 46 | } 47 | 48 | .btn-white-border { 49 | height: 38px; 50 | border: 1px solid white; 51 | color: white; 52 | &:hover { 53 | opacity: 0.8; 54 | } 55 | } 56 | 57 | .btn-black { 58 | background: #3e4545; 59 | color: white; 60 | display: inline-block; 61 | &:hover { 62 | background: #515a5a; 63 | } 64 | } 65 | 66 | .btn-blue { 67 | background: #54d9e0; 68 | color: white; 69 | display: inline-block; 70 | &:hover { 71 | background: #77e1e6; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/scss/_comments.scss: -------------------------------------------------------------------------------- 1 | // frontend-comment.vue && backend-article-comment.vue 2 | 3 | .comments { 4 | margin-left: 25px; 5 | padding-top: 20px; 6 | padding-right: 25px; 7 | border-top: 1px solid #f2f3f4; 8 | .comment-item { 9 | display: flex; 10 | padding: 18px 0; 11 | &:hover .comment-actions-hidden { 12 | opacity: 1; 13 | visibility: visible; 14 | } 15 | .comment-content-wrap { 16 | flex: 1 1 auto; 17 | margin-left: 15px; 18 | .comment-author-wrap { 19 | line-height: 1; 20 | display: block; 21 | margin-top: 1px; 22 | color: #888; 23 | } 24 | .comment-content { 25 | margin-top: 5px; 26 | } 27 | .comment-footer { 28 | font-size: 14px; 29 | color: #888; 30 | margin-top: 6px; 31 | line-height: 1; 32 | .comment-time { 33 | padding-right: 5px; 34 | } 35 | .comment-action-item { 36 | color: #888; 37 | &:hover { 38 | color: #444; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | // 发布框 45 | .comment-post-wrap { 46 | display: flex; 47 | flex-wrap: wrap; 48 | padding-bottom: 20px; 49 | .comment-post-input-wrap { 50 | flex: 1 1 auto; 51 | margin-left: 15px; 52 | textarea { 53 | display: block; 54 | width: 100%; 55 | padding: 10px 14px; 56 | height: 88px; 57 | resize: none; 58 | } 59 | } 60 | .comment-post-actions { 61 | flex: none; 62 | width: 100%; 63 | padding-top: 10px; 64 | text-align: right; 65 | } 66 | } 67 | 68 | .avatar-img { 69 | flex: none; 70 | width: 40px; 71 | height: 40px; 72 | border-radius: 20px; 73 | } 74 | 75 | .load-more-wrap { 76 | padding: 0 0 10px; 77 | margin-top: 10px; 78 | .comments-load-more { 79 | display: block; 80 | background: #f3f6f6; 81 | color: #444; 82 | border-radius: 4px; 83 | line-height: 40px; 84 | text-align: center; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/assets/scss/_frontend.scss: -------------------------------------------------------------------------------- 1 | // publish/index.html ========>>>>>>>> 2 | .app-refresh { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: 10001; 8 | height: 0; 9 | padding: 0 18px; 10 | background: #000; 11 | line-height: 52px; 12 | overflow: hidden; 13 | transition: all 0.3s ease; 14 | .app-refresh-wrap { 15 | display: flex; 16 | color: #fff; 17 | font-size: 15px; 18 | cursor: pointer; 19 | } 20 | .app-refresh-wrap label { 21 | flex: 1; 22 | } 23 | .app-refresh-show { 24 | height: 52px; 25 | } 26 | } 27 | // publish/index.html <<<<<<<<======== 28 | 29 | // app.vue ========>>>>>>>> 30 | .slide-left-enter-active, 31 | .slide-left-leave-active, 32 | .slide-right-enter-active, 33 | .slide-right-leave-active { 34 | transition: all 0.3s ease; 35 | } 36 | .slide-left-enter, 37 | .slide-right-enter { 38 | opacity: 1; 39 | transform: translate3d(0, 100px, 0); 40 | } 41 | .slide-right-leave-active, 42 | .slide-left-leave-active { 43 | opacity: 0; 44 | transform: translate3d(100px, 0, 0); 45 | } 46 | @media (max-width: 540px) { 47 | .slide-left-enter-active, 48 | .slide-left-leave-active, 49 | .slide-right-enter-active, 50 | .slide-right-leave-active { 51 | transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1); 52 | } 53 | .slide-left-enter { 54 | transform: translate(100%, 0); 55 | } 56 | .slide-right-enter { 57 | transform: translate(-100%, 0); 58 | } 59 | .slide-right-leave-active { 60 | transform: translate(100%, 0); 61 | } 62 | .slide-left-leave-active { 63 | transform: translate(-100%, 0); 64 | } 65 | } 66 | 67 | .fade-enter-active, 68 | .fade-leave-active { 69 | transition: opacity 0.3s; 70 | } 71 | .fade-enter, 72 | .fade-leave-to { 73 | opacity: 0; 74 | } 75 | // app.vue <<<<<<<<======== 76 | 77 | // about.vue ========>>>>>>>> 78 | .flex-item { 79 | display: flex; 80 | margin-bottom: 10px; 81 | .flex-label { 82 | width: 70px; 83 | } 84 | } 85 | // about.vue <<<<<<<<======== 86 | 87 | // app.vue ---> backtop.vue ========>>>>>>>> 88 | .back-top { 89 | transition: all 0.3s ease; 90 | position: fixed; 91 | left: 50%; 92 | bottom: 10%; 93 | width: 30px; 94 | height: 65px; 95 | margin-left: 520px; 96 | a { 97 | display: block; 98 | width: 100%; 99 | height: 100%; 100 | background-image: url(../images/back-top.png); 101 | background-size: cover; 102 | } 103 | } 104 | // .back-top { 105 | // transition: all 0.3s ease; 106 | // position: fixed; 107 | // left: 50%; 108 | // bottom: 10%; 109 | // width: 35px; 110 | // height: 35px; 111 | // margin-left: 520px; 112 | // a { 113 | // display: block; 114 | // width: 100%; 115 | // height: 100%; 116 | // background-image: url(../images/back-top-1.png); 117 | // background-size: cover; 118 | // &:hover { 119 | // background-image: url(../images/back-top-2.png); 120 | // } 121 | // } 122 | // } 123 | // app.vue ---> backtop.vue <<<<<<<<======== 124 | 125 | // toastr 距离顶部距离 ========>>>>>>>> 126 | .toast-top-center { 127 | top: 10px !important; 128 | } 129 | // toastr 距离顶部距离 <<<<<<<<======== 130 | 131 | // loading-spinner.vue ========>>>>>>>> 132 | .spinner { 133 | width: 90px; 134 | height: 90px; 135 | position: absolute; 136 | z-index: 10099; 137 | left: 50%; 138 | top: 50%; 139 | margin: -125px 0 0 -45px; 140 | text-align: center; 141 | animation: rotate 2s infinite linear; 142 | .dot1, 143 | .dot2 { 144 | width: 60%; 145 | height: 60%; 146 | display: inline-block; 147 | position: absolute; 148 | top: 0; 149 | background-color: #67cf22; 150 | border-radius: 100%; 151 | animation: bounce 2s infinite ease-in-out; 152 | } 153 | .dot2 { 154 | top: auto; 155 | bottom: 0; 156 | animation-delay: -1s; 157 | } 158 | } 159 | 160 | @keyframes rotate { 161 | 100% { 162 | transform: rotate(360deg); 163 | } 164 | } 165 | 166 | @keyframes bounce { 167 | 0%, 168 | 100% { 169 | transform: scale(0); 170 | } 171 | 50% { 172 | transform: scale(1); 173 | } 174 | } 175 | // loading-spinner.vue <<<<<<<<======== 176 | 177 | // progress-bar.vue ========>>>>>>>> 178 | .progress { 179 | position: fixed; 180 | top: 0; 181 | left: 0; 182 | right: 0; 183 | height: 2px; 184 | width: 0; 185 | /* transition width .2s, opacity .4s; */ 186 | opacity: 1; 187 | background-color: #efc14e; 188 | z-index: 999999; 189 | } 190 | // progress-bar.vue <<<<<<<<======== 191 | -------------------------------------------------------------------------------- /src/assets/scss/_modal.scss: -------------------------------------------------------------------------------- 1 | // signin.vue && signup.vue 2 | 3 | .modal-wrap { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | z-index: 1000; 10 | font-size: 0; 11 | padding-left: 1px; 12 | padding-right: 1px; 13 | text-align: center; 14 | font-size: 0.1px; 15 | background: black; 16 | background: rgba(0, 0, 0, 0.72); 17 | visibility: hidden; 18 | opacity: 0; 19 | pointer-events: none; 20 | &.active { 21 | visibility: visible; 22 | opacity: 1; 23 | overflow: auto; 24 | pointer-events: auto; 25 | .modal { 26 | visibility: visible; 27 | opacity: 1; 28 | } 29 | } 30 | .center-helper { 31 | display: inline-block; 32 | height: 100%; 33 | vertical-align: middle; 34 | margin-left: -1px; 35 | } 36 | .modal { 37 | display: inline-block; 38 | vertical-align: middle; 39 | font-size: 16px; 40 | text-align: left; 41 | visibility: hidden; 42 | opacity: 0; 43 | background: white; 44 | margin: auto; 45 | width: 360px; 46 | border-radius: 12px; 47 | padding: 25px; 48 | box-sizing: border-box; 49 | position: relative; 50 | .modal-prev { 51 | width: 78px; 52 | height: 78px; 53 | position: absolute; 54 | left: 0; 55 | top: 0; 56 | opacity: 0.25; 57 | &:hover { 58 | opacity: 1; 59 | } 60 | .icon { 61 | position: absolute; 62 | top: 50%; 63 | left: 50%; 64 | margin-top: -10px; 65 | margin-left: -10px; 66 | } 67 | } 68 | .modal-close { 69 | width: 78px; 70 | height: 78px; 71 | position: absolute; 72 | right: 0; 73 | top: 0; 74 | opacity: 0.25; 75 | &:hover { 76 | opacity: 1; 77 | } 78 | .icon { 79 | position: absolute; 80 | top: 50%; 81 | left: 50%; 82 | margin-top: -8px; 83 | margin-left: -8px; 84 | } 85 | } 86 | .modal-title { 87 | text-align: center; 88 | font-size: 20px; 89 | text-align: center; 90 | font-weight: normal; 91 | letter-spacing: 0.2em; 92 | text-indent: 0.2em; 93 | } 94 | .signup-form { 95 | width: 280px; 96 | margin: 20px auto 10px; 97 | text-align: center; 98 | .input-wrap { 99 | position: relative; 100 | & + .input-wrap { 101 | margin-top: 18px; 102 | } 103 | } 104 | .input-info { 105 | color: #bac1c2; 106 | font-size: 12px; 107 | margin-top: 10px; 108 | margin-left: 14px; 109 | text-align: left; 110 | &.centered { 111 | text-align: center; 112 | margin-left: 0; 113 | } 114 | } 115 | .error-info { 116 | color: #ed4656; 117 | } 118 | .base-input { 119 | width: 100%; 120 | } 121 | .textarea-input { 122 | padding: 10px 14px; 123 | height: 88px; 124 | resize: none; 125 | } 126 | .signup-btn { 127 | width: 100%; 128 | padding: 0; 129 | margin-top: 20px; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/assets/scss/_nav.scss: -------------------------------------------------------------------------------- 1 | .global-warning { 2 | display: none; 3 | height: 40px; 4 | background: #293c3d; 5 | color: white; 6 | line-height: 40px; 7 | font-size: 14px; 8 | text-align: center; 9 | &.active { 10 | display: block; 11 | } 12 | &:hover { 13 | color: rgba(255, 255, 255, 0.6); 14 | } 15 | } 16 | 17 | // navigation.vue 18 | .global-nav { 19 | position: fixed; 20 | top: 0; 21 | z-index: 999; 22 | width: 100%; 23 | height: 60px; 24 | background: #54d9e0; 25 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 26 | 27 | .backend & { 28 | position: static; 29 | top: auto; 30 | z-index: auto; 31 | } 32 | 33 | .wrap { 34 | display: flex; 35 | justify-content: space-between; 36 | } 37 | 38 | .left-part { 39 | display: flex; 40 | } 41 | .right-part { 42 | display: flex; 43 | @media (max-width: 750px) { 44 | display: none; 45 | } 46 | } 47 | 48 | .logo-link { 49 | display: block; 50 | .icon { 51 | margin-top: 15px; 52 | } 53 | } 54 | 55 | .main-nav { 56 | display: flex; 57 | margin-left: 48px; 58 | @media (max-width: 1000px) { 59 | margin-left: 25px; 60 | } 61 | .icon { 62 | vertical-align: -4px; 63 | margin-right: 12px; 64 | @media (max-width: 1000px) { 65 | margin-right: 0; 66 | } 67 | } 68 | } 69 | .nav-link { 70 | position: relative; 71 | padding: 0 24px; 72 | font-size: 18px; 73 | color: white; 74 | line-height: 60px; 75 | &.current:after { 76 | content: ''; 77 | position: absolute; 78 | left: 24px; 79 | right: 24px; 80 | bottom: 0; 81 | height: 3px; 82 | border-radius: 3px; 83 | background: #eded63; 84 | } 85 | @media (max-width: 1000px) { 86 | font-size: 17px; 87 | .text { 88 | display: none; 89 | } 90 | &.current:after { 91 | left: 10px; 92 | right: 10px; 93 | } 94 | } 95 | @media (max-width: 375px) { 96 | padding: 0 15px; 97 | } 98 | } 99 | 100 | .btn { 101 | margin-top: 10px; 102 | font-weight: bold; 103 | font-size: 16px; 104 | } 105 | 106 | .nav-signup { 107 | margin-left: 16px; 108 | } 109 | 110 | .nav-me { 111 | margin-left: 10px; 112 | .nav-avatar-img { 113 | width: 40px; 114 | height: 40px; 115 | margin-top: 10px; 116 | border-radius: 20px; 117 | } 118 | } 119 | .nav-search { 120 | position: relative; 121 | margin-left: 16px; 122 | font-size: 0; 123 | .icon-search-white { 124 | position: absolute; 125 | top: 22px; 126 | left: 14px; 127 | } 128 | 129 | .nav-search-input { 130 | width: 180px; 131 | height: 40px; 132 | margin-top: 10px; 133 | padding: 0 14px 0 40px; 134 | background: rgba(255, 255, 255, 0.25); 135 | border: none; 136 | border-radius: 4px; 137 | font-size: 16px; 138 | color: white; 139 | line-height: 18px; 140 | } 141 | 142 | .nav-search-input::-webkit-input-placeholder { 143 | color: white; 144 | } 145 | 146 | .nav-search-input::-moz-placeholder { 147 | color: white; 148 | } 149 | 150 | .nav-search-input:-moz-placeholder { 151 | color: white; 152 | } 153 | 154 | .nav-search-input:-ms-input-placeholder { 155 | color: white; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/assets/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | html { 2 | overflow-x: hidden; 3 | overflow-y: scroll; 4 | // overflow-anchor: none; 5 | background: #ebf0f0; 6 | } 7 | /* 清除内外边距 */ 8 | 9 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, 10 | /* structural elements 结构元素 */ 11 | 12 | dl, dt, dd, ul, ol, li, 13 | /* list elements 列表元素 */ 14 | 15 | pre, 16 | /* text formatting elements 文本格式元素 */ 17 | 18 | fieldset, button, input, textarea, th, td { 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | /* 设置默认字体 */ 24 | 25 | @font-face { 26 | font-family: 'Play'; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: url(../fonts/Play.woff2) format('woff2'); 30 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 31 | } 32 | 33 | body, 34 | button, 35 | input, 36 | select, 37 | textarea { 38 | color: #333; 39 | outline: none; 40 | font-family: -apple-system-font, 'Helvetica Neue', Tahoma, 'PingFang SC', 'lantinghei sc', 'Microsoft Yahei', sans-serif; 41 | font-size: 16px; 42 | line-height: 1.6; 43 | -webkit-font-smoothing: antialiased; 44 | } 45 | 46 | h1 { 47 | font-size: 20px; 48 | /* 18px / 12px = 1.5 */ 49 | } 50 | 51 | h2 { 52 | font-size: 18px; 53 | } 54 | 55 | h3 { 56 | font-size: 16px; 57 | } 58 | 59 | h4, 60 | h5, 61 | h6 { 62 | font-size: 100%; 63 | } 64 | 65 | address, 66 | cite, 67 | dfn, 68 | em, 69 | var { 70 | font-style: normal; 71 | } 72 | 73 | /* 统一等宽字体 */ 74 | 75 | /* 小于 12px 的中文很难阅读,让 small 正常化 */ 76 | small { 77 | font-size: 12px; 78 | } 79 | 80 | /* 重置列表元素 */ 81 | ul, 82 | ol { 83 | list-style: none; 84 | } 85 | 86 | /* 重置文本格式元素 */ 87 | a { 88 | text-decoration: none; 89 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 90 | } 91 | 92 | a:hover { 93 | text-decoration: none; 94 | } 95 | 96 | abbr[title], 97 | acronym[title] { 98 | /* 注:1.ie6 不支持 abbr; 2.这里用了属性选择符,ie6 下无效果 */ 99 | border-bottom: 1px dotted; 100 | cursor: help; 101 | } 102 | 103 | q:before, 104 | q:after { 105 | content: ''; 106 | } 107 | 108 | /* 重置表单元素 */ 109 | legend { 110 | color: #000; 111 | } 112 | 113 | /* for ie6 */ 114 | fieldset, 115 | img { 116 | border: none; 117 | } 118 | 119 | /* 注:optgroup 无法扶正 */ 120 | button, 121 | input, 122 | select, 123 | textarea { 124 | font-size: 100%; 125 | /* 使得表单元素在 ie 下能继承字体大小 */ 126 | } 127 | 128 | /* 重置表格元素 */ 129 | 130 | table { 131 | border-collapse: collapse; 132 | border-spacing: 0; 133 | } 134 | 135 | /* 重置 hr */ 136 | hr { 137 | border: none; 138 | height: 1px; 139 | } 140 | 141 | article, 142 | aside, 143 | canvas, 144 | figure, 145 | figure img, 146 | figcaption, 147 | hgroup, 148 | footer, 149 | header, 150 | nav, 151 | section, 152 | audio, 153 | video { 154 | position: relative; 155 | display: block; 156 | } 157 | 158 | nav ul, 159 | nav ol { 160 | list-style: none; 161 | } 162 | 163 | .hidden { 164 | display: none; 165 | } 166 | -------------------------------------------------------------------------------- /src/assets/svg/sentiment-very-satisfied.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/_input.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/components/affix.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 110 | -------------------------------------------------------------------------------- /src/components/app/ajax-form.vue: -------------------------------------------------------------------------------- 1 | 4 | 100 | -------------------------------------------------------------------------------- /src/components/app/loading-spinner.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/components/aside-account.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /src/components/aside-category.vue: -------------------------------------------------------------------------------- 1 | 15 | 24 | -------------------------------------------------------------------------------- /src/components/aside-other.vue: -------------------------------------------------------------------------------- 1 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/aside-trending.vue: -------------------------------------------------------------------------------- 1 | 19 | 33 | -------------------------------------------------------------------------------- /src/components/backend-menu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/components/backtop.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 41 | -------------------------------------------------------------------------------- /src/components/frontend-comment.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 96 | -------------------------------------------------------------------------------- /src/components/item-actions.vue: -------------------------------------------------------------------------------- 1 | 18 | 76 | -------------------------------------------------------------------------------- /src/components/navigation.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 64 | -------------------------------------------------------------------------------- /src/components/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 92 | 93 | 107 | -------------------------------------------------------------------------------- /src/components/signin.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 66 | -------------------------------------------------------------------------------- /src/components/signup.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 89 | -------------------------------------------------------------------------------- /src/components/topics-item-none.vue: -------------------------------------------------------------------------------- 1 | 10 | 15 | -------------------------------------------------------------------------------- /src/components/topics-item.vue: -------------------------------------------------------------------------------- 1 | 16 | 31 | -------------------------------------------------------------------------------- /src/entry-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file client entry 3 | * @author lincenying(lincenying@qq.com) 4 | */ 5 | 6 | import Vue from 'vue' 7 | import FastClick from 'fastclick' 8 | import mavonEditor from 'mavon-editor' 9 | import api from '~api' 10 | import VueBus from './event-bus' 11 | import { createApp } from './main' 12 | import './registerServiceWorker' 13 | 14 | import ProgressBar from '~/components/progress-bar.vue' 15 | 16 | import 'mavon-editor/dist/css/index.css' 17 | import 'toastr/build/toastr.css' 18 | import './assets/css/hljs/googlecode.css' 19 | import './assets/css/github-markdown.css' 20 | import './assets/scss/style.scss' 21 | 22 | // 全局的进度条,在组件中可通过 $loading 访问 23 | const loading = (Vue.prototype.$loading = new Vue(ProgressBar).$mount()) 24 | 25 | Vue.use(VueBus) 26 | Vue.use(mavonEditor) 27 | 28 | const { app, router, store } = createApp() 29 | 30 | store.$api = store.state.$api = api 31 | 32 | document.body.appendChild(loading.$el) 33 | FastClick.attach(document.body) 34 | 35 | Vue.mixin({ 36 | // 当复用的路由组件参数发生变化时,例如/detail/1 => /detail/2 37 | /* 38 | beforeRouteUpdate(to, from, next) { 39 | // asyncData方法中包含异步数据请求 40 | const asyncData = this.$options.asyncData 41 | if (asyncData) { 42 | loading.start() 43 | asyncData 44 | .call(this, { 45 | store: this.$store, 46 | route: to, 47 | isServer: false, 48 | isClient: true, 49 | }) 50 | .then(() => { 51 | loading.finish() 52 | next() 53 | }) 54 | .catch(next) 55 | } else { 56 | next() 57 | } 58 | }, 59 | */ 60 | 61 | // 页面渲染后, 跳转到记录的滚动条位置 62 | beforeRouteEnter(to, from, next) { 63 | next(vm => { 64 | // 通过 `vm` 访问组件实例 65 | vm.$nextTick().then(() => { 66 | const scrollTop = vm.$store.state.appShell.historyPageScrollTop[to.fullPath] || 0 67 | setTimeout(() => { 68 | window.scrollTo(0, scrollTop) 69 | }, 350) 70 | }) 71 | }) 72 | }, 73 | // 路由切换时,保存页面滚动位置 74 | beforeRouteLeave(to, from, next) { 75 | this.$store.dispatch('appShell/saveScrollTop', { 76 | path: from.fullPath, 77 | scrollTop: Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) 78 | }) 79 | next() 80 | } 81 | }) 82 | 83 | // 此时异步组件已经加载完成 84 | router.beforeResolve((to, from, next) => { 85 | const matched = router.getMatchedComponents(to) 86 | const prevMatched = router.getMatchedComponents(from) 87 | 88 | // [a, b] 89 | // [a, b, c, d] 90 | // => [c, d] 91 | let diffed = false 92 | const activated = matched.filter((c, i) => diffed || (diffed = prevMatched[i] !== c)) 93 | 94 | if (!activated.length) { 95 | return next() 96 | } 97 | 98 | loading.start() 99 | Promise.all( 100 | activated.map(c => { 101 | /** 102 | * 两种情况下执行asyncData: 103 | * 1. 非keep-alive组件每次都需要执行 104 | * 2. keep-alive组件首次执行,执行后添加标志 105 | */ 106 | if (c.asyncData && (!c.asyncDataFetched || to.meta.notKeepAlive)) { 107 | return c 108 | .asyncData({ 109 | store, 110 | route: to, 111 | isServer: false, 112 | isClient: true 113 | }) 114 | .then(() => { 115 | c.asyncDataFetched = true 116 | }) 117 | } 118 | }) 119 | ) 120 | .then(() => { 121 | loading.finish() 122 | next() 123 | }) 124 | .catch(next) 125 | }) 126 | 127 | router.onReady(() => app.$mount('#app')) 128 | -------------------------------------------------------------------------------- /src/event-bus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 事件总线 3 | * @author lincenying(lincenying@qq.com) 4 | */ 5 | 6 | const install = Vue => { 7 | const bus = new Vue({ 8 | data: { 9 | default: {} 10 | } 11 | }) 12 | 13 | Object.defineProperties(bus, { 14 | on: { 15 | get() { 16 | return this.$on 17 | } 18 | }, 19 | once: { 20 | get() { 21 | return this.$once 22 | } 23 | }, 24 | off: { 25 | get() { 26 | return this.$off 27 | } 28 | }, 29 | emit: { 30 | get() { 31 | return this.$emit 32 | } 33 | } 34 | }) 35 | 36 | Vue.bus = bus 37 | 38 | Object.defineProperty(Vue.prototype, '$bus', { 39 | get() { 40 | return bus 41 | } 42 | }) 43 | } 44 | export default { 45 | install 46 | } 47 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5' 2 | 3 | function pluralize(time, label) { 4 | return time + label 5 | } 6 | 7 | export function timeAgo(time) { 8 | const preg = /^[\d]+$/ 9 | const timestamp = preg.test(time) 10 | if (!timestamp) { 11 | const tmp = Date.parse(time) 12 | time = tmp / 1000 13 | } 14 | const between = Date.now() / 1000 - Number(time) 15 | if (between < 60) { 16 | return '刚刚' 17 | } else if (between < 3600) { 18 | return pluralize(parseInt(between / 60, 10), ' 分钟前') 19 | } else if (between < 86400) { 20 | return pluralize(parseInt(between / 3600, 10), ' 小时前') 21 | } 22 | return pluralize(parseInt(between / 86400, 10), ' 天前') 23 | } 24 | 25 | export function timeYmd(timestamp) { 26 | const preg = /^[\d]+$/ 27 | const isTimestamp = preg.test(timestamp) 28 | if (!isTimestamp) { 29 | let time = Date.parse(timestamp) 30 | time /= 1000 31 | timestamp = time 32 | } 33 | const tmp = new Date(timestamp * 1000) 34 | var year = tmp.getFullYear() 35 | var month = tmp.getMonth() + 1 36 | var date = tmp.getDate() 37 | return year + '-' + (month < 10 ? '0' + month : month) + '-' + (date < 10 ? '0' + date : date) 38 | } 39 | 40 | export function avatar(email, width) { 41 | email = email || '123456' 42 | email = decodeURIComponent(email) 43 | width = width || 256 44 | // return `https://cdn.v2ex.com/gravatar/${md5(email)}?s=${width}&d=identicon&r=g` 45 | // return `https://fdn.geekzu.org/avatar/${md5(email)}?s=${width}&d=identicon&r=g` 46 | return `https://cravatar.cn/avatar/${md5(email)}?s=${width}&d=identicon&r=g` 47 | } 48 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file entry 3 | * @author lincenying(lincenying@qq.com) 4 | */ 5 | 6 | import Vue from 'vue' 7 | import { sync } from 'vuex-router-sync' 8 | 9 | import * as filters from './filters' 10 | import titleMixin from './mixins' 11 | import { createRouter } from './router' 12 | import { createStore } from './store' 13 | import { oc } from './utils' 14 | 15 | import App from './App.vue' 16 | 17 | Vue.mixin(titleMixin) 18 | 19 | Object.keys(filters).forEach(key => { 20 | Vue.filter(key, filters[key]) 21 | }) 22 | 23 | Vue.prototype.$oc = oc 24 | Vue.config.productionTip = false 25 | 26 | /* eslint-disable no-new */ 27 | export function createApp() { 28 | const router = createRouter() 29 | const store = createStore() 30 | sync(store, router) 31 | const app = new Vue({ 32 | router, 33 | store, 34 | ...App 35 | }) 36 | return { app, router, store } 37 | } 38 | -------------------------------------------------------------------------------- /src/mixins/check-admin.js: -------------------------------------------------------------------------------- 1 | // import cookies from 'js-cookie' 2 | // import { inBrowser } from '@/utils' 3 | 4 | export default { 5 | // beforeRouteEnter(to, from, next) { 6 | // if (inBrowser && !cookies.get('b_user')) { 7 | // window.location.href = '/backend' 8 | // } 9 | // next() 10 | // } 11 | } 12 | -------------------------------------------------------------------------------- /src/mixins/check-user.js: -------------------------------------------------------------------------------- 1 | // import cookies from 'js-cookie' 2 | // import { inBrowser } from '@/utils' 3 | 4 | export default { 5 | // beforeRouteEnter(to, from, next) { 6 | // if (inBrowser && !cookies.get('user')) { 7 | // window.location.href = '/' 8 | // } 9 | // next() 10 | // } 11 | } 12 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/pages/404.vue: -------------------------------------------------------------------------------- 1 | 15 | 43 | -------------------------------------------------------------------------------- /src/pages/backend-admin-list.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 83 | -------------------------------------------------------------------------------- /src/pages/backend-admin-modify.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 97 | -------------------------------------------------------------------------------- /src/pages/backend-article-comment.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 89 | -------------------------------------------------------------------------------- /src/pages/backend-article-insert.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 125 | -------------------------------------------------------------------------------- /src/pages/backend-article-list.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 86 | -------------------------------------------------------------------------------- /src/pages/backend-article-modify.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 142 | -------------------------------------------------------------------------------- /src/pages/backend-category-insert.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | -------------------------------------------------------------------------------- /src/pages/backend-category-list.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /src/pages/backend-category-modify.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 89 | -------------------------------------------------------------------------------- /src/pages/backend-login.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | -------------------------------------------------------------------------------- /src/pages/backend-user-list.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 83 | -------------------------------------------------------------------------------- /src/pages/backend-user-modify.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 94 | -------------------------------------------------------------------------------- /src/pages/frontend-about.vue: -------------------------------------------------------------------------------- 1 | 56 | 84 | -------------------------------------------------------------------------------- /src/pages/frontend-article.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 113 | -------------------------------------------------------------------------------- /src/pages/frontend-index.vue: -------------------------------------------------------------------------------- 1 | 32 | 110 | -------------------------------------------------------------------------------- /src/pages/frontend-user-account.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 95 | -------------------------------------------------------------------------------- /src/pages/frontend-user-password.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 79 | -------------------------------------------------------------------------------- /src/polyfill/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by lincenying on 16/5/11. 3 | */ 4 | 5 | import 'core-js/features/promise' 6 | import 'core-js/features/array/find' 7 | import 'core-js/features/array/find-index' 8 | import 'core-js/features/array/includes' 9 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log('App is being served from cache by a service worker.\n For more details, visit https://goo.gl/AFskqB') 9 | }, 10 | registered() { 11 | console.log('Service worker has been registered.') 12 | }, 13 | cached() { 14 | console.log('Content has been cached for offline use.') 15 | }, 16 | updatefound() { 17 | console.log('New content is downloading.') 18 | }, 19 | updated() { 20 | console.log('New content is available; please refresh.') 21 | const metas = document.head.getElementsByTagName('meta') 22 | 23 | for (let i = 0, len = metas.length; i < len; i++) { 24 | const meta = metas[i] 25 | 26 | if (meta.name === 'theme-color') { 27 | meta.content = '#000' 28 | } 29 | } 30 | 31 | const dom = document.createElement('div') 32 | 33 | /* eslint-disable max-len */ 34 | dom.innerHTML = ` 35 |
36 |
37 | 38 | 点击刷新 39 |
40 |
41 | ` 42 | /* eslint-enable max-len */ 43 | 44 | document.body.appendChild(dom) 45 | setTimeout(function () { 46 | document.getElementById('app-refresh').className += ' app-refresh-show' 47 | }, 16) 48 | }, 49 | offline() { 50 | console.log('No internet connection found. App is running in offline mode.') 51 | }, 52 | error(error) { 53 | console.error('Error during service worker registration:', error) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/router/admin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-inline-comments */ 2 | import Vue from 'vue' 3 | import VueRouter from 'vue-router' 4 | import cookies from 'js-cookie' 5 | 6 | import { inBrowser } from '../utils' 7 | 8 | const login = () => import(/* webpackChunkName: "backend-login" */ '../pages/backend-login.vue') 9 | const articleList = () => import(/* webpackChunkName: "backend-topics" */ '../pages/backend-article-list.vue') 10 | const articleInsert = () => import(/* webpackChunkName: "backend-topics" */ '../pages/backend-article-insert.vue') 11 | const articleModify = () => import(/* webpackChunkName: "backend-topics" */ '../pages/backend-article-modify.vue') 12 | const articleComment = () => import(/* webpackChunkName: "backend-topics" */ '../pages/backend-article-comment.vue') 13 | 14 | const categoryList = () => import(/* webpackChunkName: "backend-category" */ '../pages/backend-category-list.vue') 15 | const categoryInsert = () => import(/* webpackChunkName: "backend-category" */ '../pages/backend-category-insert.vue') 16 | const categoryModify = () => import(/* webpackChunkName: "backend-category" */ '../pages/backend-category-modify.vue') 17 | 18 | const adminList = () => import(/* webpackChunkName: "backend-admin" */ '../pages/backend-admin-list.vue') 19 | const adminModify = () => import(/* webpackChunkName: "backend-admin" */ '../pages/backend-admin-modify.vue') 20 | 21 | const userList = () => import(/* webpackChunkName: "backend-user" */ '../pages/backend-user-list.vue') 22 | const userModify = () => import(/* webpackChunkName: "backend-user" */ '../pages/backend-user-modify.vue') 23 | 24 | Vue.use(VueRouter) 25 | 26 | const scrollBehavior = to => { 27 | const position = {} 28 | if (to.hash) { 29 | position.selector = to.hash 30 | } 31 | if (to.matched.some(mm => mm.meta.scrollToTop)) { 32 | position.x = 0 33 | position.y = 0 34 | } 35 | return position 36 | } 37 | 38 | const guardRoute = (to, from, next) => { 39 | const token = cookies.get('b_user') || !inBrowser 40 | if (!token) { 41 | next('/') 42 | } else { 43 | next() 44 | } 45 | } 46 | export function createRouter() { 47 | const router = new VueRouter({ 48 | mode: 'history', 49 | base: __dirname, 50 | scrollBehavior, 51 | routes: [ 52 | { name: 'login', path: '/backend', component: login }, 53 | 54 | { 55 | name: 'admin_list', 56 | path: '/backend/admin/list', 57 | component: adminList, 58 | meta: { scrollToTop: true }, 59 | beforeEnter: guardRoute 60 | }, 61 | { 62 | name: 'admin_modify', 63 | path: '/backend/admin/modify/:id', 64 | component: adminModify, 65 | meta: { scrollToTop: true }, 66 | beforeEnter: guardRoute 67 | }, 68 | 69 | { 70 | name: 'article_list', 71 | path: '/backend/article/list', 72 | component: articleList, 73 | meta: { scrollToTop: true }, 74 | beforeEnter: guardRoute 75 | }, 76 | { 77 | name: 'article_insert', 78 | path: '/backend/article/insert', 79 | component: articleInsert, 80 | meta: { scrollToTop: true }, 81 | beforeEnter: guardRoute 82 | }, 83 | { 84 | name: 'article_modify', 85 | path: '/backend/article/modify/:id', 86 | component: articleModify, 87 | meta: { scrollToTop: true }, 88 | beforeEnter: guardRoute 89 | }, 90 | { 91 | name: 'article_comment', 92 | path: '/backend/article/comment/:id', 93 | component: articleComment, 94 | meta: { scrollToTop: true }, 95 | beforeEnter: guardRoute 96 | }, 97 | 98 | { 99 | name: 'category_list', 100 | path: '/backend/category/list', 101 | component: categoryList, 102 | meta: { scrollToTop: true }, 103 | beforeEnter: guardRoute 104 | }, 105 | { 106 | name: 'category_insert', 107 | path: '/backend/category/insert', 108 | component: categoryInsert, 109 | meta: { scrollToTop: true }, 110 | beforeEnter: guardRoute 111 | }, 112 | { 113 | name: 'category_modify', 114 | path: '/backend/category/modify/:id', 115 | component: categoryModify, 116 | meta: { scrollToTop: true }, 117 | beforeEnter: guardRoute 118 | }, 119 | 120 | { 121 | name: 'user_list', 122 | path: '/backend/user/list', 123 | component: userList, 124 | meta: { scrollToTop: true }, 125 | beforeEnter: guardRoute 126 | }, 127 | { 128 | name: 'user_modify', 129 | path: '/backend/user/modify/:id', 130 | component: userModify, 131 | meta: { scrollToTop: true }, 132 | beforeEnter: guardRoute 133 | }, 134 | 135 | { path: '*', redirect: { name: 'login' } } 136 | ] 137 | }) 138 | return router 139 | } 140 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-inline-comments */ 2 | import Vue from 'vue' 3 | import VueRouter from 'vue-router' 4 | import Meta from 'vue-meta' 5 | import cookies from 'js-cookie' 6 | 7 | import { inBrowser } from '../utils' 8 | 9 | Vue.use(VueRouter) 10 | Vue.use(Meta) 11 | 12 | /** 13 | * to 如果在这个列表中,始终采用从左到右的滑动效果,首页比较适合用这种方式 14 | * 15 | * @type {Array.} 16 | * @const 17 | */ 18 | const alwaysBackPage = ['index', 'trending', 'category', 'search'] 19 | 20 | /** 21 | * to 如果在这个列表中,始终采用从右到左的滑动效果 22 | * 23 | * @type {Array.} 24 | * @const 25 | */ 26 | const alwaysForwardPage = ['article'] 27 | 28 | /** 29 | * 历史记录,记录访问过的页面的 fullPath 30 | * 31 | * @type {Array.} 32 | * @const 33 | */ 34 | const historyStack = [] 35 | 36 | /** 37 | * 判断当前是否是前进,true 表示是前进,否则是回退 38 | * 39 | * @param {Object} to 目标 route 40 | * @param {Object} from 源 route 41 | * @return {boolean} 是否表示返回 42 | */ 43 | const isForward = (to, from) => { 44 | // to 如果在这个列表中,始终认为是后退 45 | if (to.name && alwaysBackPage.indexOf(to.name) !== -1) { 46 | // 清空历史 47 | historyStack.length = 0 48 | return false 49 | } 50 | 51 | if (from.name && alwaysBackPage.indexOf(from.name) !== -1) { 52 | // 如果是从 alwaysBackPage 过来的,那么永远都是前进 53 | historyStack.push(to.fullPath) 54 | return true 55 | } 56 | 57 | if (to.name && alwaysForwardPage.indexOf(to.name) !== -1) { 58 | // to 如果在这个列表中,始终认为是前进 59 | historyStack.push(to.fullPath) 60 | return true 61 | } 62 | 63 | // 根据 fullPath 判断当前页面是否访问过,如果访问过,则属于返回 64 | const index = historyStack.indexOf(to.fullPath) 65 | if (index !== -1) { 66 | historyStack.length = index + 1 67 | return false 68 | } 69 | 70 | // 将 to.fullPath 加到栈顶 71 | historyStack.push(to.fullPath) 72 | return true 73 | } 74 | 75 | const scrollBehavior = to => { 76 | const position = {} 77 | if (to.hash) { 78 | position.selector = to.hash 79 | } 80 | if (to.matched.some(mm => mm.meta.scrollToTop)) { 81 | position.x = 0 82 | position.y = 0 83 | } 84 | return position 85 | } 86 | 87 | const index = () => import(/* webpackChunkName: "frontend-topics" */ '../pages/frontend-index.vue') 88 | 89 | const guardRoute = (to, from, next) => { 90 | var token = cookies.get('user') || !inBrowser 91 | if (!token) { 92 | next('/') 93 | } else { 94 | next() 95 | } 96 | } 97 | 98 | export function createRouter() { 99 | const router = new VueRouter({ 100 | mode: 'history', 101 | //base: __dirname, 102 | scrollBehavior, 103 | routes: [ 104 | { 105 | name: 'index', 106 | path: '/', 107 | component: index 108 | }, 109 | { 110 | name: 'trending', 111 | path: '/trending/:by', 112 | component: index 113 | }, 114 | { 115 | name: 'category', 116 | path: '/category/:id', 117 | component: index 118 | }, 119 | { 120 | name: 'search', 121 | path: '/search/:key', 122 | component: index 123 | }, 124 | { 125 | name: 'article', 126 | path: '/article/:id', 127 | component: () => import(/* webpackChunkName: "frontend-topics" */ '../pages/frontend-article.vue'), 128 | meta: { scrollToTop: true, notKeepAlive: true } 129 | }, 130 | { 131 | name: 'about', 132 | path: '/about', 133 | component: () => import(/* webpackChunkName: "frontend-about" */ '../pages/frontend-about.vue'), 134 | meta: { scrollToTop: true } 135 | }, 136 | { 137 | name: 'account', 138 | path: '/user/account', 139 | component: () => import(/* webpackChunkName: "frontend-user" */ '../pages/frontend-user-account.vue'), 140 | meta: { scrollToTop: true }, 141 | beforeEnter: guardRoute 142 | }, 143 | { 144 | name: 'password', 145 | path: '/user/password', 146 | component: () => import(/* webpackChunkName: "frontend-user" */ '../pages/frontend-user-password.vue'), 147 | meta: { scrollToTop: true }, 148 | beforeEnter: guardRoute 149 | } 150 | ] 151 | }) 152 | /** 153 | * 切换动画名称 154 | * 155 | * @type {string} 156 | * @const 157 | */ 158 | const slideLeft = 'slide-left' 159 | 160 | /** 161 | * 切换动画名称 162 | * 163 | * @type {string} 164 | * @const 165 | */ 166 | const slideRight = 'slide-right' 167 | 168 | router.beforeEach((to, from, next) => { 169 | if (router.app.$store) { 170 | // 如果不需要切换动画,直接返回 171 | if (router.app.$store.state.appShell.needPageTransition) { 172 | // 判断当前是前进还是后退,添加不同的动画效果 173 | const pageTransitionName = isForward(to, from) ? slideLeft : slideRight 174 | router.app.$store.commit(`appShell/setPageTransitionName`, { pageTransitionName }) 175 | } 176 | } 177 | next() 178 | }) 179 | 180 | return router 181 | } 182 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file store index 3 | * @author lincenying(lincenying@qq.com) 4 | */ 5 | 6 | import Vue from 'vue' 7 | import Vuex from 'vuex' 8 | 9 | import { createAppShellState } from './modules/app-shell' 10 | import backendAdmin from './modules/backend-admin' 11 | import backendArticle from './modules/backend-article' 12 | import backendUser from './modules/backend-user' 13 | import frontendArticle from './modules/frontend-article' 14 | import global from './modules/global' 15 | import globalCategory from './modules/global-category' 16 | import globalComment from './modules/global-comment' 17 | 18 | Vue.use(Vuex) 19 | 20 | export function createStore() { 21 | return new Vuex.Store({ 22 | modules: { 23 | appShell: createAppShellState(), 24 | backend: { 25 | namespaced: true, 26 | modules: { 27 | admin: backendAdmin, 28 | article: backendArticle, 29 | user: backendUser 30 | } 31 | }, 32 | frontend: { 33 | namespaced: true, 34 | modules: { 35 | article: frontendArticle 36 | } 37 | }, 38 | global: { 39 | namespaced: true, 40 | ...global, 41 | modules: { 42 | category: globalCategory, 43 | comment: globalComment 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/store/modules/app-shell.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable valid-jsdoc */ 2 | /** 3 | * @file app shell store 4 | * @author lincenying(lincenying@qq.com) 5 | */ 6 | 7 | export function createAppShellState() { 8 | const state = { 9 | /** 10 | * 是否需要页面切换动画 11 | * 12 | * @type {boolean} 13 | */ 14 | needPageTransition: true, 15 | 16 | /** 17 | * 多个页面是否处于切换中 18 | * 19 | * @type {boolean} 20 | */ 21 | isPageSwitching: false, 22 | 23 | /** 24 | * 多个页面切换效果名称 25 | * 26 | * @type {string} 27 | */ 28 | pageTransitionName: '', 29 | 30 | /** 31 | * 上个页面 scroll 的信息 32 | * 33 | * @type {Object} 34 | */ 35 | historyPageScrollTop: {} 36 | } 37 | 38 | const actions = { 39 | /** 40 | * 开启页面切换动画 41 | * 42 | * @param {Function} commit commit 43 | */ 44 | enablePageTransition({ commit }) { 45 | commit('enablePageTransition', true) 46 | }, 47 | 48 | /** 49 | * 关闭页面切换动画 50 | * 51 | * @param {Function} commit commit 52 | */ 53 | disablePageTransition({ commit }) { 54 | commit('disablePageTransition', false) 55 | }, 56 | 57 | /** 58 | * 设置页面是否处于切换中 59 | * 60 | * @param {Function} commit commit 61 | * @param {boolean} isPageSwitching isPageSwitching 62 | */ 63 | setPageSwitching({ commit }, isPageSwitching) { 64 | commit('setPageSwitching', isPageSwitching) 65 | }, 66 | 67 | /** 68 | * 保存页面 scroll 高度 69 | * 70 | * @param {[type]} options.commit [description] 71 | * @param {string} options.path path 72 | * @param {number} options.scrollTop scrollTop 73 | */ 74 | saveScrollTop({ commit }, { path, scrollTop }) { 75 | commit('saveScrollTop', { path, scrollTop }) 76 | } 77 | } 78 | 79 | const mutations = { 80 | ['setPageSwitching'](state, isPageSwitching) { 81 | state.isPageSwitching = isPageSwitching 82 | }, 83 | ['setPageTransitionName'](state, { pageTransitionName }) { 84 | state.pageTransitionName = pageTransitionName 85 | }, 86 | ['saveScrollTop'](state, { path, scrollTop }) { 87 | state.historyPageScrollTop[path] = scrollTop 88 | } 89 | } 90 | 91 | const getters = { 92 | ['get'](state) { 93 | return state 94 | } 95 | } 96 | 97 | return { 98 | namespaced: true, 99 | actions, 100 | mutations, 101 | state, 102 | getters 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/store/modules/backend-admin.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | lists: { 3 | hasNext: false, 4 | hasPrev: false, 5 | path: '', 6 | page: 1, 7 | data: [] 8 | }, 9 | item: { 10 | data: {}, 11 | path: '' 12 | } 13 | }) 14 | 15 | const actions = { 16 | async ['getAdminList']({ commit, state, rootState: { $api } }, config) { 17 | if (state.lists.data.length > 0 && config.path === state.lists.path && config.page === 1) return 18 | const { code, data } = await $api.get('backend/admin/list', { ...config, cache: true }) 19 | if (data && code === 200) { 20 | commit('receiveAdminList', { 21 | ...data, 22 | path: config.path, 23 | page: config.page 24 | }) 25 | } 26 | }, 27 | async ['getAdminItem']({ commit, rootState: { $api } }, config) { 28 | const { code, data } = await $api.get('backend/admin/item', config) 29 | if (data && code === 200) { 30 | commit('receiveAdminItem', { 31 | data, 32 | ...config 33 | }) 34 | } 35 | } 36 | } 37 | 38 | const mutations = { 39 | ['receiveAdminList'](state, { list, path, hasNext, hasPrev, page }) { 40 | if (page === 1) { 41 | list = [].concat(list) 42 | } else { 43 | list = state.lists.data.concat(list) 44 | } 45 | page++ 46 | state.lists = { 47 | data: list, 48 | hasNext, 49 | hasPrev, 50 | page, 51 | path 52 | } 53 | }, 54 | ['receiveAdminItem'](state, payload) { 55 | state.item = payload 56 | }, 57 | ['updateAdminItem'](state, payload) { 58 | state.item.data = payload 59 | const index = state.lists.data.findIndex(ii => ii._id === payload._id) 60 | if (index > -1) { 61 | state.lists.data.splice(index, 1, payload) 62 | } 63 | }, 64 | ['deleteAdmin'](state, id) { 65 | const obj = state.lists.data.find(ii => ii._id === id) 66 | if (obj) obj.is_delete = 1 67 | }, 68 | ['recoverAdmin'](state, id) { 69 | const obj = state.lists.data.find(ii => ii._id === id) 70 | if (obj) obj.is_delete = 0 71 | } 72 | } 73 | 74 | const getters = { 75 | ['getAdminList'](state) { 76 | return state.lists 77 | }, 78 | ['getAdminItem'](state) { 79 | return state.item 80 | } 81 | } 82 | 83 | export default { 84 | namespaced: true, 85 | state, 86 | actions, 87 | mutations, 88 | getters 89 | } 90 | -------------------------------------------------------------------------------- /src/store/modules/backend-article.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | lists: { 3 | data: [], 4 | path: '', 5 | hasNext: 0, 6 | hasPrev: 0, 7 | page: 1 8 | } 9 | }) 10 | 11 | const actions = { 12 | async ['getArticleList']({ commit, state, rootState: { $api } }, config) { 13 | if (state.lists.data.length > 0 && config.path === state.lists.path && config.page === 1) return 14 | const { code, data } = await $api.get('backend/article/list', config) 15 | if (data && code === 200) { 16 | commit('receiveArticleList', { 17 | ...data, 18 | ...config 19 | }) 20 | } 21 | }, 22 | async ['getArticleItem']({ rootState: { $api } }, config) { 23 | const { code, data } = await $api.get('backend/article/item', config) 24 | if (data && code === 200) { 25 | return data 26 | } 27 | }, 28 | async ['deleteArticle']({ commit, rootState: { $api } }, config) { 29 | const { code } = await $api.get('backend/article/delete', config) 30 | if (code === 200) { 31 | commit('deleteArticle', config.id) 32 | } 33 | }, 34 | async ['recoverArticle']({ commit, rootState: { $api } }, config) { 35 | const { code } = await $api.get('backend/article/recover', config) 36 | if (code === 200) { 37 | commit('recoverArticle', config.id) 38 | } 39 | } 40 | } 41 | 42 | const mutations = { 43 | ['receiveArticleList'](state, { list, path, hasNext, hasPrev, page }) { 44 | if (page === 1) { 45 | list = [].concat(list) 46 | } else { 47 | list = state.lists.data.concat(list) 48 | } 49 | state.lists = { 50 | data: list, 51 | path, 52 | hasNext, 53 | hasPrev, 54 | page 55 | } 56 | }, 57 | ['insertArticleItem'](state, payload) { 58 | if (state.lists.path) { 59 | state.lists.data = [payload].concat(state.lists.data) 60 | } 61 | }, 62 | ['updateArticleItem'](state, data) { 63 | const index = state.lists.data.findIndex(ii => ii._id === data._id) 64 | if (index > -1) { 65 | state.lists.data.splice(index, 1, data) 66 | } 67 | }, 68 | ['deleteArticle'](state, id) { 69 | const obj = state.lists.data.find(ii => ii._id === id) 70 | if (obj) obj.is_delete = 1 71 | }, 72 | ['recoverArticle'](state, id) { 73 | const obj = state.lists.data.find(ii => ii._id === id) 74 | if (obj) obj.is_delete = 0 75 | } 76 | } 77 | 78 | const getters = { 79 | ['getArticleList'](state) { 80 | return state.lists 81 | }, 82 | ['getArticleItem'](state) { 83 | return state.item 84 | } 85 | } 86 | 87 | export default { 88 | namespaced: true, 89 | state, 90 | actions, 91 | mutations, 92 | getters 93 | } 94 | -------------------------------------------------------------------------------- /src/store/modules/backend-user.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | lists: { 3 | hasNext: false, 4 | hasPrev: false, 5 | path: '', 6 | page: 1, 7 | data: [] 8 | }, 9 | item: { 10 | data: {}, 11 | path: '' 12 | } 13 | }) 14 | 15 | const actions = { 16 | async ['getUserList']({ commit, state, rootState: { $api } }, config) { 17 | if (state.lists.data.length > 0 && config.path === state.lists.path && config.page === 1) return 18 | const { code, data } = await $api.get('backend/user/list', { ...config, cache: true }) 19 | if (data && code === 200) { 20 | commit('receiveUserList', { 21 | ...data, 22 | ...config 23 | }) 24 | } 25 | }, 26 | async ['getUserItem']({ commit, rootState: { $api } }, config) { 27 | const { code, data } = await $api.get('backend/user/item', config) 28 | if (data && code === 200) { 29 | commit('receiveUserItem', { 30 | data, 31 | ...config 32 | }) 33 | } 34 | } 35 | } 36 | 37 | const mutations = { 38 | ['receiveUserList'](state, { list, path, hasNext, hasPrev, page }) { 39 | if (page === 1) { 40 | list = [].concat(list) 41 | } else { 42 | list = state.lists.data.concat(list) 43 | } 44 | page++ 45 | state.lists = { 46 | data: list, 47 | hasNext, 48 | hasPrev, 49 | page, 50 | path 51 | } 52 | }, 53 | ['receiveUserItem'](state, payload) { 54 | state.item = payload 55 | }, 56 | ['updateUserItem'](state, payload) { 57 | state.item.data = payload 58 | const index = state.lists.data.findIndex(ii => ii._id === payload._id) 59 | if (index > -1) { 60 | state.lists.data.splice(index, 1, payload) 61 | } 62 | }, 63 | ['deleteUser'](state, id) { 64 | const obj = state.lists.data.find(ii => ii._id === id) 65 | if (obj) obj.is_delete = 1 66 | }, 67 | ['recoverUser'](state, id) { 68 | const obj = state.lists.data.find(ii => ii._id === id) 69 | if (obj) obj.is_delete = 0 70 | } 71 | } 72 | 73 | const getters = { 74 | ['getUserList'](state) { 75 | return state.lists 76 | }, 77 | ['getUserItem'](state) { 78 | return state.item 79 | } 80 | } 81 | 82 | export default { 83 | namespaced: true, 84 | state, 85 | actions, 86 | mutations, 87 | getters 88 | } 89 | -------------------------------------------------------------------------------- /src/store/modules/frontend-article.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | lists: { 3 | data: [], 4 | hasNext: 0, 5 | page: 1, 6 | path: '' 7 | }, 8 | item: { 9 | data: {}, 10 | path: '', 11 | isLoad: false 12 | }, 13 | trending: [] 14 | }) 15 | 16 | const actions = { 17 | async ['getArticleList']({ commit, state, rootState: { $api } }, config) { 18 | if (state.lists.data.length > 0 && config.path === state.lists.path && config.page === 1) return 19 | const { code, data } = await $api.get('frontend/article/list', { ...config, cache: true }) 20 | if (data && code === 200) { 21 | commit('receiveArticleList', { 22 | ...config, 23 | ...data 24 | }) 25 | } 26 | }, 27 | async ['getArticleItem']({ commit, state, rootState: { $api } }, config) { 28 | if (config.path === state.item.path) return 29 | const { code, data } = await $api.get('frontend/article/item', { ...config, markdown: 1, cache: true }) 30 | if (data && code === 200) { 31 | commit('receiveArticleItem', { 32 | data, 33 | ...config 34 | }) 35 | } 36 | }, 37 | async ['getTrending']({ commit, state, rootState: { $api } }) { 38 | if (state.trending.length) return 39 | const { code, data } = await $api.get('frontend/trending', { cache: true }) 40 | if (data && code === 200) { 41 | commit('receiveTrending', data) 42 | } 43 | } 44 | } 45 | 46 | const mutations = { 47 | ['receiveArticleList'](state, { list, hasNext, hasPrev, page, path }) { 48 | if (page === 1) { 49 | list = [].concat(list) 50 | } else { 51 | list = state.lists.data.concat(list) 52 | } 53 | state.lists = { 54 | data: list, 55 | hasNext, 56 | hasPrev, 57 | page, 58 | path 59 | } 60 | }, 61 | ['receiveArticleItem'](state, { data, path }) { 62 | state.item = { 63 | data, 64 | path, 65 | isLoad: true 66 | } 67 | }, 68 | ['receiveTrending'](state, data) { 69 | state.trending = data.list 70 | }, 71 | ['modifyLikeStatus'](state, { id, status }) { 72 | if (state.item.data._id === id) { 73 | if (status) state.item.data.like++ 74 | else state.item.data.like-- 75 | state.item.data.like_status = status 76 | } 77 | const obj = state.lists.data.find(item => item._id === id) 78 | if (obj) { 79 | if (status) obj.like++ 80 | else obj.like-- 81 | obj.like_status = status 82 | } 83 | } 84 | } 85 | 86 | const getters = { 87 | ['getArticleList'](state) { 88 | return state.lists 89 | }, 90 | ['getArticleItem'](state) { 91 | return state.item 92 | }, 93 | ['getTrending'](state) { 94 | return state.trending 95 | } 96 | } 97 | 98 | export default { 99 | namespaced: true, 100 | state, 101 | actions, 102 | mutations, 103 | getters 104 | } 105 | -------------------------------------------------------------------------------- /src/store/modules/global-category.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | lists: [], 3 | item: {} 4 | } 5 | 6 | const actions = { 7 | async ['getCategoryList']({ commit, state, rootState: { $api } }, config) { 8 | if (state.lists.length) return 9 | const { code, data } = await $api.get('backend/category/list', { ...config, cache: true }) 10 | if (data && code === 200) { 11 | commit('receiveCategoryList', data.list) 12 | } 13 | }, 14 | async ['getCategoryItem']({ commit, rootState: { $api } }, config) { 15 | const { code, data } = await $api.get('backend/category/item', config) 16 | if (data && code === 200) { 17 | commit('receiveCategoryItem', { 18 | data, 19 | ...config 20 | }) 21 | } 22 | } 23 | } 24 | 25 | const mutations = { 26 | ['receiveCategoryList'](state, payload) { 27 | state.lists = payload 28 | }, 29 | ['receiveCategoryItem'](state, payload) { 30 | state.item = payload 31 | }, 32 | ['insertCategoryItem'](state, payload) { 33 | state.lists = [payload].concat(state.lists) 34 | }, 35 | ['updateCategoryItem'](state, payload) { 36 | state.item = payload 37 | const index = state.lists.findIndex(ii => ii._id === payload._id) 38 | if (index > -1) { 39 | state.lists.splice(index, 1, payload) 40 | } 41 | } 42 | } 43 | 44 | const getters = { 45 | ['getCategoryList'](state) { 46 | return state.lists 47 | }, 48 | ['getCategoryItem'](state) { 49 | return state.item 50 | } 51 | } 52 | 53 | export default { 54 | namespaced: true, 55 | state, 56 | actions, 57 | mutations, 58 | getters 59 | } 60 | -------------------------------------------------------------------------------- /src/store/modules/global-comment.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | lists: { 3 | data: [], 4 | hasNext: 0, 5 | page: 1, 6 | path: '' 7 | } 8 | }) 9 | 10 | const actions = { 11 | async ['getCommentList']({ commit, state, rootState: { $api } }, config) { 12 | if (config.path === state.lists.path && config.page === 1) return 13 | const { code, data } = await $api.get('frontend/comment/list', { ...config, cache: true }) 14 | if (data && code === 200) { 15 | commit('recevieCommentList', { 16 | ...config, 17 | ...data 18 | }) 19 | } 20 | } 21 | } 22 | 23 | const mutations = { 24 | ['recevieCommentList'](state, { list, hasNext, hasPrev, page, path }) { 25 | if (page === 1) { 26 | list = [].concat(list) 27 | } else { 28 | list = state.lists.data.concat(list) 29 | } 30 | state.lists = { 31 | data: list, 32 | hasNext, 33 | hasPrev, 34 | page, 35 | path 36 | } 37 | }, 38 | ['insertCommentItem'](state, data) { 39 | state.lists.data = [data].concat(state.lists.data) 40 | }, 41 | ['deleteComment'](state, id) { 42 | const obj = state.lists.data.find(ii => ii._id === id) 43 | obj.is_delete = 1 44 | }, 45 | ['recoverComment'](state, id) { 46 | const obj = state.lists.data.find(ii => ii._id === id) 47 | obj.is_delete = 0 48 | } 49 | } 50 | 51 | const getters = { 52 | ['getCommentList'](state) { 53 | return state.lists 54 | } 55 | } 56 | 57 | export default { 58 | namespaced: true, 59 | state, 60 | actions, 61 | mutations, 62 | getters 63 | } 64 | -------------------------------------------------------------------------------- /src/store/modules/global.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | loading: false, 3 | cookies: {}, 4 | showLoginModal: false, 5 | showRegisterModal: false 6 | }) 7 | 8 | const actions = {} 9 | 10 | const mutations = { 11 | ['showLoginModal'](state, payload) { 12 | state.showLoginModal = payload 13 | }, 14 | ['showRegisterModal'](state, payload) { 15 | state.showRegisterModal = payload 16 | }, 17 | ['setCookies'](state, cookies) { 18 | state.cookies = cookies 19 | } 20 | } 21 | 22 | const getters = { 23 | ['get'](state) { 24 | return state 25 | } 26 | } 27 | 28 | export default { 29 | actions, 30 | state, 31 | mutations, 32 | getters 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ls from 'store2' 3 | import toastr from 'toastr' 4 | import get from 'lodash.get' 5 | 6 | toastr.options.positionClass = 'toast-top-center' 7 | 8 | export const inBrowser = typeof window !== 'undefined' 9 | 10 | export const ua = () => { 11 | const userAgentInfo = inBrowser ? navigator.userAgent : '' 12 | const Agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPod'] 13 | let flag = 'PC' 14 | for (let vv = 0; vv < Agents.length; vv++) { 15 | if (userAgentInfo.indexOf(Agents[vv]) > 0) { 16 | flag = Agents[vv] 17 | break 18 | } 19 | } 20 | return flag 21 | } 22 | 23 | export const ssp = path => { 24 | if (!inBrowser) return 25 | const clientHeight = document.documentElement.clientHeight, 26 | scrollTop = ls.get(path) 27 | if (scrollTop) { 28 | Vue.nextTick().then(() => { 29 | if (document.body.clientHeight >= scrollTop + clientHeight) { 30 | window.scrollTo(0, scrollTop) 31 | } 32 | ls.remove(path) 33 | }) 34 | } 35 | } 36 | 37 | export const strlen = str => { 38 | let charCode = -1 39 | const len = str.length 40 | let realLength = 0 41 | for (let i = 0; i < len; i++) { 42 | charCode = str.charCodeAt(i) 43 | if (charCode >= 0 && charCode <= 128) realLength += 1 44 | else realLength += 2 45 | } 46 | return realLength 47 | } 48 | 49 | export const sleep = ms => { 50 | return new Promise(resolve => setTimeout(resolve, ms)) 51 | } 52 | 53 | export const showMsg = message => { 54 | let content, type 55 | if (typeof message === 'string') { 56 | content = message 57 | type = 'error' 58 | } else { 59 | content = message.content 60 | type = message.type 61 | } 62 | toastr[type](content) 63 | } 64 | 65 | export const oc = (props, property, def) => { 66 | return get(props, property, def) 67 | } 68 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-inline-comments */ 2 | const path = require('path') 3 | const SWPrecachePlugin = require('sw-precache-webpack-plugin') 4 | 5 | module.exports = { 6 | pages: { 7 | app: { 8 | // page 的入口 9 | entry: 'src/entry-client.js', 10 | // 模板来源 11 | template: 'public/index.html', 12 | // 在 dist/index.html 的输出 13 | filename: 'index.html' 14 | } 15 | }, 16 | configureWebpack: { 17 | devtool: 'source-map', 18 | performance: { 19 | hints: 'warning', // 枚举 20 | maxAssetSize: 30000000, // 整数类型(以字节为单位) 21 | maxEntrypointSize: 50000000, // 整数类型(以字节为单位) 22 | assetFilter(assetFilename) { 23 | // 提供资源文件名的断言函数 24 | return assetFilename.endsWith('.css') || assetFilename.endsWith('.js') 25 | } 26 | }, 27 | plugins: [ 28 | new SWPrecachePlugin({ 29 | cacheId: 'mmf-blog-vue2', 30 | filename: 'service-worker.js', 31 | minify: true, 32 | dontCacheBustUrlsMatching: /./, 33 | staticFileGlobsIgnorePatterns: [/\.html$/, /\.map$/, /\.json$/], 34 | runtimeCaching: [ 35 | { 36 | urlPattern: /api/, 37 | handler: 'networkFirst', 38 | options: { 39 | networkTimeoutSeconds: 1, 40 | cacheName: 'api-cache', 41 | cacheableResponse: { 42 | statuses: [0, 200] 43 | } 44 | } 45 | }, 46 | { 47 | urlPattern: /^https:\/\/cdn\.jsdelivr\.net/, 48 | handler: 'networkFirst', 49 | options: { 50 | networkTimeoutSeconds: 1, 51 | cacheName: 'cdn-cache', 52 | cacheableResponse: { 53 | statuses: [0, 200] 54 | } 55 | } 56 | }, 57 | { 58 | urlPattern: /^https:\/\/cravatar\.cn/, 59 | handler: 'NetworkFirst', 60 | options: { 61 | networkTimeoutSeconds: 1, 62 | cacheName: 'avatar-cache', 63 | cacheableResponse: { 64 | statuses: [0, 200] 65 | } 66 | } 67 | } 68 | ] 69 | }) 70 | ] 71 | }, 72 | chainWebpack: config => { 73 | config.module 74 | .rule('vue') 75 | .use('vue-loader') 76 | .tap(options => { 77 | options.compilerOptions.preserveWhitespace = true 78 | return options 79 | }) 80 | config.module.rule('eslint').uses.clear() 81 | config.module.rule('eslint').clear() 82 | config.resolve.alias.set('~', path.resolve('src')) 83 | config.resolve.alias.set('~api', path.resolve('src/api/index-client.js')) 84 | }, 85 | css: { 86 | loaderOptions: {} 87 | }, 88 | pluginOptions: {}, 89 | pwa: { 90 | name: 'M.M.F小屋', 91 | themeColor: '#54d9e0', 92 | msTileColor: '#000000', 93 | appleMobileWebAppCapable: 'yes', 94 | appleMobileWebAppStatusBarStyle: 'black', 95 | manifestPath: 'static/manifest.json', 96 | manifestOptions: { 97 | start_url: '/' 98 | }, 99 | iconPaths: { 100 | favicon32: 'static/img/icons/favicon-32x32.png', 101 | favicon16: 'static/img/icons/favicon-16x16.png', 102 | appleTouchIcon: 'static/img/icons/apple-touch-icon-152x152.png', 103 | maskIcon: 'static/img/icons/safari-pinned-tab.svg', 104 | msTileImage: 'static/img/icons/msapplication-icon-144x144.png' 105 | } 106 | }, 107 | devServer: { 108 | port: 8081, 109 | host: '0.0.0.0', 110 | hot: true, 111 | disableHostCheck: true, 112 | proxy: { 113 | '/api': { 114 | target: 'http://localhost:4000', 115 | changeOrigin: true, 116 | pathRewrite: { 117 | '^/api': '/api' 118 | } 119 | } 120 | }, 121 | historyApiFallback: { 122 | rewrites: [ 123 | // shows views/landing.html as the landing page 124 | { from: /^\/$/, to: '/index.html' }, 125 | // shows views/subpage.html for all routes starting with /subpage 126 | { from: /^\/backend/, to: '/admin.html' } 127 | ] 128 | } 129 | } 130 | } 131 | --------------------------------------------------------------------------------