├── .babelrc ├── .dockerignore ├── .eslintrc ├── .github └── workflows │ ├── pr-close.yml │ ├── pr.yml │ ├── prd.yml │ └── v5.yml ├── .gitignore ├── .installed ├── .thinkjsrc ├── README.md ├── bin ├── build.sh └── copy_package.js ├── docker ├── Dockerfile ├── Dockerfile.nginx ├── README.md └── nginx.conf ├── package.json ├── src ├── admin │ ├── config │ │ └── route.js │ ├── controller │ │ ├── api │ │ │ ├── base.js │ │ │ ├── cate.js │ │ │ ├── file.js │ │ │ ├── options.js │ │ │ ├── page.js │ │ │ ├── post.js │ │ │ ├── system.js │ │ │ ├── tag.js │ │ │ ├── theme.js │ │ │ └── user.js │ │ ├── base.js │ │ ├── post_push.js │ │ └── user.js │ ├── logic │ │ ├── api │ │ │ ├── cate.js │ │ │ └── tag.js │ │ ├── index.js │ │ └── user.js │ ├── model │ │ ├── base.js │ │ ├── cate.js │ │ ├── page.js │ │ ├── post.js │ │ ├── tag.js │ │ └── user.js │ └── service │ │ ├── import │ │ ├── base.js │ │ ├── ghost.js │ │ ├── hexo.js │ │ ├── markdown.js │ │ └── wordpress.js │ │ └── upload │ │ ├── base.js │ │ ├── local.js │ │ ├── qiniu.js │ │ └── upyun.js ├── common │ ├── bootstrap │ │ ├── crontab.js │ │ └── global.js │ ├── config │ │ ├── cache.js │ │ ├── config.js │ │ ├── db.js │ │ ├── env │ │ │ ├── development.js │ │ │ ├── production.js │ │ │ └── testing.js │ │ ├── error.js │ │ ├── hook.js │ │ ├── locale │ │ │ └── en.js │ │ ├── route.js │ │ ├── session.js │ │ └── view.js │ ├── controller │ │ ├── base.js │ │ └── error.js │ ├── middleware │ │ └── resheaders.js │ ├── model │ │ ├── cate.js │ │ ├── options.js │ │ ├── post.js │ │ └── tag.js │ └── util │ │ └── buildImg.js └── home │ ├── config │ ├── config.js │ └── route.js │ ├── controller │ ├── base.js │ ├── crontab.js │ ├── index.js │ └── post.js │ └── service │ └── comment.js ├── stc.admin.config.js ├── stc.config.js ├── view ├── admin │ └── index_index.html ├── common │ ├── error_400.html │ ├── error_403.html │ ├── error_404.html │ ├── error_500.html │ └── error_503.html └── home │ ├── inc │ ├── comment.html │ └── pagination.html │ ├── index_opensearch.xml │ ├── index_rss.xml │ ├── index_sitemap.xml │ ├── layout.html │ ├── package.json │ ├── post_archive.html │ ├── post_detail.html │ ├── post_list.html │ ├── post_page.html │ ├── post_search.html │ ├── post_tag.html │ └── template │ └── cates_list.html ├── webpack.config.babel.js ├── webpack.production.config.babel.js ├── workbox.config.js ├── www ├── 404.html ├── content-search.xml ├── development.js ├── favicon.ico ├── google343405242456511b.html ├── production.js ├── robots.txt ├── static │ ├── admin │ │ ├── css │ │ │ ├── admin.css │ │ │ └── icon.css │ │ ├── font │ │ │ ├── icomoon.eot │ │ │ ├── icomoon.svg │ │ │ ├── icomoon.ttf │ │ │ ├── icomoon.woff │ │ │ └── selection.json │ │ ├── img │ │ │ └── editor@2x.png │ │ ├── module │ │ │ └── bootstrap │ │ │ │ ├── css │ │ │ │ └── bootstrap.min.css │ │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ │ └── js │ │ │ │ └── bootstrap.js │ │ └── src │ │ │ ├── admin │ │ │ ├── action │ │ │ │ ├── cate.js │ │ │ │ ├── options.js │ │ │ │ ├── page.js │ │ │ │ ├── post.js │ │ │ │ ├── push.js │ │ │ │ ├── system.js │ │ │ │ ├── tag.js │ │ │ │ ├── theme.js │ │ │ │ └── user.js │ │ │ ├── app.jsx │ │ │ ├── component │ │ │ │ ├── App.jsx │ │ │ │ ├── Dashboard.jsx │ │ │ │ ├── appearance.jsx │ │ │ │ ├── breadcrumb.jsx │ │ │ │ ├── cate.jsx │ │ │ │ ├── cate_create.jsx │ │ │ │ ├── cate_list.jsx │ │ │ │ ├── import.jsx │ │ │ │ ├── login.jsx │ │ │ │ ├── navigation.jsx │ │ │ │ ├── options.jsx │ │ │ │ ├── options_2fa.jsx │ │ │ │ ├── options_analytic.jsx │ │ │ │ ├── options_comment.jsx │ │ │ │ ├── options_general.jsx │ │ │ │ ├── options_push.jsx │ │ │ │ ├── options_reading.jsx │ │ │ │ ├── options_upload.jsx │ │ │ │ ├── page.jsx │ │ │ │ ├── page_create.jsx │ │ │ │ ├── page_list.jsx │ │ │ │ ├── post │ │ │ │ │ └── index.jsx │ │ │ │ ├── post_create │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── style.css │ │ │ │ ├── post_list.jsx │ │ │ │ ├── push.jsx │ │ │ │ ├── push_create.jsx │ │ │ │ ├── push_list.jsx │ │ │ │ ├── sidebar.jsx │ │ │ │ ├── tag.jsx │ │ │ │ ├── tag_create.jsx │ │ │ │ ├── tag_list.jsx │ │ │ │ ├── theme.jsx │ │ │ │ ├── theme_editor.jsx │ │ │ │ ├── user.jsx │ │ │ │ ├── user_create.jsx │ │ │ │ ├── user_editpwd.jsx │ │ │ │ └── user_list.jsx │ │ │ ├── page │ │ │ │ ├── appearance │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── navigation.js │ │ │ │ │ └── theme.js │ │ │ │ ├── cate │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ │ ├── dashboard.js │ │ │ │ ├── options │ │ │ │ │ ├── analytic.js │ │ │ │ │ ├── comment.js │ │ │ │ │ ├── general.js │ │ │ │ │ ├── import.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── push.js │ │ │ │ │ ├── reading.js │ │ │ │ │ ├── two_factor_auth.js │ │ │ │ │ └── upload.js │ │ │ │ ├── page │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ │ ├── post │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ │ ├── push │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ │ ├── tag │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ │ └── user │ │ │ │ │ ├── create.js │ │ │ │ │ ├── edit.js │ │ │ │ │ ├── edit_pwd.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── list.js │ │ │ └── store │ │ │ │ ├── cate.js │ │ │ │ ├── options.js │ │ │ │ ├── page.js │ │ │ │ ├── post.js │ │ │ │ ├── push.js │ │ │ │ ├── system.js │ │ │ │ ├── tag.js │ │ │ │ ├── theme.js │ │ │ │ └── user.js │ │ │ └── common │ │ │ ├── action │ │ │ ├── modal.js │ │ │ └── tip.js │ │ │ ├── component │ │ │ ├── base.jsx │ │ │ ├── editor │ │ │ │ ├── index.jsx │ │ │ │ ├── search.jsx │ │ │ │ └── style.css │ │ │ ├── modal.jsx │ │ │ ├── modal_manage.jsx │ │ │ └── tip.jsx │ │ │ ├── store │ │ │ ├── modal.js │ │ │ └── tip.js │ │ │ └── util │ │ │ ├── auth.js │ │ │ └── firekylin.js │ └── home │ │ ├── css │ │ ├── all.css │ │ ├── article.css │ │ ├── base.css │ │ ├── comment.css │ │ ├── footer.css │ │ ├── header.css │ │ ├── highlight.css │ │ ├── icon.css │ │ ├── pagination.css │ │ ├── responsive.css │ │ ├── search.css │ │ └── sidebar.css │ │ ├── font │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ │ └── js │ │ └── firekylin.js └── sw.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", ["es2015", {"loose": true}], "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"] 5 | ], 6 | "env": { 7 | "development": { 8 | 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /app/ 3 | *.log 4 | .cache 5 | .git 6 | /dist/ 7 | runtime/ 8 | www/static/upload/ 9 | output/ 10 | output.theme/ 11 | .vscode -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/rules 2 | { 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "think": false, 11 | "firekylin": false 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:react/recommended", 16 | "plugin:import/errors", 17 | "plugin:import/warnings" 18 | ], 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "experimentalObjectRestSpread": true, 22 | "jsx": true 23 | }, 24 | "sourceType": "module" 25 | }, 26 | "plugins": [ 27 | "react", 28 | "import" 29 | ], 30 | "settings": { 31 | "import/resolver": { 32 | "webpack": { 33 | "config": "webpack.config.babel.js" 34 | } 35 | } 36 | }, 37 | "rules": { 38 | "strict": 0, 39 | "quotes": [2, "single"], 40 | "max-len": [2, { 41 | "code": 120, 42 | "ignoreTrailingComments": true, 43 | "ignoreUrls": true, 44 | "ignoreRegExpLiterals": true, 45 | "ignoreTemplateLiterals": true 46 | }], 47 | "no-underscore-dangle": 0, 48 | "no-unused-vars": 1, 49 | "no-unused-expressions": 0, 50 | "react/jsx-no-undef": 2, 51 | "react/no-find-dom-node": 0, 52 | "new-cap": 0, 53 | "no-shadow": 0, 54 | "no-use-before-define": 0, 55 | "no-case-declarations": 0, 56 | "no-console": 1, 57 | "eol-last": 2, 58 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 59 | "no-trailing-spaces": 2, 60 | "nonblock-statement-body-position": 2, 61 | "newline-per-chained-call": [2, { "ignoreChainWithDepth": 3 }], 62 | "space-before-blocks": 2, 63 | "space-in-parens": 2, 64 | "semi-spacing": 2, 65 | "comma-spacing": 2, 66 | "no-tabs": 2, 67 | "eqeqeq": 2, 68 | "no-multi-spaces": 2, 69 | "new-parens": 2, 70 | "no-implicit-globals": 2, 71 | "import/first": 2, 72 | "import/no-duplicates": 2, 73 | "import/extensions": [2, "always", { 74 | "js": "never", 75 | "jsx": "never" 76 | }], 77 | "import/no-unresolved": 2, 78 | "import/order": 2, 79 | "import/no-absolute-path": 2, 80 | "import/newline-after-import": 2, 81 | "import/no-named-default": 2 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/pr-close.yml: -------------------------------------------------------------------------------- 1 | name: clean blog-pr.dev.xuexb.com 2 | 3 | on: 4 | pull_request_target: 5 | types: [closed] 6 | 7 | env: 8 | DOMAIN: blog-pr-${{ github.event.pull_request.number }}.dev.xuexb.com 9 | 10 | jobs: 11 | clean-dev: 12 | runs-on: ubuntu-latest 13 | environment: dev 14 | steps: 15 | - name: clean 16 | uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78 17 | with: 18 | host: ${{ secrets.SSH_HOST }} 19 | username: ${{ secrets.SSH_USERNAME }} 20 | key: ${{ secrets.SSH_KEY }} 21 | port: ${{ secrets.SSH_PORT }} 22 | script: | 23 | node_container_name="${{ env.DOMAIN }}-node" 24 | nginx_container_name="${{ env.DOMAIN }}-nginx" 25 | docker ps -aq --filter "name=$node_container_name" | xargs docker rm -f || echo "Delete fail" 26 | docker ps -aq --filter "name=$nginx_container_name" | xargs docker rm -f || echo "Delete fail" 27 | dyups_result="" 28 | for((i=1;i<=5;i++)); do 29 | httpcode=$(curl -sL --max-time 5 -w '%{http_code}' -X DELETE "https://dyups.xuexb.com/api/${{ env.DOMAIN }}?r=$RANDOM&token=${{ secrets.DYUPS_TOKEN }}" -o /dev/null) 30 | if [ "$httpcode" != "200" ]; then 31 | echo "删除 dyups 失败,响应码:$httpcode ,继续尝试" 32 | dyups_result="0" 33 | else 34 | echo "删除 dyups 成功" 35 | dyups_result="1" 36 | break 37 | fi 38 | done 39 | if [ "$dyups_result" == "0" ]; then 40 | echo "尝试了5次,最终还是失败,我也没办法" 41 | exit 12 42 | fi -------------------------------------------------------------------------------- /.github/workflows/prd.yml: -------------------------------------------------------------------------------- 1 | name: deploy xuexb.com 2 | 3 | on: 4 | push: 5 | tags: 6 | - v5** 7 | 8 | env: 9 | IMAGE_NAME: ${{ github.repository }} 10 | DOMAIN: xuexb.com 11 | DOMAIN_PORT: 8080 12 | 13 | jobs: 14 | build-prd: 15 | runs-on: ubuntu-latest 16 | environment: prd 17 | outputs: 18 | version: ${{ steps.meta.outputs.version }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | - name: Login to Container Registry 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.PERSONAL_ACCESS_RW_NAME }} 26 | password: ${{ secrets.PERSONAL_ACCESS_RW_TOKEN }} 27 | - name: Extract metadata (tags, labels) for Docker 28 | id: meta 29 | uses: docker/metadata-action@v2 30 | with: 31 | images: ${{ env.IMAGE_NAME }} 32 | flavor: | 33 | latest=false 34 | tags: | 35 | type=ref,event=branch 36 | type=ref,event=pr 37 | type=semver,pattern={{version}} 38 | - name: Update Nginx Dockerfile path 39 | run: | 40 | a=xuexb/blog:node-latest 41 | b=${{ env.IMAGE_NAME }}:node-${{ steps.meta.outputs.version }} 42 | sed -i "s?$a?$b?g" docker/Dockerfile.nginx 43 | cat docker/Dockerfile.nginx 44 | - name: Build Node.js Docker image 45 | uses: docker/build-push-action@v3 46 | with: 47 | context: . 48 | file: ./docker/Dockerfile 49 | push: true 50 | tags: ${{ env.IMAGE_NAME }}:node-${{ steps.meta.outputs.version }} 51 | - name: Build Nginx Docker image 52 | uses: docker/build-push-action@v3 53 | with: 54 | context: . 55 | file: ./docker/Dockerfile.nginx 56 | push: true 57 | tags: ${{ env.IMAGE_NAME }}:nginx-${{ steps.meta.outputs.version }} 58 | deploy-prd: 59 | runs-on: ubuntu-latest 60 | environment: prd 61 | needs: build-prd 62 | if: ${{ needs.build-prd.result == 'success' }} 63 | steps: 64 | - name: deploy 65 | uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78 66 | with: 67 | host: ${{ secrets.SSH_HOST }} 68 | username: ${{ secrets.SSH_USERNAME }} 69 | key: ${{ secrets.SSH_KEY }} 70 | port: ${{ secrets.SSH_PORT }} 71 | script: | 72 | node_container_name="${{ env.DOMAIN }}-${{ env.DOMAIN_PORT }}-node" 73 | node_image="${{ env.IMAGE_NAME }}:node-${{ needs.build-prd.outputs.version }}" 74 | nginx_container_name="${{ env.DOMAIN }}-${{ env.DOMAIN_PORT }}-nginx" 75 | nginx_image="${{ env.IMAGE_NAME }}:nginx-${{ needs.build-prd.outputs.version }}" 76 | docker pull $node_image \ 77 | && docker pull $nginx_image \ 78 | && docker ps -aq --filter "name=$node_container_name" | xargs docker rm -f || echo "Delete fail" \ 79 | && docker ps -aq --filter "name=$nginx_container_name" | xargs docker rm -f || echo "Delete fail" \ 80 | && docker run \ 81 | --env BLOG_ENV="`hostname`" \ 82 | --env DB_HOST="${{ secrets.DB_HOST }}" \ 83 | --env DB_PORT=${{ secrets.DB_PORT }} \ 84 | --env DB_DATABASE=${{ secrets.DB_DATABASE }} \ 85 | --env DB_USER=${{ secrets.DB_USER }} \ 86 | --env DB_PASSWORD=${{ secrets.DB_PASSWORD }} \ 87 | --env DB_PREFIX=${{ secrets.DB_PREFIX }} \ 88 | --name $node_container_name \ 89 | --restart=always \ 90 | -d \ 91 | $node_image \ 92 | && docker run \ 93 | --env BLOG_ENV="`hostname`" \ 94 | -p ${{ env.DOMAIN_PORT }}:8080 \ 95 | --name $nginx_container_name \ 96 | --link $node_container_name:blog \ 97 | -d \ 98 | --restart=always \ 99 | $nginx_image 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # node-waf configuration 17 | .lock-wscript 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | app 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 24 | node_modules 25 | 26 | # IDE config 27 | .idea 28 | 29 | # Temporary folder 30 | .tmp 31 | 32 | # Compiled Release 33 | release/ 34 | 35 | # OS func folder 36 | .DS_Store 37 | 38 | runtime/ 39 | www/static/upload/ 40 | sitemap.xml 41 | 42 | output/ 43 | output.theme/ 44 | .version 45 | .installed 46 | build 47 | 48 | stc/ 49 | jsconfig.json 50 | .vscode 51 | db/ 52 | 53 | .eslintcache 54 | 55 | /dist/ 56 | /www/static/admin/js/ -------------------------------------------------------------------------------- /.installed: -------------------------------------------------------------------------------- 1 | firekylin -------------------------------------------------------------------------------- /.thinkjsrc: -------------------------------------------------------------------------------- 1 | { 2 | "createAt": "2016-01-16 12:50:58", 3 | "mode": "module", 4 | "es": true 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前端小武博客 2 | 3 | 基于 [Firekylin](https://github.com/firekylin/firekylin) 上扩展开发搭建,服务器运行于阿里云,域名为 [xuexb.com](https://xuexb.com/)。 4 | 5 | image 6 | 7 | 8 | ## 历史版本 9 | 10 | #### 2011.08.09 - v0 11 | 12 | DIV+CSS 静态页面,刚注册完成 13 | 14 | #### ASP 版本 - 2011-2012 - v1.1 15 | 16 | - 动态页面+后台、主页、详情页生成静态化 17 | - 列表页生成静态化、一键网站静态化,一键生成 Sitemap 18 | - IIS 全站伪静态 19 | 1. 无刷新评论 - 基于 jQuery 20 | 1. 无刷新弹出层登录 21 | 1. 第三方登录接入(腾讯QQ、新浪微博) 22 | 23 | #### PHP 版本 - 2013-2014 - v1.5 24 | 25 | - v3.1:纯原生 PHP 实现 26 | 1. 全站原生 JS 完成 Ajax 、评论 27 | - v3.2:基于 [CodeIgniter](http://www.codeigniter.com/) 重构 28 | 29 | #### Node.js 版本 - 2014-2015 - v2.x 30 | 31 | - 基于 [ThinkJS 1.x](https://thinkjs.org/zh-cn/doc/1.2/index.html) 开发 32 | 33 | #### Node.js 版本 - 2015-2016 - v3.x 34 | 35 | - 基于 [ThinkJS 2.x](https://thinkjs.org/zh-cn/doc/2.0/index.html) 开发 36 | 37 | #### Node.js 版本 - 2017-2021 - v4.x 38 | 39 | - 基于 [Firekylin](https://github.com/firekylin/firekylin) v0.15.6 上扩展开发搭建 40 | - Nginx 前置缓存 41 | - MIP 、AMP 42 | 43 | #### Node.js 版本 - 2022 - v5.x 44 | 45 | - [x] Service Worker 46 | - [x] CDN 47 | - [ ] WebP 48 | - [x] GitHub Actions CI/CD(测试环境+生产环境) 49 | - [x] 多地区部署 50 | - [x] Docker 51 | ```bash 52 | docker run \ 53 | -d \ 54 | --env BLOG_ENV="`hostname`" \ 55 | --name blog-node \ 56 | --rm \ 57 | -it \ 58 | ghcr.io/xuexb/blog:node-latest 59 | 60 | docker run \ 61 | -d \ 62 | --env BLOG_ENV="`hostname`" \ 63 | -p 8080:8080 \ 64 | -it \ 65 | --name blog-nginx \ 66 | --rm \ 67 | --link blog-node:blog \ 68 | ghcr.io/xuexb/blog:nginx-latest 69 | ``` 70 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf output; 4 | mkdir output; 5 | 6 | cp .thinkjsrc output/ 7 | cp .installed output/ 8 | 9 | echo 'copy www/* start ...'; 10 | mkdir output/www; 11 | cp -r www/* output/www; 12 | rm -rf output/www/static; 13 | echo 'copy www/* end'; 14 | 15 | echo 'webpack start ...'; 16 | yarn webpack.build.production; 17 | echo 'webpack end'; 18 | 19 | echo 'stc start ...'; 20 | node stc.config.js; 21 | echo 'stc end'; 22 | echo 'stc admin start ...'; 23 | node stc.admin.config.js; 24 | echo 'stc admin end'; 25 | 26 | yarn compile; 27 | yarn copy-package; 28 | 29 | # 复制app 30 | cp -r app output; 31 | cp -r yarn.lock output; 32 | 33 | # 删除没用文件 34 | rm -rf output/app/common/runtime; 35 | #rm -rf output/www/static/admin/js/*.map; 36 | find output/ -name '*.map' | xargs rm; 37 | 38 | # 删除原始文件 39 | # find output/www/static/home -type f -regex ".*/[a-z0-9]*\.[a-z]*$" | xargs rm; 40 | 41 | # Service Worker 42 | yarn build:sw -------------------------------------------------------------------------------- /bin/copy_package.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var content = fs.readFileSync('./package.json', 'utf8'); 3 | var data = JSON.parse(content); 4 | delete data.devDependencies; 5 | data.scripts = { 6 | start: 'node www/production.js' 7 | }; 8 | fs.writeFileSync('output/package.json', JSON.stringify(data, undefined, 4)); 9 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine AS builder 2 | WORKDIR /root/app 3 | COPY yarn.lock . 4 | COPY package.json . 5 | RUN yarn 6 | COPY . . 7 | RUN ls -lah 8 | RUN yarn build 9 | 10 | FROM node:12-alpine 11 | LABEL maintainer="xuexb " 12 | LABEL org.opencontainers.image.source https://github.com/xuexb/blog 13 | ENV LANG en_US.UTF-8 14 | ENV TZ Asia/Shanghai 15 | ENV PORT=8360 16 | WORKDIR /root/app 17 | COPY --from=builder /root/app/output . 18 | RUN yarn install --production 19 | RUN yarn cache clean \ 20 | && rm -rf /opt/yarn-v* 21 | EXPOSE 8360 22 | CMD [ "node", "www/production.js" ] -------------------------------------------------------------------------------- /docker/Dockerfile.nginx: -------------------------------------------------------------------------------- 1 | FROM shangxian/nginx:2.1-alpine3.9 2 | LABEL maintainer="xuexb " 3 | LABEL org.opencontainers.image.source https://github.com/xuexb/blog 4 | ENV LANG en_US.UTF-8 5 | ENV TZ Asia/Shanghai 6 | WORKDIR /etc/nginx/app 7 | COPY --from=xuexb/blog:node-latest /root/app/www /etc/nginx/app 8 | COPY docker/nginx.conf /etc/nginx/nginx.conf 9 | EXPOSE 8080 -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker 运行、部署 2 | 3 | ### 1. 构建 Node.js 镜像 4 | 5 | 构建出 Node.js 运行时镜像: 6 | 7 | - 镜像名称:`xuexb/blog` 必须的,因为在下个镜像还需要使用该名称复制文件 8 | - 镜像版本:`node-latest` 必须的,因为在下个镜像还需要使用该版本复制文件,也可以构建多个版本,如: 9 | - `xuexb/blog:node-5.0.0` 10 | - `xuexb/blog:node-latest` 11 | - 运行时端口:`8360` 必须的,因为在下个镜像还需要该端口进行反向代理 12 | 13 | ```bash 14 | docker build -t xuexb/blog:node-latest -f docker/Dockerfile . 15 | ``` 16 | 17 | 程序默认使用【前端小武博客】开发测试环境的数据库,也可以变量配置: 18 | 19 | ```bash 20 | docker run --name blog-node \ 21 | -p 8360:8360 \ 22 | --rm \ 23 | -it \ 24 | -e DB_HOST=数据库连接地址 \ 25 | -e DB_PORT=数据库连接端口 \ 26 | -e DB_DATABASE=数据库名称 \ 27 | -e DB_USER=数据库用户名 \ 28 | -e DB_PASSWORD=数据库密码 \ 29 | -e DB_PREFIX=表名称前缀 \ 30 | xuexb/blog:node-latest 31 | ``` 32 | 33 | 默认生成 Node.js 镜像可以独立运行,跟 Nginx 镜像配合更优,对比如下: 34 | 35 | | case | Node.js 镜像独立运行 | Node.js + Nginx | 36 | | --- | --- | --- | 37 | | 镜像名称 | 可随意 | `xuexb/blog` | 38 | | 镜像版本 | 可随意 | 必须存在一个 `node-latest` | 39 | | Node.js 运行时端口 | 可随意,如: `-e PORT=8080` | `8360` | 40 | | 浏览器缓存 | 未配置 | Nginx 镜像配置: | 41 | | 数据库配置 | 支持 | 支持 | 42 | 43 | ### 2. 构建 Nginx 镜像 44 | 45 | Node.js 镜像提供了完整的独立运行文件,包括:后端、静态文件等,但不太文件配置文件缓存、跨域配置、Gzip 压缩等策略,当构建 Node.js 镜像后支持再基于 Node.js 镜像内的静态文件再构建一个 Nginx 镜像。 46 | 47 | - 镜像名称:随意 48 | - 镜像版本:随意 49 | - 运行时端口:`8080` 50 | 51 | ```bash 52 | docker build -t xuexb/blog:nginx-latest -f docker/Dockerfile.nginx . 53 | ``` 54 | 55 | ### 3. 运行 56 | 57 | 独立运行 Node.js 程序: 58 | 59 | ```bash 60 | docker run -p 8360:8360 --name blog-node --rm -it xuexb/blog:node-latest 61 | ``` 62 | 63 | 代理模式运行: 64 | 65 | ```bash 66 | # 先运行 Node.js 程序 67 | # 代理模式运行时,Node.js 程序就不需要暴露端口了 68 | docker run --name blog-node --rm -it xuexb/blog:node-latest 69 | 70 | # 把 `blog-node` 链接到 Nginx 容器,且名称为 `blog` 71 | docker run \ 72 | -p 8080:8080 \ 73 | -it \ 74 | --name blog-nginx \ 75 | --rm \ 76 | --link blog-node:blog \ 77 | xuexb/blog:nginx-latest 78 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "description": "前端小武博客", 4 | "version": "5.1.0", 5 | "scripts": { 6 | "start": "node www/development.js", 7 | "compile": "babel --presets es2015,stage-1 --plugins transform-runtime src/ --out-dir app/", 8 | "watch-compile": "node -e \"console.log(' no longer need, use command directly.');console.log();\"", 9 | "watch": "npm run watch-compile", 10 | "copy-package": "node bin/copy_package.js", 11 | "build": "sh bin/build.sh", 12 | "webpack": "webpack --watch", 13 | "webpack.build": "webpack", 14 | "webpack.build.production": "webpack --config=webpack.production.config.babel.js", 15 | "lint": "eslint ./src ./*.js ./www/*.js ./www/static/admin/src ./www/static/home --ignore-path .gitignore --cache -f table --ext .js,.jsx || true", 16 | "lint-fix": "eslint ./src ./*.js ./www/*.js ./www/static/admin/src ./www/static/home --ignore-path .gitignore -f table --ext .js,.jsx --fix || true", 17 | "lint-break-on-errors": "echo '\n[INFO] Running ESLint...\n[INFO] (Autofix with `npm run lint-fix`)' && eslint ./src ./*.js ./www/*.js ./www/static/admin/src ./www/static/home --ignore-path .gitignore -f table --ext .js,.jsx", 18 | "check-extraneous-pkgs": "npm list --depth=0 && echo '[SUCCESS] No extraneous packages.' || (echo '[INFO] Pruning extraneous packages...\n' && npm prune)", 19 | "build:sw": "workbox injectManifest workbox.config.js && terser output/www/sw.js -o output/www/sw.js" 20 | }, 21 | "dependencies": { 22 | "babel-runtime": "6.x.x", 23 | "highlight.js": "^9.2.0", 24 | "markdown-toc": "^1.0.2", 25 | "marked": "^0.3.5", 26 | "moment": "^2.11.2", 27 | "node-crontab": "0.0.8", 28 | "nodemailer": "^2.3.2", 29 | "nunjucks": "^2.3.0", 30 | "phpass": "^0.1.1", 31 | "push-to-firekylin": "^0.2.2", 32 | "qiniu": "^6.1.13", 33 | "request": "^2.69.0", 34 | "semver": "^5.1.0", 35 | "source-map-support": "^0.4.0", 36 | "speakeasy": "2.0.0", 37 | "thinkjs": "^2.1.5", 38 | "to-markdown": "^3.0.3", 39 | "upyun": "^2.0.4", 40 | "xml2js": "^0.4.16" 41 | }, 42 | "devDependencies": { 43 | "autobind-decorator": "^1.4.1", 44 | "babel-cli": "6.x.x", 45 | "babel-core": "6.x.x", 46 | "babel-eslint": "^6.0.4", 47 | "babel-loader": "^6.2.1", 48 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 49 | "babel-plugin-transform-runtime": "6.x.x", 50 | "babel-polyfill": "^6.7.4", 51 | "babel-preset-es2015": "^6.22.0", 52 | "babel-preset-react": "^6.3.13", 53 | "babel-preset-stage-0": "6.x.x", 54 | "babel-preset-stage-1": "^6.5.0", 55 | "classnames": "^2.2.3", 56 | "codemirror": "^5.26.0", 57 | "css-loader": "^0.23.1", 58 | "eslint": "^4.18.2", 59 | "eslint-import-resolver-webpack": "^0.8.1", 60 | "eslint-plugin-import": "^2.3.0", 61 | "eslint-plugin-react": "^6.10.3", 62 | "history": "^1.17.0", 63 | "md5": "^2.0.0", 64 | "qrcode-react": "0.1.14", 65 | "rc-select": "^5.10.0", 66 | "react": "^0.14.9", 67 | "react-bootstrap": "~0.28.3", 68 | "react-bootstrap-validation": "^0.1.11", 69 | "react-codemirror": "^0.3.0", 70 | "react-color": "^2.12.0", 71 | "react-datetime": "^2.8.10", 72 | "react-dom": "^0.14.9", 73 | "react-mixin": "^3.0.3", 74 | "react-router": "^2.0.0-rc5", 75 | "react-ui-tree": "^2.6.0", 76 | "reflux": "^0.3.0", 77 | "stc": "^2.0.2", 78 | "stc-css-combine": "^1.0.1", 79 | "stc-css-compress": "^1.0.1", 80 | "stc-html-compress": "^1.0.1", 81 | "stc-replace": "^1.0.5", 82 | "stc-resource-version": "^1.0.0", 83 | "stc-uglify": "^1.0.1", 84 | "style-loader": "^0.13.0", 85 | "superagent": "^5.2.1", 86 | "terser": "^5.13.1", 87 | "webpack": "^2.6.1", 88 | "workbox-cli": "^6.5.3" 89 | }, 90 | "repository": "https://github.com/xuexb/blog", 91 | "license": "MIT", 92 | "resolutions": { 93 | "react-bootstrap-validation/react-bootstrap": "~0.28.4" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/admin/config/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default [ 4 | // [/^admin\/(.+?)$/, ':1'] 5 | ]; -------------------------------------------------------------------------------- /src/admin/controller/api/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * base rest controller 3 | */ 4 | export default class extends think.controller.rest { 5 | /** 6 | * allow list for user 7 | * @type {Array} 8 | */ 9 | allowList = ['api/post/put', 'api/post/post', 'api/post/delete', 'api/file/post']; 10 | /** 11 | * [constructor description] 12 | * @param {[type]} http [description] 13 | * @return {[type]} [description] 14 | */ 15 | constructor(http) { 16 | super(http); 17 | this._method = 'method'; 18 | } 19 | /** 20 | * before 21 | * @return {} [] 22 | */ 23 | async __before() { 24 | let userInfo = await this.session('userInfo') || {}; 25 | if(think.isEmpty(userInfo)) { 26 | return this.fail('USER_NOT_LOGIN'); 27 | } 28 | this.userInfo = userInfo; 29 | let type = userInfo.type | 0; 30 | //not admin 31 | if(type !== 1) { 32 | let action = this.http.action; 33 | if(action === 'get') { 34 | return; 35 | } 36 | let name = this.http.controller + '/' + this.http.action; 37 | if(this.allowList.indexOf(name) > -1) { 38 | return; 39 | } 40 | return this.fail('USER_NO_PERMISSION'); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/admin/controller/api/cate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | 5 | export default class extends Base { 6 | /** 7 | * get 8 | * @return {[type]} [description] 9 | */ 10 | async getAction() { 11 | let result; 12 | if(this.get('pid')) { 13 | this.modelInstance.where({pid: this.get('pid')}); 14 | } 15 | if(this.id) { 16 | result = await this.modelInstance.where({id: this.id}).find(); 17 | result.post_cate = result.post_cate.length; 18 | } else { 19 | result = await this.modelInstance.select(); 20 | result = result.map(item => { 21 | item.post_cate = item.post_cate.length; 22 | return item; 23 | }); 24 | } 25 | return this.success(result); 26 | } 27 | 28 | /** 29 | * add user 30 | * @return {[type]} [description] 31 | */ 32 | async postAction() { 33 | let data = this.post(); 34 | 35 | let ret = await this.modelInstance.addCate(data); 36 | if(ret.type === 'exist') { 37 | return this.fail('CATE_EXIST'); 38 | } 39 | return this.success({id: ret.id}); 40 | } 41 | /** 42 | * update user info 43 | * @return {[type]} [description] 44 | */ 45 | async putAction() { 46 | if (!this.id) { 47 | return this.fail('PARAMS_ERROR'); 48 | } 49 | let data = this.post(); 50 | data.id = this.id; 51 | let rows = await this.modelInstance.saveCate(data); 52 | return this.success({affectedRows: rows}); 53 | } 54 | 55 | async deleteAction() { 56 | if(!this.id) { 57 | return this.fail('PARAMS_ERROR'); 58 | } 59 | await this.modelInstance.deleteCate(this.id); 60 | return this.success(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/admin/controller/api/file.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import request from 'request'; 5 | 6 | import Base from './base'; 7 | 8 | request.defaults({ 9 | strictSSL: false, 10 | rejectUnauthorized: false 11 | }); 12 | 13 | const getFileContent = think.promisify(request.get, request); 14 | const writeFileAsync = think.promisify(fs.writeFile, fs); 15 | 16 | export default class extends Base { 17 | uploadConfig = {}; 18 | 19 | async __before() { 20 | this.uploadConfig = await this.getUploadConfig(); 21 | } 22 | 23 | async postAction() { 24 | let config = this.uploadConfig; 25 | let {type} = config; 26 | let file; 27 | 28 | /** 处理远程抓取 **/ 29 | if(this.post('fileUrl')) { 30 | try { 31 | file = await this.getUrlFile(this.post('fileUrl')); 32 | } catch(e) { 33 | return this.fail(e.message); 34 | } 35 | } else { 36 | file = this.file('file'); 37 | } 38 | if(!file) { return this.fail('FILE_UPLOAD_ERROR'); } 39 | 40 | /** 处理导入数据 **/ 41 | if(this.post('importor')) { 42 | return this.serviceImport(this.post('importor'), file); 43 | } 44 | 45 | /** 检查文件类型 */ 46 | // let contentType = file.headers['content-type']; 47 | 48 | // 处理其它上传 49 | if(!type) { return this.fail(); } 50 | if(type === 'local') { 51 | config = {name: this.post('name')}; 52 | } 53 | 54 | return this.serviceUpload(type, file.path, config); 55 | } 56 | 57 | // 获取上传设置 58 | async getUploadConfig() { 59 | const options = await this.model('options').getOptions(); 60 | return options.upload; 61 | } 62 | 63 | /** 64 | * 上传文件 65 | */ 66 | async serviceUpload(service, file, config) { 67 | try { 68 | const uploader = think.service(`upload/${service}`, 'admin'); 69 | const result = await (new uploader()).run(file, config); 70 | return this.success(result); 71 | } catch (e) { 72 | return this.fail(e || 'FILE_UPLOAD_ERROR'); 73 | } 74 | } 75 | 76 | /** 77 | * 从其他平台导入数据 78 | */ 79 | async serviceImport(service, file) { 80 | try { 81 | let importor = think.service(`import/${service}`, 'admin'); 82 | let {post, page, category, tag} = await (new importor(this)).run(file); 83 | return this.success(`共导入文章 ${post} 篇,页面 ${page} 页,分类 ${category} 个,标签 ${tag} 个`); 84 | } catch(e) { 85 | return this.fail(e); 86 | } 87 | } 88 | 89 | async getUrlFile(url) { 90 | let resp = await getFileContent({ 91 | url, 92 | headers: { 93 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) Chrome/47.0.2526.111 Safari/537.36' 94 | }, 95 | strictSSL: false, 96 | timeout: 1000, 97 | encoding: 'binary' 98 | }).catch(() => { throw new Error('UPLOAD_URL_ERROR'); }); 99 | 100 | if(resp.headers['content-type'].indexOf('image') === -1) { 101 | throw new Error('UPLOAD_TYPE_ERROR'); 102 | } 103 | 104 | let uploadDir = this.config('post').file_upload_path; 105 | if(!uploadDir) { 106 | uploadDir = path.join(os.tmpdir(), 'thinkjs/upload'); 107 | } 108 | if(!think.isDir(uploadDir)) { 109 | think.mkdir(uploadDir); 110 | } 111 | 112 | let uploadName = think.uuid(20) + path.extname(url); 113 | let uploadPath = path.join(uploadDir, uploadName); 114 | await writeFileAsync(uploadPath, resp.body, 'binary'); 115 | 116 | return { 117 | fieldName: 'file', 118 | originalFilename: path.basename(url), 119 | path: uploadPath, 120 | size: resp.headers['content-length'] 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/admin/controller/api/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Post from './post'; 4 | 5 | export default class extends Post { 6 | 7 | postModel = this.model('post'); 8 | 9 | constructor(http) { 10 | super(http); 11 | this._modelInstance = this.modelInstance; 12 | Object.defineProperty(this, 'modelInstance', { 13 | get() { 14 | return this._modelInstance.setRelation('user').where({type: 1}); 15 | } 16 | }); 17 | } 18 | 19 | getAction(self) { 20 | if(!this.id) { 21 | let field = [ 22 | 'id', 23 | 'title', 24 | 'user_id', 25 | 'create_time', 26 | 'update_time', 27 | 'status', 28 | 'pathname', 29 | 'is_public' 30 | ]; 31 | this.modelInstance.order('create_time DESC').field(field); 32 | } 33 | 34 | if(this.get('page') !== '-1') { 35 | this.modelInstance.page(this.get('page'), 20); 36 | } 37 | return super.getBaseAction(self); 38 | } 39 | 40 | async postAction() { 41 | let data = this.post(); 42 | 43 | //check pathname 44 | let post = await this.modelInstance.where({pathname: data.pathname}).find(); 45 | 46 | if(!think.isEmpty(post)) { 47 | return this.fail('PATHNAME_EXIST'); 48 | } 49 | 50 | data.type = 1; 51 | data.user_id = this.userInfo.id; 52 | data = await this.postModel.getContentAndSummary(data); 53 | data = this.postModel.getPostTime(data); 54 | 55 | let insert = await this.modelInstance.addPost(data); 56 | return this.success(insert); 57 | } 58 | 59 | async putAction() { 60 | if (!this.id) { 61 | return this.fail('PARAMS_ERROR'); 62 | } 63 | let data = this.post(); 64 | data.id = this.id; 65 | data.type = 1; 66 | data = await this.postModel.getContentAndSummary(data); 67 | data = this.postModel.getPostTime(data); 68 | 69 | let rows = await this.modelInstance.savePost(data); 70 | return this.success({affectedRows: rows}); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/admin/controller/api/system.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import {exec} from 'child_process'; 4 | import semver from 'semver'; 5 | import request from 'request'; 6 | import pack from '../../../../package.json'; 7 | import base from './base'; 8 | 9 | const cluster = require('cluster'); 10 | 11 | request.defaults({ 12 | timeout: 1000, 13 | strictSSL: false, 14 | rejectUnauthorized: false 15 | }); 16 | 17 | const reqIns = think.promisify(request.get); 18 | 19 | export default class extends base { 20 | 21 | init(http) { 22 | super.init(http); 23 | 24 | this.modelInstance = this.model('options'); 25 | } 26 | 27 | async getAction() { 28 | let needUpdate = false; 29 | try { 30 | let res = await reqIns('http://firekylin.org/release/.latest'); 31 | let onlineVersion = res.body.trim(); 32 | if(semver.gt(onlineVersion, pack.version)) { 33 | needUpdate = onlineVersion; 34 | } 35 | } catch(e) { 36 | console.log(e); // eslint-disable-line no-console 37 | } 38 | 39 | let mysql = await this.modelInstance.query('SELECT VERSION() as version'); 40 | let data = { 41 | nodeVersion: process.versions.node, 42 | v8Version: process.versions.v8, 43 | platform: process.platform, 44 | thinkjsVersion: think.version, 45 | firekylinVersion: pack.version, 46 | mysqlVersion: mysql[0].version, 47 | needUpdate 48 | }; 49 | 50 | //非管理员只统计当前用户文章 51 | let where = this.userInfo.type !== 1 ? {user_id: this.userInfo.id} : {}; 52 | return this.success({ 53 | versions: data, 54 | config: await this.getConfig(), 55 | count: { 56 | posts: await this.model('post').where(where).count(), 57 | cates: await this.model('cate').count(), 58 | comments: await this.model('post').where(where).sum('comment_num') 59 | } 60 | }); 61 | } 62 | 63 | async updateAction() { 64 | if(/^win/.test(process.platform)) { 65 | return this.fail('PLATFORM_NOT_SUPPORT'); 66 | } 67 | 68 | let {step} = this.get(); 69 | switch(step) { 70 | /** 下载文件 */ 71 | case '1': 72 | default: 73 | return request({uri: 'http://firekylin.org/release/latest.tar.gz'}) 74 | .pipe(fs.createWriteStream(path.join(think.RESOURCE_PATH, 'latest.tar.gz'))) 75 | .on('close', () => this.success()) 76 | .on('error', err => this.fail(err)); 77 | 78 | /** 解压覆盖,删除更新文件 */ 79 | case '2': 80 | return exec(` 81 | cd ${think.RESOURCE_PATH}; 82 | tar zvxf latest.tar.gz; 83 | cp -r firekylin/* ../; 84 | rm -rf firekylin latest.tar.gz`, error => { 85 | if(error) { 86 | this.fail(error); 87 | } 88 | 89 | this.success(); 90 | }); 91 | 92 | /** 安装依赖 */ 93 | case '3': 94 | let registry = think.config('registry') || 'https://registry.npm.taobao.org'; 95 | return exec(`npm install --registry=${registry}`, error => { 96 | if(error) { 97 | this.fail(error); 98 | } 99 | 100 | this.success(); 101 | }); 102 | 103 | /** 重启服务 */ 104 | case '4': 105 | if(cluster.isWorker) { 106 | this.success(); 107 | setTimeout(() => cluster.worker.kill(), 200); 108 | } 109 | 110 | break; 111 | } 112 | } 113 | 114 | async getConfig() { 115 | let items = await this.modelInstance.select(); 116 | let siteConfig = {}; 117 | 118 | items.forEach(item => siteConfig[item.key] = item.value); 119 | 120 | return siteConfig; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/admin/controller/api/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | 5 | export default class extends Base { 6 | /** 7 | * get 8 | * @return {[type]} [description] 9 | */ 10 | async getAction() { 11 | let result; 12 | if(this.id) { 13 | result = await this.modelInstance.where({id: this.id}).find(); 14 | result.post_tag = result.post_tag.length; 15 | } else { 16 | result = await this.modelInstance.select(); 17 | result = result.map(item => { 18 | item.post_tag = item.post_tag.length; 19 | return item; 20 | }); 21 | } 22 | return this.success(result); 23 | } 24 | /** 25 | * add user 26 | * @return {[type]} [description] 27 | */ 28 | async postAction() { 29 | let data = this.post(); 30 | 31 | let ret = await this.modelInstance.addTag(data); 32 | if(ret.type === 'exist') { 33 | return this.fail('TAG_EXIST'); 34 | } 35 | return this.success({id: ret.id}); 36 | } 37 | /** 38 | * update user info 39 | * @return {[type]} [description] 40 | */ 41 | async putAction() { 42 | if (!this.id) { 43 | return this.fail('PARAMS_ERROR'); 44 | } 45 | let data = this.post(); 46 | data.id = this.id; 47 | let rows = await this.modelInstance.saveTag(data); 48 | return this.success({affectedRows: rows}); 49 | } 50 | 51 | async deleteAction() { 52 | if(!this.id) { 53 | return this.fail('PARAMS_ERROR'); 54 | } 55 | await this.modelInstance.deleteTag(this.id); 56 | return this.success(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/admin/controller/api/theme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import {execSync} from 'child_process'; 6 | import Base from './base'; 7 | 8 | const cluster = require('cluster'); 9 | 10 | const statsAsync = think.promisify(fs.stat); 11 | const readdirAsync = think.promisify(fs.readdir); 12 | const readFileAsync = think.promisify(fs.readFile); 13 | const writeFileAsync = think.promisify(fs.writeFile); 14 | const THEME_DIR = path.join(think.ROOT_PATH, '/view/home/'); 15 | 16 | export default class extends Base { 17 | /** 18 | * forbidden ../ style path 19 | */ 20 | pathCheck(themePath, basePath = THEME_DIR) { 21 | if(themePath.indexOf(basePath) !== 0) { 22 | this.fail(); 23 | throw Error(`theme path ${themePath} error`); 24 | } 25 | return true; 26 | } 27 | 28 | async getAction() { 29 | switch(this.get('type')) { 30 | case 'templateList': 31 | return await this.getPageTemplateList(); 32 | case 'themeList': 33 | default: 34 | return await this.getThemeList(); 35 | } 36 | } 37 | 38 | /** 39 | * Fork theme 40 | */ 41 | async putAction() { 42 | try { 43 | await this.model('options').updateOptions('theme', new_theme); 44 | return this.success(); 45 | } catch(e) { 46 | return this.fail(e); 47 | } 48 | } 49 | 50 | /** 51 | * 获取主题列表 52 | */ 53 | async getThemeList() { 54 | let result = []; 55 | let infoFile = path.join(THEME_DIR, 'package.json'); 56 | try { 57 | await statsAsync(infoFile); 58 | let pkg = think.require(infoFile); 59 | result.push(think.extend({id: pkg.name}, pkg)); 60 | } catch(e) { 61 | console.log(e); // eslint-disable-line no-console 62 | } 63 | return this.success(result); 64 | } 65 | 66 | /** 67 | * 获取主题的自定义模板 68 | */ 69 | async getPageTemplateList() { 70 | let templatePath = path.join(THEME_DIR, 'template'); 71 | this.pathCheck(templatePath); 72 | 73 | let templates = []; 74 | try { 75 | let stat = await statsAsync(templatePath); 76 | if(!stat.isDirectory()) { 77 | throw Error(); 78 | } 79 | } catch(e) { 80 | return this.success(templates); 81 | } 82 | templates = await readdirAsync(templatePath); 83 | templates = templates.filter(t => /\.html$/.test(t)); 84 | return this.success(templates); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/admin/controller/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class extends think.controller.base { 4 | /** 5 | * before 6 | */ 7 | async __before() { 8 | 9 | let http = this.http; 10 | if(http.controller === 'user' && http.action === 'login') { 11 | return; 12 | } 13 | let userInfo = await this.session('userInfo') || {}; 14 | if(think.isEmpty(userInfo)) { 15 | if(this.isAjax()) { 16 | return this.fail('NOT_LOGIN'); 17 | } 18 | } 19 | this.userInfo = userInfo; 20 | if(!this.isAjax()) { 21 | this.assign('userInfo', {id: userInfo.id, name: userInfo.name, type: userInfo.type}); 22 | } 23 | } 24 | /** 25 | * call magic method 26 | * @return {} [] 27 | */ 28 | async __call() { 29 | if(this.isAjax()) { 30 | return this.fail('ACTION_NOT_FOUND'); 31 | } 32 | let model = this.model('options'); 33 | let options = await model.getOptions(); 34 | //不显示具体的密钥 35 | options.two_factor_auth = !!options.two_factor_auth; 36 | options.analyze_code = escape(options.analyze_code); 37 | options.comment.name = escape(options.comment.name); 38 | try { 39 | options.navigation = JSON.parse(options.navigation); 40 | } catch(e) { options.navigation = []; } 41 | delete options.push_sites; //不显示推送的配置,会有安全问题 42 | this.assign('options', options); 43 | return this.display('index/index'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/admin/controller/post_push.js: -------------------------------------------------------------------------------- 1 | import {PasswordHash} from 'phpass'; 2 | import Post from './api/post'; 3 | 4 | export default class extends Post { 5 | modelInstance = this.model('post'); 6 | 7 | async __before() { 8 | 9 | } 10 | 11 | async checkAuth(data) { 12 | let {app_key, auth_key, ...post} = data; 13 | //check user 14 | let poster = await this.model('user').where({app_key}).find(); 15 | if(think.isEmpty(poster)) { 16 | return this.fail('POSTER_NOT_EXIST'); 17 | } 18 | 19 | this.poster = poster; 20 | let {app_secret} = poster; 21 | let passwordHash = new PasswordHash(); 22 | let result = passwordHash.checkPassword(`${app_secret}${post.markdown_content}`, auth_key); 23 | return result; 24 | } 25 | 26 | async updatePost(post) { 27 | if(post.markdown_content) { post = this.modelInstance.getContentAndSummary(post); } 28 | if(post.create_time) { post = this.modelInstance.getPostTime(post); } 29 | if(post.tag) { post = await this.getTagIds(post.tag); } 30 | let rows = await this.modelInstance.savePost(post); 31 | return this.success({affectedRows: rows}); 32 | } 33 | 34 | async getAction() { 35 | if(!this.get('app_key') || !this.get('auth_key')) { 36 | return this.fail('PARAMS_ERROR'); 37 | } 38 | 39 | let {app_key, auth_key} = this.get(); 40 | let result = await this.checkAuth({app_key, auth_key, markdown_content: 'Firekylin'}); 41 | return result ? this.success('KEY_CHECK_SUCCESS') : this.fail('KEY_CHECK_FAILED'); 42 | } 43 | 44 | async postAction() { 45 | let post = this.post(); 46 | if(!this.checkAuth(post)) { return this.fail('POST_CONTENT_ERROR'); } 47 | 48 | //check pathname 49 | let exPost = await this.modelInstance.where({pathname: post.pathname}).find(); 50 | if(!think.isEmpty(exPost)) { 51 | if(exPost.user.id !== this.poster.id) { 52 | return this.fail('POST_USER_ERROR'); 53 | } 54 | post.id = exPost.id; 55 | return this.updatePost(post); 56 | } 57 | 58 | post.user_id = this.poster.id; 59 | post = await this.modelInstance.getContentAndSummary(post); 60 | post = this.modelInstance.getPostTime(post); 61 | post.tag = await this.getTagIds(post.tag); 62 | 63 | if(post.status === 3 && this.poster.type !== 1) { 64 | post.status = 1; 65 | } 66 | 67 | let insertId = await this.modelInstance.addPost(post); 68 | return this.success({id: insertId}); 69 | } 70 | 71 | async putAction() { 72 | } 73 | 74 | async deleteAction() { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/admin/controller/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import speakeasy from 'speakeasy'; 4 | import Base from './base'; 5 | 6 | export default class extends Base { 7 | /** 8 | * login 9 | * @return {} [] 10 | */ 11 | async loginAction() { 12 | //二步验证 13 | let model = this.model('options'); 14 | let options = await model.getOptions(); 15 | if(options.two_factor_auth) { 16 | let two_factor_auth = this.post('two_factor_auth'); 17 | let verified = speakeasy.totp.verify({ 18 | secret: options.two_factor_auth, 19 | encoding: 'base32', 20 | token: two_factor_auth, 21 | window: 2 22 | }); 23 | if(!verified) { 24 | return this.fail('TWO_FACTOR_AUTH_ERROR'); 25 | } 26 | } 27 | 28 | //校验帐号和密码 29 | let username = this.post('username'); 30 | let userModel = this.model('user'); 31 | let userInfo = await userModel.where({name: username}).find(); 32 | if(think.isEmpty(userInfo)) { 33 | return this.fail('ACCOUNT_ERROR'); 34 | } 35 | 36 | //帐号是否被禁用,且投稿者不允许登录 37 | if((userInfo.status | 0) !== 1 || userInfo.type === 3) { 38 | return this.fail('ACCOUNT_FORBIDDEN'); 39 | } 40 | 41 | //校验密码 42 | let password = this.post('password'); 43 | if(!userModel.checkPassword(userInfo, password)) { 44 | return this.fail('ACCOUNT_ERROR'); 45 | } 46 | 47 | await this.session('userInfo', userInfo); 48 | 49 | return this.success(); 50 | } 51 | /** 52 | * logout 53 | * @return {} 54 | */ 55 | async logoutAction() { 56 | await this.session('userInfo', ''); 57 | return this.redirect('/'); 58 | } 59 | 60 | /** 61 | * update user password 62 | */ 63 | async passwordAction() { 64 | let userInfo = await this.session('userInfo') || {}; 65 | if(think.isEmpty(userInfo)) { 66 | return this.fail('USER_NOT_LOGIN'); 67 | } 68 | 69 | let rows = await this.model('user').saveUser({ 70 | password: this.post('password'), 71 | id: userInfo.id 72 | }, this.ip()); 73 | 74 | return this.success(rows); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/admin/logic/api/cate.js: -------------------------------------------------------------------------------- 1 | export default class extends think.logic.base { 2 | 3 | /** 4 | * set cate pathname with encoding name when user haven't set. 5 | */ 6 | checkPathname() { 7 | if(this.post('pathname')) { return true; } 8 | 9 | let name = this.post('name'); 10 | let pathname = encodeURIComponent(name); 11 | this.post('pathname', pathname); 12 | } 13 | 14 | postAction() { 15 | this.rules = { 16 | name: 'required' 17 | }; 18 | 19 | this.checkPathname(); 20 | } 21 | 22 | putAction() { 23 | this.rules = { 24 | name: 'required' 25 | }; 26 | 27 | this.checkPathname(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/admin/logic/api/tag.js: -------------------------------------------------------------------------------- 1 | export default class extends think.logic.base { 2 | 3 | /** 4 | * set tag pathname with encoding name when user haven't set. 5 | */ 6 | checkPathname() { 7 | if(this.post('pathname')) { return true; } 8 | 9 | let name = this.post('name'); 10 | let pathname = encodeURIComponent(name); 11 | this.post('pathname', pathname); 12 | } 13 | 14 | postAction() { 15 | this.rules = { 16 | name: 'required' 17 | }; 18 | 19 | this.checkPathname(); 20 | } 21 | 22 | putAction() { 23 | this.rules = { 24 | name: 'required' 25 | }; 26 | 27 | this.checkPathname(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/admin/logic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * logic 4 | * @param {} [] 5 | * @return {} [] 6 | */ 7 | export default class extends think.logic.base { 8 | /** 9 | * index action logic 10 | * @return {} [] 11 | */ 12 | indexAction() { 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/admin/logic/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * logic 4 | * @param {} [] 5 | * @return {} [] 6 | */ 7 | export default class extends think.logic.base { 8 | /** 9 | * index action logic 10 | * @return {} [] 11 | */ 12 | indexAction() { 13 | 14 | } 15 | /** 16 | * 添加或者修改用户 17 | * @return {} [] 18 | */ 19 | saveAction() { 20 | this.allowMethods = 'post'; 21 | this.rules = { 22 | 23 | } 24 | } 25 | /** 26 | * login 27 | * @return {} [] 28 | */ 29 | loginAction() { 30 | this.allowMethods = 'get,post'; 31 | if(this.isGet()) { 32 | return; 33 | } 34 | this.rules = { 35 | username: { 36 | required: true, 37 | minLength: 4 38 | }, 39 | password: { 40 | required: true, 41 | length: [32, 32] 42 | }, 43 | factor: { 44 | regexp: /^\d{6}$/ 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/admin/model/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * relation model 4 | */ 5 | export default class extends think.model.relation { 6 | init(...args) { 7 | super.init(...args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/admin/model/cate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | /** 5 | * relation model 6 | */ 7 | export default class extends Base { 8 | /** 9 | * init 10 | * @param {} args [] 11 | * @return {} [] 12 | */ 13 | init(...args) { 14 | super.init(...args); 15 | 16 | this.relation = { 17 | post_cate: { 18 | type: think.model.HAS_MANY, 19 | fKey: 'cate_id' 20 | } 21 | } 22 | } 23 | 24 | /** 25 | * 添加分类 26 | * @param {[type]} data [description] 27 | * @param {[type]} ip [description] 28 | */ 29 | addCate(data) { 30 | let where = { 31 | name: data.name, 32 | _logic: 'OR' 33 | }; 34 | if(data.pathname) { 35 | where.pathname = data.pathname; 36 | } 37 | return this.where(where).thenAdd(data); 38 | } 39 | 40 | async saveCate(data) { 41 | let info = await this.where({id: data.id}).find(); 42 | if(think.isEmpty(info)) { 43 | return Promise.reject(new Error('CATE_NOT_EXIST')); 44 | } 45 | 46 | return this.where({id: data.id}).update(data); 47 | } 48 | 49 | async deleteCate(cate_id) { 50 | this.model('post_cate').where({cate_id}).delete(); 51 | return this.where({id: cate_id}).delete(); 52 | } 53 | /** 54 | * get count posts 55 | * @param {Number} userId [] 56 | * @return {Promise} [] 57 | */ 58 | getCount(userId) { 59 | if(userId) { 60 | return this.where({user_id: userId}).count(); 61 | } 62 | return this.count(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/admin/model/page.js: -------------------------------------------------------------------------------- 1 | import Post from './post'; 2 | 3 | export default class extends Post { 4 | tableName = 'post'; 5 | 6 | addPost(data) { 7 | let create_time = think.datetime(); 8 | data = Object.assign({ 9 | type: 1, 10 | status: 0, 11 | create_time, 12 | update_time: create_time, 13 | is_public: 1 14 | }, data); 15 | 16 | return this.where({pathname: data.pathname}).thenAdd(data); 17 | } 18 | 19 | async savePost(data) { 20 | let info = await this.where({id: data.id, type: 1}).find(); 21 | if(think.isEmpty(info)) { 22 | return Promise.reject(new Error('PAGE_NOT_EXIST')); 23 | } 24 | 25 | data.update_time = think.datetime(); 26 | return this.where({id: data.id}).update(data); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/admin/model/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | /** 5 | * relation model 6 | */ 7 | export default class extends Base { 8 | /** 9 | * init 10 | * @param {} args [] 11 | * @return {} [] 12 | */ 13 | init(...args) { 14 | super.init(...args); 15 | 16 | this.relation = { 17 | post_tag: { 18 | type: think.model.HAS_MANY, 19 | fKey: 'tag_id' 20 | } 21 | } 22 | } 23 | 24 | addTag(data) { 25 | let where = { 26 | name: data.name, 27 | _logic: 'OR' 28 | }; 29 | if(data.pathname) { 30 | where.pathname = data.pathname; 31 | } 32 | return this.where(where).thenAdd(data); 33 | } 34 | 35 | async saveTag(data) { 36 | let info = await this.where({id: data.id}).find(); 37 | if(think.isEmpty(info)) { 38 | return Promise.reject(new Error('TAG_NOT_EXIST')); 39 | } 40 | 41 | return this.where({id: data.id}).update(data); 42 | } 43 | 44 | async deleteTag(tag_id) { 45 | this.model('post_tag').where({tag_id}).delete(); 46 | return this.where({id: tag_id}).delete(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/admin/model/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {PasswordHash} from 'phpass'; 4 | import Base from './base'; 5 | /** 6 | * model 7 | */ 8 | export default class extends Base { 9 | 10 | /** 11 | * get password 12 | * @param {String} username [] 13 | * @param {String} salt [] 14 | * @return {String} [] 15 | */ 16 | getEncryptPassword(password) { 17 | let passwordHash = new PasswordHash(); 18 | let hash = passwordHash.hashPassword(password); 19 | return hash; 20 | } 21 | /** 22 | * check password 23 | * @param {[type]} userInfo [description] 24 | * @param {[type]} password [description] 25 | * @return {[type]} [description] 26 | */ 27 | checkPassword(userInfo, password) { 28 | let passwordHash = new PasswordHash(); 29 | return passwordHash.checkPassword(password, userInfo.password); 30 | } 31 | 32 | generateKey(userId, app_key, app_secret, status) { 33 | let data = {app_key, app_secret}; 34 | if(status) { data.status = status; } 35 | this.where({id: userId}).update(data); 36 | } 37 | 38 | /** 39 | * after select 40 | * @param {[type]} data [description] 41 | * @return {[type]} [description] 42 | */ 43 | afterSelect(data) { 44 | return data.map(item => { 45 | return this.afterFind(item); 46 | }); 47 | } 48 | afterFind(data) { 49 | if(data.create_time) { 50 | data.create_time = think.datetime(new Date(data.create_time)); 51 | } 52 | if(data.last_login_time) { 53 | data.last_login_time = think.datetime(new Date(data.last_login_time)); 54 | } 55 | return data; 56 | } 57 | /** 58 | * 添加用户 59 | * @param {[type]} data [description] 60 | * @param {[type]} ip [description] 61 | */ 62 | addUser(data, ip) { 63 | let create_time = think.datetime(); 64 | let encryptPassword = this.getEncryptPassword(data.password); 65 | return this.where({name: data.username, email: data.email, _logic: 'OR'}).thenAdd({ 66 | name: data.username, 67 | email: data.email, 68 | display_name: data.display_name, 69 | password: encryptPassword, 70 | create_time: create_time, 71 | last_login_time: create_time, 72 | create_ip: ip, 73 | last_login_ip: ip, 74 | type: data.type, 75 | status: data.status 76 | }); 77 | } 78 | /** 79 | * 保存用户信息 80 | * @param {[type]} data [description] 81 | * @return {[type]} [description] 82 | */ 83 | async saveUser(data, ip) { 84 | let info = await this.where({id: data.id}).find(); 85 | if(think.isEmpty(info)) { 86 | return Promise.reject(new Error('UESR_NOT_EXIST')); 87 | } 88 | let password = data.password; 89 | if(password) { 90 | password = this.getEncryptPassword(password); 91 | } 92 | let updateData = {}; 93 | ['display_name', 'type', 'status'].forEach(item => { 94 | if(data[item]) { 95 | updateData[item] = data[item]; 96 | } 97 | }); 98 | if(password) { 99 | updateData.password = password; 100 | } 101 | if(think.isEmpty(updateData)) { 102 | return Promise.reject('DATA_EMPTY'); 103 | } 104 | if(!info.email && data.email) { 105 | let count = await this.where({email: data.email}).count('email'); 106 | if(!count) { 107 | updateData.email = data.email; 108 | } 109 | } 110 | updateData.last_login_time = think.datetime(); 111 | updateData.last_login_ip = ip; 112 | return this.where({id: data.id}).update(updateData); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/admin/service/import/base.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import toMarkdown from 'to-markdown'; 3 | 4 | export default class extends think.service.base { 5 | static DEFAULT_USER_PWD = 'admin12345678'; 6 | 7 | init(...args) { 8 | super.init(...args); 9 | this.userModelInstance = this.model('user'); 10 | this.cateModelInstance = this.model('cate'); 11 | this.tagModelInstance = this.model('tag'); 12 | this.postModelInstance = this.model('post'); 13 | this.pageModelInstance = this.model('page').setRelation('user'); 14 | } 15 | 16 | formatDate(date) { 17 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 18 | } 19 | 20 | toMarkdown(content) { 21 | return toMarkdown(content); 22 | } 23 | 24 | /** 25 | * 导入用户 26 | */ 27 | async user() { 28 | 29 | } 30 | 31 | /** 32 | * 导入分类 33 | */ 34 | async category() { 35 | 36 | } 37 | 38 | /** 39 | * 导入标签 40 | */ 41 | async tag() { 42 | 43 | } 44 | 45 | /** 46 | * 导入文章 47 | */ 48 | async post() { 49 | 50 | } 51 | 52 | /** 53 | * 导入页面 54 | */ 55 | async page() { 56 | 57 | } 58 | 59 | /** 60 | * 处理上传文件获取导入数据 61 | */ 62 | async parseFile(file) { // eslint-disable-line no-unused-vars 63 | 64 | } 65 | 66 | async importData(data) { 67 | let user = await this.user(data); 68 | let category = await this.category(data); 69 | let tag = await this.tag(data); 70 | let post = await this.post(data); 71 | let page = await this.page(data); 72 | 73 | return {user, post, page, tag, category}; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/admin/service/import/hexo.js: -------------------------------------------------------------------------------- 1 | import Ghost from './ghost'; 2 | 3 | export default class extends Ghost { 4 | /** 5 | * 导入标签 6 | */ 7 | async tag({tags}) { 8 | if(!tags || !Array.isArray(tags)) { 9 | return 0; 10 | } 11 | 12 | var len = 0; 13 | for(let tag of tags) { 14 | let result = await this.tagModelInstance.addTag({ 15 | name: tag.name, 16 | pathname: tag.slug 17 | }); 18 | 19 | if(result.type === 'add') { 20 | len += 1; 21 | } 22 | } 23 | 24 | return len; 25 | } 26 | 27 | /** 28 | * 导入分类 29 | * 为了简单不支持子分类导入,默认所有分类为一级分类 30 | */ 31 | async category({categories}) { 32 | if(!categories || !Array.isArray(categories)) { 33 | return 0; 34 | } 35 | 36 | var len = 0; 37 | for(let category of categories) { 38 | let result = await this.cateModelInstance.addCate({ 39 | name: category.name, 40 | pathname: category.slug, 41 | pid: 0 42 | }); 43 | 44 | if(result.type === 'add') { 45 | len += 1; 46 | } 47 | } 48 | 49 | return len; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/admin/service/import/markdown.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import {execSync} from 'child_process'; 4 | import Post from '../../controller/api/post'; 5 | import Base from './base'; 6 | 7 | const PATH = path.join(think.RUNTIME_PATH, 'importMarkdownFileToFirekylin'); 8 | export default class extends Base { 9 | constructor(think) { 10 | super(think); 11 | this._think = think; 12 | } 13 | /** 14 | * 导入用户 15 | */ 16 | async user() { 17 | return 0; 18 | } 19 | 20 | /** 21 | * 导入分类 22 | */ 23 | async category() { 24 | return 0; 25 | } 26 | 27 | /** 28 | * 导入标签 29 | */ 30 | async tag() { 31 | return 0; 32 | } 33 | 34 | /** 35 | * 导入文章 36 | */ 37 | async post(posts = []) { 38 | if(!Array.isArray(posts)) { 39 | return 0; 40 | } 41 | 42 | 43 | const postsPromise = posts.map(async item => { 44 | try{ 45 | //获取用户 46 | const user = await this._think.session('userInfo'); 47 | 48 | const post = { 49 | title: item.title, 50 | pathname: item.pathname, 51 | markdown_content: item.markdown_content, 52 | create_time: this.formatDate(new Date(item.created_at)), 53 | update_time: this.formatDate(new Date(item.updated_at)), 54 | status: 3, 55 | user_id: user.id, 56 | comment_num: 0, 57 | allow_comment: 1, 58 | is_public: 1, 59 | type: 0 60 | }; 61 | await Post.prototype.getContentAndSummary(post); 62 | await this.postModelInstance.addPost(post); 63 | } catch(e) { 64 | console.log(e); // eslint-disable-line no-console 65 | } 66 | }); 67 | Promise.all(postsPromise); 68 | 69 | return posts.length; 70 | } 71 | 72 | /** 73 | * 导入页面 74 | */ 75 | async page() { 76 | return 0; 77 | } 78 | 79 | parseFile(file) { 80 | try { 81 | execSync(`rm -rf ${PATH}; mkdir ${PATH}; cd ${PATH}; tar zxvf ${file.path}`); 82 | let files = fs.readdirSync(PATH, {encoding: 'utf-8'}); 83 | if(!files.length) { return []; } 84 | 85 | return files.map(function(file) { 86 | let tar = path.join(PATH, file); 87 | let title = file.split('.').slice(0, -1).join(''); 88 | let content = fs.readFileSync(tar, {encoding: 'utf-8'}); 89 | let stat = fs.statSync(tar); 90 | return { 91 | created_at: stat.birthtime.getTime(), 92 | updated_at: stat.mtime.getTime(), 93 | title, 94 | pathname: title, 95 | markdown_content: content 96 | }; 97 | }); 98 | } catch(e) { 99 | throw new Error(e); 100 | } 101 | } 102 | 103 | /** 104 | * 执行导入 105 | */ 106 | async run(file) { 107 | return await this.importData(this.parseFile(file)); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/admin/service/upload/base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import moment from 'moment'; 3 | 4 | export default class extends think.service.base { 5 | init(...args) { 6 | super.init(...args); 7 | } 8 | 9 | // 域名不带http/https自动补全http 10 | getAbsOrigin(origin) { 11 | const reg = /^(https?:)?\/\/.+/; 12 | if (!reg.test(origin)) { 13 | return `http://${origin}`; 14 | } 15 | return origin; 16 | } 17 | 18 | // 获取当前的格式化时间 19 | formatNow() { 20 | return moment(new Date()).format('YYYYMMDD'); 21 | } 22 | 23 | // 获取存储路径 24 | getSavePath(filename, prefix) { 25 | prefix = prefix ? `${prefix}/` : ''; 26 | const dir = this.formatNow(); 27 | const basename = path.basename(filename); 28 | return `${prefix}${dir}/${basename}`; 29 | } 30 | 31 | // 导入方法 32 | async uploadMethod() {} 33 | 34 | async upload(filename, config) { 35 | const result = await this.uploadMethod(filename, config); 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/admin/service/upload/local.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import url from 'url'; 3 | import path from 'path'; 4 | import Base from './base'; 5 | 6 | const renameAsync = think.promisify(fs.rename, fs); 7 | export default class extends Base { 8 | async uploadMethod(file, {name}) { 9 | let ext = /^\.\w+$/.test(path.extname(file)) ? path.extname(file) : '.png'; 10 | let basename = (name || path.basename(file, ext)) + ext; 11 | 12 | let destDir = this.formatNow(); 13 | let destPath = path.join(think.UPLOAD_PATH, destDir); 14 | if(!think.isDir(destPath)) { 15 | think.mkdir(destPath); 16 | } 17 | 18 | try { 19 | // 上传文件路径 20 | let filepath = path.join(destPath, basename); 21 | await renameAsync(file, filepath); 22 | return url.resolve(think.UPLOAD_BASE_URL, filepath.replace(think.RESOURCE_PATH, '')); 23 | } catch(e) { 24 | throw Error('FILE_UPLOAD_MOVE_ERROR'); 25 | } 26 | } 27 | 28 | async run(file, config) { 29 | return await this.uploadMethod(file, config); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/admin/service/upload/qiniu.js: -------------------------------------------------------------------------------- 1 | import qiniu from 'qiniu'; 2 | import Base from './base'; 3 | 4 | export default class extends Base { 5 | // 导入方法 6 | async uploadMethod(filename, config) { 7 | qiniu.conf.ACCESS_KEY = config.accessKey; 8 | qiniu.conf.SECRET_KEY = config.secretKey; 9 | const savePath = this.getSavePath(filename, config.prefix); 10 | const token = new qiniu.rs.PutPolicy(`${config.bucket}:${savePath}`).token(); 11 | const extra = new qiniu.io.PutExtra(); 12 | return new Promise((resolve, reject) => { 13 | qiniu.io.putFile(token, savePath, filename, extra, (err, ret) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | const origin = this.getAbsOrigin(config.origin); 18 | const compeletePath = `${origin}/${ret.key}`; 19 | resolve(compeletePath); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | // 执行方法 26 | async run(file, config) { 27 | return await this.upload(file, config); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/admin/service/upload/upyun.js: -------------------------------------------------------------------------------- 1 | import upyun from 'upyun'; 2 | import Base from './base'; 3 | 4 | export default class extends Base { 5 | // 导入方法 6 | async uploadMethod(filename, config) { 7 | const upyunInstance = new upyun( 8 | config.upyunBucket, config.operater, config.password, 'v0.api.upyun.com', { apiVersion: 'v2' } 9 | ); 10 | const savePath = this.getSavePath(filename, config.upyunPrefix); 11 | return new Promise((resolve, reject) => { 12 | upyunInstance.putFile(savePath, filename, null, false, { 13 | 'save-key': '/{year}{mon}{day}/{filename}{.suffix}' 14 | }, (err, res) => { 15 | if (err) { 16 | reject(err); 17 | } else { 18 | if (res.statusCode === 200) { 19 | const origin = this.getAbsOrigin(config.upyunOrigin); 20 | const compeletePath = `${origin}/${savePath}`; 21 | resolve(compeletePath); 22 | } else { 23 | reject(res); 24 | } 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | // 执行方法 31 | async run(file, config) { 32 | return await this.upload(file, config); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/common/bootstrap/crontab.js: -------------------------------------------------------------------------------- 1 | import crontab from 'node-crontab'; 2 | 3 | import './global'; 4 | 5 | if(!think.cli) { 6 | 7 | let syncComment = () => { 8 | if(!firekylin.isInstalled) { 9 | return; 10 | } 11 | think.http('/crontab/sync_comment', true); 12 | } 13 | crontab.scheduleJob('0 */1 * * *', syncComment); 14 | 15 | //服务启动时同步一次 16 | syncComment(); 17 | } 18 | -------------------------------------------------------------------------------- /src/common/bootstrap/global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this file will be loaded before server started 3 | * you can define global functions used in controllers, models, templates 4 | */ 5 | import fs from 'fs'; 6 | 7 | global.firekylin = { 8 | POST_PUBLIC: 1, 9 | POST_ALLOW_COMMENT: 1, 10 | POST_ARTICLE: 0, 11 | POST_PAGE: 1, 12 | POST_DRAFT: 0, 13 | POST_AUDITING: 1, 14 | POST_REJECT: 2, 15 | POST_PUBLISH: 3, 16 | USER_ADMIN: 1, 17 | USER_EDITOR: 2, 18 | USER_CONTRIBUTOR: 3, 19 | USER_AVAILABLE: 1, 20 | USER_DISABLED: 2 21 | } 22 | 23 | /** 24 | * is installed 25 | * @type {Boolean} 26 | */ 27 | firekylin.isInstalled = false; 28 | try{ 29 | let installedFile = think.ROOT_PATH + think.sep + '.installed'; 30 | if(fs.accessSync && fs.accessSync(installedFile, fs.F_OK)) { 31 | firekylin.isInstalled = true; 32 | } 33 | if(fs.existsSync(installedFile)) { 34 | firekylin.isInstalled = true; 35 | } 36 | }catch(e) { 37 | // fs.accessSync failed 38 | } 39 | 40 | /** 41 | * set app is installed 42 | * @return {[type]} [description] 43 | */ 44 | firekylin.setInstalled = () => { 45 | firekylin.isInstalled = true; 46 | let installedFile = think.ROOT_PATH + think.sep + '.installed'; 47 | fs.writeFileSync(installedFile, 'firekylin'); 48 | } 49 | -------------------------------------------------------------------------------- /src/common/config/cache.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'file', //缓存类型 3 | timeout: 6 * 3600, //失效时间,单位:秒 4 | adapter: { //不同 adapter 下的配置 5 | file: { 6 | path: think.RUNTIME_PATH + '/cache', //缓存文件的根目录 7 | path_depth: 2, //缓存文件生成子目录的深度 8 | file_ext: '.json' //缓存文件的扩展名 9 | }, 10 | redis: { 11 | prefix: 'firekylin_' 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/common/config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | 5 | let port; 6 | let portFile = think.ROOT_PATH + think.sep + 'port'; 7 | if(think.isFile(portFile)) { 8 | port = fs.readFileSync(portFile, 'utf8'); 9 | } 10 | /** 11 | * config 12 | */ 13 | export default { 14 | port: port || process.env.PORT || 8360, 15 | resource_reg: /^(sw\.js|static\/|[^\/]+\.(?!js|html|xml)\w+$)/, 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/config/db.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'mysql', 3 | adapter: { 4 | mysql: { 5 | encoding: 'utf8mb4', 6 | host: process.env.DB_HOST || 'rm-2zentm3d7v9zsuy4nuo.mysql.rds.aliyuncs.com', 7 | port: process.env.DB_PORT || 3306, 8 | database: process.env.DB_DATABASE || 'xiaowudev_blog_dev', 9 | user: process.env.DB_USER || 'xiaowu', 10 | password: process.env.DB_PASSWORD || 'xiaowu@123', 11 | prefix: process.env.DB_PREFIX || 'xiaowudev_', 12 | type: 'mysql' 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/config/env/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | }; 5 | -------------------------------------------------------------------------------- /src/common/config/env/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | cluster_on: 1, 5 | }; 6 | -------------------------------------------------------------------------------- /src/common/config/env/testing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /src/common/config/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * err config 4 | */ 5 | export default { 6 | //key: value 7 | key: 'errno', //error number 8 | msg: 'errmsg' //error message 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/config/hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * hook config 5 | * https://thinkjs.org/doc/middleware.html#toc-df6 6 | */ 7 | export default { 8 | request_begin: ['resheaders'] 9 | } 10 | -------------------------------------------------------------------------------- /src/common/config/locale/en.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | USER_NOT_LOGIN: [403, '未登录'], 5 | USER_EXIST: [100, '用户名或邮箱已经存在'], 6 | USER_NO_PERMISSION: [101, '没有权限'], 7 | PARAMS_ERROR: [102, '参数错误'], 8 | DATA_EMPTY: [103, '数据不能为空'], 9 | ACCOUNT_ERROR: [104, '用户名或者密码错误'], 10 | CATE_EXIST: [105, '分类名称或者缩略名已经存在'], 11 | TAG_EXIST: [106, '标签名称或者缩略名已经存在'], 12 | TWO_FACTOR_AUTH_ERROR: [107, '两步校验码错误'], 13 | TWO_FACTOR_AUTH_ERROR_DETAIL: [107, '两步校验码错误,请确认校验码或服务器时间是否正确'], 14 | APP_KEY_SECRET_ERROR: [108, '推送公钥或者私钥错误'], 15 | FILE_UPLOAD_ERROR: [109, '文件上传错误'], 16 | FILE_UPLOAD_MOVE_ERROR: [109, '文件上传后移动错误'], 17 | UPLOAD_URL_ERROR: [109, 'URL参数不合法或者请求失败!'], 18 | UPLOAD_TYPE_ERROR: [109, '请求的资源不是一张图片'], 19 | INVALID_FILE: [109, '文件格式错误无法解析'], 20 | KEY_EMPTY: [110, '公钥不能为空'], 21 | KEY_EXIST: [110, '公钥已经存在'], 22 | PATHNAME_EXIST: [111, '文章地址已经存在'], 23 | ACCESS_ERROR: [112, '权限错误'], 24 | NOT_LOGIN: [115, '暂未登录'], 25 | ACTION_NOT_FOUND: [116, '暂无该路由'], 26 | POSTER_NOT_EXIST: [117, '该密钥对暂无对应用户'], 27 | KEY_CHECK_SUCCESS: [117, '密钥认证成功'], 28 | KEY_CHECK_FAILED: [117, '密钥认证失败'], 29 | POST_CONTENT_ERROR: [118, '文章内容错误'], 30 | POST_USER_ERROR: [118, '当前用户与文章用户不一致'], 31 | ACCOUNT_FORBIDDEN: [119, '禁止登录'], 32 | SYSTERM_INSTALLED: [120, '程序已安装请勿重复安装'], 33 | PUSH_CLOSED: [121, '推送申请功能未开启'], 34 | DELETE_CURRENT_USER_ERROR: [122, '不能删除当前登录用户'] 35 | }; 36 | -------------------------------------------------------------------------------- /src/common/config/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default [ 4 | [/^archives$/, 'post/archive'], 5 | [/^cate\/(.+)$/, 'post/list?cate=:1'], 6 | [/^tag\/(.+)$/, 'post/list?tag=:1'], 7 | [/^author\/([^/]+)$/, 'post/list?name=:1'], 8 | [/^tags$/, 'post/tag'], 9 | [/^links$/, 'post/page?pathname=links'], 10 | [/^rss(?:\.xml)?$/, 'index/rss'], 11 | [/^sitemap(?:\.xml)?$/, 'index/sitemap'], 12 | [/^search$/, 'post/search'], 13 | [/^page\/([^/]+)$/, 'post/page?pathname=:1'], 14 | [/^post\/([^/]+?)\.md$/, 'post/detailmd?pathname=:1'], 15 | [/^post\/([^/]+)$/, 'post/detail?pathname=:1'], 16 | ['opensearch.xml', 'index/opensearch'] 17 | ] 18 | -------------------------------------------------------------------------------- /src/common/config/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * session configs 5 | */ 6 | export default { 7 | name: 'thinkjs', 8 | type: 'db', 9 | secret: '!N71PV5J', 10 | timeout: 24 * 3600, 11 | cookie: { // cookie options 12 | length: 32, 13 | httponly: true 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/config/view.js: -------------------------------------------------------------------------------- 1 | import {parse} from 'url'; 2 | import buildImg from '../util/buildImg'; 3 | 4 | const build_query = obj => '?' + 5 | Object.keys(obj).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(obj[k])).join('&'); 6 | /** 7 | * template config 8 | */ 9 | export default { 10 | type: 'nunjucks', 11 | content_type: 'text/html', 12 | file_ext: '.html', 13 | file_depr: '_', 14 | root_path: think.ROOT_PATH + '/view', 15 | adapter: { 16 | nunjucks: { 17 | trimBlocks: true, 18 | lstripBlocks: true, 19 | prerender: function(nunjucks, env) { 20 | env.addFilter('buildLazyImg', content => { 21 | return buildImg.lazy(content || ''); 22 | }); 23 | 24 | env.addFilter('buildImageslim', (content, isWebp) => { 25 | return buildImg.buildImageslim(content, isWebp); 26 | }); 27 | 28 | env.addFilter('utc', time => (new Date(time)).toUTCString()); 29 | env.addFilter('pagination', function(page) { 30 | let {pathname, query} = parse(this.ctx.http.url, true); 31 | 32 | query.page = page; 33 | return pathname + build_query(query); 34 | }); 35 | 36 | env.addFilter('xml', str => { 37 | let NOT_SAFE_IN_XML = /[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm; 38 | return str.replace(NOT_SAFE_IN_XML, ''); 39 | }); 40 | } 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/common/controller/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 前台base主控制器 3 | * @author xuexb 4 | */ 5 | 6 | export default class extends think.controller.base { 7 | 8 | /** 9 | * init 10 | * @param {[type]} http [description] 11 | * @return {[type]} [description] 12 | */ 13 | init(http) { 14 | super.init(http); 15 | } 16 | 17 | /** 18 | * some base method in here 19 | */ 20 | async __before() { 21 | let model = this.model('options'); 22 | let options = await model.getOptions(); 23 | this.options = options; 24 | this.assign('options', options); 25 | 26 | // 网站地址 27 | let siteUrl = this.options.site_url; 28 | if (!siteUrl) { 29 | siteUrl = 'http://' + this.http.host; 30 | } 31 | this.assign('site_url', siteUrl); 32 | 33 | let {navigation, themeConfig} = this.options; 34 | try { 35 | navigation = JSON.parse(navigation); 36 | } 37 | catch (e) { 38 | navigation = []; 39 | } 40 | try { 41 | themeConfig = JSON.parse(themeConfig); 42 | } 43 | catch (e) { 44 | themeConfig = {}; 45 | } 46 | 47 | this.assign('navigation', navigation); 48 | this.assign('themeConfig', themeConfig); 49 | 50 | // 所有的分类 51 | let categories = await this.model('cate').getCateArchive(); 52 | this.assign('categories', categories); 53 | 54 | // 当前环境 55 | this.assign('env', think.env); 56 | 57 | this.assign('currentYear', (new Date()).getFullYear()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/common/controller/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const stats = think.promisify(fs.stat); 6 | /** 7 | * error controller 8 | */ 9 | export default class extends think.controller.base { 10 | /** 11 | * display error page 12 | * @param {Number} status [] 13 | * @return {Promise} [] 14 | */ 15 | async displayError(status) { 16 | 17 | //hide error message on production env 18 | if(think.env === 'production') { 19 | this.http.error = null; 20 | } 21 | 22 | let errorConfig = this.config('error'); 23 | let message = this.http.error && this.http.error.message || ''; 24 | if(this.isJsonp()) { 25 | return this.jsonp({ 26 | [errorConfig.key]: status, 27 | [errorConfig.msg]: message 28 | }) 29 | }else if(this.isAjax()) { 30 | return this.fail(status, message); 31 | } 32 | 33 | let file = `common/error/${status}`; 34 | 35 | // 优先尝试主题内的错误文件 36 | let themeErrorFilePath = path.join(think.ROOT_PATH, `view/${this.http.module}/error/${status}.html`); 37 | try { 38 | await stats(themeErrorFilePath); 39 | file = themeErrorFilePath; 40 | } catch(e) { 41 | } 42 | 43 | let options = this.config('tpl'); 44 | options = think.extend({}, options, {type: 'base', file_depr: '_'}); 45 | this.fetch(file, {}, options).then(content => { 46 | content = content.replace('ERROR_MESSAGE', message); 47 | this.status(status); 48 | this.type(options.content_type); 49 | this.end(content); 50 | }); 51 | } 52 | /** 53 | * Bad Request 54 | * @return {Promise} [] 55 | */ 56 | async _400Action() { 57 | return await this.displayError(400); 58 | } 59 | /** 60 | * Forbidden 61 | * @return {Promise} [] 62 | */ 63 | async _403Action() { 64 | return await this.displayError(403); 65 | } 66 | /** 67 | * Not Found 68 | * @return {Promise} [] 69 | */ 70 | async _404Action() { 71 | //管理端 72 | if(this.http.module === 'admin' && !this.isAjax()) { 73 | let controller = this.controller('admin/base'); 74 | this.status(200); 75 | return controller.invoke('__call'); 76 | } 77 | return await this.displayError(404); 78 | } 79 | /** 80 | * Internal Server Error 81 | * @return {Promise} [] 82 | */ 83 | async _500Action() { 84 | return await this.displayError(500); 85 | } 86 | /** 87 | * Service Unavailable 88 | * @return {Promise} [] 89 | */ 90 | async _503Action() { 91 | return await this.displayError(503); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/common/middleware/resheaders.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * middleware 4 | */ 5 | export default class extends think.middleware.base { 6 | /** 7 | * run 8 | * @return {} [] 9 | */ 10 | run(){ 11 | if (process.env.BLOG_ENV) { 12 | this.http.res.setHeader('x-blog-env', process.env.BLOG_ENV); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/common/model/cate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * model 4 | */ 5 | export default class extends think.model.relation { 6 | 7 | relation = { 8 | post_cate: { 9 | type: think.model.HAS_MANY, 10 | field: 'cate_id' 11 | } 12 | }; 13 | 14 | async getCateArchive() { 15 | let data = await this.model('post_cate') 16 | .join({ 17 | table: 'post', 18 | on: ['post_id', 'id'] 19 | }) 20 | .join({ 21 | table: 'cate', 22 | on: ['cate_id', 'id'] 23 | }) 24 | .where({ 25 | type: 0, 26 | status: 3, 27 | is_public: 1 28 | }) 29 | .order('update_time DESC') 30 | .select(); 31 | 32 | let result = {}; 33 | for(let cate of data) { 34 | if(result[cate.pathname]) { 35 | result[cate.pathname].count += 1; 36 | } else { 37 | result[cate.pathname] = { 38 | name: cate.name, 39 | pathname: encodeURIComponent(cate.pathname), 40 | update_time: cate.update_time, 41 | count: 1 42 | }; 43 | } 44 | } 45 | return Object.values(result).sort((a, b)=> a.count>b.count ? -1 : 1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/common/model/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * model 4 | */ 5 | export default class extends think.model.base { 6 | /** 7 | * options cache key 8 | * @type {String} 9 | */ 10 | cacheKey = 'website_options'; 11 | /** 12 | * cache options 13 | * @type {Object} 14 | */ 15 | cacheOptions = { 16 | timeout: 30 * 24 * 3600 * 1000, 17 | type: !think.isMaster ? 'file' : 'memory' 18 | }; 19 | /** 20 | * get options 21 | * @return {} [] 22 | */ 23 | async getOptions(flag) { 24 | if(flag === true) { 25 | await think.cache(this.cacheKey, null); 26 | } 27 | let ret = await think.cache(this.cacheKey, async () => { 28 | let data = await this.select(); 29 | let result = {}; 30 | data.forEach(item => { 31 | result[item.key] = item.value; 32 | }); 33 | return result; 34 | }, this.cacheOptions); 35 | //comment type 36 | if(ret) { 37 | if(ret.comment && think.isString(ret.comment)) { 38 | ret.comment = JSON.parse(ret.comment); 39 | } 40 | if(!ret.comment) { 41 | ret.comment = {type: 'disqus'}; 42 | } 43 | // upload settings 44 | if(ret.upload && think.isString(ret.upload)) { 45 | ret.upload = JSON.parse(ret.upload); 46 | } 47 | if(!ret.upload) { 48 | ret.upload = {type: 'local'}; 49 | } 50 | if(ret.push_sites && think.isString(ret.push_sites)) { 51 | ret.push_sites = JSON.parse(ret.push_sites); 52 | } 53 | if(!ret.push_sites) { 54 | ret.push_sites = {}; 55 | } 56 | } 57 | return ret; 58 | } 59 | /** 60 | * update options 61 | * @return {} [] 62 | */ 63 | async updateOptions(key, value) { 64 | let data = think.isObject(key) ? think.extend({}, key) : {[key] : value}; 65 | let cacheData = await think.cache(this.cacheKey, undefined, this.cacheOptions); 66 | if(think.isEmpty(cacheData)) { 67 | cacheData = await this.getOptions(); 68 | } 69 | let changedData = {}; 70 | for(let key in data) { 71 | if(data[key] !== cacheData[key]) { 72 | changedData[key] = data[key]; 73 | } 74 | } 75 | //data is not changed 76 | if(think.isEmpty(changedData)) { 77 | return; 78 | } 79 | let p1 = think.cache(this.cacheKey, think.extend(cacheData, changedData), this.cacheOptions); 80 | let promises = [p1]; 81 | for(let key in changedData) { 82 | let value = changedData[key]; 83 | let exist = await this.where({key: key}).count('key'); 84 | let p; 85 | if(exist) { 86 | p = this.where({key: key}).update({value: value}); 87 | }else{ 88 | p = this.add({key, value}); 89 | } 90 | promises.push(p); 91 | } 92 | await Promise.all(promises); 93 | await this.getOptions(true); 94 | 95 | // if `auto_summary` is changed, then rebuild all summaries of posts 96 | if (typeof changedData.auto_summary !== 'undefined') { 97 | const postModel = think.model('post', {}, 'admin'); 98 | // doesn't wait for return 99 | await postModel.updateAllPostSummaries(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/common/model/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * model 4 | */ 5 | export default class extends think.model.relation { 6 | 7 | relation = { 8 | post_tag: { 9 | type: think.model.HAS_MANY, 10 | field: 'tag_id' 11 | } 12 | }; 13 | 14 | /** 15 | * get hot tags 16 | * @return {} [] 17 | */ 18 | async getHotTags() { 19 | let data = await this.getTagArchive(); 20 | return data.slice(0, 5); 21 | } 22 | 23 | async getTagArchive() { 24 | let data = await this.model('post_tag') 25 | .join({ 26 | table: 'post', 27 | on: ['post_id', 'id'] 28 | }) 29 | .join({ 30 | table: 'tag', 31 | on: ['tag_id', 'id'] 32 | }) 33 | .where({ 34 | type: 0, 35 | status: 3, 36 | is_public: 1 37 | }) 38 | .order('update_time DESC') 39 | .select(); 40 | 41 | let result = {}; 42 | for(let tag of data) { 43 | if(result[tag.pathname]) { 44 | result[tag.pathname].count += 1; 45 | } else { 46 | result[tag.pathname] = { 47 | name: tag.name, 48 | pathname: encodeURIComponent(tag.pathname), 49 | update_time: tag.update_time, 50 | count: 1 51 | }; 52 | } 53 | } 54 | return Object.values(result).sort((a, b)=> a.count>b.count ? -1 : 1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/common/util/buildImg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 图片占位符 3 | * 4 | * @type {tring} 5 | */ 6 | const placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; 7 | 8 | /** 9 | * 图片匹配正则,需要结合后台上传图片生成的规则 10 | * 11 | * @type {RegExp} 12 | */ 13 | const reg_img = //g; 14 | 15 | export default { 16 | /** 17 | * 输出主站带有占位+延迟加载的img标签 18 | * 19 | * @param {string} content 内容 20 | * 21 | * @return {string} 22 | */ 23 | lazy(content) { 24 | return content.replace(reg_img, (all, src, alt, width, height) => { 25 | return [ 26 | `
`, 27 | ``, 28 | ``, 29 | '
' 30 | ].join(''); 31 | }); 32 | }, 33 | 34 | /** 35 | * 自动七牛云压缩 36 | * 37 | * @param {string} content 内容 38 | * @return {string} 39 | */ 40 | buildImageslim(content, isWebp) { 41 | const imgstr = 'imageslim'; 42 | return content.replace(/ { 43 | return ` 4 | */ 5 | 6 | import Base from '../../common/controller/base'; 7 | 8 | export default class extends Base { 9 | 10 | /** 11 | * 初始化 12 | * 13 | * @param {Object} http http对象 14 | */ 15 | init(http) { 16 | super.init(http); 17 | } 18 | 19 | /** 20 | * 前置方法 21 | */ 22 | async __before() { 23 | await super.__before(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/home/controller/crontab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | 5 | export default class extends Base { 6 | /** 7 | * sync comment num 8 | * @return {[type]} [description] 9 | */ 10 | async syncCommentAction() { 11 | let SyncService = this.service('comment'); 12 | let instance = new SyncService(); 13 | await instance.sync(); 14 | this.success(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/home/controller/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Base from './base'; 4 | 5 | 6 | export default class extends Base { 7 | /** 8 | * 首页如果设置了自定义首页则渲染对应页面 9 | * @return {[type]} [description] 10 | */ 11 | async indexAction() { 12 | let {frontPage} = await this.model('options').getOptions(); 13 | if(frontPage) { 14 | this.get('pathname', frontPage); 15 | return this.action('post', 'page'); 16 | } 17 | 18 | return this.action('post', 'list'); 19 | } 20 | 21 | /** 22 | * 输出opensearch 23 | */ 24 | opensearchAction() { 25 | this.type('text/xml'); 26 | return this.display('opensearch.xml'); 27 | } 28 | 29 | /** 30 | * rss 31 | * @return {[type]} [description] 32 | */ 33 | async rssAction() { 34 | let model = this.model('post'); 35 | let list = await model.getPostRssList(); 36 | this.assign('list', list); 37 | this.assign('currentTime', (new Date()).toString()); 38 | 39 | this.type('text/xml'); 40 | return super.display('rss.xml'); 41 | } 42 | 43 | /** 44 | * sitemap action 45 | * @return {[type]} [description] 46 | */ 47 | async sitemapAction() { 48 | let postModel = this.model('post'); 49 | let postList = postModel.getPostSitemapList(); 50 | this.assign('postList', postList); 51 | 52 | let tagModel = this.model('tag'); 53 | let tagList = tagModel.getTagArchive(); 54 | this.assign('tags', tagList); 55 | 56 | this.type('text/xml'); 57 | 58 | return this.display('sitemap.xml'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /stc.admin.config.js: -------------------------------------------------------------------------------- 1 | var stc = require('stc'); 2 | var cssCompress = require('stc-css-compress'); 3 | var resourceVersion = require('stc-resource-version'); 4 | var cssCombine = require('stc-css-combine'); 5 | var htmlCompress = require('stc-html-compress'); 6 | var replace = require('stc-replace'); 7 | // var uglify = require("stc-uglify"); 8 | 9 | stc.config({ 10 | product: "admin", 11 | include: ["view/admin/", "www/static/admin/"], 12 | exclude: [/www\/static\/admin\/src\//], 13 | outputPath: "output", 14 | tpl: { 15 | engine: "nunjucks", 16 | ld: ["{%", "{{", "{#"], 17 | rd: ["%}", "}}", "#}"], 18 | }, 19 | }); 20 | 21 | stc.workflow({ 22 | // uglify: { 23 | // plugin: uglify, 24 | // exclude: [/static\/admin\//], // 后台管理不压缩,因为 webpack 压缩过了 25 | // }, 26 | cssCombine: { plugin: cssCombine, include: /\.css$/ }, 27 | cssCompress: { 28 | plugin: cssCompress, 29 | }, 30 | htmlCompress: { 31 | plugin: htmlCompress, 32 | options: { 33 | trim: true, 34 | }, 35 | }, 36 | resourceVersion: { 37 | options: { 38 | length: 5, 39 | type: "query", 40 | }, 41 | plugin: resourceVersion, 42 | }, 43 | }); 44 | 45 | stc.start(); 46 | -------------------------------------------------------------------------------- /stc.config.js: -------------------------------------------------------------------------------- 1 | var stc = require('stc'); 2 | var cssCompress = require('stc-css-compress'); 3 | var resourceVersion = require('stc-resource-version'); 4 | var cssCombine = require('stc-css-combine'); 5 | var htmlCompress = require('stc-html-compress'); 6 | var uglify = require('stc-uglify'); 7 | var replace = require('stc-replace'); 8 | 9 | stc.config({ 10 | product: "home", 11 | include: ["view/", "www/static/"], 12 | exclude: [/www\/static\/admin\//, /view\/admin\//], 13 | outputPath: "output", 14 | tpl: { 15 | engine: "nunjucks", 16 | ld: ["{%", "{{", "{#"], 17 | rd: ["%}", "}}", "#}"], 18 | }, 19 | }); 20 | 21 | stc.workflow({ 22 | uglify: { 23 | plugin: uglify, 24 | }, 25 | cssCombine: { plugin: cssCombine, include: /\.css$/ }, 26 | cssCompress: { 27 | plugin: cssCompress, 28 | }, 29 | htmlCompress: { 30 | plugin: htmlCompress, 31 | options: { 32 | trim: true, 33 | }, 34 | }, 35 | resourceVersion: { 36 | options: { 37 | length: 5, 38 | type: "hash", 39 | }, 40 | plugin: resourceVersion, 41 | }, 42 | }); 43 | 44 | stc.start(); 45 | -------------------------------------------------------------------------------- /view/admin/index_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{options.title}} 7 | 8 | 9 | 10 | 11 | 12 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /view/common/error_404.html: -------------------------------------------------------------------------------- 1 | 404-前端小武博客 2 | 3 | 4 | -------------------------------------------------------------------------------- /view/home/inc/comment.html: -------------------------------------------------------------------------------- 1 |
2 |

Comments

3 | {% if options.comment.name %} 4 | 5 | {% if options.comment.type == 'disqus' %} 6 |
7 | 评论加载中... 8 |
9 |
10 | 注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。 11 |
12 | {% elif options.comment.type == 'duoshuo'%} 13 |
14 |
15 | {% elif options.comment.type == 'changyan' %} 16 |
17 | {% elif options.comment.type == 'netease' %} 18 |
19 | {% else %} 20 | {{ options.comment.name | safe }} 21 | {% endif %} 22 | {% else %} 23 |

请在后台配置评论类型和相关的值。

24 | {% endif %} 25 |
26 | -------------------------------------------------------------------------------- /view/home/inc/pagination.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /view/home/index_opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ options.title }} 4 | {{ options.description }} 5 | 6 | 7 | -------------------------------------------------------------------------------- /view/home/index_rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ options.title }} 5 | {{ site_url }} 6 | {{ options.description }} 7 | 8 | zh-cn 9 | {{ currentTime | utc }} 10 | {% for item in list %} 11 | 12 | {{ item.title }} 13 | {{ site_url }}/post/{{ item.pathname }}.html 14 | 15 | {% if item.summary %} 16 | {{ item.summary | buildImageslim | xml }} 17 | {% if item.summary != item.content %} 18 |

19 | [...] 20 |

21 | {% endif %} 22 | {% else %}{{ item.content | buildImageslim | xml }}{% endif %} 23 |
24 | {{ item.create_time | utc }} 25 | {{ site_url }}/post/{{ item.pathname }}.html 26 |
27 | {% endfor %} 28 |
29 |
30 | -------------------------------------------------------------------------------- /view/home/index_sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ site_url }} 5 | {{ think.datetime(postList[0] and postList[0].update_time, 'YYYY-MM-DD') }} 6 | 0.8 7 | 8 | {% for item in postList %} 9 | 10 | {{ site_url }}/{{ 'page' if item.type === 1 else 'post' }}/{{ item.pathname }}.html 11 | {{ think.datetime(item.update_time, 'YYYY-MM-DD') }} 12 | {{ 0.7 if item.type === 1 else 0.6 }} 13 | 14 | {% endfor %} 15 | {% if tagList[0] %} 16 | 17 | {{ site_url }}/tags 18 | {{ think.datetime(tagList[0].update_time, 'YYYY-MM-DD') }} 19 | 0.5 20 | 21 | {% endif %} 22 | {% for item in tags %} 23 | {% for pageNum in range(1, item.count / options.postsListSize + 1) %} 24 | 25 | {{ site_url }}/tag/{{ item.pathname }}{{ '?page='+pageNum if pageNum > 1 else '' }} 26 | {{ think.datetime(item.update_time, 'YYYY-MM-DD') }} 27 | 0.5 28 | 29 | {% endfor %} 30 | {% endfor %} 31 | {% for item in categories %} 32 | {% for pageNum in range(1, item.count / options.postsListSize + 1) %} 33 | 34 | {{ site_url }}/cate/{{ item.pathname }}{{ '?page='+pageNum if pageNum > 1 else '' }} 35 | {{ think.datetime(item.update_time, 'YYYY-MM-DD') }} 36 | 0.5 37 | 38 | {% endfor %} 39 | {% endfor %} 40 | 41 | -------------------------------------------------------------------------------- /view/home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home", 3 | "version": "0.1.0", 4 | "configElements": [ 5 | { 6 | "type": "text", 7 | "name": "sidebarBackground", 8 | "label": "主题左侧背景图片", 9 | "help": "在这里,输入您需要在顶部显示的图片的URL地址。 图像宽度应大于 250 像素。 建议宽度 500 像素。 建议高度 1200 像素。" 10 | }, 11 | { 12 | "type": "color", 13 | "name": "sidebarBackgroundColor", 14 | "label": "主题左侧背景颜色", 15 | "help": "选择左侧菜单背景颜色" 16 | }, 17 | { 18 | "type": "color", 19 | "name": "sidebarColor", 20 | "label": "主题左侧文字颜色" 21 | }, 22 | { 23 | "type": "css", 24 | "name": "customCSS", 25 | "label": "自定义 CSS", 26 | "help": "设置网站自定义 CSS" 27 | }, 28 | { 29 | "type": "javascript", 30 | "name": "customJS", 31 | "label": "自定义 JS", 32 | "help": "设置网站自定义 JS" 33 | }, 34 | { 35 | "type": "html", 36 | "name": "cc", 37 | "label": "版权信息", 38 | "help": "在每篇文章之后添加版权信息" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /view/home/post_archive.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | 归档 - {{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |

归档

14 | {% for yearMonth,data in list%} 15 |
16 |

{{ yearMonth }} ({{ data.length }})

17 |
    18 | {% for item in data %} 19 |
  • 20 | {{ item.title }}  21 | {{ think.datetime(item.create_time, 'YYYY-MM-DD') }} 22 |
  • 23 | {% endfor %} 24 |
25 |
26 | {% endfor %} 27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /view/home/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | {{ post.title }} - {{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |
{{ think.datetime(post.create_time, 'MM月DD, YYYY') }}
16 | 19 |
20 |

{{ post.title }}

21 |
22 | {{ post.content | buildImageslim | buildLazyImg | safe }} 23 |
24 |

本文链接:{{ site_url }}/post/{{ post.pathnameSource }}.html

25 |

-- EOF --

26 | 27 | 59 | 60 | {% if post.update_time_diff_day > 120 %} 61 |

提醒: 本文最后更新于 {{ post.update_time_diff_day }} 天前,文中所描述的信息可能已发生改变,请谨慎使用。

62 | {% endif %} 63 |
64 | {% if post.prev.title or post.next.title %} 65 | 74 | {% endif %} 75 | {% if post.allow_comment %} 76 | {% include "./inc/comment.html" %} 77 | {% endif %} 78 |
79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /view/home/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | {% if tag %}标签 {{ tag }} - {% elif cate %}分类 {{ cate }} - {% endif %}{% if http.get('page') > 1 %}第{{ http.get('page') }}页 - {% endif %}{{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | 14 | {% if tag %} 15 |

标签{{ tag }}下的文章

16 | {% elif cate %} 17 |

分类{{ cate }}下的文章

18 | {% endif %} 19 | 20 | {% for post in posts %} 21 | 43 | {% endfor %} 44 | {% include './inc/pagination.html' %} 45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /view/home/post_page.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | {{ page.title }} - {{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | {% if page.title %} 14 |

{{ page.title }}

15 |
16 | {{ page.content | buildImageslim | buildLazyImg | safe }} 17 |
18 | {% if page.allow_comment %} 19 | {% include "./inc/comment.html" %} 20 | {% endif %} 21 | {% else %} 22 |

提示

23 |
24 |

未找到相关页面。

25 |

如果是管理员,请到后台添加地址为 {{ pathname }} 的页面,点击这里添加。

26 |
27 | {% endif %} 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /view/home/post_search.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | 站内搜索{% if keyword %} - {{ keyword }}{% endif %} - {% if http.get('page') > 1 %}第{{ http.get('page') }}页 - {% endif %}{{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |

站内搜索

14 | 15 |
16 | 24 | 25 | {% if keyword %} 26 |
27 | {% if searchData.count %} 28 |
29 | 本次搜索找到结果 {{ searchData.count }} 条 30 |
31 | {% endif %} 32 | 33 | {% if not searchData.count %} 34 |
没有找到任何结果,请更换查询词试试~
35 |
36 |
或者试试 Google 站内搜索: site:{{ http.hostname }} {{ keyword }}
37 |
38 | {% endif %} 39 | 40 | {% for item in searchData.data %} 41 |
42 | 43 |
{{ item.summary | buildImageslim | buildLazyImg | safe }}
44 |
45 | {% endfor %} 46 |
47 | {% else %} 48 |
49 |
50 | 热搜词: 51 | {% for tag in hotTags %} 52 | {{ tag.name }} 53 | {% endfor %} 54 |
55 |
56 | {% endif %} 57 |
58 |
59 | 60 | {% include './inc/pagination.html' %} 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /view/home/post_tag.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block title -%} 4 | 标签 - {{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block headers %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /view/home/template/cates_list.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {% block title -%} 4 | {{ page.title }} - {{ options.title }} 5 | {%- endblock %} 6 | 7 | {% block content %} 8 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | var base = path.join(__dirname, 'www/static/admin'); 5 | module.exports = { 6 | devtool: 'source-map', 7 | entry: { 8 | admin: `${base}/src/admin/app.jsx`, 9 | vendor: [ 10 | 'md5', 11 | 'react', 12 | 'moment', 13 | 'react-dom', 14 | 'classnames', 15 | 'react-router', 16 | 'qrcode-react', 17 | 'react-bootstrap', 18 | 'react-bootstrap-validation' 19 | ] 20 | }, 21 | output: { 22 | path: `${base}/js`, 23 | filename: '[name].js', 24 | publicPath: '/static/admin/js/', 25 | chunkFilename: '[name].js' 26 | }, 27 | resolve: { 28 | extensions: ['.js', '.jsx'], 29 | alias: { 30 | admin: `${base}/src/admin`, 31 | common: `${base}/src/common`, 32 | base: `${base}/src/common/component/base` 33 | } 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.jsx?$/, 39 | loader: 'babel-loader', 40 | options: { 41 | cacheDirectory: true, 42 | presets: ['react', ['es2015', {loose: true, module: false}], 'stage-0'], 43 | plugins: ['transform-runtime', 'transform-decorators-legacy'] 44 | }, 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.css?$/, 49 | use: [ 50 | 'style-loader', 51 | 'css-loader' 52 | ] 53 | } 54 | ] 55 | }, 56 | plugins: [ 57 | new webpack.optimize.CommonsChunkPlugin({name: 'vendor', filename: 'common.js'}) 58 | ] 59 | }; 60 | -------------------------------------------------------------------------------- /webpack.production.config.babel.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const config = require('./webpack.config.babel.js'); 3 | 4 | config.plugins.push( 5 | new webpack.optimize.UglifyJsPlugin({ 6 | compress: { 7 | warnings: false 8 | } 9 | }), 10 | new webpack.DefinePlugin({ 11 | 'process.env': { 12 | NODE_ENV: JSON.stringify('production') 13 | } 14 | }) 15 | ); 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /workbox.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | swSrc: path.join("./www/sw.js"), 5 | swDest: path.join("./output/www/sw.js"), 6 | globDirectory: "./output/www/", 7 | globPatterns: ["static/home/**/*.{js,css,ttf,svg,eot}"], 8 | dontCacheBustURLsMatching: /[0-9a-z]+_([0-9a-z]{5})\.[a-z]+$/, 9 | manifestTransforms: [ 10 | (originalManifest) => { 11 | const manifest = originalManifest 12 | .filter((item) => item.revision === null) 13 | .map((item) => ({ 14 | ...item, 15 | url: `/${item.url}`, 16 | })); 17 | return { 18 | manifest, 19 | warnings: [], 20 | }; 21 | }, 22 | ], 23 | }; -------------------------------------------------------------------------------- /www/404.html: -------------------------------------------------------------------------------- 1 | 404-前端小武博客 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/content-search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 前端小武 4 | 搜索前端小武博客 5 | UTF-8 6 | https://xuexb.com/favicon.ico 7 | 8 | -------------------------------------------------------------------------------- /www/development.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var thinkjs = require('thinkjs'); 3 | 4 | var rootPath = path.dirname(__dirname); 5 | 6 | var instance = new thinkjs({ 7 | APP_PATH: rootPath + path.sep + 'app', 8 | RUNTIME_PATH: rootPath + path.sep + 'runtime', 9 | ROOT_PATH: rootPath, 10 | RESOURCE_PATH: __dirname, 11 | UPLOAD_PATH: path.join(__dirname, 'static/upload'), 12 | UPLOAD_BASE_URL: '', 13 | env: 'development' 14 | }); 15 | 16 | //compile src/ to app/ 17 | instance.compile({ 18 | retainLines: false, 19 | log: true 20 | }); 21 | 22 | instance.run(); 23 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/favicon.ico -------------------------------------------------------------------------------- /www/google343405242456511b.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google343405242456511b.html -------------------------------------------------------------------------------- /www/production.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var thinkjs = require('thinkjs'); 3 | 4 | var rootPath = path.dirname(__dirname); 5 | 6 | var instance = new thinkjs({ 7 | APP_PATH: rootPath + path.sep + 'app', 8 | RUNTIME_PATH: rootPath + path.sep + 'runtime', 9 | ROOT_PATH: rootPath, 10 | RESOURCE_PATH: __dirname, 11 | UPLOAD_PATH: path.join(__dirname, 'static/upload'), 12 | UPLOAD_BASE_URL: '', 13 | env: 'production' 14 | }); 15 | 16 | instance.run(); 17 | -------------------------------------------------------------------------------- /www/robots.txt: -------------------------------------------------------------------------------- 1 | # xiaowu 2 | User-agent: Baiduspider 3 | Allow: / 4 | Disallow: /admin/ 5 | Disallow: /*.php$ 6 | Disallow: /post/*.md$ 7 | 8 | User-agent: Sosospider 9 | Allow: / 10 | Disallow: /admin/ 11 | Disallow: /*.php$ 12 | Disallow: /post/*.md$ 13 | 14 | User-agent: sogou spider 15 | Allow: / 16 | Disallow: /admin/ 17 | Disallow: /*.php$ 18 | Disallow: /post/*.md$ 19 | 20 | User-agent: Googlebot 21 | Allow: / 22 | Disallow: /admin/ 23 | Disallow: /*.php$ 24 | Disallow: /post/*.md$ 25 | 26 | User-agent: Bingbot 27 | Allow: / 28 | Disallow: /admin/ 29 | Disallow: /*.php$ 30 | Disallow: /post/*.md$ 31 | 32 | User-agent: MSNBot 33 | Allow: / 34 | Disallow: /admin/ 35 | Disallow: /*.php$ 36 | Disallow: /post/*.md$ 37 | 38 | User-agent: googlebot-mobile 39 | Allow: / 40 | Disallow: /admin/ 41 | Disallow: /*.php$ 42 | Disallow: /post/*.md$ 43 | 44 | User-agent: 360Spider 45 | Allow: / 46 | Disallow: /admin/ 47 | Disallow: /*.php$ 48 | Disallow: /post/*.md$ 49 | 50 | User-agent: HaosouSpider 51 | Allow: / 52 | Disallow: /admin/ 53 | Disallow: /*.php$ 54 | Disallow: /post/*.md$ 55 | 56 | User-agent: * 57 | Disallow: / 58 | 59 | Sitemap: https://xuexb.com/sitemap.xml -------------------------------------------------------------------------------- /www/static/admin/font/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/font/icomoon.eot -------------------------------------------------------------------------------- /www/static/admin/font/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/font/icomoon.ttf -------------------------------------------------------------------------------- /www/static/admin/font/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/font/icomoon.woff -------------------------------------------------------------------------------- /www/static/admin/img/editor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/img/editor@2x.png -------------------------------------------------------------------------------- /www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/admin/module/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/cate.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | selectParent: {children: ['completed', 'failed']}, 6 | delete: {children: ['completed', 'failed']}, 7 | save: {children: ['completed', 'failed'], asyncResult: true} 8 | }); 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/options.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | save: {children: ['completed', 'failed'], asyncResult: true}, 5 | auth: {children: ['completed', 'failed'], asyncResult: true}, 6 | qrcode: {children: ['completed', 'failed'], asyncResult: true}, 7 | comment: {children: ['completed', 'failed'], asyncResult: true}, 8 | upload: {children: ['completed', 'failed'], asyncResult: true}, 9 | navigation: {children: ['completed', 'failed'], asyncResult: true}, 10 | defaultCategory: {children: ['completed', 'failed'], asyncResult: true} 11 | }); 12 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/page.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | selectList: {children: ['completed', 'failed']}, 6 | delete: {children: ['completed', 'failed']}, 7 | save: {children: ['completed', 'failed'], asyncResult: true} 8 | }); 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/post.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | selectList: {children: ['completed', 'failed']}, 6 | selectLastest: {children: ['completed', 'failed']}, 7 | delete: {children: ['completed', 'failed']}, 8 | save: {children: ['completed', 'failed'], asyncResult: true}, 9 | pass: {children: ['completed', 'failed'], asyncResult: true}, 10 | deny: {children: ['completed', 'failed'], asyncResult: true}, 11 | search: {children: ['completed', 'failed'], asyncResult: true} 12 | }); 13 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/push.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | delete: {children: ['completed', 'failed']}, 6 | save: {children: ['completed', 'failed'], asyncResult: true} 7 | }); 8 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/system.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | updateSystem: {childen: ['completed', 'failed']} 6 | }); 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/tag.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | delete: {children: ['completed', 'failed']}, 6 | save: {children: ['completed', 'failed'], asyncResult: true} 7 | }); 8 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/theme.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | list: {children: ['completed', 'failed'], asyncResult: true}, 5 | save: {children: ['completed', 'failed'], asyncResult: true}, 6 | forkTheme: {children: ['completed', 'failed'], asyncResult: true}, 7 | getThemeFile: {children: ['completed', 'failed'], asyncResult: true}, 8 | saveThemeConfig: {children: ['completed', 'failed'], asyncResult: true}, 9 | getThemeFileList: {children:['completed', 'failed'], asyncResult: true}, 10 | updateThemeFile: {children: ['completed', 'failed'], asyncResult: true}, 11 | getPageTemplateList: {children: ['completed', 'failed'], asyncResult: true} 12 | }); 13 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/action/user.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | select: {children: ['completed', 'failed']}, 5 | delete: {children: ['completed', 'failed']}, 6 | pass: {children: ['completed', 'failed']}, 7 | save: {children: ['completed', 'failed'], asyncResult: true}, 8 | savepwd: {children: ['completed', 'failed'], asyncResult: true}, 9 | login: {children: ['completed', 'failed'], asyncResult: true}, 10 | generateKey: {children: ['completed', 'failed'], asyncResult: true} 11 | }); 12 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'babel-polyfill'; 4 | import {Router, useRouterHistory} from 'react-router'; 5 | 6 | import {createHistory} from 'history'; 7 | 8 | if(Object.freeze) { 9 | Object.freeze(window.SysConfig.userInfo); 10 | } 11 | 12 | let history = useRouterHistory(createHistory)({ 13 | basename: '/admin/', 14 | queryKey: false 15 | }); 16 | 17 | let rootRoute = { 18 | path: '/', 19 | indexRoute: { 20 | onEnter: (nextState, replace) => replace('/dashboard') 21 | }, 22 | getChildRoutes(location, callback) { 23 | require.ensure([], function(require) { 24 | callback(null, [ 25 | require('./page/tag'), 26 | require('./page/post'), 27 | require('./page/page'), 28 | require('./page/user'), 29 | require('./page/push'), 30 | require('./page/cate'), 31 | require('./page/options'), 32 | require('./page/dashboard'), 33 | require('./page/appearance') 34 | ]); 35 | }, 'app'); 36 | }, 37 | getComponent(location, callback) { 38 | callback(null, require('./component/App')); 39 | } 40 | }; 41 | 42 | ReactDOM.render( 43 | , 44 | document.getElementById('app') 45 | ); 46 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Login from './login'; 3 | import Sidebar from './sidebar'; 4 | import Base from 'base'; 5 | import Tip from 'common/component/tip'; 6 | import ModalManage from 'common/component/modal_manage'; 7 | 8 | 9 | module.exports = class App extends Base { 10 | state = { 11 | 12 | }; 13 | 14 | render() { 15 | if(!window.SysConfig.userInfo.name) { 16 | return ( 17 |
18 | 19 | 20 |
); 21 | } 22 | return ( 23 |
24 | 25 | 26 | {this.props.children} 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/appearance.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/breadcrumb.jsx: -------------------------------------------------------------------------------- 1 | import ReactDom from 'react-dom'; 2 | import React from 'react'; 3 | import classnames from 'classnames'; 4 | import {Link} from 'react-router'; 5 | import Sidebar from './sidebar'; 6 | import Base from 'base'; 7 | 8 | module.exports = class extends Base { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | userOpen: false, 14 | crumb: [] 15 | } 16 | this.bindHandleDocumentClick = this.handleDocumentClick.bind(this); 17 | 18 | this.crumbs = {}; 19 | (new Sidebar()).state.routes.forEach(route => { 20 | //console.log(route); 21 | if(!route.children) { return; } 22 | route.children.forEach(child => { 23 | this.crumbs[child.url] = [ 24 | {title: route.title, url: route.url, children: route.children}, 25 | child 26 | ]; 27 | }); 28 | }); 29 | 30 | if(this.crumbs[this.props.location.pathname]) { 31 | this.state.crumb = this.crumbs[this.props.location.pathname]; 32 | } 33 | } 34 | 35 | componentDidMount() { 36 | document.addEventListener('click', this.bindHandleDocumentClick, false); 37 | } 38 | 39 | componentWillUnmount() { 40 | document.removeEventListener('click', this.bindHandleDocumentClick, false); 41 | } 42 | 43 | handleDocumentClick(event) { 44 | if (!ReactDom.findDOMNode(this.refs.userinfo).contains(event.target)) { 45 | this.setState({ 46 | userOpen: false 47 | }); 48 | } 49 | } 50 | 51 | toggleUser() { 52 | this.setState({ 53 | userOpen: !this.state.userOpen 54 | }) 55 | } 56 | getUserClass() { 57 | return classnames({ 58 | dropdown: true, 59 | open: this.state.userOpen 60 | }) 61 | } 62 | render() { 63 | let breadcrumb; 64 | if(this.state.crumb.length > 0) { 65 | breadcrumb = ( 66 |
    67 |
  1. 68 | 首页 69 |
  2. 70 | {this.state.crumb.map((item, i) => { 71 | if(item.url === this.props.location.pathname) { 72 | return ( 73 |
  3. {item.title}
  4. 74 | ); 75 | } 76 | return ( 77 |
  5. 78 | {item.title} 79 |
  6. 80 | ); 81 | })} 82 |
83 | ) 84 | }else{ 85 | breadcrumb = ( 86 |
    87 |
  1. 首页
  2. 88 |
89 | ) 90 | } 91 | return ( 92 |
93 |
94 | {breadcrumb} 95 |
96 | 107 |
108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/cate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import md5 from 'md5'; 3 | import { Form, ValidatedInput } from 'react-bootstrap-validation'; 4 | 5 | import UserAction from '../action/user'; 6 | import UserStore from '../store/user'; 7 | import Base from 'base'; 8 | import TipAction from 'common/action/tip'; 9 | 10 | 11 | module.exports = class extends Base { 12 | componentDidMount() { 13 | this.listenTo(UserStore, this.handleTrigger.bind(this)); 14 | } 15 | handleTrigger(data, type) { 16 | switch(type) { 17 | case 'LoginSuccess': 18 | TipAction.success('登录成功'); 19 | setTimeout(() => {location.reload()}, 1000) 20 | break; 21 | } 22 | } 23 | /** 24 | * get two factor auth 25 | * @return {} [] 26 | */ 27 | getTwoFactorAuth() { 28 | if(window.SysConfig.options.two_factor_auth) { 29 | return ( 30 |
31 | 43 |
44 | ); 45 | } 46 | } 47 | handleValidSubmit(values) { 48 | values.password = md5(window.SysConfig.options.password_salt + values.password); 49 | UserAction.login(values); 50 | } 51 | handleInvalidSubmit() { 52 | 53 | } 54 | render() { 55 | return ( 56 |
57 |
58 |
59 |

60 | {window.SysConfig.options.title} 61 |

62 |
67 |
68 | 80 |
81 |
82 | 94 |
95 | {this.getTwoFactorAuth()} 96 | 97 |
98 |
99 |
100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/options.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/options_analytic.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, ValidatedInput } from 'react-bootstrap-validation'; 3 | 4 | import OptionsAction from '../action/options'; 5 | import OptionsStore from '../store/options'; 6 | import Base from 'base'; 7 | import BreadCrumb from 'admin/component/breadcrumb'; 8 | import TipAction from 'common/action/tip'; 9 | 10 | module.exports = class extends Base { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | submitting: false, 15 | options: window.SysConfig.options 16 | }; 17 | if(!this.state.options.hasOwnProperty('push')) { 18 | this.state.options.push = '0'; 19 | } 20 | this.state.options.analyze_code = unescape(window.SysConfig.options.analyze_code); 21 | } 22 | componentDidMount() { 23 | this.listenTo(OptionsStore, this.handleTrigger.bind(this)); 24 | } 25 | handleTrigger(data, type) { 26 | switch(type) { 27 | case 'saveOptionsSuccess': 28 | this.setState({submitting: false}); 29 | TipAction.success('更新成功'); 30 | for(let key in this.optionsSavedValue) { 31 | window.SysConfig.options[key] = this.optionsSavedValue[key]; 32 | } 33 | break; 34 | } 35 | } 36 | changeInput(type, event) { 37 | let value = event.target.value; 38 | let options = this.state.options; 39 | options[type] = value; 40 | this.setState({options: options}); 41 | } 42 | getProps(name) { 43 | let props = { 44 | value: this.state.options[name] || '', 45 | onChange: this.changeInput.bind(this, name) 46 | } 47 | if(['title', 'description'].indexOf(name) > -1) { 48 | props.validate = 'required' 49 | } 50 | return props; 51 | } 52 | handleValidSubmit(values) { 53 | this.setState({submitting: true}); 54 | this.optionsSavedValue = values; 55 | OptionsAction.save(values); 56 | } 57 | handleInvalidSubmit() { 58 | 59 | } 60 | render() { 61 | let BtnProps = {} 62 | if(this.state.submitting) { 63 | BtnProps.disabled = true; 64 | } 65 | return ( 66 |
67 | 68 |
69 |

网站统计代码

70 |
75 |
76 | 84 |

直接贴入百度统计或者 Google 统计代码

85 |
86 | 89 |
90 |
91 |
92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/options_push.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Radio, RadioGroup } from 'react-bootstrap-validation'; 3 | 4 | import OptionsAction from '../action/options'; 5 | import OptionsStore from '../store/options'; 6 | import Base from 'base'; 7 | import BreadCrumb from 'admin/component/breadcrumb'; 8 | import TipAction from 'common/action/tip'; 9 | 10 | module.exports = class extends Base { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | submitting: false, 15 | options: window.SysConfig.options 16 | }; 17 | if(!this.state.options.hasOwnProperty('push')) { 18 | this.state.options.push = '0'; 19 | } 20 | this.state.options.analyze_code = unescape(window.SysConfig.options.analyze_code); 21 | } 22 | componentDidMount() { 23 | this.listenTo(OptionsStore, this.handleTrigger.bind(this)); 24 | } 25 | handleTrigger(data, type) { 26 | switch(type) { 27 | case 'saveOptionsSuccess': 28 | this.setState({submitting: false}); 29 | TipAction.success('更新成功'); 30 | for(let key in this.optionsSavedValue) { 31 | window.SysConfig.options[key] = this.optionsSavedValue[key]; 32 | } 33 | break; 34 | } 35 | } 36 | changeInput(type, event) { 37 | let value = event.target.value; 38 | let options = this.state.options; 39 | options[type] = value; 40 | this.setState({options: options}); 41 | } 42 | getProps(name) { 43 | let props = { 44 | value: this.state.options[name] || '', 45 | onChange: this.changeInput.bind(this, name) 46 | } 47 | if(['title', 'description'].indexOf(name) > -1) { 48 | props.validate = 'required' 49 | } 50 | return props; 51 | } 52 | handleValidSubmit(values) { 53 | this.setState({submitting: true}); 54 | this.optionsSavedValue = values; 55 | OptionsAction.save(values); 56 | } 57 | handleInvalidSubmit() { 58 | 59 | } 60 | render() { 61 | let BtnProps = {} 62 | if(this.state.submitting) { 63 | BtnProps.disabled = true; 64 | } 65 | let url = location.protocol + '//' + location.host + '/index/contributor'; 66 | return ( 67 |
68 | 69 |
70 |

推送设置

71 |
76 |

开启后他人可以通过 {url} 申请成为投稿者。

77 | 81 | 82 | 83 | 84 | 85 | 88 |
89 |
90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/page_create.jsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import PostCreate from './post_create'; 4 | import PageAction from 'admin/action/page'; 5 | import PageStore from 'admin/store/page'; 6 | import TipAction from 'common/action/tip'; 7 | import ThemeAction from 'admin/action/theme'; 8 | import ThemeStore from 'admin/store/theme'; 9 | 10 | 11 | module.exports = class extends PostCreate { 12 | type = 1; 13 | 14 | componentWillMount() { 15 | this.listenTo(PageStore, this.handleTrigger.bind(this)); 16 | 17 | if(this.id) { 18 | PageAction.select(this.id); 19 | } 20 | 21 | this.state.postInfo.pathname = this.props.location.query.pathname; 22 | 23 | 24 | this.listenTo(ThemeStore, this.getThemeTemplateList.bind(this)); 25 | ThemeAction.getPageTemplateList(window.SysConfig.options.theme || 'firekylin'); 26 | } 27 | 28 | componentWillReceiveProps(nextProps) { 29 | this.id = nextProps.params.id | 0; 30 | if(this.id) { 31 | PageAction.select(this.id); 32 | } 33 | let initialState = this.initialState(); 34 | this.setState(initialState); 35 | } 36 | 37 | handleTrigger(data, type) { 38 | switch(type) { 39 | case 'savePageFail': 40 | this.setState({draftSubmitting: false, postSubmitting: false}); 41 | break; 42 | case 'savePageSuccess': 43 | TipAction.success(this.id ? '保存成功' : '添加成功'); 44 | this.setState({draftSubmitting: false, postSubmitting: false}); 45 | setTimeout(() => this.redirect('page/list'), 1000); 46 | break; 47 | case 'getPageInfo': 48 | if(data.create_time === '0000-00-00 00:00:00') { 49 | data.create_time = ''; 50 | } 51 | data.create_time = data.create_time ? 52 | moment(new Date(data.create_time)).format('YYYY-MM-DD HH:mm:ss') : 53 | data.create_time; 54 | if(!data.options) { 55 | data.options = {push_sites: []}; 56 | } else if(typeof(data.options) === 'string') { 57 | data.options = JSON.parse(data.options); 58 | } else { 59 | data.options.push_sites = data.options.push_sites || []; 60 | } 61 | this.setState({postInfo: data}); 62 | break; 63 | } 64 | } 65 | 66 | handleValidSubmit(values) { 67 | if(!this.state.status) { 68 | this.setState({draftSubmitting: true}); 69 | } else { 70 | this.setState({postSubmitting: true}); 71 | } 72 | 73 | if(this.id) { 74 | values.id = this.id; 75 | } 76 | 77 | values.create_time = this.state.postInfo.create_time; 78 | values.status = this.state.status; 79 | values.type = this.type; //type: 0为文章,1为页面 80 | values.allow_comment = Number(this.state.postInfo.allow_comment); 81 | values.markdown_content = this.state.postInfo.markdown_content; 82 | values.options = JSON.stringify(this.state.postInfo.options); 83 | if(values.status === 3 && !values.markdown_content) { 84 | this.setState({draftSubmitting: false, postSubmitting: false}); 85 | return TipAction.fail('没有内容不能提交呢!'); 86 | } 87 | PageAction.save(values); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/post/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/post_create/style.css: -------------------------------------------------------------------------------- 1 | .pathname { 2 | overflow: hidden; 3 | margin-bottom: 15px; 4 | } 5 | .pathname > span { 6 | float: left; 7 | line-height: 39px; 8 | } 9 | .pathname > .form-group { 10 | float: left; 11 | min-width: 320px; 12 | margin: 0 5px; 13 | display: inline-block; 14 | } 15 | 16 | 17 | .md-panel { 18 | height: 550px; 19 | } 20 | 21 | .md-panel.fullscreen { 22 | height: auto; 23 | } 24 | 25 | .rc-select-dropdown-menu { 26 | max-height: 200px; 27 | overflow: hidden; 28 | } 29 | 30 | .is-public-radiogroup { 31 | padding: 0 1px; 32 | } 33 | /** 文章编辑右侧 **/ 34 | .post-create .col-xs-3 > div.button-group { 35 | margin-top: 25px; 36 | margin-bottom: 15px; 37 | } 38 | .post-create .col-xs-3 > div.button-group > button { 39 | width: 45%; 40 | } 41 | .post-create .col-xs-3 > div.button-group > button:last-child { 42 | float: right; 43 | } 44 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/push.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/push_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | import Base from 'base'; 5 | import BreadCrumb from 'admin/component/breadcrumb'; 6 | import ModalAction from 'common/action/modal'; 7 | import TipAction from 'common/action/tip'; 8 | import PushAction from 'admin/action/push'; 9 | import PushStore from 'admin/store/push'; 10 | 11 | module.exports = class extends Base { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | loading: false, 16 | pushList: [] 17 | } 18 | } 19 | componentDidMount() { 20 | this.listenTo(PushStore, this.handleTrigger.bind(this)); 21 | PushAction.select(); 22 | } 23 | handleTrigger(data, type) { 24 | switch(type) { 25 | case 'deletePushFail': 26 | TipAction.fail(data); 27 | break; 28 | case 'deletePushSuccess': 29 | TipAction.success('删除成功'); 30 | this.setState({loading: true}, PushAction.select.bind(PushAction)); 31 | break; 32 | case 'getPushList': 33 | this.setState({pushList: data, loading: false}); 34 | break; 35 | } 36 | } 37 | getPushList() { 38 | if(this.state.loading) { 39 | return (加载中……); 40 | } 41 | if(!this.state.pushList.length) { 42 | return (暂无记录); 43 | } 44 | return this.state.pushList.map(item => { 45 | return ( 46 | 47 | {item.title} 48 | {item.url} 49 | {item.appKey} 50 | {item.appSecret} 51 | 52 | 53 | 57 | 58 | 59 | 74 | 75 | 76 | ); 77 | }) 78 | } 79 | 80 | render() { 81 | return ( 82 |
83 | 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {this.getPushList()} 97 | 98 |
网站名称网站地址推送公钥推送秘钥操作
99 |
100 |
101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/tag.jsx: -------------------------------------------------------------------------------- 1 | import Cate from './cate'; 2 | 3 | module.exports = Cate; 4 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/tag_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | import Base from 'base'; 5 | import TagAction from 'admin/action/tag'; 6 | import TagStore from 'admin/store/tag'; 7 | import TipAction from 'common/action/tip'; 8 | import ModalAction from 'common/action/modal'; 9 | import BreadCrumb from 'admin/component/breadcrumb'; 10 | 11 | module.exports = class extends Base { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | loading: true, 16 | tagList: [] 17 | } 18 | } 19 | componentDidMount() { 20 | this.listenTo(TagStore, this.handleTrigger.bind(this)); 21 | TagAction.select(); 22 | } 23 | 24 | handleTrigger(data, type) { 25 | switch(type) { 26 | case 'deleteTagFail': 27 | TipAction.fail(data); 28 | break; 29 | case 'deleteTagSuccess': 30 | TipAction.success('删除成功'); 31 | this.setState({loading: true}, TagAction.select); 32 | break; 33 | case 'getTagList': 34 | this.setState({tagList: data, loading: false}); 35 | break; 36 | } 37 | } 38 | 39 | getTagList() { 40 | if(this.state.loading) { 41 | return (加载中……); 42 | } 43 | if(!this.state.tagList.length) { 44 | return (暂无标签); 45 | } 46 | return this.state.tagList.map(item => { 47 | return ( 48 | 49 | {item.name} 50 | {item.pathname} 51 | {item.post_tag} 52 | 53 | 54 | 58 | 59 | 60 | 75 | 76 | 77 | ); 78 | }) 79 | } 80 | render() { 81 | return ( 82 |
83 | 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {this.getTagList()} 96 | 97 |
名称缩略名文章数操作
98 |
99 |
100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/component/user.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from 'base'; 3 | 4 | module.exports = class extends Base { 5 | render() { 6 | return (
{this.props.children}
) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/appearance/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/theme_editor')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/appearance/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'appearance', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | indexRoute: { 9 | onEnter(nextState, replace) { 10 | return replace({pathname: '/appearance/theme'}); 11 | } 12 | }, 13 | getComponent(nextState, callback) { 14 | callback(null, require('../../component/appearance')); 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | callback(null, [ 18 | require('./edit'), 19 | require('./theme'), 20 | require('./navigation') 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/appearance/navigation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'navigation', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/navigation')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/appearance/theme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'theme', 3 | getComponent(nextState, callback) { 4 | require.ensure([], function(require) { 5 | callback(null, require('../../component/theme')); 6 | }, 'theme'); 7 | } 8 | }; 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/cate/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/cate_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/cate/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/cate_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/cate/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'cate', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | getComponent(nextState, callback) { 9 | callback(null, require('../../component/cate')); 10 | }, 11 | indexRoute: { 12 | onEnter(nextState, replace) { 13 | return replace({pathname: '/cate/list'}); 14 | } 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | callback(null, [ 18 | require('./list'), 19 | require('./edit'), 20 | require('./create') 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/cate/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/cate_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/dashboard.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'dashboard', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../component/Dashboard')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/analytic.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'analytic', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_analytic')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/comment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'comment', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_comment')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/general.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'general', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_general')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/import.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'import', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/import')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'options', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | getComponent(nextState, callback) { 9 | callback(null, require('admin/component/options')); 10 | }, 11 | indexRoute: { 12 | onEnter(nextState, replace) { 13 | return replace({pathname: '/options/general'}); 14 | } 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | callback(null, [ 18 | require('./general'), 19 | require('./reading'), 20 | require('./two_factor_auth'), 21 | require('./comment'), 22 | require('./upload'), 23 | require('./analytic'), 24 | require('./push'), 25 | require('./import') 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/push.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'push', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_push')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/reading.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'reading', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_reading')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/two_factor_auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'two_factor_auth', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_2fa')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/options/upload.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'upload', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/options_upload')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/page/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/page_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/page/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/page_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/page/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'page', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | getComponent(nextState, callback) { 9 | callback(null, require('../../component/page')); 10 | }, 11 | indexRoute: { 12 | onEnter(nextState, replace) { 13 | return replace({pathname: '/page/list'}); 14 | } 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | require.ensure([], function(require) { 18 | callback(null, [ 19 | require('./list'), 20 | require('./edit'), 21 | require('./create') 22 | ]) 23 | }, 'page'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/page/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/page_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/post/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/post_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/post/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/post_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/post/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'post', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/post')); 5 | }, 6 | indexRoute: { 7 | onEnter(nextState, replace) { 8 | return replace({pathname: '/post/list'}); 9 | } 10 | }, 11 | getChildRoutes(nextState, callback) { 12 | require.ensure([], function(require) { 13 | callback(null, [ 14 | require('./list'), 15 | require('./edit'), 16 | require('./create') 17 | ]) 18 | }, 'post'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/post/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/post_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/push/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/push_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/push/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/push_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/push/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'push', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | getComponent(nextState, callback) { 9 | callback(null, require('../../component/push')); 10 | }, 11 | indexRoute: { 12 | onEnter(nextState, replace) { 13 | return replace({pathname: '/push/list'}); 14 | } 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | callback(null, [ 18 | require('./list'), 19 | require('./edit'), 20 | require('./create') 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/push/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/push_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/tag/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/tag_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/tag/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/tag_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/tag/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'tag', 5 | onEnter(nextState, replace) { 6 | return auth(replace); 7 | }, 8 | getComponent(nextState, callback) { 9 | callback(null, require('../../component/tag')); 10 | }, 11 | indexRoute: { 12 | onEnter(nextState, replace) { 13 | return replace({pathname: '/tag/list'}); 14 | } 15 | }, 16 | getChildRoutes(nextState, callback) { 17 | callback(null, [ 18 | require('./list'), 19 | require('./edit'), 20 | require('./create') 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/tag/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/tag_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/user/create.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'create', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/user_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/user/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit/:id', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/user_create')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/user/edit_pwd.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'edit_pwd', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/user_editpwd')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/user/index.js: -------------------------------------------------------------------------------- 1 | import auth from 'common/util/auth'; 2 | 3 | module.exports = { 4 | path: 'user', 5 | onEnter(nextState, replace) { 6 | let {pathname} = nextState.location; 7 | if(pathname !== '/user/edit_pwd') { 8 | return auth(replace); 9 | } else return true; 10 | }, 11 | getComponent(nextState, callback) { 12 | callback(null, require('../../component/user')); 13 | }, 14 | indexRoute: { 15 | onEnter(nextState, replace) { 16 | return replace({pathname: '/user/list'}); 17 | } 18 | }, 19 | getChildRoutes(nextState, callback) { 20 | callback(null, [ 21 | require('./list'), 22 | require('./edit'), 23 | require('./create'), 24 | require('./edit_pwd') 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/page/user/list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'list', 3 | getComponent(nextState, callback) { 4 | callback(null, require('../../component/user_list')); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/cate.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | 4 | import firekylin from '../../common/util/firekylin'; 5 | 6 | import CateAction from '../action/cate'; 7 | 8 | export default Reflux.createStore({ 9 | 10 | listenables: CateAction, 11 | /** 12 | * select user data 13 | * @param {[type]} id [description] 14 | * @return {[type]} [description] 15 | */ 16 | onSelect(id) { 17 | let url = '/admin/api/cate'; 18 | if(id) { 19 | url += '/' + id; 20 | } 21 | let req = superagent.get(url); 22 | return firekylin.request(req).then( 23 | data => this.trigger(data, id ? 'getCateInfo' : 'getCateList') 24 | ); 25 | }, 26 | onSelectParent() { 27 | let url = '/admin/api/cate?pid=0'; 28 | let req = superagent.get(url); 29 | return firekylin.request(req).then( 30 | data => this.trigger(data, 'getCateParent') 31 | ); 32 | }, 33 | /** 34 | * save user 35 | * @param {Object} data [] 36 | * @return {Promise} [] 37 | */ 38 | onSave(data) { 39 | let id = data.id; 40 | delete data.id; 41 | let url = '/admin/api/cate'; 42 | if(id) { 43 | url += '/' + id + '?method=put'; 44 | } 45 | let req = superagent.post(url); 46 | req.type('form').send(data); 47 | return firekylin.request(req).then( 48 | data => { 49 | if(data.id && data.id.type === 'exist') { 50 | this.trigger('CATE_EXIST', 'saveCateFail'); 51 | } else this.trigger(data, 'saveCateSuccess'); 52 | }, 53 | err => this.trigger(err, 'saveCateFail') 54 | ); 55 | }, 56 | 57 | onDelete(id) { 58 | let url = '/admin/api/cate'; 59 | if(id) { 60 | url += '/' + id + '?method=delete'; 61 | } 62 | 63 | let req = superagent.post(url); 64 | return firekylin.request(req).then( 65 | data => this.trigger(data, 'deleteCateSuccess'), 66 | err => this.trigger(err, 'deleteCateFail') 67 | ); 68 | } 69 | 70 | }) 71 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/options.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | 4 | import firekylin from '../../common/util/firekylin'; 5 | 6 | import OptionsAction from '../action/options'; 7 | 8 | export default Reflux.createStore({ 9 | 10 | listenables: OptionsAction, 11 | /** 12 | * save user 13 | * @param {Object} data [] 14 | * @return {Promise} [] 15 | */ 16 | onSave(data) { 17 | let req = superagent.post('/admin/api/options?method=put'); 18 | req.type('form').send(data); 19 | return firekylin.request(req).then(data => { 20 | this.trigger(data, 'saveOptionsSuccess'); 21 | }).catch(err => { 22 | this.trigger(err, 'saveOptionsFail'); 23 | }) 24 | }, 25 | onAuth(data) { 26 | let req = superagent.post('/admin/api/options?type=2faAuth'); 27 | req.type('form').send(data); 28 | return firekylin.request(req).then(data => { 29 | this.trigger(data, 'Auth2FASuccess'); 30 | }).catch(err => { 31 | this.trigger(err, 'Auth2FAFail'); 32 | }) 33 | }, 34 | onQrcode() { 35 | let req = superagent.get('/admin/api/options?type=2fa'); 36 | return firekylin.request(req).then(data => { 37 | this.trigger(data, 'getQrcodeSuccess'); 38 | }).catch(err => { 39 | this.trigger(err, 'getQrcodeFail'); 40 | }) 41 | }, 42 | onComment(data) { 43 | let req = superagent.post('/admin/api/options?method=put'); 44 | req.type('form').send({'comment': JSON.stringify(data)}); 45 | return firekylin.request(req).then(data => { 46 | this.trigger(data, 'saveCommentSuccess'); 47 | }).catch(err => { 48 | this.trigger(err, 'saveCommentFail'); 49 | }); 50 | }, 51 | onUpload(data) { 52 | let req = superagent.post('/admin/api/options?method=put'); 53 | req.type('form').send({'upload': JSON.stringify(data)}); 54 | return firekylin.request(req).then(data => { 55 | this.trigger(data, 'saveUploadSuccess'); 56 | }).catch(err => { 57 | this.trigger(err, 'saveUploadFail'); 58 | }); 59 | }, 60 | onNavigation(data) { 61 | let req = superagent.post('/admin/api/options?method=put'); 62 | req.type('form').send({'navigation': JSON.stringify(data)}); 63 | return firekylin.request(req).then( 64 | data => this.trigger(data, 'saveNavigationSuccess'), 65 | err => this.trigger(err, 'saveNavigationFailed') 66 | ); 67 | }, 68 | onDefaultCategory(id) { 69 | let url = '/admin/api/options?type=defaultCategory', req; 70 | if(id) { 71 | url += '&method=put'; 72 | req = superagent.post(url).type('form').send({id}); 73 | return firekylin.request(req) 74 | .then(data => this.trigger(data, 'saveDefaultCategorySuccess')) 75 | .catch(err => this.trigger(err, 'saveDefaultCategoryFailed')); 76 | } else { 77 | req = superagent.get(url); 78 | return firekylin.request(req) 79 | .then(data => this.trigger(data, 'getDefaultCategorySuccess')) 80 | .catch(err => this.trigger(err, 'getDefaultCategoryFailed')); 81 | } 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/page.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | import PageAction from '../action/page'; 4 | import firekylin from 'common/util/firekylin'; 5 | 6 | export default Reflux.createStore({ 7 | 8 | listenables: PageAction, 9 | /** 10 | * select user data 11 | * @param {[type]} id [description] 12 | * @return {[type]} [description] 13 | */ 14 | onSelect(id) { 15 | let url = '/admin/api/page'; 16 | if(id) { 17 | url += '/' + id; 18 | } 19 | let req = superagent.get(url); 20 | return firekylin.request(req).then( 21 | data => this.trigger(data, id ? 'getPageInfo' : 'getPageList') 22 | ); 23 | }, 24 | onSelectList(page) { 25 | return firekylin.request(superagent.get('/admin/api/page?page='+page)).then( 26 | data => this.trigger(data, 'getPageList') 27 | ); 28 | }, 29 | 30 | onSave(data) { 31 | let id = data.id; 32 | delete data.id; 33 | let url = '/admin/api/page'; 34 | if(id) { 35 | url += '/' + id + '?method=put'; 36 | } 37 | let req = superagent.post(url); 38 | req.type('form').send(data); 39 | return firekylin.request(req).then( 40 | data => this.trigger(data, 'savePageSuccess'), 41 | err => this.trigger(err, 'savePageFail') 42 | ); 43 | }, 44 | 45 | onDelete(id) { 46 | let url = '/admin/api/page'; 47 | if(id) { 48 | url += '/' + id + '?method=delete'; 49 | } 50 | 51 | let req = superagent.post(url); 52 | return firekylin.request(req).then( 53 | data => this.trigger(data, 'deletePageSuccess'), 54 | err => this.trigger(err, 'deletePageFail') 55 | ); 56 | } 57 | 58 | }) 59 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/post.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | 4 | import firekylin from '../../common/util/firekylin'; 5 | 6 | import PostAction from '../action/post'; 7 | 8 | export default Reflux.createStore({ 9 | 10 | listenables: PostAction, 11 | /** 12 | * select user data 13 | * @param {[type]} id [description] 14 | * @return {[type]} [description] 15 | */ 16 | onSelect(id) { 17 | let url = '/admin/api/post'; 18 | if(id) { 19 | url += '/' + id; 20 | } 21 | let req = superagent.get(url); 22 | return firekylin.request(req).then( 23 | data => this.trigger(data, id ? 'getPostInfo' : 'getPostList') 24 | ); 25 | }, 26 | onSelectList(page, status, keyword) { 27 | let url = `/admin/api/post?page=${page}`; 28 | if(status) { url += `&status=${status}` } 29 | if(keyword) { url += `&keyword=${keyword}`} 30 | return firekylin.request(superagent.get(url)).then( 31 | data => this.trigger(data, 'getPostList') 32 | ); 33 | }, 34 | onSelectLastest() { 35 | let req = superagent.get('/admin/api/post/lastest'); 36 | return firekylin.request(req).then( 37 | data => this.trigger(data, 'getPostLastest') 38 | ); 39 | }, 40 | /** 41 | * save user 42 | * @param {Object} data [] 43 | * @return {Promise} [] 44 | */ 45 | onSave(data) { 46 | let id = data.id; 47 | delete data.id; 48 | let url = '/admin/api/post'; 49 | if(id) { 50 | url += '/' + id + '?method=put'; 51 | } 52 | let req = superagent.post(url); 53 | req.type('form').send(data); 54 | return firekylin.request(req).then( 55 | data => this.trigger(data, 'savePostSuccess'), 56 | err => this.trigger(err, 'savePostFail') 57 | ); 58 | }, 59 | 60 | onDelete(id) { 61 | let url = '/admin/api/post'; 62 | if(id) { 63 | url += '/' + id + '?method=delete'; 64 | } 65 | 66 | let req = superagent.post(url); 67 | return firekylin.request(req).then( 68 | data => this.trigger(data, 'deletePostSuccess'), 69 | err => this.trigger(err, 'deletePostFail') 70 | ); 71 | }, 72 | 73 | onPass({id, create_time}) { 74 | const postData = {id, status: 3}; 75 | const {auditFreshCreateTime = '1'} = window.SysConfig.options; 76 | 77 | if (Number(auditFreshCreateTime)) { 78 | postData.create_time = create_time 79 | } 80 | 81 | this.onSave(postData); 82 | }, 83 | 84 | onDeny(id) { 85 | this.onSave({id, status: 2}); 86 | }, 87 | 88 | onSearch(data) { 89 | let url = '/admin/api/search'; 90 | let req = superagent.post(url); 91 | req.type('form').send(data); 92 | return firekylin.request(req).then((data)=>{ 93 | //PostAction.search.completed(data); 94 | this.trigger(data, 'search') 95 | }); 96 | } 97 | 98 | }) 99 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/push.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | import PushAction from '../action/push'; 4 | import firekylin from 'common/util/firekylin'; 5 | 6 | export default Reflux.createStore({ 7 | 8 | listenables: PushAction, 9 | /** 10 | * select user data 11 | * @param {[type]} id [description] 12 | * @return {[type]} [description] 13 | */ 14 | onSelect(id) { 15 | let url = '/admin/api/options?type=push'; 16 | if(id) { url += `&key=${id}`; } 17 | let req = superagent.get(url); 18 | return firekylin.request(req).then( 19 | data => this.trigger(data, id ? 'getPushInfo' : 'getPushList') 20 | ); 21 | }, 22 | 23 | onSave(data) { 24 | let url = '/admin/api/options?method=put&type=push'; 25 | let req = superagent.post(url); 26 | req.type('form').send(data); 27 | return firekylin.request(req).then( 28 | data => this.trigger(data, 'savePushSuccess'), 29 | err => this.trigger(err, 'savePushFailed') 30 | ); 31 | }, 32 | 33 | onDelete(id) { 34 | let url = '/admin/api/options?method=delete&type=push'; 35 | if(id) { 36 | url += `&key=${id}`; 37 | } 38 | 39 | let req = superagent.post(url); 40 | return firekylin.request(req).then( 41 | data => this.trigger(data, 'deletePushSuccess'), 42 | err => this.trigger(err, 'deletePushFail') 43 | ); 44 | } 45 | 46 | }) 47 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/system.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | import firekylin from '../../common/util/firekylin'; 4 | import SystemAction from 'admin/action/system'; 5 | 6 | export default Reflux.createStore({ 7 | 8 | listenables: SystemAction, 9 | /** 10 | * select user data 11 | * @param {[type]} id [description] 12 | * @return {[type]} [description] 13 | */ 14 | onSelect() { 15 | let url = '/admin/api/system'; 16 | let req = superagent.get(url); 17 | return firekylin.request(req).then( 18 | data => this.trigger(data, 'getSystemInfo') 19 | ); 20 | }, 21 | onUpdateSystem(step) { 22 | let url = '/admin/api/system?method=update&step='+step; 23 | let req = superagent.get(url); 24 | return firekylin.request(req).then( 25 | () => this.trigger(null, 'updateSystem') 26 | ); 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/tag.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | import firekylin from 'common/util/firekylin'; 4 | import TagAction from 'admin/action/tag'; 5 | 6 | export default Reflux.createStore({ 7 | listenables: TagAction, 8 | 9 | onSelect(id) { 10 | let url = '/admin/api/tag'; 11 | if(id) { 12 | url += '/' + id; 13 | } 14 | let req = superagent.get(url); 15 | return firekylin.request(req).then( 16 | data => this.trigger(data, id ? 'getTagInfo' : 'getTagList') 17 | ); 18 | }, 19 | 20 | onSave(data) { 21 | let id = data.id; 22 | delete data.id; 23 | let url = '/admin/api/tag'; 24 | if(id) { 25 | url += '/' + id + '?method=put'; 26 | } 27 | let req = superagent.post(url); 28 | req.type('form').send(data); 29 | return firekylin.request(req).then( 30 | data => { 31 | if(data.id && data.id.type === 'exist') { 32 | this.trigger('TAG_EXIST', 'saveTagFail'); 33 | } else this.trigger(data, 'saveTagSuccess'); 34 | }, 35 | err => this.trigger(err, 'saveTagFail') 36 | ); 37 | }, 38 | 39 | onDelete(id) { 40 | let url = '/admin/api/tag'; 41 | if(id) { 42 | url += '/' + id + '?method=delete'; 43 | } 44 | 45 | let req = superagent.post(url); 46 | return firekylin.request(req).then( 47 | data => this.trigger(data, 'deleteTagSuccess'), 48 | err => this.trigger(err, 'deleteTagFail') 49 | ); 50 | } 51 | 52 | }) 53 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/theme.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | import firekylin from 'common/util/firekylin'; 4 | import ThemeAction from 'admin/action/theme'; 5 | 6 | export default Reflux.createStore({ 7 | listenables: ThemeAction, 8 | onList() { 9 | let url = '/admin/api/theme'; 10 | let req = superagent.get(url); 11 | return firekylin.request(req).then( 12 | data => this.trigger(data, 'getThemeSuccess'), 13 | err => this.trigger(err, 'getThemeFailed') 14 | ); 15 | }, 16 | onSave(data) { 17 | let req = superagent.post('/admin/api/options?method=put'); 18 | req.type('form').send({'theme': data}); 19 | return firekylin.request(req).then( 20 | data => this.trigger(data, 'saveThemeSuccess'), 21 | err => this.trigger(err, 'saveThemeFailed') 22 | ); 23 | }, 24 | onForkTheme(theme, new_theme) { 25 | let req = superagent.post('/admin/api/theme?method=put'); 26 | req.type('form').send({theme, new_theme}); 27 | return firekylin.request(req).then( 28 | data => this.trigger(data, 'forkThemeSuccess'), 29 | err => this.trigger(err, 'forkThemeFailed') 30 | ); 31 | }, 32 | onGetThemeFile(filePath) { 33 | let req = superagent.get('/admin/api/theme?type=file&filePath=' + filePath); 34 | return firekylin.request(req).then( 35 | data => this.trigger(data, 'getThemeFileSuccess'), 36 | err => this.trigger(err, 'getThemeFileFailed') 37 | ); 38 | }, 39 | onSaveThemeConfig(data) { 40 | let req = superagent.post('/admin/api/options?method=put'); 41 | req.type('form').send({'themeConfig': JSON.stringify(data)}); 42 | return firekylin.request(req).then( 43 | data => this.trigger(data, 'saveThemeConfigSuccess'), 44 | err => this.trigger(err, 'saveThemeConfigFailed') 45 | ); 46 | }, 47 | onGetThemeFileList(theme) { 48 | let req = superagent.get('/admin/api/theme?type=fileList&theme=' + theme); 49 | return firekylin.request(req).then( 50 | data => this.trigger(data, 'getThemeFileListSuccess'), 51 | err => this.trigger(err, 'getThemeFileListFailed') 52 | ); 53 | }, 54 | onUpdateThemeFile(filePath, content) { 55 | let req = superagent.post('/admin/api/theme?method=update'); 56 | req.type('form').send({filePath, content}); 57 | return firekylin.request(req).then( 58 | data => this.trigger(data, 'updateThemeFileSuccess'), 59 | err => this.trigger(err, 'updateThemeFileFailed') 60 | ); 61 | }, 62 | onGetPageTemplateList(theme) { 63 | let req = superagent.get('/admin/api/theme?type=templateList&theme=' + theme); 64 | return firekylin.request(req).then( 65 | data => this.trigger(data, 'getThemeTemplateListSuccess'), 66 | err => this.trigger(err, 'getThemeTemplateListFailed') 67 | ); 68 | } 69 | }) 70 | -------------------------------------------------------------------------------- /www/static/admin/src/admin/store/user.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import superagent from 'superagent'; 3 | 4 | import firekylin from '../../common/util/firekylin'; 5 | 6 | import UserAction from '../action/user'; 7 | 8 | export default Reflux.createStore({ 9 | 10 | listenables: UserAction, 11 | /** 12 | * select user data 13 | * @param {[type]} id [description] 14 | * @return {[type]} [description] 15 | */ 16 | onSelect(id, filter) { 17 | let url = '/admin/api/user'; 18 | if(id) { 19 | url += '/' + id; 20 | } 21 | if(filter) { 22 | url += '?type='+filter; 23 | } 24 | let req = superagent.get(url); 25 | return firekylin.request(req).then(data => { 26 | this.trigger(data, id ? 'getUserInfo' : 'getUserList'); 27 | }).catch(() => { 28 | 29 | }) 30 | }, 31 | /** 32 | * save user 33 | * @param {Object} data [] 34 | * @return {Promise} [] 35 | */ 36 | onSave(data) { 37 | let id = data.id; 38 | delete data.id; 39 | let url = '/admin/api/user'; 40 | if(id) { 41 | url += '/' + id + '?method=put'; 42 | } 43 | let req = superagent.post(url); 44 | req.type('form').send(data); 45 | return firekylin.request(req).then(data => { 46 | this.trigger(data, 'saveUserSuccess'); 47 | }).catch(err => { 48 | this.trigger(err, 'saveUserFail'); 49 | }) 50 | }, 51 | onSavepwd(data) { 52 | let url = '/admin/user/password'; 53 | let req = superagent.post(url); 54 | req.type('form').send(data); 55 | return firekylin.request(req).then(data => { 56 | this.trigger(data, 'saveUserSuccess'); 57 | }).catch(err => { 58 | this.trigger(err, 'saveUserFail'); 59 | }) 60 | }, 61 | /** 62 | * login 63 | * @param {[type]} data [description] 64 | * @return {[type]} [description] 65 | */ 66 | onLogin(data) { 67 | let req = superagent.post('/admin/user/login'); 68 | req.type('form').send(data); 69 | return firekylin.request(req).then(data => { 70 | this.trigger(data, 'LoginSuccess'); 71 | }).catch(err => { 72 | this.trigger(err, 'LoginFail'); 73 | }) 74 | }, 75 | 76 | onDelete(userId) { 77 | let url = '/admin/api/user/' + userId + '?method=delete'; 78 | let req = superagent.post(url); 79 | req.type('form').send(); 80 | return firekylin.request(req).then(data => { 81 | this.trigger(data, 'deleteUserSuccess'); 82 | }).catch(err => { 83 | this.trigger(err, 'deleteUserFail'); 84 | }) 85 | }, 86 | 87 | onPass(userId) { 88 | let url = '/admin/api/user/' + userId + '?method=put&type=contributor'; 89 | let req = superagent.post(url); 90 | req.type('form').send(); 91 | return firekylin.request(req).then( 92 | data => this.trigger(data, 'passUserSuccess'), 93 | err => this.trigger(err, 'passUserFailed') 94 | ); 95 | }, 96 | 97 | onGenerateKey(userId) { 98 | let url = '/admin/api/user/' + userId + '?type=key'; 99 | let req = superagent.post(url); 100 | req.type('form').send(); 101 | return firekylin.request(req).then( 102 | data => this.trigger(data, 'getUserInfo'), 103 | err => this.trigger(err, 'getUserInfoFailed') 104 | ); 105 | } 106 | }) 107 | -------------------------------------------------------------------------------- /www/static/admin/src/common/action/modal.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | confirm: {children: ['completed', 'failed']}, 5 | panel: {children: ['completed', 'failed']}, 6 | alert: {children: ['completed', 'failed']}, 7 | remove: {} 8 | }); 9 | -------------------------------------------------------------------------------- /www/static/admin/src/common/action/tip.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | export default Reflux.createActions({ 4 | success: {}, 5 | fail: {} 6 | }); 7 | -------------------------------------------------------------------------------- /www/static/admin/src/common/component/base.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { decorate as mixin } from 'react-mixin'; 3 | import { ListenerMixin } from 'reflux'; 4 | 5 | @mixin(ListenerMixin) 6 | class Base extends React.Component { 7 | 8 | static contextTypes = { 9 | router: () => React.PropTypes.func.isRequired 10 | }; 11 | 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state = {}; 15 | this.__stores = []; 16 | this.__listens = []; 17 | } 18 | 19 | componentDidMount() { 20 | 21 | } 22 | /** 23 | * redirect route 24 | * @param {String} route [] 25 | * @return {void} [] 26 | */ 27 | redirect(route) { 28 | this.context.router.push(route); 29 | } 30 | 31 | _getStoreIndex(store) { 32 | let index = this.__stores.indexOf(store); 33 | if(index > -1) { 34 | return index; 35 | } 36 | this.__stores.push(store); 37 | return this.__stores.length - 1; 38 | } 39 | /** 40 | * listen store data change 41 | * @param {[type]} store [] 42 | * @param {[type]} type [] 43 | * @param {Function} callback [] 44 | * @return {[type]} [] 45 | */ 46 | listen(store, callback, type) { 47 | let index = this._getStoreIndex(store); 48 | if(!this.__listens[index]) { 49 | this.__listens[index] = []; 50 | //添加监听 51 | this.listenTo(store, (data, triggerType) => { 52 | this.__listens[index].forEach(fn => { 53 | fn(data, triggerType); 54 | }); 55 | }); 56 | } 57 | 58 | this.__listens[index].push((data, triggerType) => { 59 | if(type && type === triggerType || !type) { 60 | callback.bind(this)(data, triggerType); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | 67 | export default Base; 68 | -------------------------------------------------------------------------------- /www/static/admin/src/common/component/editor/search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Autobind from 'autobind-decorator'; 3 | import Select, {Option} from 'rc-select'; 4 | import superagent from 'superagent'; 5 | import firekylin from 'common/util/firekylin'; 6 | 7 | class Search extends React.Component { 8 | state = { 9 | options: [] 10 | }; 11 | 12 | fetchData(value) { 13 | let req = superagent.get('/admin/api/post?status=3&keyword='+encodeURIComponent(value)); 14 | firekylin.request(req).then( 15 | resp => this.setState({options: resp.data}) 16 | ).catch(() => {}); 17 | } 18 | render() { 19 | return ( 20 | 35 | ); 36 | } 37 | } 38 | 39 | Search.propTypes = { 40 | onSelect: React.PropTypes.func.isRequired 41 | }; 42 | 43 | export default Autobind(Search); 44 | -------------------------------------------------------------------------------- /www/static/admin/src/common/component/modal_manage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ModalAction from '../action/modal'; 4 | import ModalStore from '../store/modal'; 5 | 6 | import Base from './base'; 7 | import Modal from './modal'; 8 | 9 | 10 | /** 11 | * 通用弹出层管理,支持多个弹出层 12 | */ 13 | export default class extends Base { 14 | state = { 15 | list: [] 16 | } 17 | 18 | componentDidMount() { 19 | this.listen(ModalStore, this.changeModalList, 'modalList'); 20 | document.addEventListener('keydown', this.keyEvent.bind(this)); 21 | } 22 | 23 | keyEvent(e) { 24 | if(!this.state.list.length) { 25 | return true; 26 | } 27 | 28 | if(e.keyCode === 27) { 29 | let data = this.state.list[this.state.list.length - 1]; 30 | ModalAction.remove(data.idx); 31 | if(data.cancelCallback) { 32 | data.cancelCallback(); 33 | } 34 | } 35 | 36 | if(e.keyCode === 13) { 37 | let okBtn = document.querySelector('.modal .modal-footer button.btn-primary'); 38 | if(okBtn) { 39 | okBtn.focus(); 40 | } 41 | } 42 | } 43 | 44 | changeModalList(data) { 45 | this.setState({list: data}); 46 | } 47 | 48 | render() { 49 | if(!this.state.list || this.state.list.length === 0) { 50 | return (
); 51 | } 52 | let content = this.state.list.map((item, index) => { 53 | return (); 54 | }); 55 | 56 | return (
{content}
); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /www/static/admin/src/common/component/tip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TipStore from '../store/tip'; 4 | import Base from './base'; 5 | 6 | /** 7 | * 通用Tip 8 | */ 9 | export default class extends Base { 10 | 11 | componentDidMount() { 12 | this.listenTo(TipStore, this.change.bind(this)); 13 | } 14 | /** 15 | * data : {type: 'success', text: ''} 16 | * @param {[type]} data [description] 17 | * @return {[type]} [description] 18 | */ 19 | change(data) { 20 | this.setState(data); 21 | } 22 | 23 | render() { 24 | if(this.state.isOpen) { 25 | let className = 'fk-alert alert alert-' + this.state.type; 26 | return ( 27 |
28 | {this.state.text} 29 |
30 | ); 31 | }else{ 32 | return (
); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /www/static/admin/src/common/store/modal.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import ModalActions from '../action/modal'; 4 | 5 | export default Reflux.createStore({ 6 | 7 | listenables: ModalActions, 8 | /** 9 | * modal的id 10 | * @type {Number} 11 | */ 12 | idx: 100, 13 | /** 14 | * 存放modal的列表 15 | * @type {Array} 16 | */ 17 | modalList: [], 18 | /** 19 | * 添加一个弹出层 20 | * @param {[type]} data [description] 21 | */ 22 | add: function(data) { 23 | let idx = this.idx++; 24 | data.idx = idx; 25 | 26 | this.modalList.push(data); 27 | this.trigger(this.modalList, 'modalList'); 28 | 29 | return idx; 30 | }, 31 | /** 32 | * 删除一个弹出层 33 | * @param {[type]} index [description] 34 | * @return {[type]} [description] 35 | */ 36 | remove: function(idx) { 37 | this.modalList = this.modalList.filter(item => { 38 | return item.idx !== idx; 39 | }); 40 | this.trigger(this.modalList, 'modalList'); 41 | this.trigger(null, 'removeModal'); 42 | }, 43 | /** 44 | * 关闭一个弹出层 45 | * @param {[type]} idx [description] 46 | * @return {[type]} [description] 47 | */ 48 | onRemove: function(idx) { 49 | this.remove(idx); 50 | }, 51 | /** 52 | * 弹出面板 53 | * @param {[type]} title [description] 54 | * @param {[type]} content [description] 55 | * @param {[type]} options [description] 56 | * @return {[type]} [description] 57 | */ 58 | onPanel: function(title, content, options) { 59 | let data = { 60 | type: 'panel', 61 | title: title, 62 | content: content, 63 | options: options || {} 64 | }; 65 | let idx = this.add(data); 66 | ModalActions.panel.completed(idx); 67 | }, 68 | /** 69 | * confirm 70 | * @param {[type]} text [description] 71 | * @return {[type]} [description] 72 | */ 73 | onConfirm: function(title, content, callback, className, options, cancelCallback) { 74 | let data = { 75 | type: 'confirm', 76 | title: title, 77 | content: content, 78 | callback: callback, 79 | className: className || '', 80 | options: options||{}, 81 | cancelCallback: cancelCallback 82 | }; 83 | let idx = this.add(data); 84 | ModalActions.confirm.completed(idx); 85 | }, 86 | /** 87 | * 失败 88 | * @param {[type]} text [description] 89 | * @return {[type]} [description] 90 | */ 91 | onAlert: function(title, content) { 92 | let data = { 93 | type: 'alert', 94 | title: title, 95 | content: content 96 | }; 97 | let idx = this.add(data); 98 | ModalActions.alert.completed(idx); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /www/static/admin/src/common/store/tip.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import TipActions from '../action/tip'; 4 | import firekylin from '../util/firekylin'; 5 | 6 | 7 | export default Reflux.createStore({ 8 | 9 | listenables: TipActions, 10 | 11 | clear: function(timeout) { 12 | clearTimeout(this.timer); 13 | if(this.deferred) { 14 | this.deferred.resolve(); 15 | } 16 | let deferred = firekylin.defer(); 17 | this.timer = setTimeout(() => { 18 | this.trigger({isOpen: false}); 19 | deferred.resolve(); 20 | }, timeout || 1500); 21 | this.deferred = deferred; 22 | return deferred.promise; 23 | }, 24 | 25 | /** 26 | * 成功 27 | * @param {[type]} text [description] 28 | * @return {[type]} [description] 29 | */ 30 | onSuccess: function(text, timeout) { 31 | if(typeof(text) !== 'string') { 32 | text = JSON.stringify(text); 33 | } 34 | 35 | this.trigger({ 36 | type: 'success', 37 | text: text || '操作成功', 38 | isOpen: true 39 | }); 40 | return this.clear(timeout); 41 | }, 42 | /** 43 | * 失败 44 | * @param {[type]} text [description] 45 | * @return {[type]} [description] 46 | */ 47 | onFail: function(text, timeout) { 48 | if(typeof(text) !== 'string') { 49 | text = JSON.stringify(text); 50 | } 51 | 52 | this.trigger({ 53 | type: 'danger', 54 | text: text || '操作失败', 55 | isOpen: true 56 | }); 57 | return this.clear(timeout); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /www/static/admin/src/common/util/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = function(replace = new Function()) { 2 | const user = window.SysConfig.userInfo; 3 | if(user.type !== 1) { 4 | replace({pathname: '/dashboard'}); 5 | return false; 6 | } else return true; 7 | } 8 | -------------------------------------------------------------------------------- /www/static/home/css/all.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | @import url('icon.css'); 3 | @import url('highlight.css'); 4 | @import url('sidebar.css'); 5 | @import url('header.css'); 6 | @import url('pagination.css'); 7 | @import url('article.css'); 8 | @import url('search.css'); 9 | @import url('footer.css'); 10 | @import url('comment.css'); 11 | @import url('responsive.css'); -------------------------------------------------------------------------------- /www/static/home/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | color: #666; 5 | -webkit-text-size-adjust: none; 6 | -webkit-transition: -webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 7 | transition: -webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 8 | transition: transform .2s cubic-bezier(.4,.01,.165,.99); 9 | transition: transform .2s cubic-bezier(.4,.01,.165,.99),-webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 10 | -webkit-tap-highlight-color: transparent; 11 | font-family: "Helvetica Neue",Arial,"Hiragino Sans GB",STHeiti,"Microsoft YaHei"; 12 | -webkit-font-smoothing: antialiased; 13 | -webkit-overflow-scrolling: touch; 14 | } 15 | body, 16 | html { 17 | width: 100%; 18 | height: 100%; 19 | } 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | a,a:hover { 30 | color: #2479CC; 31 | text-decoration: none; 32 | } 33 | ul,ol{padding: 0} 34 | * { 35 | -webkit-box-sizing: border-box; 36 | box-sizing: border-box; 37 | } 38 | body.side { 39 | position: fixed; 40 | -webkit-transform: translate3D(250px,0,0); 41 | -ms-transform: translate3D(250px,0,0); 42 | transform: translate3D(250px,0,0); 43 | } 44 | 45 | #main { 46 | background-color: #fff; 47 | padding-left: 290px; 48 | padding-right: 40px; 49 | max-width: 1390px; 50 | -webkit-overflow-scrolling: touch; 51 | } 52 | h1.intro{ 53 | padding: 20px 30px; 54 | background-color: #f6f9fa; 55 | text-align: center; 56 | color: #999; 57 | } -------------------------------------------------------------------------------- /www/static/home/css/comment.css: -------------------------------------------------------------------------------- 1 | #comments { 2 | border-top:1px solid #fff; 3 | border-bottom:1px solid #ddd; 4 | padding:30px 0; 5 | min-height:350px; 6 | } 7 | 8 | #comments h1.title { 9 | font-size:25px; 10 | font-weight:300; 11 | line-height:35px; 12 | margin-bottom:20px; 13 | } -------------------------------------------------------------------------------- /www/static/home/css/footer.css: -------------------------------------------------------------------------------- 1 | #footer { 2 | line-height:1.8; 3 | text-align:center; 4 | padding:15px; 5 | border-top:1px solid #fff; 6 | font-size:.9em; 7 | } 8 | 9 | #footer .beian { 10 | color:#666; 11 | } -------------------------------------------------------------------------------- /www/static/home/css/header.css: -------------------------------------------------------------------------------- 1 | 2 | #header { 3 | display: none; 4 | } 5 | 6 | #header { 7 | width: 100%; 8 | height: 50px; 9 | line-height: 50px; 10 | overflow: hidden; 11 | position: fixed; 12 | left: 0; 13 | top: 0; 14 | z-index: 9; 15 | background-color: #323436; 16 | } 17 | body.side #header .btn-bar:before { 18 | width: 24px; 19 | -webkit-transform: rotate(-45deg); 20 | -ms-transform: rotate(-45deg); 21 | transform: rotate(-45deg); 22 | top: 25px; 23 | } 24 | body.side #header .btn-bar:after { 25 | width: 24px; 26 | -webkit-transform: rotate(45deg); 27 | -ms-transform: rotate(45deg); 28 | transform: rotate(45deg); 29 | bottom: 24px; 30 | } 31 | body.side #header .btn-bar i { 32 | opacity: 0; 33 | } 34 | #header h1 { 35 | text-align: center; 36 | font-size: 16px; 37 | } 38 | #header h1 a { 39 | color: #999; 40 | } 41 | #header .btn-bar { 42 | width: 50px; 43 | height: 50px; 44 | position: absolute; 45 | left: 0; 46 | top: 0; 47 | } 48 | #header .btn-bar i, 49 | #header .btn-bar:after, 50 | #header .btn-bar:before { 51 | width: 22px; 52 | height: 1px; 53 | position: absolute; 54 | left: 14px; 55 | background-color: #999; 56 | -webkit-transition: all .2s cubic-bezier(.4,.01,.165,.99) .3s; 57 | transition: all .2s cubic-bezier(.4,.01,.165,.99) .3s; 58 | } 59 | 60 | #header .btn-bar i { 61 | top: 25px; 62 | opacity: 1; 63 | } 64 | #header .btn-bar:before { 65 | content: ''; 66 | top: 17px; 67 | } 68 | #header .btn-bar:after { 69 | content: ''; 70 | bottom: 16px; 71 | } 72 | #header a.me, 73 | #header a.me img { 74 | width: 30px; 75 | height: 30px; 76 | border-radius: 30px; 77 | overflow: hidden; 78 | } 79 | #header a.me { 80 | position: absolute; 81 | right: 10px; 82 | top: 10px; 83 | } -------------------------------------------------------------------------------- /www/static/home/css/icon.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: iconfont; 4 | src: url(../font/iconfont.eot); 5 | src: url(../font/iconfont.eot?#iefix) format("embedded-opentype"),url(../font/iconfont.ttf) format("truetype"),url(../font/iconfont.svg#iconfont) format("svg"); 6 | } 7 | .iconfont { 8 | font-family: iconfont!important; 9 | font-size: 16px; 10 | font-style: normal; 11 | -webkit-font-smoothing: antialiased; 12 | -webkit-text-stroke-width: .2px; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-weibo:before { 17 | content: "\e600"; 18 | } 19 | .icon-archive:before { 20 | content: "\e601"; 21 | } 22 | .icon-user:before { 23 | content: "\e602"; 24 | } 25 | .icon-rss-v:before { 26 | content: "\e603"; 27 | } 28 | .icon-tags:before { 29 | content: "\e604"; 30 | } 31 | .icon-home:before { 32 | content: "\e605"; 33 | } 34 | .icon-search:before { 35 | content: "\e606"; 36 | } 37 | .icon-googleplus:before { 38 | content: "\e607"; 39 | } 40 | .icon-weixin:before { 41 | content: "\e608"; 42 | } 43 | .icon-mail:before { 44 | content: "\e609"; 45 | } 46 | .icon-twitter-v:before { 47 | content: "\e60a"; 48 | } 49 | .icon-linkedin:before { 50 | content: "\e60b"; 51 | } 52 | .icon-stackoverflow:before { 53 | content: "\e60c"; 54 | } 55 | .icon-github-v:before { 56 | content: "\e60d"; 57 | } 58 | .icon-facebook:before { 59 | content: "\e60e"; 60 | } 61 | .icon-right:before { 62 | content: "\e60f"; 63 | } 64 | .icon-left:before { 65 | content: "\e610"; 66 | } 67 | .icon-link:before { 68 | content: "\e611"; 69 | } 70 | .icon-https:before { 71 | content: "\e612"; 72 | } -------------------------------------------------------------------------------- /www/static/home/css/pagination.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | width:100%; 3 | line-height:20px; 4 | position:relative; 5 | border-top:1px solid #fff; 6 | border-bottom:1px solid #ddd; 7 | padding:20px 0; 8 | overflow:hidden; 9 | } 10 | 11 | .pagination .prev { 12 | float:left; 13 | } 14 | 15 | .pagination .next { 16 | float:right; 17 | } 18 | .pagination .center { 19 | text-align:center; 20 | width:80px; 21 | margin:auto; 22 | } -------------------------------------------------------------------------------- /www/static/home/css/responsive.css: -------------------------------------------------------------------------------- 1 | 2 | @media screen and (max-width:768px) { 3 | #header { 4 | -webkit-transform: translate3D(0,0,0); 5 | -ms-transform: translate3D(0,0,0); 6 | transform: translate3D(0,0,0); 7 | -webkit-transition: all .2s cubic-bezier(.4,.01,.165,.99); 8 | transition: all .2s cubic-bezier(.4,.01,.165,.99); 9 | display: block; 10 | } 11 | a.anchor { 12 | top: -50px; 13 | } 14 | } 15 | 16 | @media screen and (max-width:768px) { 17 | #sidebar.behavior_1 { 18 | -webkit-transform: translate3D(-250px,0,0); 19 | -ms-transform: translate3D(-250px,0,0); 20 | transform: translate3D(-250px,0,0); 21 | } 22 | #sidebar.behavior_2 { 23 | -webkit-transform: translate3D(0,0,0); 24 | -ms-transform: translate3D(0,0,0); 25 | transform: translate3D(0,0,0); 26 | } 27 | #sidebar { 28 | -webkit-transition: -webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 29 | transition: -webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 30 | transition: transform .2s cubic-bezier(.4,.01,.165,.99); 31 | transition: transform .2s cubic-bezier(.4,.01,.165,.99),-webkit-transform .2s cubic-bezier(.4,.01,.165,.99); 32 | } 33 | #sidebar .profile { 34 | padding-top: 20px; 35 | padding-bottom: 20px; 36 | } 37 | #sidebar .profile a, 38 | #sidebar .profile img { 39 | width: 100px; 40 | height: 100px; 41 | border-radius: 100px; 42 | } 43 | #sidebar .profile span { 44 | display: none; 45 | } 46 | } 47 | 48 | @media screen and (min-width:769px) and (max-width:1024px) { 49 | #sidebar { 50 | width: 75px; 51 | } 52 | #sidebar .profile { 53 | padding-top: 20px; 54 | } 55 | #sidebar .profile a, 56 | #sidebar .profile img { 57 | width: 40px; 58 | height: 40px; 59 | border-radius: 40px; 60 | } 61 | #sidebar .profile span { 62 | display: none; 63 | } 64 | #sidebar .buttons li a { 65 | padding: 0; 66 | } 67 | #sidebar .buttons li a.inline { 68 | width: 100%; 69 | } 70 | #sidebar .buttons li a i { 71 | font-size: 18px; 72 | display: block; 73 | margin: 0 auto; 74 | } 75 | #sidebar .buttons li a span { 76 | display: none; 77 | } 78 | } 79 | 80 | @media screen and (min-width:768px) and (max-width:1024px) { 81 | #main { 82 | padding-left: 115px; 83 | } 84 | } 85 | 86 | @media screen and (max-width:769px) { 87 | #main { 88 | width: 100%; 89 | min-height: 100%; 90 | padding-top: 50px; 91 | padding-left: 10px; 92 | padding-right: 10px; 93 | } 94 | } 95 | 96 | @media screen and (max-width:769px) { 97 | article { 98 | background-color: #fff; 99 | padding: 10px; 100 | } 101 | article .meta { 102 | display: none; 103 | } 104 | article h1 { 105 | font-size: 22px; 106 | padding: 5px 0 10px; 107 | margin: 0; 108 | } 109 | article .desc { 110 | color: #999; 111 | font-size: 14px; 112 | } 113 | article p.more { 114 | font-size: 14px; 115 | margin: 5px 0; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /www/static/home/css/search.css: -------------------------------------------------------------------------------- 1 | #search .wrapper { 2 | margin-right: 72px; 3 | } 4 | 5 | #search form { 6 | position:relative; 7 | } 8 | 9 | #search .wrapper input { 10 | -webkit-appearance: none; 11 | border: 1px solid #bbb; 12 | border-radius: 0; 13 | box-sizing: border-box; 14 | display: block; 15 | font-size: 16px; 16 | height: 40px; 17 | outline: 0; 18 | padding: 4px 6px; 19 | width: 100%; 20 | } 21 | 22 | #search .submit { 23 | -webkit-appearance: none; 24 | background-color: #e7e7e7; 25 | border: 1px solid #bbb; 26 | border-left: 0; 27 | border-radius: 0; 28 | color: #222; 29 | display: block; 30 | font-size: 16px; 31 | height: 40px; 32 | outline: 0; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | width: 72px; 37 | } 38 | 39 | #searchResult { 40 | min-height:350px; 41 | } 42 | 43 | #searchResult .info { 44 | color: #676767; 45 | font-size: 13px; 46 | padding: 15px 0; 47 | border-bottom:1px solid #e9e9e9; 48 | } 49 | 50 | #searchResult .no-result { 51 | padding: 5px; 52 | margin: 15px 0; 53 | border: 1px solid rgb(255,204,51); 54 | background-color: rgb(255,244,194); 55 | font-size:13px; 56 | } 57 | 58 | #searchResult .loading { 59 | margin-top:20px; 60 | } 61 | 62 | #searchResult .hot-words { 63 | margin-top:20px; 64 | } 65 | 66 | #searchResult .hot-words a { 67 | margin-right:20px; 68 | } 69 | 70 | #searchResult .item { 71 | padding:.5em 0 0.3em 0; 72 | } 73 | 74 | #searchResult .item .title a { 75 | font-size:16px; 76 | text-decoration:underline; 77 | } 78 | 79 | #searchResult .item .title .type { 80 | display:inline-block; 81 | background-color:#eee; 82 | color:#888; 83 | margin-right:8px; 84 | padding:0 5px; 85 | font-size: 13px; 86 | border-radius: 3px; 87 | } 88 | 89 | #searchResult .item .desc { 90 | font-size:14px; 91 | line-height:1.6; 92 | } 93 | 94 | #searchResult .item .tags { 95 | font-size: 14px; 96 | line-height:1.8; 97 | } 98 | 99 | #searchResult .item .tags a { 100 | margin-right:12px; 101 | color: #666; 102 | } 103 | 104 | #searchResult .item .title b, 105 | #searchResult .item .desc b { 106 | color:#C00; 107 | font-weight:normal; 108 | } -------------------------------------------------------------------------------- /www/static/home/css/sidebar.css: -------------------------------------------------------------------------------- 1 | 2 | #sidebar { 3 | width: 250px; 4 | height: 100%; 5 | position: fixed; 6 | left: 0; 7 | top: 0; 8 | background-color: #202020; 9 | overflow: auto; 10 | z-index: 1; 11 | -webkit-overflow-scrolling: touch; 12 | } 13 | #sidebar li, 14 | #sidebar ul { 15 | margin: 0; 16 | padding: 0; 17 | list-style: none; 18 | } 19 | #sidebar .profile { 20 | padding-top: 40px; 21 | padding-bottom: 10px; 22 | } 23 | #sidebar .profile a, 24 | #sidebar .profile img { 25 | width: 140px; 26 | height: 140px; 27 | border-radius: 70px; 28 | overflow: hidden; 29 | } 30 | #sidebar .profile a { 31 | display: block; 32 | margin: 0 auto; 33 | } 34 | #sidebar .profile span { 35 | display: block; 36 | padding: 10px 0; 37 | font-size: 18px; 38 | color: #999; 39 | text-align: center; 40 | } 41 | #sidebar .buttons { 42 | margin: 0 0 20px; 43 | } 44 | #sidebar .buttons li { 45 | display: block; 46 | width: 100%; 47 | height: 45px; 48 | line-height: 45px; 49 | font-size: 16px; 50 | } 51 | #sidebar .buttons li a { 52 | padding-left: 25px; 53 | display: block; 54 | color: #999; 55 | -webkit-transition: color .2s cubic-bezier(.4,.01,.165,.99); 56 | transition: color .2s cubic-bezier(.4,.01,.165,.99); 57 | text-decoration: none; 58 | } 59 | #sidebar .buttons li a i, 60 | #sidebar .buttons li a span { 61 | display: inline-block; 62 | vertical-align: middle; 63 | } 64 | #sidebar .buttons li a i { 65 | font-size: 20px; 66 | width: 25px; 67 | height: 45px; 68 | line-height: 45px; 69 | text-align: center; 70 | margin-right: 20px; 71 | } 72 | #sidebar .buttons li a:hover { 73 | color: rgba(153,153,153,.8); 74 | } 75 | #sidebar .buttons li a.inline{ 76 | display: inline-block; 77 | width: 40px; 78 | } 79 | #sidebar-mask { 80 | position: absolute; 81 | left: 0; 82 | top: 0; 83 | right: 0; 84 | bottom: 0; 85 | z-index: 999; 86 | overflow: hidden; 87 | display: none; 88 | background-color: rgba(255,255,255,0); 89 | } -------------------------------------------------------------------------------- /www/static/home/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/home/font/iconfont.eot -------------------------------------------------------------------------------- /www/static/home/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/home/font/iconfont.ttf -------------------------------------------------------------------------------- /www/static/home/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/blog/34d731ed4b6d457b65c474b54a03e780b772fd36/www/static/home/font/iconfont.woff -------------------------------------------------------------------------------- /www/sw.js: -------------------------------------------------------------------------------- 1 | importScripts("https://cdn.staticfile.org/workbox-sw/6.5.3/workbox-sw.js"); 2 | 3 | // workbox.setConfig({ 4 | // debug: true, 5 | // }); 6 | 7 | const { registerRoute } = workbox.routing; 8 | const { StaleWhileRevalidate, CacheFirst } = workbox.strategies; 9 | const { ExpirationPlugin } = workbox.expiration; 10 | const { CacheableResponsePlugin } = workbox.cacheableResponse; 11 | 12 | workbox.core.setCacheNameDetails({ 13 | prefix: "", 14 | precache: "xiaowudev-precache", 15 | suffix: "", 16 | }); 17 | 18 | workbox.core.skipWaiting(); 19 | workbox.core.clientsClaim(); 20 | 21 | // 百度统计.js缓存7天 22 | registerRoute( 23 | ({ url }) => url.origin === "https://hm.baidu.com" && url.pathname === "/hm.js", 24 | getRouteHandle(getCacheFirstHandle({ maxAgeSeconds: 24 * 60 * 60 * 7 })) 25 | ); 26 | 27 | // `/post/*.html` 做1天缓存 28 | registerRoute( 29 | ({ url, sameOrigin }) => 30 | sameOrigin && /^\/post\/[0-9a-zA-Z-_]+\.html$/.test(url.pathname), 31 | getRouteHandle(getCacheFirstHandle({ maxAgeSeconds: 24 * 60 * 60 })), 32 | ); 33 | 34 | // 首页做1小时间缓存 35 | registerRoute( 36 | ({ url, sameOrigin }) => 37 | sameOrigin && ["/", ""].includes(url.pathname) && !url.search.includes('page='), 38 | getRouteHandle(getCacheFirstHandle({ maxAgeSeconds: 60 * 60 })), 39 | ); 40 | 41 | // 列表相关页面做3小时间缓存 42 | registerRoute( 43 | ({ url, sameOrigin }) => 44 | sameOrigin && /^\/(archives|tags|links)\/?$/.test(url.pathname), 45 | getRouteHandle(getCacheFirstHandle({ maxAgeSeconds: 3 * 60 * 60 })), 46 | ); 47 | 48 | // 静态文件预缓存 49 | workbox.precaching.precacheAndRoute(self.__WB_MANIFEST || []); 50 | 51 | function getHeaders(headers) { 52 | const data = {}; 53 | for (const [key, value] of headers.entries()) { 54 | data[key] = value; 55 | } 56 | return data; 57 | } 58 | 59 | function getCacheFirstHandle({ maxAgeSeconds = 24 * 60 * 60, cacheName = "xiaowudev-page" } = {}) { 60 | return new CacheFirst({ 61 | cacheName, 62 | plugins: [ 63 | { 64 | cacheKeyWillBeUsed: async ({ request }) => request.url.split("?")[0], 65 | }, 66 | new CacheableResponsePlugin({ 67 | statuses: [0, 200], 68 | }), 69 | new ExpirationPlugin({ 70 | maxAgeSeconds, 71 | maxEntries: 100, 72 | }) 73 | ] 74 | }); 75 | } 76 | 77 | function getRouteHandle(CacheHandle) { 78 | return async ({ event, request }) => { 79 | let cache = "HIT"; 80 | let body; 81 | let headers = {}; 82 | 83 | try { 84 | const res = await CacheHandle.handle({ event, request }); 85 | body = await res.text(); 86 | headers = getHeaders(res.headers); 87 | } catch (e) { 88 | const res = await fetch(request); 89 | body = await res.text(); 90 | cache = "MISS"; 91 | headers = getHeaders(res.headers); 92 | } 93 | return new Response(body, { 94 | headers: { 95 | ...headers, 96 | "x-service-worker": cache, 97 | }, 98 | }); 99 | }; 100 | } 101 | --------------------------------------------------------------------------------