├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── CNAME ├── favicon.ico └── index.html ├── src ├── App.vue ├── ajax │ └── request.js ├── assets │ ├── images │ │ ├── author.jpg │ │ ├── avatar.jpg │ │ └── weixin.jpg │ └── styles │ │ ├── icon │ │ └── myIcon │ │ │ ├── iconfont.css │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.js │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── main.scss │ │ └── reset.scss ├── components │ ├── banner.vue │ ├── bread-crumb.vue │ ├── comment.vue │ ├── lang-dropdown.vue │ ├── loading.vue │ └── redirect.vue ├── filters │ └── index.js ├── layout │ ├── admin │ │ ├── components │ │ │ ├── app-header.vue │ │ │ ├── app-main.vue │ │ │ └── app-menu.vue │ │ └── index.vue │ └── home │ │ ├── components │ │ ├── app-footer.vue │ │ ├── app-header.vue │ │ └── app-main.vue │ │ └── index.vue ├── main.js ├── router │ ├── index.js │ └── oauth.js ├── settings.js ├── utils │ ├── common.js │ ├── cookie-util.js │ ├── gitee-api.js │ ├── loading.js │ ├── oauth-util.js │ └── page-title-util.js └── views │ ├── admin │ ├── category │ │ └── index.vue │ └── post-list │ │ ├── detail.vue │ │ └── index.vue │ ├── error-page │ └── 404.vue │ └── home │ ├── about │ └── index.vue │ ├── article │ ├── components │ │ ├── index-primary.vue │ │ ├── index-secondary.vue │ │ └── post-list.vue │ ├── index.vue │ └── post-detail.vue │ └── works │ └── index.vue └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [奇思笔记](https://github.com/zclzone/blog) 2 | 3 | > 基于 Gitee-api + Vue + Markdown 的个人博客网站,完全免费,无需个人服务器即可在线维护博客 4 | 5 | ### 预览 6 | 7 | github: [blog.zclzone.com](http://blog.zclzone.com) 8 | 9 | gitee: [zclzone.gitee.io/blog](https://zclzone.gitee.io/blog) 10 | 11 | ### 亮点 12 | 13 | 1. 无需服务器即可实现在后台网站在线维护博客文章(增删改查),增减文章类型,后续计划将更多的网站配置放到后台在线维护。 14 | 2. 使用 Gitee Api,在国内访问速度快,体验好,当然如果有朋友愿意开发一个 github 版的也是相当不错的,本人由于访问 github 速度太慢,即使开发出来也没多少实用性,所以暂不折腾了。 15 | 3. 网站内容配置文件化,后续计划增加更多可配置化的设置,再慢慢将网站配置转移到后台网站维护,如主题色配置,多模板等等。 16 | 4. 后台网站使用 Gitee Oauth 登录验证,其他人可访问后台网站但无法更改数据。 17 | 5. 使用 Vue 开发,国内会使用 Vue 开发的程序员还是挺多的,考虑到 SEO 后期可能会使用 nuxtjs 重构。 18 | 19 | ### 使用方式 20 | 21 |
1. Fork & Clone 此仓库并安装依赖 22 | 23 | 当然了,Fork 是可选的,不过对此项目感兴趣的朋友可以点点 Star,或者 Fork 此项目,如果您对此项目有足够兴趣,欢迎提交您宝贵的 Pr 让此项目变得更加完备和好用 24 | 25 | ``` 26 | git clone https://github.com/zclzone/blog.git 27 | cd blog 28 | npm install 29 | ``` 30 | 31 |
32 | 33 |
2. 修改配置 src 文件夹下的配置文件 settings.js 34 | 35 | 需确保将 giteeApiOptions 的配置信息修改成自己的,至于其他配置信息我计划写一篇博客单独介绍,请留意我的[博客网站](http://blog.qszone.com) 36 | ![settings.js](https://gitee.com/zclzone/res/raw/master/images/gitee-blog-settings.png) 37 | 38 |
39 | 40 |
3. 构建项目并发布 41 | 42 | 1. 如果您没有 gitee 账号,您需要先创建一个 [gitee](https://gitee.com/) 账号,并确保账号下没有名为**gitee-blog-db**的仓库 43 | 2. Fork gitee 仓库 [gitee-db](https://gitee.com/qszone/gitee-db),此仓库将作为博客的“数据库”,或者您也可以 Clone 此仓库再推送到 gitee,仓库名可以自定义,但必须跟 settings.js 下 giteeApiOptions 的 repo 保持一致 44 | 3. 执行语句构建项目(当然您可以不急着构建,可以直接在本地运行看下效果,执行 npm run serve 即可) 45 | 4. 创建一个空的 gitee 或者 github 仓库,将构建好的代码(dist 文件夹下)推送到此仓库并发布 pages,理论上您可以发布到任何外网能访问的地方,如码云、github 等,具体如何发布这里不作详细介绍,我会在我的[博客](http://blog.qszone.com)专门写一篇博客介绍如何发布 pages 46 | 5. 发布之后就可以访问了,默认页面是博客首页,后台地址是[您的发布地址/#/admin],如:[https://blog.zclzone.com/#/admin](http://blog.qszone.com) 47 | 48 | 以下指令假设您 gitee “数据库” 已经存在并且创建了一个空仓库用于发布 pages 49 | 50 | ``` 51 | npm run build 52 | cd dist 53 | git init 54 | git add . 55 | git commit -m 'init blog' 56 | git remote add origin [您创建的空仓库地址] 57 | git push -u origin master 58 | ``` 59 | 60 |
61 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "@vssue/api-github-v3": "^1.4.4", 11 | "axios": "^0.20.0", 12 | "core-js": "^3.6.5", 13 | "element-ui": "^2.13.2", 14 | "github-markdown-css": "^4.0.0", 15 | "js-cookie": "^2.2.1", 16 | "mavon-editor": "^2.9.0", 17 | "vssue": "^1.4.6", 18 | "vue": "^2.6.11", 19 | "vue-router": "^3.2.0" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "~4.4.0", 23 | "@vue/cli-plugin-router": "~4.4.0", 24 | "@vue/cli-service": "~4.4.0", 25 | "node-sass": "^4.12.0", 26 | "sass-loader": "^8.0.2", 27 | "vue-template-compiler": "^2.6.11" 28 | }, 29 | "browserslist": [ 30 | "> 1%", 31 | "last 2 versions", 32 | "not dead" 33 | ] 34 | } -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | blog.zclzone.com -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/ajax/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const service = axios.create({ 4 | // baseURL: 'https://gitee.com/', 5 | timeout: 10000, 6 | withCredentials: false 7 | }) 8 | 9 | service.interceptors.request.use( 10 | config => { 11 | if (config.method === 'get') { 12 | const url = config.url 13 | url.indexOf('?') === -1 ? config.url = url + '?_=' + (new Date().getTime()) : config.url = url + '&_=' + (new Date().getTime()) 14 | } 15 | return config 16 | }, 17 | err => Promise.reject(err) 18 | ) 19 | 20 | service.interceptors.response.use( 21 | response => response, 22 | err => Promise.reject(err) 23 | ) 24 | 25 | export default service -------------------------------------------------------------------------------- /src/assets/images/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/images/author.jpg -------------------------------------------------------------------------------- /src/assets/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/images/avatar.jpg -------------------------------------------------------------------------------- /src/assets/images/weixin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/images/weixin.jpg -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "myIcon"; 2 | src: url('iconfont.eot?t=1568436289437'); /* IE9 */ 3 | src: url('iconfont.eot?t=1568436289437#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 5 | url('iconfont.woff?t=1568436289437') format('woff'), 6 | url('iconfont.ttf?t=1568436289437') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1568436289437#myIcon') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .myIcon { 11 | font-family: "myIcon" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .myIcon-icon-search:before { 19 | content: "\e615"; 20 | } 21 | 22 | .myIcon-check-fail:before { 23 | content: "\e631"; 24 | } 25 | 26 | .myIcon-nice:before { 27 | content: "\e639"; 28 | } 29 | 30 | .myIcon-icon-location:before { 31 | content: "\e61b"; 32 | } 33 | 34 | .myIcon-weixin:before { 35 | content: "\e614"; 36 | } 37 | 38 | .myIcon-github:before { 39 | content: "\e632"; 40 | } 41 | 42 | .myIcon-tag:before { 43 | content: "\e63a"; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/styles/icon/myIcon/iconfont.eot -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.js: -------------------------------------------------------------------------------- 1 | !function (a) { var t, e = '', c = (t = document.getElementsByTagName("script"))[t.length - 1].getAttribute("data-injectcss"); if (c && !a.__iconfont__svg__cssinject__) { a.__iconfont__svg__cssinject__ = !0; try { document.write("") } catch (t) { console && console.log(t) } } !function (t) { if (document.addEventListener) if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) setTimeout(t, 0); else { var c = function () { document.removeEventListener("DOMContentLoaded", c, !1), t() }; document.addEventListener("DOMContentLoaded", c, !1) } else document.attachEvent && (n = t, o = a.document, l = !1, (i = function () { try { o.documentElement.doScroll("left") } catch (t) { return void setTimeout(i, 50) } e() })(), o.onreadystatechange = function () { "complete" == o.readyState && (o.onreadystatechange = null, e()) }); function e() { l || (l = !0, n()) } var n, o, l, i }(function () { var t, c; (t = document.createElement("div")).innerHTML = e, e = null, (c = t.getElementsByTagName("svg")[0]) && (c.setAttribute("aria-hidden", "true"), c.style.position = "absolute", c.style.width = 0, c.style.height = 0, c.style.overflow = "hidden", function (t, c) { c.firstChild ? function (t, c) { c.parentNode.insertBefore(t, c) }(t, c.firstChild) : c.appendChild(t) }(c, document.body)) }) }(window); -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/styles/icon/myIcon/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/styles/icon/myIcon/iconfont.woff -------------------------------------------------------------------------------- /src/assets/styles/icon/myIcon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zclzone/gitee-blog/6fb881e9097a3310482cc2309a80ae0ae8335ec1/src/assets/styles/icon/myIcon/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import url('./reset.scss'); 2 | @import url('./icon/myIcon/iconfont.css'); 3 | 4 | :root { 5 | --main-color: #2b4c59; //主色,最深 6 | --secondary-color: #316c72; //次色,稍浅色 7 | --light-color: #84a59d; //浅色 8 | --front-color: #fff; //前景色 9 | --bg-color: #f3f3f3; //背景色 10 | } 11 | 12 | html, 13 | body { 14 | height: 100%; 15 | // scrollbar-width: none; 16 | // -ms-overflow-style: none; 17 | // overflow-x: hidden; 18 | // overflow-y: auto; 19 | // &::-webkit-scrollbar { 20 | // display: none; 21 | // } 22 | } 23 | 24 | html[theme='dark-mode'] { 25 | filter: invert(1) hue-rotate(180deg); 26 | } 27 | 28 | .el-row { 29 | padding: 0 !important; 30 | margin: 0 !important; 31 | } 32 | 33 | .el-tag { 34 | color: var(--front-color) !important; 35 | font-weight: 600; 36 | margin: 10px 5px 0; 37 | .el-tag__close { 38 | color: var(--front-color) !important; 39 | } 40 | } 41 | 42 | .el-tabs__item { 43 | color: var(--main-color) !important; 44 | &.is-active { 45 | color: var(--secondary-color) !important; 46 | } 47 | &:hover { 48 | color: var(--secondary-color) !important; 49 | } 50 | } 51 | 52 | .el-tabs__active-bar { 53 | background: var(--secondary-color) !important; 54 | } 55 | 56 | @media screen and (max-width: 768px) { 57 | .sm-hide { 58 | display: none; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/styles/reset.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | *, 4 | ::before, 5 | ::after { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | -webkit-box-sizing: border-box; 10 | -webkit-tap-highlight-color: transparent; /* 清除点击高亮效果 */ 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: #333; 16 | } 17 | 18 | a:hover, 19 | a:link, 20 | a:visited, 21 | a:active { 22 | text-decoration: none; 23 | } 24 | 25 | input, 26 | textarea { 27 | outline: none; 28 | border: none; 29 | resize: none; 30 | -webkit-appearance: none; /* 清除移动端输入框特有的样式 */ 31 | } 32 | 33 | body { 34 | font-size: 14px; 35 | font-weight: 400; 36 | color: #000; 37 | -webkit-font-smoothing: antialiased; 38 | font-family: 'Arial', 'Microsoft YaHei', '黑体', '宋体', sans-serif !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/banner.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/bread-crumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/comment.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/lang-dropdown.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 23 | -------------------------------------------------------------------------------- /src/components/redirect.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | function formatNumber(n) { 2 | const str = n.toString(); 3 | return str[1] ? str : `0${str}`; 4 | } 5 | 6 | export function dateFormat(time) { 7 | const date = new Date(time) 8 | 9 | const year = date.getFullYear() 10 | const month = date.getMonth() + 1 11 | const day = date.getDate() 12 | const hour = date.getHours() 13 | const minute = date.getMinutes() 14 | const second = date.getSeconds() 15 | 16 | const t1 = [year, month, day].map(formatNumber).join('-') 17 | const t2 = [hour, minute, second].map(formatNumber).join(':') 18 | return `${t1} ${t2}` 19 | } 20 | -------------------------------------------------------------------------------- /src/layout/admin/components/app-header.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 62 | 63 | -------------------------------------------------------------------------------- /src/layout/admin/components/app-main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | -------------------------------------------------------------------------------- /src/layout/admin/components/app-menu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 63 | 64 | -------------------------------------------------------------------------------- /src/layout/admin/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 69 | 70 | -------------------------------------------------------------------------------- /src/layout/home/components/app-footer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/layout/home/components/app-header.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/layout/home/components/app-main.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/layout/home/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | // 引入Element UI 6 | import Element from 'element-ui' 7 | import 'element-ui/lib/theme-chalk/index.css' 8 | import 'element-ui/lib/theme-chalk/display.css' 9 | Vue.use(Element) 10 | 11 | // 引入mavonEditor 12 | import mavonEditor from 'mavon-editor' 13 | import 'mavon-editor/dist/css/index.css' 14 | Vue.use(mavonEditor) 15 | 16 | import loading from '@/utils/loading' 17 | Vue.use(loading) 18 | 19 | 20 | // 引入样式 21 | import '@/assets/styles/main.scss' 22 | import 'github-markdown-css/github-markdown.css' 23 | 24 | 25 | // 扩充Vue的全局prototype属性 26 | import axios from '@/ajax/request' 27 | import { to } from '@/utils/common' 28 | Vue.prototype.$axios = axios 29 | Vue.prototype.$to = to 30 | Vue.prototype.guid = () => { 31 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 32 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8) 33 | return v.toString(16) 34 | }) 35 | } 36 | 37 | //全局过滤器 38 | import * as filters from './filters' 39 | Object.keys(filters).forEach(key => { 40 | Vue.filter(key, filters[key]) 41 | }) 42 | 43 | Vue.config.productionTip = false 44 | 45 | new Vue({ 46 | router, 47 | render: h => h(App) 48 | }).$mount('#app') 49 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import { setPageTitle } from '@/utils/page-title-util' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | component: _ => import('@/layout/home'), 12 | redirect: '/article', 13 | hidden: true, 14 | children: [ 15 | { 16 | path: 'index', 17 | component: _ => import('@/views/home/article'), 18 | meta: { 19 | title: '首页' 20 | } 21 | }, 22 | { 23 | path: 'article', 24 | component: _ => import('@/views/home/article'), 25 | meta: { 26 | title: '文章' 27 | } 28 | }, 29 | { 30 | path: 'article/:name*', 31 | component: _ => import('@/views/home/article/post-detail'), 32 | }, 33 | { 34 | path: 'works', 35 | component: _ => import('@/views/home/article'), 36 | meta: { 37 | title: '作品' 38 | } 39 | }, 40 | { 41 | path: 'about', 42 | component: _ => import('@/views/home/about'), 43 | }, 44 | ] 45 | }, 46 | { 47 | path: '/admin', 48 | component: _ => import('@/layout/admin'), 49 | redirect: '/admin/list', 50 | meta: { 51 | title: '博客管理' 52 | }, 53 | children: [ 54 | { 55 | path: 'list', 56 | component: _ => import('@/views/admin/post-list'), 57 | meta: { 58 | title: '文章列表' 59 | } 60 | }, 61 | { 62 | path: 'list/:action', 63 | component: _ => import('@/views/admin/post-list/detail'), 64 | hidden: true, 65 | meta: { 66 | title: '文章详情' 67 | } 68 | }, 69 | { 70 | path: 'category', 71 | component: _ => import('@/views/admin/category'), 72 | meta: { 73 | title: '文章分类' 74 | } 75 | } 76 | 77 | ] 78 | }, 79 | { 80 | path: '/redirect', 81 | hidden: true, 82 | component: _ => import('@/components/redirect'), 83 | }, 84 | { 85 | path: '*', 86 | component: () => import('@/views/error-page/404'), 87 | hidden: true, 88 | meta: { 89 | title: '404-错误页' 90 | } 91 | } 92 | ] 93 | 94 | 95 | const router = new VueRouter({ 96 | scrollBehavior: () => ({ y: 0 }), 97 | routes, 98 | }) 99 | 100 | // 路由钩子修改页面title 101 | router.beforeEach((to, from, next) => { 102 | if (to.meta && to.meta.title) { 103 | setPageTitle(to.meta.title) 104 | } 105 | next() 106 | }) 107 | 108 | // 取消重复打开相同路由时控制台报错误 109 | const VueRouterPush = VueRouter.prototype.push 110 | VueRouter.prototype.push = function push(to) { 111 | return VueRouterPush.call(this, to).catch(err => err) 112 | } 113 | 114 | export default router 115 | -------------------------------------------------------------------------------- /src/router/oauth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 此文件已停用,main.js并未引用此文件 3 | */ 4 | 5 | import router from './index' 6 | import { getToken, setToken } from '@/utils/cookie-util' 7 | import { getAccessToken, getOauthUrl } from '@/utils/oauth-util' 8 | 9 | const whiteList = ['/', '/index', '/index/article', '/index/works', '/index/about'] 10 | 11 | router.beforeEach(async (to, from, next) => { 12 | if (whiteList.indexOf(to.path) !== -1) { 13 | next() 14 | return 15 | } 16 | if (to.path === '/auth') { 17 | if (to.query && to.query.code) { 18 | const access_token = await getAccessToken(to.query.code) 19 | if (access_token) { 20 | setToken(access_token) 21 | next({ path: '/admin' }) 22 | } 23 | } 24 | } 25 | const token = getToken() 26 | if (token) { 27 | //每次路由改变延长Cookie过期时间 28 | setToken(token) 29 | next() 30 | } else { 31 | location.hash = '' 32 | window.name = location.href 33 | location.href = getOauthUrl() 34 | } 35 | }) 36 | 37 | router.afterEach(() => { 38 | }) 39 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 页面title 3 | title: '奇思笔记', 4 | // 发布pages的仓库名(博客部署文件存放的仓库名) 5 | pagesRepo: 'blog', 6 | // 网站配置信息 7 | siteOptions: { 8 | title: '奇思笔记', 9 | subtitle: '孤光自照,肝胆皆冰雪', 10 | author: '张传龙', 11 | description: '前端全栈开发者', 12 | github: 'https://github.com/zclzone/blog', 13 | navMenu: { 14 | 'http://www.qszone.com': '首页', //外链 15 | '/article': '文章', 16 | 'https://coolshell.cn': '友情链接', 17 | // '/works': '作品', 18 | '/article/关于本站': '关于本站' 19 | }, 20 | skills: ['Vue', 'React', 'JavaScript', 'Css', 'Java', 'C#', 'MySql', 'Sql Server', 'MongoDB'], 21 | }, 22 | // 评论配置信息 23 | commentOptions: { 24 | owner: 'zclzone', 25 | repo: 'blog', 26 | clientId: '13f12c4d3203ceee7d22', 27 | clientSecret: '4de1abe7fa4d21bc1c76b6e03a86f0cc2d4afc83' 28 | }, 29 | // gitee oauth 30 | giteeOauthOptions: { 31 | baseOauthUrl: 'https://gitee.com/oauth', 32 | clientId: '5469618e0b5d3fb058336b82c907bc9459f3c727ad1eb060325a803d91209a52', 33 | clientSecret: 'cb90ca23bd37be8d8a1aa53551db8cef4a5b148babef6aa78f3ad58e778c0abd', 34 | redirectUri: 'https://zclzone.gitee.io/redirect' 35 | }, 36 | // gitee api 37 | giteeApiOptions: { 38 | baseApiURL: 'https://gitee.com/api/v5', 39 | owner: 'zclzone', 40 | repo: 'gitee-db' 41 | } 42 | } -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import { Message, Notification } from 'element-ui' 2 | 3 | const to = promise => { 4 | return promise.then(res => [null, res]).catch(err => [err]) 5 | } 6 | 7 | const guid = () => { 8 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 9 | let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8) 10 | return v.toString(16) 11 | }) 12 | } 13 | 14 | const utf8ToBase64 = (str) => { 15 | return btoa(unescape(encodeURIComponent(str))) 16 | } 17 | 18 | const base64ToUtf8 = (str) => { 19 | return decodeURIComponent(escape(atob(str))) 20 | } 21 | 22 | const eleNotification = (message, title = '提醒') => { 23 | Notification({ 24 | title, 25 | dangerouslyUseHTMLString: true, //是否将 message 属性作为 HTML 片段处理 26 | duration: 0, 27 | message 28 | }) 29 | } 30 | 31 | const eleMessage = (message, type = 'info') => { 32 | Message({ 33 | type, 34 | message 35 | }) 36 | } 37 | 38 | 39 | 40 | export { 41 | to, 42 | guid, 43 | utf8ToBase64, 44 | base64ToUtf8, 45 | eleNotification, 46 | eleMessage 47 | } -------------------------------------------------------------------------------- /src/utils/cookie-util.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TOKEN_CODE = 'access_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TOKEN_CODE) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TOKEN_CODE, token, { expires: new Date(new Date().getTime() + 2 * 60 * 60 * 1000) }) 11 | } 12 | 13 | export function removeToken() { 14 | Cookies.remove(TOKEN_CODE) 15 | Cookies.remove('User') 16 | } 17 | 18 | export function getUser() { 19 | return Cookies.get('User') 20 | } 21 | 22 | export function setUser(userInfo) { 23 | return Cookies.set('User', userInfo) 24 | } -------------------------------------------------------------------------------- /src/utils/gitee-api.js: -------------------------------------------------------------------------------- 1 | import axios from '@/ajax/request' 2 | import { getToken } from '@/utils/cookie-util' 3 | import { to, utf8ToBase64, base64ToUtf8 } from '@/utils/common' 4 | 5 | const { giteeApiOptions } = require('@/settings') 6 | const { baseApiURL, owner, repo } = giteeApiOptions 7 | 8 | const baseRepoURL = `${baseApiURL}/repos/${owner}/${repo}` 9 | 10 | const giteeApi = { 11 | getFileSha: async (fileName) => { 12 | const [err, res] = await to(axios.get(`${baseRepoURL}/contents/${fileName}`)) 13 | if (err) { 14 | return '' 15 | } 16 | if (!res.data.sha) { 17 | return '' 18 | } 19 | return res.data.sha 20 | }, 21 | 22 | getFile: async (fileName) => { 23 | const [err, res] = await to(axios.get(`${baseRepoURL}/contents/${fileName}`)) 24 | if (err) { 25 | return null 26 | } 27 | if (res.data.length) { 28 | return res.data 29 | } 30 | if (res.data.sha) { 31 | return { 32 | sha: res.data.sha, 33 | name: res.data.name, 34 | path: res.data.path, 35 | content: base64ToUtf8(res.data.content) 36 | } 37 | } 38 | return null 39 | }, 40 | 41 | addFile: async (fileName, content) => { 42 | const sha = await giteeApi.getFileSha(fileName) 43 | if (sha) { 44 | return { 45 | status: 'Fail', 46 | msg: `已经存在 ${fileName}` 47 | } 48 | } 49 | content = utf8ToBase64(content) 50 | const data = { access_token: getToken(), message: `add ${fileName}`, content } 51 | const [err, res] = await to(axios.post(`${baseRepoURL}/contents/${fileName}`, data)) 52 | if (err) { 53 | return { 54 | status: 'Fail', 55 | msg: `添加失败: ${err}` 56 | } 57 | } 58 | return { 59 | status: 'OK', 60 | msg: `添加成功: ${fileName}`, 61 | } 62 | }, 63 | 64 | removeFile: async (fileName) => { 65 | const sha = await giteeApi.getFileSha(fileName) 66 | if (!sha) { 67 | return { 68 | status: 'Fail', 69 | msg: `不存在 ${fileName}` 70 | } 71 | } 72 | let params = { access_token: getToken(), message: `remove ${fileName}`, sha } 73 | const [err, res] = await to(axios.delete(`${baseRepoURL}/contents/${fileName}`, { params })) 74 | 75 | if (err) { 76 | return { 77 | status: 'Fail', 78 | msg: `删除失败: ${err}` 79 | } 80 | } 81 | return { 82 | status: 'OK', 83 | msg: `删除成功: ${fileName}` 84 | } 85 | }, 86 | 87 | updateFile: async (fileName, sha, content) => { 88 | content = utf8ToBase64(content) 89 | const data = { access_token: getToken(), message: `update ${fileName}`, content, sha } 90 | const [err, res] = await to(axios.put(`${baseRepoURL}/contents/${fileName}`, data)) 91 | if (err) { 92 | return { 93 | status: "Fail", 94 | msg: `更新失败: ${err}` 95 | } 96 | } 97 | return { 98 | status: 'OK', 99 | msg: `更新成功: ${fileName}` 100 | } 101 | }, 102 | 103 | checkRepo: async _ => { 104 | const params = { access_token: getToken() } 105 | const [err, res] = await to(axios.get(baseRepoURL, { params })) 106 | if (err) { 107 | return false 108 | } 109 | return true 110 | }, 111 | 112 | forkRepo: async _ => { 113 | const data = { access_token: getToken() } 114 | const [err, res] = await to(axios.post(`${baseApiURL}/repos/zclzone/gitee-db/forks`, data)) 115 | if (err) { 116 | return { 117 | status: 'Fail' 118 | } 119 | } 120 | return { 121 | status: 'OK' 122 | } 123 | }, 124 | } 125 | 126 | export { 127 | giteeApi 128 | } -------------------------------------------------------------------------------- /src/utils/loading.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import loadingComponent from '@/components/loading.vue' 3 | 4 | const LoadingConstructor = Vue.extend(loadingComponent) 5 | 6 | const instance = new LoadingConstructor({ 7 | el: document.createElement('div') 8 | }) 9 | 10 | instance.show = false // 默认隐藏 11 | const loading = { 12 | show () { // 显示方法 13 | instance.show = true 14 | document.body.appendChild(instance.$el) 15 | }, 16 | hide () { // 隐藏方法 17 | instance.show = false 18 | } 19 | } 20 | 21 | export default { 22 | install () { 23 | if (!Vue.$loading) { 24 | Vue.$loading = loading 25 | } 26 | Vue.mixin({ 27 | created () { 28 | this.$loading = Vue.$loading 29 | } 30 | }) 31 | } 32 | } -------------------------------------------------------------------------------- /src/utils/oauth-util.js: -------------------------------------------------------------------------------- 1 | import axios from '@/ajax/request' 2 | import { to } from './common' 3 | 4 | import { setUser } from '@/utils/cookie-util' 5 | 6 | const { giteeOauthOptions } = require('@/settings.js') 7 | const { baseOauthUrl, clientId, clientSecret, redirectUri } = giteeOauthOptions 8 | 9 | export function getOauthUrl() { 10 | return `${baseOauthUrl}/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code` 11 | } 12 | 13 | export async function getAccessToken(code) { 14 | const [err, res] = await to(axios.post(`${baseOauthUrl}/token?grant_type=authorization_code&code=${code}&client_id=${clientId}&redirect_uri=${redirectUri}`, { client_secret: clientSecret })) 15 | if (err) { 16 | return null 17 | } 18 | return res.data.access_token 19 | } 20 | 21 | export async function getUserInfo(access_token) { 22 | const [err, res] = await to(axios.get(`https://gitee.com/api/v5/user?access_token=${access_token}`)) 23 | if (err) { 24 | return null 25 | } 26 | const { avatar_url, email, id, name, login, html_url } = res.data 27 | return setUser({ avatar_url, email, id, name, login, html_url }) 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/page-title-util.js: -------------------------------------------------------------------------------- 1 | import settings from '@/settings' 2 | 3 | const title = settings.title || '奇思笔记' 4 | 5 | export function setPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return document.title = `${pageTitle} | ${title}` 8 | } 9 | document.title = title 10 | } -------------------------------------------------------------------------------- /src/views/admin/category/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 78 | 79 | -------------------------------------------------------------------------------- /src/views/admin/post-list/detail.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /src/views/admin/post-list/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 211 | 212 | -------------------------------------------------------------------------------- /src/views/error-page/404.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/home/about/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/home/article/components/index-primary.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | 33 | -------------------------------------------------------------------------------- /src/views/home/article/components/index-secondary.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 150 | 151 | -------------------------------------------------------------------------------- /src/views/home/article/components/post-list.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/views/home/article/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 93 | 94 | -------------------------------------------------------------------------------- /src/views/home/article/post-detail.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | 57 | -------------------------------------------------------------------------------- /src/views/home/works/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const settings = require('./src/settings.js') 2 | const title = settings.title || '奇思笔记' 3 | 4 | module.exports = { 5 | publicPath: './', 6 | // publicPath: process.env.NODE_ENV === 'production' ? `/${settings.pagesRepo}/` : './', 7 | chainWebpack: config => { 8 | config.plugin('html').tap(args => { 9 | args[0].title = title 10 | return args 11 | }) 12 | }, 13 | configureWebpack: config => { 14 | if (process.env.NODE_ENV === 'production') { 15 | config.mode = 'production' 16 | //打包文件大小配置 17 | config['performance'] = { 18 | maxEntrypointSize: 10000000, 19 | maxAssetSize: 30000000 20 | } 21 | } 22 | }, 23 | devServer: { 24 | // proxy: 'http://localhost:3000' 25 | } 26 | } 27 | --------------------------------------------------------------------------------