├── .eslintignore ├── .vscode └── settings.json ├── .prettierignore ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── assets │ ├── icons │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.json │ │ └── iconfont.css │ └── images │ │ └── avatar.jpeg ├── utils │ ├── dayjs.js │ ├── download.js │ ├── config.js │ ├── storage.js │ ├── axios.js │ ├── index.js │ └── antdIcon.js ├── views │ ├── web │ │ ├── about │ │ │ ├── index.less │ │ │ ├── index.jsx │ │ │ └── MyInfo.jsx │ │ ├── archives │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── tag │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── categories │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── article │ │ │ ├── Navigation.jsx │ │ │ ├── index.less │ │ │ ├── index.css │ │ │ └── index.jsx │ │ └── home │ │ │ ├── QuickLink.jsx │ │ │ ├── List.jsx │ │ │ ├── index.jsx │ │ │ ├── index.css │ │ │ └── index.less │ └── admin │ │ ├── article │ │ └── edit │ │ │ ├── index.less │ │ │ ├── Tag.jsx │ │ │ └── index.jsx │ │ ├── home │ │ └── index.jsx │ │ └── user │ │ └── index.jsx ├── hooks │ ├── useMount.js │ ├── useBreadcrumb.js │ ├── useModal.js │ ├── useBoolean.js │ ├── useBus.js │ ├── useAjaxLoading.js │ ├── useFetchList.js │ └── useAntdTable.js ├── routes │ ├── index.js │ ├── admin.js │ └── web.js ├── redux │ ├── rootReducers.js │ ├── types.js │ ├── article │ │ ├── actions.js │ │ └── reducer.js │ ├── user │ │ ├── actions.js │ │ └── reducer.js │ └── index.js ├── layout │ ├── web │ │ ├── header │ │ │ ├── right │ │ │ │ ├── index.jsx │ │ │ │ ├── navList.js │ │ │ │ ├── Navbar.jsx │ │ │ │ ├── Search.jsx │ │ │ │ └── UserInfo.jsx │ │ │ ├── index.jsx │ │ │ └── left │ │ │ │ └── index.jsx │ │ ├── AppMain.jsx │ │ ├── index.jsx │ │ └── sidebar │ │ │ └── index.jsx │ └── admin │ │ ├── sidebar │ │ ├── menu.js │ │ └── index.jsx │ │ ├── index.jsx │ │ └── header │ │ └── index.jsx ├── components │ ├── 404 │ │ └── index.jsx │ ├── Href │ │ └── index.jsx │ ├── Avatar │ │ ├── index.less │ │ └── index.jsx │ ├── SvgIcon │ │ └── index.jsx │ ├── MdEditor │ │ └── index.jsx │ ├── Pagination │ │ └── index.jsx │ ├── Lazy.jsx │ ├── Breadcrumb │ │ └── index.jsx │ ├── Public │ │ ├── index.jsx │ │ ├── SignModal │ │ │ └── index.jsx │ │ └── UploadModal │ │ │ └── index.jsx │ ├── ArticleTag │ │ └── index.jsx │ ├── GithubLogining │ │ └── index.jsx │ └── Discuss │ │ ├── index.less │ │ ├── index.jsx │ │ └── list.jsx ├── index.js ├── styles │ ├── admin.less │ ├── index.less │ ├── atom-one-light.css │ └── markdown.less ├── config.js └── App.jsx ├── .editorconfig ├── .babelrc ├── .prettierrc.js ├── jsconfig.json ├── server ├── router │ ├── index.js │ ├── user.js │ ├── discuss.js │ ├── home.js │ └── article.js ├── initData.js ├── utils │ ├── index.js │ ├── context.js │ ├── bcrypt.js │ ├── response.js │ ├── token.js │ ├── email.js │ └── file.js ├── models │ ├── ip.js │ ├── category.js │ ├── tag.js │ ├── reply.js │ ├── article.js │ ├── comment.js │ ├── user.js │ └── index.js ├── package.json ├── controllers │ ├── tag.js │ └── discuss.js ├── app.js ├── middlewares │ └── authHandler.js └── config │ └── index.js ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── webpack.dll.js ├── modules.js ├── paths.js └── env.js ├── .gitignore ├── LICENSE ├── scripts ├── test.js └── start.js ├── LICENSE.996ICU └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/assets 2 | /config 3 | /public -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false // 每次保存的时候自动格式化 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # sth you do not want to style... 2 | public/ 3 | .eslintrc.js 4 | config/ -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/src/assets/icons/iconfont.eot -------------------------------------------------------------------------------- /src/assets/icons/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/src/assets/icons/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/icons/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/src/assets/icons/iconfont.woff -------------------------------------------------------------------------------- /src/assets/icons/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/src/assets/icons/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/images/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin0216/react-blog/HEAD/src/assets/images/avatar.jpeg -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/dayjs.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | 4 | dayjs.extend(relativeTime) 5 | 6 | export default dayjs 7 | -------------------------------------------------------------------------------- /src/views/web/about/index.less: -------------------------------------------------------------------------------- 1 | .app-about { 2 | .about-list { 3 | &, 4 | ul { 5 | list-style: circle; 6 | margin-left: 20px; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useMount.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export default function useMount(func) { 4 | useEffect(() => { 5 | typeof func === 'function' && func() 6 | }, []) 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import webRoutes from './web' 2 | import adminRoutes from './admin' 3 | 4 | const routes = [ 5 | adminRoutes, 6 | webRoutes 7 | 8 | // .. 9 | ] 10 | 11 | export default routes 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, // 每行代码长度(默认80) 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | jsxBracketSameLine: true, // 多行JSX中的>放置在最后一行的结尾,而不是另起一行(默认false) 7 | jsxSingleQuote: true 8 | } 9 | -------------------------------------------------------------------------------- /src/redux/rootReducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import article from './article/reducer' 4 | import user from './user/reducer' 5 | 6 | export default combineReducers({ 7 | user, 8 | article 9 | }) 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": "src", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | }, 8 | "jsx": "react" 9 | }, 10 | "exclude": ["node_modules", "build", "config", "scripts"] 11 | } 12 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = (app) => { 4 | fs.readdirSync(__dirname).forEach(file => { 5 | if (file === 'index.js') return 6 | const route = require(`./${file}`) 7 | app.use(route.routes()).use(route.allowedMethods()) 8 | }) 9 | } -------------------------------------------------------------------------------- /src/redux/types.js: -------------------------------------------------------------------------------- 1 | // article 2 | export const ARTICLE_GET_CATEGORY_LIST = 'ARTICLE_GET_CATEGORY_LIST' 3 | export const ARTICLE_GET_TAG_LIST = 'ARTICLE_GET_TAG_LIST' 4 | 5 | // user 6 | export const USER_LOGIN = 'USER_LOGIN' 7 | export const USER_REGISTER = 'USER_REGISTER' 8 | export const USER_LOGIN_OUT = 'USER_LOGIN_OUT' 9 | -------------------------------------------------------------------------------- /src/views/web/archives/index.less: -------------------------------------------------------------------------------- 1 | .app-archives { 2 | padding: 40px 20px; 3 | .desc { 4 | // font-size: 14px; 5 | font-weight: 600; 6 | } 7 | .year { 8 | font-size: 22px; 9 | font-weight: 600; 10 | position: relative; 11 | top: -4px; 12 | } 13 | .ant-timeline-item { 14 | font-size: 15px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useBreadcrumb.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useBus from '@/hooks/useBus' 3 | 4 | export default function useBreadcrumb(list = []) { 5 | const bus = useBus() 6 | useEffect(() => { 7 | bus.emit('breadcrumbList', list) 8 | return () => { 9 | bus.emit('breadcrumbList', []) 10 | } 11 | }, []) 12 | } 13 | -------------------------------------------------------------------------------- /server/router/user.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const router = new Router({ prefix: '/user' }) 3 | const { getList, updateUser, delete: del }= require('../controllers/user') 4 | 5 | router 6 | .get('/list', getList) // 获取列表 7 | .put('/:userId', updateUser) // 更新用户信息 8 | .delete('/:userId', del) // 删除用户 9 | 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/views/admin/article/edit/index.less: -------------------------------------------------------------------------------- 1 | .admin-edit-article { 2 | .form-list { 3 | li { 4 | display: flex; 5 | line-height: 36px; 6 | .label { 7 | width: 50px; 8 | font-weight: bold; 9 | } 10 | } 11 | } 12 | 13 | .action-icon { 14 | position: fixed; 15 | bottom: 100px; 16 | right: 100px; 17 | z-index: 2; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/layout/web/header/right/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Search from './Search' 3 | import Navbar from './Navbar' 4 | import UserInfo from './UserInfo' 5 | 6 | function HeaderRight(props) { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default HeaderRight 17 | -------------------------------------------------------------------------------- /src/views/web/tag/index.less: -------------------------------------------------------------------------------- 1 | .app-tags { 2 | font-family: 'Lato', 'PingFang SC', 'Microsoft YaHei', sans-serif; 3 | .timeline { 4 | margin-top: 5px; 5 | } 6 | .list-title { 7 | font-size: 22px; 8 | position: relative; 9 | top: -5px; 10 | .type-name { 11 | color: #bbb; 12 | margin-left: 5px; 13 | text-transform: capitalize; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/initData.js: -------------------------------------------------------------------------------- 1 | const { ADMIN_GITHUB_LOGIN_NAME } = require('./config') 2 | const UserController = require('./controllers/user') 3 | const ArticleController = require('./controllers/article') 4 | 5 | /** 6 | * init Data 7 | */ 8 | 9 | module.exports = () => { 10 | UserController.initGithubUser(ADMIN_GITHUB_LOGIN_NAME) // 创建 role === 1 的账号 from github... 11 | ArticleController.initAboutPage() 12 | } 13 | -------------------------------------------------------------------------------- /src/layout/web/header/right/navList.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | icon: 'home', 4 | title: '首页', 5 | link: '/' 6 | }, 7 | { 8 | icon: 'edit', 9 | title: '归档', 10 | link: '/archives' 11 | }, 12 | { 13 | icon: 'folder', 14 | title: '分类', 15 | link: '/categories' 16 | }, 17 | { 18 | icon: 'user', 19 | title: '关于', 20 | link: '/about' 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /server/router/discuss.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const router = new Router({ prefix: '/discuss' }) 3 | const { create, deleteComment, deleteReply } = require('../controllers/discuss') 4 | 5 | router 6 | .post('/', create) // 创建评论或者回复 articleId 文章 id 7 | .delete('/comment/:commentId', deleteComment) // 删除一级评论 8 | .delete('/reply/:replyId', deleteReply) // 删除回复 9 | 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /src/utils/download.js: -------------------------------------------------------------------------------- 1 | import { API_BASE_URL } from '@/config' 2 | import { getToken } from '@/utils' 3 | 4 | export default function download(router) { 5 | const $a = document.createElement('a') 6 | const token = getToken() 7 | $a.href = token ? `${API_BASE_URL}${router}?token=${token}` : `${API_BASE_URL}${router}` 8 | document.body.appendChild($a) 9 | $a.click() 10 | document.body.removeChild($a) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Href/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isExternal } from '@/utils' 3 | 4 | // a 标签跳转新窗口 5 | function Href({ children, href, ...rest }) { 6 | let url = href 7 | if (!isExternal(href)) { 8 | url = `http://${href}` 9 | } 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | 17 | export default Href 18 | -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * 4 | * 解码 url 请求 5 | * @param {String} url 6 | * @returns {Object} 7 | */ 8 | decodeQuery(url) { 9 | const params = {} 10 | const paramsStr = url.replace(/(\S*)\?/, '') // a=1&b=2&c=&d=xxx&e 11 | paramsStr.split('&').forEach(v => { 12 | const d = v.split('=') 13 | if (d[1] && d[0]) params[d[0]] = d[1] 14 | }) 15 | return params 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # upload 26 | /server/upload/* 27 | /server/output/* -------------------------------------------------------------------------------- /src/hooks/useModal.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export default function useModal() { 4 | const [visible, setVisible] = useState(true) 5 | const show = useCallback(() => setVisible(true), [visible]) 6 | const close = useCallback(() => setVisible(false), [visible]) 7 | 8 | const modalProps = { 9 | visible, 10 | onCancel: close 11 | } 12 | 13 | return { 14 | visible, 15 | show, 16 | close, 17 | modalProps 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useBoolean.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | 3 | export default function useBoolean(defaultValue = false) { 4 | const [value, setValue] = useState(defaultValue) 5 | 6 | const setTrue = useCallback(() => setValue(true), [value]) 7 | const setFalse = useCallback(() => setValue(false), [value]) 8 | 9 | const toggle = useCallback(() => { 10 | typeof value === 'boolean' && setValue(value) 11 | }, [value]) 12 | 13 | return { value, setTrue, setFalse, toggle } 14 | } 15 | -------------------------------------------------------------------------------- /server/router/home.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const router = new Router() 3 | const { login, register } = require('../controllers/user') 4 | const { getTagList, getCategoryList } = require('../controllers/tag') 5 | 6 | // tag category 7 | router.get('/tag/list', getTagList) // 获取所有的 tag 列表 8 | router.get('/category/list', getCategoryList) // 获取 category 列表 9 | 10 | // root 11 | router.post('/login', login) // 登录 12 | router.post('/register', register) // 注册 13 | 14 | module.exports = router 15 | -------------------------------------------------------------------------------- /src/components/Avatar/index.less: -------------------------------------------------------------------------------- 1 | .avatar-popover { 2 | .mr10 { 3 | margin-right: 10px; 4 | } 5 | .popover-content { 6 | display: flex; 7 | align-items: center; 8 | .popover-cotent-avatar { 9 | height: 100%; 10 | margin-right: 10px; 11 | } 12 | .github-info { 13 | margin-bottom: 0; 14 | li { 15 | line-height: 26px; 16 | } 17 | 18 | .github-name { 19 | font-size: 18px; 20 | padding-right: 10px; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/redux/article/actions.js: -------------------------------------------------------------------------------- 1 | import * as TYPES from '@/redux/types' 2 | import axios from '@/utils/axios' 3 | 4 | export const getTagList = () => dispatch => 5 | axios.get('/tag/list').then(list => { 6 | dispatch({ 7 | type: TYPES.ARTICLE_GET_TAG_LIST, 8 | payload: list 9 | }) 10 | }) 11 | 12 | export const getCategoryList = () => dispatch => 13 | axios.get('/category/list').then(list => { 14 | dispatch({ 15 | type: TYPES.ARTICLE_GET_CATEGORY_LIST, 16 | payload: list 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { Provider as BusProvider } from '@/hooks/useBus' 5 | 6 | // redux 7 | import { Provider } from 'react-redux' 8 | import store from '@/redux' 9 | 10 | // styles 11 | import '@/assets/icons/iconfont' 12 | import '@/styles/index.less' 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ) 22 | -------------------------------------------------------------------------------- /server/models/ip.js: -------------------------------------------------------------------------------- 1 | // ip 表 2 | module.exports = (sequelize, dataTypes) => { 3 | const Ip = sequelize.define('ip', { 4 | id: { type: dataTypes.INTEGER(11), primaryKey: true, autoIncrement: true }, 5 | ip: { type: dataTypes.TEXT, allowNull: false }, // ip 地址 6 | auth: { type: dataTypes.BOOLEAN, defaultValue: true } // 是否可用 7 | }) 8 | 9 | Ip.associate = models => { 10 | Ip.belongsTo(models.user, { 11 | foreignKey: 'userId', 12 | targetKey: 'id', 13 | constraints: false 14 | }) 15 | } 16 | return Ip 17 | } 18 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | // iconfont svg 5 | const SvgIcon = props => { 6 | return ( 7 | 10 | ) 11 | } 12 | 13 | SvgIcon.propTypes = { 14 | type: PropTypes.string.isRequired, 15 | className: PropTypes.string 16 | } 17 | 18 | SvgIcon.defaultProps = { 19 | className: '' 20 | } 21 | 22 | export default SvgIcon 23 | -------------------------------------------------------------------------------- /src/components/404/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Result, Button } from 'antd' 4 | 5 | function PageNotFound(props) { 6 | return ( 7 | { 15 | props.history.push('/') 16 | }}> 17 | Back Home 18 | 19 | } 20 | /> 21 | ) 22 | } 23 | 24 | export default PageNotFound 25 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | export const OAUTH_CLIENT_ID = 'c6a96a84105bb0be1fe5' 2 | export const OAUTH_URL = 'https://github.com/login/oauth/authorize' 3 | 4 | // color List colr 5 | export const COLOR_LIST = [ 6 | 'magenta', 7 | 'blue', 8 | 'red', 9 | 'volcano', 10 | 'orange', 11 | 'gold', 12 | 'lime', 13 | 'green', 14 | 'cyan', 15 | 'geekblue', 16 | 'purple' 17 | ] 18 | 19 | // pageSize 20 | export const ARCHIVES_PAGESIZE = 15 // archives pageSize 21 | export const TAG_PAGESIZE = 15 // tag / category pageSize 22 | export const HOME_PAGESIZE = 10 // home pageSize 23 | -------------------------------------------------------------------------------- /server/models/category.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | // category 表 3 | module.exports = (sequelize, dataTypes) => { 4 | const Category = sequelize.define('category', { 5 | id: { type: dataTypes.INTEGER(11), primaryKey: true, autoIncrement: true }, 6 | name: { type: dataTypes.STRING(100), allowNull: false } 7 | }) 8 | 9 | Category.associate = models => { 10 | Category.belongsTo(models.article, { 11 | as: 'article', 12 | foreignKey: 'articleId', 13 | targetKey: 'id', 14 | constraints: false 15 | }) 16 | } 17 | 18 | return Category 19 | } 20 | -------------------------------------------------------------------------------- /src/views/web/categories/index.less: -------------------------------------------------------------------------------- 1 | .app-categories { 2 | .title { 3 | font-size: 1.7em; 4 | text-align: center; 5 | word-break: break-word; 6 | font-weight: 400; 7 | } 8 | .category-all-title { 9 | font-size: 14px; 10 | line-height: 2; 11 | color: #555; 12 | text-align: center; 13 | } 14 | .categories-list { 15 | text-align: center; 16 | padding: 20px 80px; 17 | .ant-badge { 18 | margin: 10px 26px 0 0; 19 | } 20 | a { 21 | color: inherit; 22 | } 23 | .ant-badge { 24 | z-index: 2; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/layout/admin/sidebar/menu.js: -------------------------------------------------------------------------------- 1 | const menu = [ 2 | { 3 | path: '/admin', 4 | icon: 'home', 5 | name: '首页' 6 | }, 7 | { 8 | path: '/admin/article', 9 | icon: 'switcher', 10 | name: '文章', 11 | children: [ 12 | { 13 | path: '/admin/article/manager', 14 | icon: 'folder', 15 | name: '管理' 16 | }, 17 | { 18 | path: '/admin/article/add', 19 | icon: 'edit', 20 | name: '新增' 21 | } 22 | ] 23 | }, 24 | { 25 | path: '/admin/user', 26 | icon: 'user', 27 | name: '用户管理' 28 | } 29 | ] 30 | 31 | export default menu 32 | -------------------------------------------------------------------------------- /server/models/tag.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | // article 表 3 | module.exports = (sequelize, dataTypes) => { 4 | const Tag = sequelize.define('tag', { 5 | id: { 6 | type: dataTypes.INTEGER(11), 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | name: { 11 | type: dataTypes.STRING(100), 12 | allowNull: false 13 | } 14 | }) 15 | 16 | Tag.associate = models => { 17 | Tag.belongsTo(models.article, { 18 | as: 'article', 19 | foreignKey: 'articleId', 20 | targetKey: 'id', 21 | constraints: false 22 | }) 23 | } 24 | 25 | return Tag 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useBus.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import mitt from 'mitt' 3 | 4 | export const BusContext = React.createContext(null) 5 | 6 | export default function useBus() { 7 | return React.useContext(BusContext) 8 | } 9 | 10 | export function useListener(name, fn) { 11 | const bus = useBus() 12 | React.useEffect(() => { 13 | bus.on(name, fn) 14 | return () => { 15 | bus.off(name, fn) 16 | } 17 | }, [bus, name, fn]) 18 | } 19 | 20 | export function Provider({ children }) { 21 | const [bus] = React.useState(() => mitt()) 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/MdEditor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useEffect, useState } from 'react' 2 | 3 | import SimpleMDE from 'react-simplemde-editor' 4 | import 'easymde/dist/easymde.min.css' 5 | 6 | import { translateMarkdown } from '@/utils' 7 | 8 | function MdEditor(props) { 9 | // useEffect(() => {}, []) 10 | 11 | // return