├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .snyk ├── .stats ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── controller │ ├── home.js │ └── yuque.js ├── public │ └── service-worker.js ├── router.js ├── schedule │ └── refresh-bing-images.js ├── service │ ├── bing.js │ └── yuque.js └── util │ └── md.js ├── config.yml ├── config ├── config.default.js ├── config.local.js ├── config.prod.js ├── config.unittest.js ├── plugin.js └── webpack.config.js ├── jsconfig.json ├── package.json ├── test └── controller │ ├── home.test.js │ └── yuque.test.js └── themes └── txd ├── common └── router.jsx ├── containers ├── Footer │ ├── index.jsx │ └── index.scss ├── Header │ ├── index.jsx │ └── index.scss ├── HomeFooter │ ├── index.jsx │ └── index.scss ├── HomeHeader │ ├── index.jsx │ └── index.scss ├── Layout │ └── index.jsx └── ModalNav │ ├── index.jsx │ └── index.scss ├── index.jsx ├── pages ├── 404 │ ├── index.jsx │ └── index.scss ├── About │ ├── index.jsx │ └── index.scss ├── Blog │ ├── index.jsx │ └── index.scss ├── Careers │ └── index.jsx ├── Home │ ├── index.jsx │ └── index.scss └── Post │ ├── index.jsx │ └── index.scss ├── services └── yuque.js ├── stores ├── app.js └── post.js ├── styles ├── base │ ├── _animate.scss │ ├── _base.scss │ ├── _common.scss │ ├── _layout.scss │ └── _type.scss ├── markdown.scss ├── mixins │ ├── _anim.scss │ ├── _breakpoints.scss │ ├── _manifest.scss │ ├── _responsive.scss │ ├── _type.scss │ └── _util.scss ├── site.scss └── variables │ ├── _colors.scss │ ├── _dimensions.scss │ ├── _fontfamily.scss │ └── _manifest.scss ├── utils ├── axios.js └── format.js └── widgets ├── Hscroll ├── hscroll.js └── index.jsx ├── IconLink ├── index.jsx └── index.scss ├── Icons ├── index.jsx ├── svgicon.js └── svgicon_config.js ├── Loader └── index.jsx ├── MobileSummary ├── index.jsx └── index.scss ├── PostCard ├── index.jsx └── index.scss ├── PostContent ├── index.jsx └── index.scss ├── PostLink ├── index.jsx ├── index.scss └── post_link.jsx ├── PostMeta ├── index.jsx └── index.scss └── PostSummary ├── index.jsx └── index.scss /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-decorators-legacy"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app/proxy* 2 | coverage 3 | dist 4 | fntest 5 | mocks 6 | node_modules 7 | spm_modules 8 | test/fixtures 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-beidou', 3 | rules: { 4 | 'react/jsx-uses-react': 'error', 5 | 'react/jsx-uses-vars': 'error', 6 | 'react/forbid-prop-types': [1, { forbid: ['any'] }], 7 | 'react/prefer-stateless-function': 0, 8 | 'no-template-curly-in-string': 0, 9 | 'react/no-danger': 0, 10 | 'react/prop-types': 0, 11 | 'no-mixed-operators': 0 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .isomorphic 4 | .node-diamond-client-cache 5 | .project 6 | .settings 7 | .npminstall.done 8 | app/proxy 9 | app/proxy_class_map.js 10 | app/proxy-class 11 | app/proxy-enums 12 | assembly 13 | assembly/ 14 | assets 15 | build/* 16 | config/serverEnv 17 | config/env 18 | debug 19 | logs 20 | mocks_data 21 | mocks_data/proxy/**/__* 22 | node_modules 23 | npm-debug.log 24 | run 25 | track 26 | .run/ 27 | .vscode/ 28 | !.run/isomorphic 29 | 30 | package-lock.json 31 | yarn.lock 32 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | package.json 4 | app/ 5 | config/ 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.stats: -------------------------------------------------------------------------------- 1 | Hash: 3c16366ce0f452ee741f 2 | Version: webpack 3.12.0 3 | Time: 9112ms 4 | Asset Size Chunks Chunk Names 5 | index.js?3c16366ce0f452ee741f 338 kB 0 [emitted] [big] index 6 | manifest.js 805 bytes 1 [emitted] manifest 7 | index.css 94.1 kB 0 [emitted] index 8 | precache-manifest.60144109bbe17651b1eb050c29e4f6f4.js 30 bytes [emitted] 9 | service-worker.js 1.31 kB [emitted] 10 | [0] ./node_modules/react/index.js 190 bytes {0} [built] 11 | [24] ./node_modules/react-dom/index.js 1.36 kB {0} [built] 12 | [38] ./themes/txd/pages/Home/index.scss 41 bytes {0} [built] 13 | [39] multi ./themes/txd/index.jsx 28 bytes {0} [built] 14 | [40] ./themes/txd/index.jsx 6 kB {0} [built] 15 | [49] ./themes/txd/common/router.jsx 1.62 kB {0} [built] 16 | [77] ./themes/txd/stores/app.js 3.64 kB {0} [built] 17 | [101] ./themes/txd/containers/Layout/index.jsx 1.89 kB {0} [built] 18 | [114] ./themes/txd/pages/Home/index.jsx 4.72 kB {0} [built] 19 | [117] ./themes/txd/pages/Post/index.jsx 4.7 kB {0} [built] 20 | [127] ./themes/txd/pages/Blog/index.jsx 5.03 kB {0} [built] 21 | [131] ./themes/txd/pages/About/index.jsx 5.87 kB {0} [built] 22 | [134] ./themes/txd/pages/Careers/index.jsx 134 bytes {0} [built] 23 | [135] ./themes/txd/pages/404/index.jsx 2.11 kB {0} [built] 24 | [137] ./themes/txd/styles/site.scss 41 bytes {0} [built] 25 | + 143 hidden modules 26 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Home/index.scss: 27 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Home/index.scss 5.12 kB {0} [built] 28 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 29 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/404/index.scss: 30 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/404/index.scss 217 bytes {0} [built] 31 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 32 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/styles/markdown.scss: 33 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/styles/markdown.scss 37.7 kB {0} [built] 34 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 35 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/styles/site.scss: 36 | [0] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 37 | [1] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/styles/site.scss 6.09 kB {0} [built] 38 | [2] ./node_modules/css-loader?{"importLoaders":1,"minimize":true,"sourceMap":false,"modules":false}!./node_modules/postcss-loader/lib??postcss!./node_modules/normalize.css/normalize.css 2.06 kB {0} [built] 39 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Blog/index.scss: 40 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Blog/index.scss 4.08 kB {0} [built] 41 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 42 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/About/index.scss: 43 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/About/index.scss 3.71 kB {0} [built] 44 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 45 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostMeta/index.scss: 46 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostMeta/index.scss 3.62 kB {0} [built] 47 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 48 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostSummary/index.scss: 49 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostSummary/index.scss 4.5 kB {0} [built] 50 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 51 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/pages/Post/index.scss: 52 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/pages/Post/index.scss 4.13 kB {0} [built] 53 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 54 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/Footer/index.scss: 55 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/Footer/index.scss 2.83 kB {0} [built] 56 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 57 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostCard/index.scss: 58 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostCard/index.scss 5.41 kB {0} [built] 59 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 60 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/widgets/PostContent/index.scss: 61 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/widgets/PostContent/index.scss 3.48 kB {0} [built] 62 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 63 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/HomeFooter/index.scss: 64 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/HomeFooter/index.scss 2.91 kB {0} [built] 65 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 66 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/HomeHeader/index.scss: 67 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/HomeHeader/index.scss 3.94 kB {0} [built] 68 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 69 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/Header/index.scss: 70 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/Header/index.scss 3.98 kB {0} [built] 71 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] 72 | Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--3-2!node_modules/postcss-loader/lib/index.js??postcss!node_modules/sass-loader/lib/loader.js!themes/txd/containers/ModalNav/index.scss: 73 | [0] ./node_modules/css-loader??ref--3-2!./node_modules/postcss-loader/lib??postcss!./node_modules/sass-loader/lib/loader.js!./themes/txd/containers/ModalNav/index.scss 3.51 kB {0} [built] 74 | [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.11.3-alpine 2 | 3 | ENV TIME_ZONE=Asia/Shanghai 4 | 5 | RUN \ 6 | mkdir -p /usr/src/app \ 7 | && apk add --no-cache tzdata \ 8 | && echo "${TIME_ZONE}" > /etc/timezone \ 9 | && ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime 10 | 11 | WORKDIR /usr/src/app 12 | 13 | COPY package.json /usr/src/app/ 14 | 15 | # RUN npm i 16 | 17 | RUN npm i --registry=https://registry.npmmirror.com 18 | 19 | COPY . /usr/src/app 20 | 21 | EXPOSE 7001 22 | 23 | CMD npm run start -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 xcold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yuque-blog 2 | 3 | yuque—blog 是一款基于[语雀](http://yuque.com/)内容管理平台的博客系统,用户可以在语雀上进行文档仓库的管理,然后在自定义的站点中展示这些内容 4 | 5 | ## 主要特性 6 | 7 | - 优秀的文档编辑和管理体验(Powered by 语雀) 8 | - 极速输出博客页面 9 | - 可定制的博客主题 10 | - 支持服务端渲染 11 | - 支持 PWA 及离线访问 12 | - 便捷的运维体验,提供一键部署的 Docker 镜像 13 | 14 | ## 技术栈 15 | 16 | - 后端:Beidou (基于 Egg 和 React 的高性能同构框架) 17 | - 前端:React / Reach-Router / Mobx / Axios / Mock.js / WorkBox 18 | 19 | ## 配置文件 20 | 21 | > config.yml 22 | 23 | ``` 24 | # 主题 25 | theme: txd 26 | 27 | # 语雀 API 设置 28 | yuque: 29 | base: https://www.yuque.com/api/v2 30 | login: yinzhi 31 | repo: blog 32 | 33 | # Site 34 | title: 小冷的备忘录 35 | subtitle: 但凡能引起思考的句子,都是些好句子 36 | keywords: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5 37 | description: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5 38 | author: 小冷 39 | language: zh-CN 40 | 41 | # 友情链接 42 | links: 43 | - 44 | name: 阿里巴巴 45 | url: https://www.alibaba.com 46 | - 47 | name: 阿里巴巴国际UED 48 | url: http://www.aliued.com/ 49 | - 50 | name: 阿里巴巴U一点 51 | url: http://www.aliued.cn/ 52 | 53 | # 导航链接 54 | navigators: 55 | - 56 | name: HOME 57 | url: / 58 | - 59 | name: BLOG 60 | url: /blogs 61 | 62 | ``` 63 | 64 | ## Usage 65 | 66 | ### Install 67 | 68 | ``` 69 | npm install 70 | ``` 71 | 72 | ### 启动开发环境 73 | 74 | ``` 75 | npm run dev 76 | ``` 77 | 78 | ### 生产环境前端构建 79 | 80 | ``` 81 | npm run build 82 | ``` 83 | 84 | ### 生产环境开启 85 | 86 | ``` 87 | npm start 88 | ``` 89 | 90 | 访问: http://localhost:6001/ 91 | 92 | ## Docker 93 | 94 | ``` 95 | $ docker build -t egg-boilerplate . 96 | $ docker run -p 7001:7001 egg-boilerplate 97 | ``` 98 | 99 | ## TODO 100 | 101 | - [x] 后端项目初始化 102 | - [x] 前端项目初始化 103 | - [x] 语雀仓库相关开放 API 服务 104 | - [x] 接口单元测试 105 | - [x] 接口代理开发 106 | - [x] mock 数据准备 107 | - [x] PWA 整站离线支持 108 | - [x] 页面开发 109 | - [x] 内容可配置 110 | - [x] 主题可定制 111 | - [ ] 页面优化 112 | 113 | 114 | ## License 115 | 116 | [MIT](LICENSE) 117 | -------------------------------------------------------------------------------- /app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const mobileUserAgentRegx = 8 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/; 9 | 10 | module.exports = (app) => { 11 | const { config } = app; 12 | const { blog: blogConfig } = config; 13 | 14 | class HomeController extends Controller { 15 | async renderPage(page, data) { 16 | const { ctx } = this; 17 | const userAgent = ctx.get('User-Agent'); 18 | const mobileMode = !!userAgent && mobileUserAgentRegx.test(userAgent); 19 | await ctx.render('index', { 20 | config: blogConfig, 21 | env: config.env, 22 | mobileMode, 23 | ...data, 24 | }); 25 | } 26 | 27 | async defaultRoute() { 28 | const { ctx } = this; 29 | const posts = await ctx.service.yuque.getArticleList(); 30 | await this.renderPage('index', { 31 | posts: posts.data, 32 | }); 33 | } 34 | 35 | async postRoute() { 36 | const { ctx } = this; 37 | const { slug } = ctx.params; 38 | const post = await ctx.service.yuque.getArticleDetail(slug); 39 | if (!post.data) { 40 | return ctx.redirect('/404.html'); 41 | } 42 | await this.renderPage('index', { 43 | post: post.data, 44 | }); 45 | } 46 | 47 | async serviceWorker() { 48 | const { ctx } = this; 49 | const filePath = path.join(app.baseDir, 'app/public/service-worker.js'); 50 | const promise = new Promise((resolve, reject) => { 51 | fs.readFile(filePath, (err, data) => { 52 | if (err) { 53 | return reject(err); 54 | } 55 | resolve(data); 56 | }); 57 | }); 58 | const text = await promise; 59 | ctx.type = 'application/javascript'; 60 | ctx.body = text; 61 | } 62 | } 63 | return HomeController; 64 | }; 65 | 66 | -------------------------------------------------------------------------------- /app/controller/yuque.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | 5 | class YuqueController extends Controller { 6 | async getArticleList() { 7 | const { ctx } = this; 8 | const result = await ctx.service.yuque.getArticleList(); 9 | ctx.body = result; 10 | } 11 | 12 | async getArticleDetail() { 13 | const { ctx } = this; 14 | const { slug } = ctx.params; 15 | const result = await ctx.service.yuque.getArticleDetail(slug); 16 | ctx.body = result; 17 | } 18 | 19 | async getArticleToc() { 20 | const { ctx } = this; 21 | const result = await ctx.service.yuque.getArticleToc(); 22 | ctx.body = result; 23 | } 24 | 25 | async getUser() { 26 | const { ctx } = this; 27 | const { id } = ctx.params; 28 | const result = await ctx.service.yuque.getUser(id); 29 | ctx.body = result; 30 | } 31 | } 32 | 33 | module.exports = YuqueController; 34 | -------------------------------------------------------------------------------- /app/public/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | /* eslint no-undef: "off" */ 14 | /* eslint no-restricted-globals: "off" */ 15 | 16 | importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js'); 17 | 18 | workbox.setConfig({ 19 | modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/', 20 | }); 21 | 22 | workbox.core.setCacheNameDetails({ prefix: 'webpack-pwa' }); 23 | 24 | workbox.skipWaiting(); 25 | workbox.clientsClaim(); 26 | 27 | /** 28 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 29 | * requests for URLs in the manifest. 30 | * See https://goo.gl/S9QRab 31 | */ 32 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 33 | workbox.precaching.suppressWarnings(); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, { 35 | directoryIndex: '/', 36 | }); 37 | 38 | workbox.routing.registerRoute( 39 | /\.(?:png|jpg|jpeg|svg)$/, 40 | workbox.strategies.cacheFirst({ 41 | cacheName: 'images1', 42 | plugins: [ 43 | new workbox.expiration.Plugin({ 44 | maxEntries: 100, 45 | purgeOnQuotaError: false, 46 | }), 47 | ], 48 | }), 49 | 'GET' 50 | ); 51 | 52 | workbox.routing.registerRoute(/\.*/, workbox.strategies.networkFirst(), 'GET'); 53 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | // 语雀文章 API 3 | app.get('/api/yuque/ariticles', app.controller.yuque.getArticleList); 4 | app.get('/api/yuque/ariticleToc', app.controller.yuque.getArticleToc); 5 | app.get('/api/yuque/ariticle/:slug', app.controller.yuque.getArticleDetail); 6 | app.get('/api/yuque/user/:id', app.controller.yuque.getUser); 7 | 8 | // sw 9 | app.get('', '/service-worker.js', app.controller.home.serviceWorker); 10 | 11 | // 页面相关 12 | app.get('', '/post/:slug', app.controller.home.postRoute); 13 | app.get('', '/404.html', app.controller.home.defaultRoute); 14 | app.get('', '/*', app.controller.home.defaultRoute); 15 | }; 16 | -------------------------------------------------------------------------------- /app/schedule/refresh-bing-images.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const lodash = require('lodash'); 4 | 5 | module.exports = (app) => { 6 | return { 7 | schedule: { 8 | cron: '0 0 */3 * * *', 9 | type: 'all', 10 | immediate: true, 11 | }, 12 | 13 | async task(ctx) { 14 | const { service } = ctx; 15 | const images = await service.bing.getImages(); 16 | let bingImages = lodash.get(images, 'images', []); 17 | bingImages = bingImages.map( 18 | item => `//cn.bing.com/${item.urlbase}_800x600.jpg` 19 | ); 20 | if (bingImages.length > 0) { 21 | app.bingImages = bingImages; 22 | } 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /app/service/bing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Service } = require('egg'); 4 | 5 | module.exports = () => { 6 | const Api = 'https://cn.bing.com/HPImageArchive.aspx'; 7 | class BingService extends Service { 8 | async getImages() { 9 | const { ctx } = this; 10 | const now = Date.now(); 11 | const api = `${Api}?format=js&idx=0&n=8&nc=${now}&pid=hp`; 12 | const result = await ctx.curl(api, { 13 | dataType: 'json', 14 | }); 15 | return result.data; 16 | } 17 | } 18 | return BingService; 19 | }; 20 | -------------------------------------------------------------------------------- /app/service/yuque.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const lodash = require('lodash'); 5 | const { Service } = require('egg'); 6 | 7 | const md = require('../util/md'); 8 | 9 | module.exports = (app) => { 10 | const { config } = app; 11 | const { blog } = config; 12 | const { yuque: yuqueConfig } = blog; 13 | const { base, login, repo } = yuqueConfig; 14 | assert(login && repo, 'login 和 repo 必须配置'); 15 | const namespace = `${login}/${repo}`; 16 | const API_HOST = base || 'https://www.yuque.com/api/v2'; 17 | 18 | class YuqueService extends Service { 19 | getRandomImage() { 20 | const { bingImages = [] } = app; 21 | const rand = Math.floor(Math.random() * bingImages.length); 22 | return bingImages[rand]; 23 | } 24 | 25 | async getArticleList() { 26 | const { ctx } = this; 27 | const api = `${API_HOST}/repos/${namespace}/docs`; 28 | const result = await ctx.curl(api, { 29 | dataType: 'json', 30 | }); 31 | const { data } = result; 32 | let list; 33 | try { 34 | list = data.data; 35 | list.forEach((article) => { 36 | article.thumb = this.getRandomImage(); 37 | }); 38 | } catch (error) { 39 | ctx.logger.error(error); 40 | } 41 | data.data = list; 42 | return data; 43 | } 44 | 45 | async getArticleDetail(slug) { 46 | const { ctx } = this; 47 | const data = await this.getArticleDetailRaw(slug); 48 | let article; 49 | try { 50 | article = data.data; 51 | article = lodash.pick(article, [ 52 | 'id', 53 | 'slug', 54 | 'title', 55 | 'book_id', 56 | 'user_id', 57 | 'format', 58 | 'public', 59 | 'status', 60 | 'likes_count', 61 | 'comments_count', 62 | 'content_updated_at', 63 | 'deleted_at', 64 | 'created_at', 65 | 'updated_at', 66 | 'published_at', 67 | 'word_count', 68 | '_serializer', 69 | 'book', 70 | 'creator', 71 | 'body', 72 | 'body_html', 73 | 'body_draft', 74 | ]); 75 | article.body_html = md.render(article.body); 76 | article.thumb = this.getRandomImage(); 77 | } catch (error) { 78 | ctx.logger.error(error); 79 | } 80 | data.data = article; 81 | return data; 82 | } 83 | 84 | async getArticleDetailRaw(slug) { 85 | const { ctx } = this; 86 | const api = `${API_HOST}/repos/${namespace}/docs/${slug}?raw=true`; 87 | const result = await ctx.curl(api, { 88 | dataType: 'json', 89 | }); 90 | return result.data; 91 | } 92 | 93 | async getArticleToc() { 94 | const { ctx } = this; 95 | const api = `${API_HOST}/repos/${namespace}/toc`; 96 | const result = await ctx.curl(api, { 97 | dataType: 'json', 98 | }); 99 | return result.data; 100 | } 101 | 102 | async getUser(id) { 103 | const { ctx } = this; 104 | const api = `${API_HOST}/users/${id}`; 105 | const result = await ctx.curl(api, { 106 | dataType: 'json', 107 | }); 108 | return result.data; 109 | } 110 | } 111 | return YuqueService; 112 | }; 113 | -------------------------------------------------------------------------------- /app/util/md.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mkit = require('markdown-it'); 4 | const hljs = require('highlight.js'); 5 | const markdownItGithubPreamble = require('markdown-it-github-preamble'); 6 | const markdownItFootnote = require('markdown-it-footnote'); 7 | const markdownItKatex = require('markdown-it-katex'); 8 | 9 | const md = new Mkit({ 10 | html: true, 11 | linkify: true, 12 | highlight(str, lang) { 13 | if (lang && hljs.getLanguage(lang)) { 14 | try { 15 | return hljs.highlight(lang, str).value; 16 | } catch (err) { 17 | console.log(err); 18 | } 19 | } 20 | return ''; // use external default escaping 21 | }, 22 | }) 23 | .use(markdownItGithubPreamble) 24 | .use(markdownItFootnote) 25 | .use(markdownItKatex); 26 | 27 | module.exports = md; 28 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # 主题 2 | theme: txd 3 | 4 | # 语雀 API 设置 5 | yuque: 6 | base: https://www.yuque.com/api/v2 7 | login: yinzhi 8 | repo: blog 9 | 10 | # Site 11 | title: 小冷的备忘录 12 | subtitle: 但凡能引起思考的句子,都是些好句子 13 | keywords: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5 14 | description: 小冷的备忘录,HTML/CSS/JAVASCRIPT,前端工程师,Angular,Ionic,Vue,React,Node.js,Powershell,Qt5 15 | author: 小冷 16 | language: zh-CN 17 | 18 | # 友情链接 19 | links: 20 | - 21 | name: 阿里巴巴 22 | url: https://www.alibaba.com 23 | - 24 | name: 阿里巴巴国际UED 25 | url: http://www.aliued.com/ 26 | - 27 | name: 阿里巴巴U一点 28 | url: http://www.aliued.cn/ 29 | 30 | # 导航链接 31 | navigators: 32 | - 33 | name: HOME 34 | url: / 35 | - 36 | name: BLOG 37 | url: /blogs 38 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const yaml = require('js-yaml'); 6 | 7 | module.exports = (appInfo) => { 8 | let blogConfig = {}; 9 | // Get document, or throw exception on error 10 | try { 11 | blogConfig = yaml.safeLoad( 12 | fs.readFileSync( 13 | path.join(appInfo.baseDir, 'config.yml'), 14 | 'utf8' 15 | ) 16 | ); 17 | } catch (e) { 18 | console.error(e.message); 19 | process.exit(-1); 20 | } 21 | const clientViewRoot = path.join( 22 | appInfo.baseDir, `themes/${blogConfig.theme}` 23 | ); 24 | const serverViewRoot = path.join(appInfo.baseDir, '/app/view'); 25 | const config = { 26 | keys: 'key', 27 | client: clientViewRoot, 28 | view: { 29 | defaultExtension: '.jsx', 30 | root: `$${serverViewRoot},${clientViewRoot}`, 31 | }, 32 | isomorphic: { 33 | babel: { 34 | plugins: [ 35 | require.resolve('babel-plugin-transform-decorators-legacy'), 36 | ], 37 | }, 38 | alias: { 39 | client: clientViewRoot, 40 | }, 41 | }, 42 | webpack: { 43 | custom: { 44 | configPath: path.join(appInfo.baseDir, 'config/webpack.config.js'), 45 | }, 46 | }, 47 | blog: blogConfig, 48 | }; 49 | return config; 50 | }; 51 | -------------------------------------------------------------------------------- /config/config.local.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /config/config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /config/config.unittest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const config = { 5 | view: { 6 | defaultViewEngine: 'react', 7 | defaultExtension: '.jsx', 8 | }, 9 | }; 10 | return config; 11 | }; 12 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 4 | 5 | const isDev = process.env.NODE_ENV === 'development'; 6 | 7 | module.exports = (app, defaultConfig) => { 8 | // For mobx decorators 9 | for (const loader of defaultConfig.module.rules) { 10 | if (loader.test.test('.jsx') && loader.test.test('.js')) { 11 | if (!Array.isArray(loader.use.options.plugins)) { 12 | loader.use.options.plugins = []; 13 | } 14 | loader.use.options.plugins.push( 15 | require.resolve('babel-plugin-transform-decorators-legacy') 16 | ); 17 | break; 18 | } 19 | } 20 | 21 | // development 22 | if (isDev) { 23 | return defaultConfig; 24 | } 25 | 26 | // production 27 | defaultConfig.plugins.push( 28 | new BundleAnalyzerPlugin() 29 | ); 30 | 31 | return defaultConfig; 32 | }; 33 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yuque-blog", 3 | "version": "1.0.0", 4 | "description": "backend by yuque.com", 5 | "scripts": { 6 | "start": "beidou start --daemon", 7 | "debug": "beidou debug", 8 | "stop": "beidou stop", 9 | "dev": "beidou dev", 10 | "build": "beidou build", 11 | "build:node": "beidou build --target=node", 12 | "lint": "eslint --fix --ext .jsx,.js client", 13 | "lint:style": "stylelint \"clent/**/*.scss\" --syntax scss", 14 | "prettier": "prettier --write './client/**/**/**/*.{js,json,scss,css}' '*.js'", 15 | "test": "egg-bin test", 16 | "snyk-protect": "snyk protect", 17 | "prepublish": "npm run snyk-protect" 18 | }, 19 | "devDependencies": { 20 | "babel-eslint": "^8.2.1", 21 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 22 | "egg-mock": "^3.19.2", 23 | "eslint": "^4.15.0", 24 | "eslint-config-beidou": "^1.0.3", 25 | "eslint-config-prettier": "^2.9.0", 26 | "eslint-plugin-import": "^2.8.0", 27 | "eslint-plugin-react": "^7.5.1", 28 | "prettier": "^1.14.0", 29 | "stylelint": "^9.4.0", 30 | "stylelint-config-prettier": "^4.0.0", 31 | "webpack-bundle-analyzer": "^3.6.0" 32 | }, 33 | "dependencies": { 34 | "axios": "^1.6.4", 35 | "babel-polyfill": "^6.26.0", 36 | "beidou-cli": "^1.0.5", 37 | "beidou-core": "^2.0.0", 38 | "classnames": "^2.2.6", 39 | "domready": "^1.0.8", 40 | "highlight.js": "^10.4.1", 41 | "js-yaml": "^3.12.0", 42 | "markdown-it": "^10.0.0", 43 | "markdown-it-footnote": "^3.0.1", 44 | "markdown-it-github-preamble": "^1.0.0", 45 | "markdown-it-katex": "^2.0.3", 46 | "marked": "^2.0.0", 47 | "mobx": "^5.0.3", 48 | "mobx-devtools": "^0.9.18", 49 | "mobx-react": "^5.2.3", 50 | "mobx-react-devtools": "^6.0.2", 51 | "normalize.css": "^8.0.0", 52 | "parallax-js": "^3.1.0", 53 | "prop-types": "^15.6.2", 54 | "react": "^16.3.0", 55 | "react-dom": "^16.3.0", 56 | "react-router-dom": "^4.3.1", 57 | "snyk": "^1.316.1" 58 | }, 59 | "engines": { 60 | "node": ">= 8.0.0" 61 | }, 62 | "license": "MIT", 63 | "snyk": true 64 | } 65 | -------------------------------------------------------------------------------- /test/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app } = require('egg-mock/bootstrap'); 4 | const assert = require('assert'); 5 | 6 | // unittest don't support ssr! 7 | describe('test/controller/home.test.js', () => { 8 | describe('GET /', () => { 9 | it('should GET /', async () => { 10 | const result = await app 11 | .httpRequest() 12 | .get('/'); 13 | // Excuse me! I didn't find a solution to support ssr unit test. 14 | assert.equal(result.status, 500); 15 | }); 16 | }); 17 | 18 | describe('GET /post/:slug', () => { 19 | it('should GET /post/:slug', async () => { 20 | const result = await app 21 | .httpRequest() 22 | .get('/post/gdquyk'); 23 | assert.equal(result.status, 500); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/controller/yuque.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app } = require('egg-mock/bootstrap'); 4 | const assert = require('assert'); 5 | 6 | describe('test/controller/yuque.test.js', () => { 7 | describe('GET /api/yuque/ariticles', () => { 8 | it('should GET /api/yuque/ariticles', async () => { 9 | const result = await app 10 | .httpRequest() 11 | .get('/api/yuque/ariticles'); 12 | assert.equal(result.status, 200); 13 | assert.equal(Array.isArray(result.body.data), true); 14 | }); 15 | }); 16 | 17 | describe('GET /api/yuque/ariticleToc', () => { 18 | it('should GET /api/yuque/ariticleToc', async () => { 19 | const result = await app 20 | .httpRequest() 21 | .get('/api/yuque/ariticleToc'); 22 | assert.equal(result.status, 200); 23 | assert.equal(Array.isArray(result.body.data), true); 24 | }); 25 | }); 26 | 27 | describe('GET /api/yuque/ariticle/:slug', () => { 28 | it('should GET /api/yuque/ariticle/:slug', async () => { 29 | const result = await app 30 | .httpRequest() 31 | .get('/api/yuque/ariticle/gdquyk'); 32 | assert.equal(result.status, 200); 33 | assert.equal(typeof result.body.data, 'object'); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /themes/txd/common/router.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { BrowserRouter, StaticRouter, Switch } from 'react-router-dom'; 5 | import { Provider } from 'mobx-react'; 6 | import DevTool from 'mobx-react-devtools'; 7 | 8 | import AppStore from '../stores/app'; 9 | import PostStore from '../stores/post'; 10 | 11 | import { DefaultLayout, HomeLayout, BlogLayout } from '../containers/Layout'; 12 | import Home from '../pages/Home'; 13 | import Post from '../pages/Post'; 14 | import Blog from '../pages/Blog'; 15 | import About from '../pages/About'; 16 | import Careers from '../pages/Careers'; 17 | import NotFound from '../pages/404'; 18 | 19 | import '../styles/site.scss'; 20 | 21 | const isDev = process.env.NODE_ENV === 'development'; 22 | 23 | const Router = __CLIENT__ ? BrowserRouter : StaticRouter; 24 | 25 | let postStore; 26 | let appStore; 27 | 28 | if (__CLIENT__) { 29 | const initialState = window.initialState || {}; 30 | postStore = PostStore.fromJS(initialState); 31 | appStore = AppStore.fromJS(initialState); 32 | // 监听窗口变化 33 | appStore.listenWindow(); 34 | } 35 | 36 | export default (props) => { 37 | const { context, location } = props; 38 | return ( 39 |
40 | { isDev && } 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /themes/txd/containers/Footer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | 4 | import './index.scss'; 5 | 6 | @inject('appStore') 7 | @observer 8 | export default class Footer extends React.Component { 9 | renderLinks() { 10 | if (!this.props.showLinks) { 11 | return null; 12 | } 13 | const { appStore } = this.props; 14 | const { config } = appStore; 15 | const { links = [] } = config; 16 | return links.map(link => ( 17 | 23 | { link.name } 24 | 25 | )); 26 | } 27 | 28 | render() { 29 | return ( 30 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /themes/txd/containers/Footer/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables/manifest'; 2 | @import '../../styles/mixins/manifest'; 3 | 4 | .footer { 5 | $padding: 24px; 6 | margin-top: $padding; 7 | text-align: center; 8 | .link-list { 9 | display: inline-flex; 10 | a { 11 | padding: 14px; 12 | font-size: 12px; 13 | color: #999; 14 | opacity: 1; 15 | } 16 | } 17 | .copyright { 18 | p { 19 | margin: 0; 20 | font-size: 14px; 21 | color: #999; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /themes/txd/containers/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import classNames from 'classnames'; 4 | 5 | import ModalNav from '../ModalNav'; 6 | import { HamburgerCross } from '../../widgets/Icons'; 7 | 8 | import './index.scss'; 9 | 10 | @inject('appStore') 11 | @observer 12 | export default class Header extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | isExpanded: false, 17 | }; 18 | this.toggleExpandedState = this.toggleExpandedState.bind(this); 19 | } 20 | 21 | render() { 22 | const cls = classNames({ 23 | header: true, 24 | 'header--transparent': true, 25 | 'header--expanded': this.state.isExpanded, 26 | }); 27 | 28 | const { appStore } = this.props; 29 | const { config = {} } = appStore; 30 | const { navigators = [] } = config; 31 | 32 | return ( 33 |
34 | 42 | 47 |
); 48 | } 49 | 50 | toggleExpandedState() { 51 | this.setState({ 52 | isExpanded: !this.state.isExpanded, 53 | }); 54 | this.btn && this.btn.toggle(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /themes/txd/containers/Header/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables/manifest'; 2 | @import '../../styles/mixins/manifest'; 3 | 4 | .header { 5 | pointer-events:auto; 6 | background-color: $blue-transparent; 7 | @include standard-shadow; 8 | overflow: visible; 9 | justify-content: space-between; 10 | display: flex; 11 | height: $header-height-small; 12 | 13 | transition: transform 0.5s, background-color 0.5s; 14 | color: $black; 15 | z-index: 101; 16 | 17 | @mixin expand { 18 | @include bp-w(600px) { 19 | @content; 20 | } 21 | } 22 | 23 | @include expand { 24 | height: $header-height; 25 | } 26 | 27 | &--transparent { 28 | background-color: $blue-invisible; 29 | transition: background-color 0.5s; 30 | box-shadow: none; 31 | } 32 | 33 | &--hidden { 34 | transform: translate3d(0, -$header-height-small, 0); 35 | transition: transform 0.5s; 36 | 37 | @include expand { 38 | transform: translate3d(0, -$header-height, 0); 39 | } 40 | } 41 | 42 | &__icon { 43 | height: 100%; 44 | width: 70px; 45 | cursor: pointer; 46 | padding: 20px; 47 | opacity: 0.7; 48 | z-index: 103; 49 | 50 | &:hover { 51 | opacity: 1; 52 | } 53 | 54 | svg { 55 | width: 30px; 56 | height: 30px; 57 | fill: $black; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /themes/txd/containers/HomeFooter/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import './index.scss'; 4 | 5 | export default class Footer extends Component { 6 | render() { 7 | const { mobileMode } = window; 8 | if (mobileMode) { 9 | return null; 10 | } 11 | return ( 12 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /themes/txd/containers/HomeFooter/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables/manifest'; 2 | @import '../../styles/mixins/manifest'; 3 | 4 | .home-footer { 5 | box-sizing: border-box; 6 | position: absolute; 7 | bottom: 0; 8 | right: 0; 9 | .copyright { 10 | padding: 12px 32px; 11 | @include portrait { 12 | padding: 4px 12px; 13 | } 14 | p { 15 | margin: 0; 16 | padding: 0; 17 | color: #999; 18 | font-size: 12px; 19 | text-align: right; 20 | font-family: 'PT Sans'; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /themes/txd/containers/HomeHeader/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Link } from 'react-router-dom'; 4 | import './index.scss'; 5 | import CommonHeader from '../Header'; 6 | 7 | @inject('appStore') 8 | @observer 9 | export default class HomeHeader extends Component { 10 | PCHeader(navigators) { 11 | return ( 12 | 27 | ); 28 | } 29 | 30 | MoblieHeader() { 31 | return ; 32 | } 33 | 34 | render() { 35 | const { appStore } = this.props; 36 | const { config = {} } = appStore; 37 | const { navigators = [] } = config; 38 | const { mobileMode } = window; 39 | 40 | return ( 41 |
42 | { mobileMode ? this.MoblieHeader() : this.PCHeader(navigators)} 43 |
44 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /themes/txd/containers/HomeHeader/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables/manifest'; 2 | @import '../../styles/mixins/manifest'; 3 | 4 | .home-navigator { 5 | position: relative; 6 | padding: 0 40px 0 80px; 7 | min-width: 480px; 8 | 9 | .logo { 10 | position: absolute; 11 | bottom: 80px; 12 | left: 80px; 13 | width: 360px; 14 | img { 15 | height: auto; 16 | } 17 | } 18 | .nav-list { 19 | margin: 40px 0; 20 | .nav-item { 21 | width: 120px; 22 | margin-bottom: 45px; 23 | } 24 | } 25 | } 26 | 27 | @include phone { 28 | .home-wrapper{ 29 | display: flex; 30 | } 31 | .home-navigator { 32 | position: fixed; 33 | padding: 0 40px 0 0; 34 | z-index: 2; 35 | height: 100%; 36 | min-width: 100%; 37 | pointer-events: none; 38 | .logo { 39 | position: unset; 40 | margin-left: 30px; 41 | margin-top: 30px; 42 | width: 315px; 43 | height: 80px; 44 | img { 45 | height: auto; 46 | } 47 | } 48 | } 49 | } 50 | // @media only screen and (min-width: 321px) and (max-width: 1024px) { 51 | 52 | // /* Styles */ 53 | 54 | // } 55 | 56 | 57 | .nav-list { 58 | .nav-item { 59 | text-align: center; 60 | line-height: 1.2; 61 | cursor: pointer; 62 | a { 63 | opacity: 1; 64 | position: relative; 65 | font-size: 18px; 66 | font-family: $spec-font-family; 67 | &::before { 68 | content: ''; 69 | display: block; 70 | position: absolute; 71 | left: -17px; 72 | top: 0; 73 | bottom: 0; 74 | margin: auto; 75 | width: 0; 76 | height: 0; 77 | border-top: 3px solid transparent; 78 | border-left: 9px solid #666; 79 | border-bottom: 3px solid transparent; 80 | opacity: 0; 81 | transform: translateX(-100%); 82 | transition: all 0.3s; 83 | } 84 | &:hover, 85 | &.active { 86 | &::before { 87 | opacity: 1; 88 | transform: translateX(0); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /themes/txd/containers/Layout/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Route } from 'react-router-dom'; 4 | 5 | import HScroll from '../../widgets/Hscroll'; 6 | import CommonHeader from '../Header'; 7 | import HomeHeader from '../HomeHeader'; 8 | import HomeFooter from '../HomeFooter'; 9 | 10 | export const DefaultLayout = ({ component: Component, ...rest }) => { 11 | return ( 12 | ( 15 |
16 | 17 | 18 |
19 | )} 20 | /> 21 | ); 22 | }; 23 | 24 | export const BlogLayout = ({ component: Component, ...rest }) => { 25 | return ( 26 | ( 29 |
30 | 31 | 32 |
33 | )} 34 | /> 35 | ); 36 | }; 37 | 38 | export const HomeLayout = ({ component: Component, ...rest }) => { 39 | return ( 40 | ( 43 | 44 | 45 | 46 | 47 | 48 | )} 49 | /> 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /themes/txd/containers/ModalNav/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withRouter, Link } from 'react-router-dom'; 4 | 5 | import './index.scss'; 6 | 7 | function ModalNav(props) { 8 | const { location = {} } = props; 9 | const active = props.isExpanded; 10 | const list = props.buttons.map((button) => { 11 | const { url, name } = button; 12 | const buttonActive = location.pathname === url; 13 | return ( 14 |
  • 15 | { props.toggleExpandedState(true); }} 19 | > 20 | {name} 21 | 22 |
  • 23 | ); 24 | }); 25 | 26 | return ( 27 |
    28 |
      29 | {list} 30 |
    31 |
    32 | ); 33 | } 34 | 35 | ModalNav.propTypes = { 36 | buttons: PropTypes.array.isRequired, 37 | isExpanded: PropTypes.bool.isRequired, 38 | }; 39 | 40 | export default withRouter(ModalNav); 41 | -------------------------------------------------------------------------------- /themes/txd/containers/ModalNav/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables/manifest'; 2 | @import '../../styles/mixins/manifest'; 3 | 4 | .modal-nav { 5 | z-index: 102; 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | bottom: 0; 10 | right: 0; 11 | transition: transform 0.6s; 12 | transform: translateX(-100%); 13 | background-color: rgb(255, 255, 255); 14 | 15 | &.active { 16 | transform: translateX(0); 17 | } 18 | 19 | @mixin expand { 20 | @include bp-w(600px) { 21 | @content; 22 | } 23 | } 24 | 25 | @include portrait { 26 | display: flex; 27 | align-items: center; 28 | } 29 | .nav-list { 30 | position: absolute; 31 | padding: 24px; 32 | left: 40px; 33 | .nav-item { 34 | width: 120px; 35 | margin-bottom: 36px; 36 | a { 37 | font-family: $spec-font-family; 38 | } 39 | @include portrait { 40 | margin: 36px auto; 41 | } 42 | } 43 | @include portrait { 44 | width: 100%; 45 | left: 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /themes/txd/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import IndexRouter from './common/router'; 4 | 5 | export default class Main extends Component { 6 | static getPartial(props) { 7 | const { ctx, store } = props; 8 | const { posts, post, config, mobileMode = false } = store; 9 | return { 10 | html: , 16 | }; 17 | } 18 | 19 | /** 20 | * construct store for server side 21 | */ 22 | static getStore(props) { 23 | const { posts = [], post = {}, config = {} } = props; 24 | const store = { 25 | posts, 26 | post, 27 | config, 28 | }; 29 | return store; 30 | } 31 | 32 | renderHead() { 33 | const { props } = this; 34 | const { helper, config = {}, post = {} } = props; 35 | const { cnzz = {} } = config; 36 | return ( 37 | 38 | {config.title || 'Untitled'} 39 | 40 | 41 | 42 | 43 | 44 | 45 | { 46 | cnzz.siteId &&