jumpTo(item.id)}
31 | className='article-detail content'
32 | dangerouslySetInnerHTML={{ __html: item.content }}
33 | />
34 |
35 |
36 |
37 |
{calcCommentsCount(item.comments)}
38 |
39 |
40 |
{item.viewCount}
41 |
42 |
43 |
44 |
45 | ))}
46 |
47 | )
48 | }
49 |
50 | export default ArticleList
51 |
--------------------------------------------------------------------------------
/src/styles/atom-one-light.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Atom One Light by Daniel Gamage
4 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax
5 |
6 | base: #fafafa
7 | mono-1: #383a42
8 | mono-2: #686b77
9 | mono-3: #a0a1a7
10 | hue-1: #0184bb
11 | hue-2: #4078f2
12 | hue-3: #a626a4
13 | hue-4: #50a14f
14 | hue-5: #e45649
15 | hue-5-2: #c91243
16 | hue-6: #986801
17 | hue-6-2: #c18401
18 |
19 | */
20 |
21 | .hljs {
22 | display: block;
23 | overflow-x: auto;
24 | padding: 0.5em;
25 | color: #383a42;
26 | background: #fafafa;
27 | }
28 |
29 | .hljs-comment,
30 | .hljs-quote {
31 | color: #a0a1a7;
32 | font-style: italic;
33 | }
34 |
35 | .hljs-doctag,
36 | .hljs-keyword,
37 | .hljs-formula {
38 | color: #a626a4;
39 | }
40 |
41 | .hljs-section,
42 | .hljs-name,
43 | .hljs-selector-tag,
44 | .hljs-deletion,
45 | .hljs-subst {
46 | color: #e45649;
47 | }
48 |
49 | .hljs-literal {
50 | color: #0184bb;
51 | }
52 |
53 | .hljs-string,
54 | .hljs-regexp,
55 | .hljs-addition,
56 | .hljs-attribute,
57 | .hljs-meta-string {
58 | color: #50a14f;
59 | }
60 |
61 | .hljs-built_in,
62 | .hljs-class .hljs-title {
63 | color: #c18401;
64 | }
65 |
66 | .hljs-attr,
67 | .hljs-variable,
68 | .hljs-template-variable,
69 | .hljs-type,
70 | .hljs-selector-class,
71 | .hljs-selector-attr,
72 | .hljs-selector-pseudo,
73 | .hljs-number {
74 | color: #986801;
75 | }
76 |
77 | .hljs-symbol,
78 | .hljs-bullet,
79 | .hljs-link,
80 | .hljs-meta,
81 | .hljs-selector-id,
82 | .hljs-title {
83 | color: #4078f2;
84 | }
85 |
86 | .hljs-emphasis {
87 | font-style: italic;
88 | }
89 |
90 | .hljs-strong {
91 | font-weight: bold;
92 | }
93 |
94 | .hljs-link {
95 | text-decoration: underline;
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const koaBody = require('koa-body')
3 | const cors = require('koa2-cors')
4 | const error = require('koa-json-error')
5 | const logger = require('koa-logger')
6 |
7 | // config
8 | const config = require('./config')
9 |
10 | const loadRouter = require('./router')
11 | const db = require('./models')
12 |
13 | // app...
14 | const app = new Koa()
15 | // context binding...
16 | const context = require('./utils/context')
17 | Object.keys(context).forEach(key => {
18 | app.context[key] = context[key] // 绑定上下文对象
19 | })
20 |
21 | // moddlewares
22 | const authHandler = require('./middlewares/authHandler')
23 | const path = require('path')
24 |
25 | app
26 | .use(cors())
27 | .use(
28 | koaBody({
29 | multipart: true,
30 | formidable: {
31 | // uploadDir: path.resolve(__dirname, './upload'),
32 | keepExtensions: true, // 保持文件的后缀
33 | maxFileSize: 2000 * 1024 * 1024 // 设置上传文件大小最大限制,默认20M
34 | }
35 | })
36 | )
37 | .use(
38 | error({
39 | postFormat: (e, { stack, ...rest }) => (process.env.NODE_ENV !== 'development' ? rest : { stack, ...rest })
40 | })
41 | )
42 | .use(authHandler)
43 | .use(logger())
44 |
45 | loadRouter(app)
46 |
47 | app.listen(config.PORT, () => {
48 | db.sequelize
49 | .sync({ force: false }) // If force is true, each DAO will do DROP TABLE IF EXISTS ..., before it tries to create its own table
50 | .then(async () => {
51 | const initData = require('./initData')
52 | initData() // 创建初始化数据
53 | console.log('sequelize connect success')
54 | console.log(`sever listen on http://127.0.0.1:${config.PORT}`)
55 | })
56 | .catch(err => {
57 | console.log(err)
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/src/components/ArticleTag/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import PropTypes from 'prop-types'
4 | import { withRouter, Link } from 'react-router-dom'
5 | import { useSelector } from 'react-redux'
6 | import { Icon, Tag, Divider } from 'antd'
7 | import SvgIcon from '@/components/SvgIcon'
8 |
9 | function getColor(name, colorList) {
10 | const target = colorList.find(c => c.name === name)
11 | return target ? target.color : ''
12 | }
13 |
14 | const ArticleTag = props => {
15 | const tagColorList = useSelector(state => state.article.tagList) // 相当于 connect(state => state.article.tagList)(ArticleTag)
16 | const { tagList, categoryList } = props
17 |
18 | return (
19 | <>
20 | {tagList.length > 0 && (
21 | <>
22 |
23 |
24 | {tagList.map((tag, i) => (
25 |
26 | {tag.name}
27 |
28 | ))}
29 | >
30 | )}
31 | {categoryList.length > 0 && (
32 | <>
33 |
34 |
35 | {categoryList.map((cate, i) => (
36 |
37 | {cate.name}
38 |
39 | ))}
40 | >
41 | )}
42 | >
43 | )
44 | }
45 |
46 | ArticleTag.propTypes = {
47 | tagList: PropTypes.array.isRequired,
48 | categoryList: PropTypes.array.isRequired
49 | }
50 |
51 | export default withRouter(ArticleTag)
52 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon } from 'antd'
3 | import SvgIcon from '@/components/SvgIcon'
4 |
5 | import Href from '@/components/Href'
6 | import MyInfo from '@/views/web/about/MyInfo'
7 |
8 | // API_BASE_URL
9 | export const API_BASE_URL = 'http://localhost:6060'
10 |
11 | // project config
12 | export const HEADER_BLOG_NAME = '郭大大的博客' // header title 显示的名字
13 |
14 | // === sidebar
15 | export const SIDEBAR = {
16 | avatar: require('@/assets/images/avatar.jpeg'), // 侧边栏头像
17 | title: '郭大大', // 标题
18 | subTitle: '学而知不足', // 子标题
19 | // 个人主页
20 | homepages: {
21 | github: {
22 | link: 'https://github.com/alvin0216',
23 | icon:
24 | },
25 | juejin: {
26 | link: 'https://juejin.im/user/5acac6c4f265da2378408f92',
27 | icon:
28 | }
29 | }
30 | }
31 |
32 | // === discuss avatar
33 | export const DISCUSS_AVATAR = SIDEBAR.avatar // 评论框博主头像
34 |
35 | /**
36 | * github config
37 | */
38 | export const GITHUB = {
39 | enable: true, // github 第三方授权开关
40 | client_id: 'c6a96a84105bb0be1fe5', // Setting > Developer setting > OAuth applications => client_id
41 | url: 'https://github.com/login/oauth/authorize' // 跳转的登录的地址
42 | }
43 |
44 | export const ABOUT = {
45 | avatar: SIDEBAR.avatar,
46 | describe: SIDEBAR.subTitle,
47 | discuss: true, // 关于页面是否开启讨论
48 | renderMyInfo:
// 我的介绍 自定义组件 => src/views/web/about/MyInfo.jsx
49 | }
50 |
51 | // 公告 announcement
52 | export const ANNOUNCEMENT = {
53 | enable: true, // 是否开启
54 | content: (
55 | <>
56 | 个人笔记网站,请访问
57 |
alvin's note
58 | >
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/GithubLogining/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch } from 'react-redux'
3 |
4 | // ..
5 | import { Spin } from 'antd'
6 | import { decodeQuery } from '@/utils'
7 | import { login } from '@/redux/user/actions'
8 | import { get, remove } from '@/utils/storage'
9 |
10 | function AppLoading(props) {
11 | const dispatch = useDispatch() // dispatch hooks
12 |
13 | const [loading, setLoading] = useState('')
14 |
15 | function jumpToBefore() {
16 | const url = get('prevRouter') || '/'
17 | if (url.includes('?code=')) {
18 | props.history.push('/')
19 | } else {
20 | props.history.push(url)
21 | }
22 | }
23 |
24 | // github 加载中状态 方案1
25 | useEffect(() => {
26 | let componentWillUnmount = false
27 | // component did mount
28 | const params = decodeQuery(props.location.search)
29 | if (params.code) {
30 | // github callback code
31 | setLoading(true)
32 | dispatch(login({ code: params.code }))
33 | .then(() => {
34 | jumpToBefore()
35 | if (componentWillUnmount) return
36 | setLoading(false)
37 | })
38 | .catch(e => {
39 | jumpToBefore()
40 | if (componentWillUnmount) return
41 | setLoading(false)
42 | })
43 | }
44 |
45 | return () => {
46 | componentWillUnmount = true
47 | }
48 | }, [])
49 |
50 | return (
51 |
52 |
53 |
58 |
59 |
Loading activity...
60 |
61 | )
62 | }
63 |
64 | export default AppLoading
65 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment')
2 |
3 | module.exports = (sequelize, dataTypes) => {
4 | const User = sequelize.define(
5 | 'user',
6 | {
7 | // id sequelize 默认创建...
8 | id: {
9 | type: dataTypes.INTEGER(11),
10 | primaryKey: true,
11 | autoIncrement: true
12 | },
13 | username: {
14 | type: dataTypes.STRING(50),
15 | allowNull: false
16 | // unique: true
17 | },
18 | password: {
19 | type: dataTypes.STRING,
20 | comment: '通过 bcrypt 加密后的密码' // 仅限站内注册用户
21 | },
22 | email: {
23 | type: dataTypes.STRING(50)
24 | },
25 | notice: {
26 | type: dataTypes.BOOLEAN, // 是否开启邮件通知
27 | defaultValue: true
28 | },
29 | role: {
30 | type: dataTypes.TINYINT,
31 | defaultValue: 2,
32 | comment: '用户权限:1 - admin, 2 - 普通用户'
33 | },
34 | github: {
35 | type: dataTypes.TEXT // github 登录用户 直接绑定在 user 表
36 | },
37 | disabledDiscuss: {
38 | type: dataTypes.BOOLEAN, // 是否禁言
39 | defaultValue: false
40 | },
41 | createdAt: {
42 | type: dataTypes.DATE,
43 | defaultValue: dataTypes.NOW,
44 | get() {
45 | return moment(this.getDataValue('createdAt')).format('YYYY-MM-DD HH:mm:ss')
46 | }
47 | },
48 | updatedAt: {
49 | type: dataTypes.DATE,
50 | defaultValue: dataTypes.NOW,
51 | get() {
52 | return moment(this.getDataValue('updatedAt')).format('YYYY-MM-DD HH:mm:ss')
53 | }
54 | }
55 | },
56 | {
57 | timestamps: true
58 | }
59 | )
60 |
61 | User.associate = models => {
62 | User.hasMany(models.comment)
63 | User.hasMany(models.reply)
64 | User.hasMany(models.ip)
65 | }
66 |
67 | return User
68 | }
69 |
--------------------------------------------------------------------------------
/server/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const { DATABASE } = require('../config')
4 | const Sequelize = require('sequelize')
5 |
6 | const Op = Sequelize.Op
7 |
8 | const sequelize = new Sequelize(DATABASE.database, DATABASE.user, DATABASE.password, {
9 | ...DATABASE.options,
10 | logging: false,
11 | // 在 sequelize V4 版本以后新加了符号运算符来代替 Op.xxx
12 | // https://sequelize.org/master/manual/querying.html#operators
13 | operatorsAliases: {
14 | $eq: Op.eq,
15 | $ne: Op.ne,
16 | $gte: Op.gte,
17 | $gt: Op.gt,
18 | $lte: Op.lte,
19 | $lt: Op.lt,
20 | $not: Op.not,
21 | $in: Op.in,
22 | $notIn: Op.notIn,
23 | $is: Op.is,
24 | $like: Op.like,
25 | $notLike: Op.notLike,
26 | $iLike: Op.iLike,
27 | $notILike: Op.notILike,
28 | $regexp: Op.regexp,
29 | $notRegexp: Op.notRegexp,
30 | $iRegexp: Op.iRegexp,
31 | $notIRegexp: Op.notIRegexp,
32 | $between: Op.between,
33 | $notBetween: Op.notBetween,
34 | $overlap: Op.overlap,
35 | $contains: Op.contains,
36 | $contained: Op.contained,
37 | $adjacent: Op.adjacent,
38 | $strictLeft: Op.strictLeft,
39 | $strictRight: Op.strictRight,
40 | $noExtendRight: Op.noExtendRight,
41 | $noExtendLeft: Op.noExtendLeft,
42 | $and: Op.and,
43 | $or: Op.or,
44 | $any: Op.any,
45 | $all: Op.all,
46 | $values: Op.values,
47 | $col: Op.col
48 | }
49 | })
50 |
51 | const db = {}
52 |
53 | fs.readdirSync(__dirname)
54 | .filter(file => file !== 'index.js')
55 | .forEach(file => {
56 | const model = sequelize.import(path.join(__dirname, file))
57 | db[model.name] = model
58 | })
59 |
60 | Object.keys(db).forEach(modelName => {
61 | if (db[modelName].associate) {
62 | db[modelName].associate(db)
63 | }
64 | })
65 |
66 | db.sequelize = sequelize
67 |
68 | module.exports = db
69 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'
3 | import { useSelector } from 'react-redux'
4 |
5 | // config
6 | import routes from '@/routes'
7 |
8 | // components
9 | import PublicComponent from '@/components/Public'
10 |
11 | const App = props => {
12 | const role = useSelector(state => state.user.role) // 相当于 connect(state => state.user.role)(App)
13 |
14 | // 解构 route
15 | function renderRoutes(routes, contextPath) {
16 | const children = []
17 |
18 | const renderRoute = (item, routeContextPath) => {
19 | let newContextPath = item.path ? `${routeContextPath}/${item.path}` : routeContextPath
20 | newContextPath = newContextPath.replace(/\/+/g, '/')
21 | if (newContextPath.includes('admin') && role !== 1) {
22 | item = {
23 | ...item,
24 | component: () =>
,
25 | children: []
26 | }
27 | }
28 | if (!item.component) return
29 |
30 | if (item.childRoutes) {
31 | const childRoutes = renderRoutes(item.childRoutes, newContextPath)
32 | children.push(
33 |
{childRoutes} }
36 | path={newContextPath}
37 | />
38 | )
39 | item.childRoutes.forEach(r => renderRoute(r, newContextPath))
40 | } else {
41 | children.push( )
42 | }
43 | }
44 |
45 | routes.forEach(item => renderRoute(item, contextPath))
46 |
47 | return {children}
48 | }
49 |
50 | const children = renderRoutes(routes, '/')
51 | return (
52 |
53 | {children}
54 |
55 | )
56 | }
57 |
58 | export default App
59 |
--------------------------------------------------------------------------------
/server/middlewares/authHandler.js:
--------------------------------------------------------------------------------
1 | const { checkToken } = require('../utils/token')
2 |
3 | /**
4 | * role === 1 需要权限的路由
5 | * @required 'all': get post put delete 均需要权限。
6 | */
7 | const verifyList1 = [
8 | { regexp: /\/article\/output/, required: 'get', verifyTokenBy: 'url' }, // 导出文章 verifyTokenBy 从哪里验证 token
9 | { regexp: /\/article/, required: 'post, put, delete' }, // 普通用户 禁止修改或者删除、添加文章
10 | { regexp: /\/discuss/, required: 'delete, post' }, // 普通用户 禁止删除评论
11 | { regexp: /\/user/, required: 'get, put, delete' } // 普通用户 禁止获取用户、修改用户、以及删除用户
12 | ]
13 |
14 | // role === 2 需要权限的路由
15 | const verifyList2 = [
16 | { regexp: /\/discuss/, required: 'post' } // 未登录用户 禁止评论
17 | ]
18 |
19 | /**
20 | * 检查路由是否需要权限,返回一个权限列表
21 | *
22 | * @return {Array} 返回 roleList
23 | */
24 | function checkAuth(method, url) {
25 | function _verify(list, role) {
26 | const target = list.find(v => {
27 | return v.regexp.test(url) && (v.required === 'all' || v.required.toUpperCase().includes(method))
28 | })
29 |
30 | return target
31 | }
32 |
33 | const roleList = []
34 | const result1 = _verify(verifyList1)
35 | const result2 = _verify(verifyList2)
36 |
37 | result1 && roleList.push({ role: 1, verifyTokenBy: result1.verifyTokenBy || 'headers' })
38 | result2 && roleList.push({ role: 2, verifyTokenBy: result1.verifyTokenBy || 'headers' })
39 |
40 | return roleList
41 | }
42 |
43 | // auth example token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoyLCJpZCI6MSwiaWF0IjoxNTY3MDcyOTE4LCJleHAiOjE1Njk2NjQ5MTh9.-V71bEfuUczUt_TgK0AWUJTbAZhDAN5wAv8RjmxfDKI
44 | module.exports = async (ctx, next) => {
45 | const roleList = checkAuth(ctx.method, ctx.url)
46 | // 该路由需要验证
47 | if (roleList.length > 0) {
48 | if (checkToken(ctx, roleList)) {
49 | await next()
50 | } else {
51 | // ctx.status = 401
52 | // ctx.client(401)
53 | ctx.throw(401)
54 | }
55 | } else {
56 | await next()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/web/tag/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, useEffect, useState } from 'react'
2 | import './index.less'
3 |
4 | import axios from '@/utils/axios'
5 | import { TAG_PAGESIZE } from '@/utils/config'
6 |
7 | import { Link } from 'react-router-dom'
8 | import { Timeline, Spin } from 'antd'
9 | import Pagination from '@/components/Pagination'
10 |
11 | // hooks
12 | import useFetchList from '@/hooks/useFetchList'
13 |
14 | function TimeLineList({ list, name, type }) {
15 | return (
16 |
17 |
18 |
19 |
20 | {name}
21 | {type}
22 |
23 |
24 |
25 | {list.map(item => (
26 |
27 | {item.createdAt.slice(5, 10)}
28 | {item.title}
29 |
30 | ))}
31 |
32 |
33 | )
34 | }
35 |
36 | // 根据 tag / category 获取文章列表
37 | function List(props) {
38 | const type = props.location.pathname.includes('categories') ? 'category' : 'tag'
39 | const name = props.match.params.name
40 |
41 | const { loading, pagination, dataList } = useFetchList({
42 | requestUrl: '/article/list',
43 | queryParams: { [type]: name, pageSize: TAG_PAGESIZE },
44 | fetchDependence: [props.location.search, props.location.pathname]
45 | })
46 |
47 | return (
48 |
49 |
56 |
57 | )
58 | }
59 |
60 | export default List
61 |
--------------------------------------------------------------------------------
/src/components/Discuss/index.less:
--------------------------------------------------------------------------------
1 | #discuss {
2 | font-family: 'Lato', 'PingFang SC', 'Microsoft YaHei', sans-serif;
3 | .discuss-header {
4 | padding: 20px 0;
5 | color: #555;
6 | font-size: 16px;
7 | .discuss-count {
8 | color: #6190e8;
9 | border-bottom: 1px dotted #6190e8;
10 | margin-right: 5px;
11 | }
12 | .hr {
13 | margin: 10px 0 0 0;
14 | }
15 | .discuss-user {
16 | float: right;
17 | cursor: pointer;
18 | }
19 | }
20 | .controls {
21 | float: right;
22 | .controls-tip-icon {
23 | color: #6190e8;
24 | margin-right: 4px;
25 | }
26 | .controls-tip {
27 | margin-right: 20px;
28 | color: #6190e8;
29 | }
30 | }
31 |
32 | .reply-form {
33 | overflow: hidden;
34 | .reply-form-controls {
35 | float: right;
36 | padding: 10px 0;
37 | .tip {
38 | color: #c2c2c2;
39 | margin-right: 8px;
40 | font-size: 13px;
41 | }
42 | }
43 | }
44 |
45 | .icon-delete {
46 | color: #f00;
47 | vertical-align: middle;
48 | cursor: pointer;
49 | display: none;
50 | }
51 |
52 | .ant-comment-content:hover {
53 | .icon-delete {
54 | display: inline-block;
55 | }
56 | }
57 |
58 | .ant-popover-open {
59 | .icon-delete {
60 | display: inline-block;
61 | }
62 | }
63 |
64 | .icon-delete.active {
65 | display: inline-block;
66 | }
67 |
68 | .ant-comment-inner {
69 | padding: 6px 0;
70 | }
71 |
72 | .ant-comment-actions {
73 | margin-top: 0;
74 | }
75 | .ant-comment-content-detail p {
76 | white-space: inherit;
77 | margin-bottom: 0;
78 | }
79 | }
80 |
81 | @media screen and (max-width: 736px) {
82 | .controls {
83 | width: 100%;
84 | margin-top: -10px;
85 |
86 | .disscus-btn {
87 | width: 100%;
88 | }
89 | .controls-tip,
90 | .controls-tip-icon {
91 | display: none;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/assets/icons/iconfont.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "1383706",
3 | "name": "react-blog-v2",
4 | "font_family": "iconfont",
5 | "css_prefix_text": "icon",
6 | "description": "",
7 | "glyphs": [
8 | {
9 | "icon_id": "5194",
10 | "name": "tags",
11 | "font_class": "tags",
12 | "unicode": "e602",
13 | "unicode_decimal": 58882
14 | },
15 | {
16 | "icon_id": "455825",
17 | "name": "icon-location",
18 | "font_class": "location",
19 | "unicode": "e620",
20 | "unicode_decimal": 58912
21 | },
22 | {
23 | "icon_id": "3255327",
24 | "name": "e-mail",
25 | "font_class": "email",
26 | "unicode": "e603",
27 | "unicode_decimal": 58883
28 | },
29 | {
30 | "icon_id": "3510896",
31 | "name": "掘金",
32 | "font_class": "juejin",
33 | "unicode": "e600",
34 | "unicode_decimal": 58880
35 | },
36 | {
37 | "icon_id": "4472443",
38 | "name": "tip-2",
39 | "font_class": "tips",
40 | "unicode": "e704",
41 | "unicode_decimal": 59140
42 | },
43 | {
44 | "icon_id": "5296170",
45 | "name": "blog",
46 | "font_class": "blog2",
47 | "unicode": "e601",
48 | "unicode_decimal": 58881
49 | },
50 | {
51 | "icon_id": "6428398",
52 | "name": "post",
53 | "font_class": "post",
54 | "unicode": "e84c",
55 | "unicode_decimal": 59468
56 | },
57 | {
58 | "icon_id": "6586104",
59 | "name": "飞机",
60 | "font_class": "blog",
61 | "unicode": "e604",
62 | "unicode_decimal": 58884
63 | },
64 | {
65 | "icon_id": "7823880",
66 | "name": "评 论",
67 | "font_class": "comment",
68 | "unicode": "e65c",
69 | "unicode_decimal": 58972
70 | },
71 | {
72 | "icon_id": "8872521",
73 | "name": "view",
74 | "font_class": "view",
75 | "unicode": "e700",
76 | "unicode_decimal": 59136
77 | }
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/src/views/web/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import './index.less'
3 |
4 | import { decodeQuery, translateMarkdown } from '@/utils'
5 | import { HOME_PAGESIZE } from '@/utils/config'
6 |
7 | // components
8 | import QuickLink from './QuickLink'
9 | import ArticleList from './List'
10 |
11 | import { Empty, Spin } from 'antd'
12 | import Pagination from '@/components/Pagination'
13 |
14 | // hooks
15 | import useFetchList from '@/hooks/useFetchList'
16 |
17 | const Home = props => {
18 | const { loading, pagination, dataList } = useFetchList({
19 | requestUrl: '/article/list',
20 | queryParams: { pageSize: HOME_PAGESIZE },
21 | fetchDependence: [props.location.search]
22 | })
23 |
24 | const list = useMemo(() => {
25 | return [...dataList].map(item => {
26 | const index = item.content.indexOf('')
27 | item.content = translateMarkdown(item.content.slice(0, index))
28 | return item
29 | })
30 | }, [dataList])
31 |
32 | const { keyword } = decodeQuery(props.location.search)
33 |
34 | return (
35 |
36 |
37 | {/* list */}
38 |
39 |
40 | {/* quick link */}
41 |
42 |
43 | {/* serach empty result */}
44 | {list.length === 0 && keyword && (
45 |
46 |
48 | 不存在标题/内容中含有 {keyword} 的文章!
49 |
50 | )} />
51 |
52 | )}
53 |
54 |
{
58 | document.querySelector('.app-main').scrollTop = 0 // turn to the top
59 | pagination.onChange(page)
60 | }
61 | } />
62 |
63 |
64 | )
65 | }
66 |
67 | export default Home
68 |
--------------------------------------------------------------------------------
/src/layout/web/sidebar/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { SIDEBAR } from '@/config'
3 | import axios from '@/utils/axios'
4 | import { useSelector } from 'react-redux'
5 |
6 | // components
7 | import { Link } from 'react-router-dom'
8 | import Href from '@/components/Href'
9 | import { Icon, Divider, Tag } from 'antd'
10 |
11 | import { Alert } from 'antd'
12 | import { ANNOUNCEMENT } from '@/config'
13 |
14 | import useFetchList from '@/hooks/useFetchList'
15 |
16 | function SideBar(props) {
17 | const tagList = useSelector(state => state.article.tagList || [])
18 |
19 | const { dataList: articleList } = useFetchList({
20 | withLoading: false,
21 | requestUrl: '/article/list',
22 | queryParams: {
23 | order: 'viewCount DESC',
24 | page: 1,
25 | pageSize: 6
26 | }
27 | })
28 |
29 | return (
30 |
31 |
32 | {SIDEBAR.title}
33 | {SIDEBAR.subTitle}
34 |
35 | {Object.entries(SIDEBAR.homepages).map(([linkName, item]) => (
36 |
37 | {item.icon}
38 | {linkName}
39 |
40 | ))}
41 |
42 |
43 | {ANNOUNCEMENT.enable && }
44 |
45 | 热门文章
46 |
47 | {articleList.map(d => (
48 |
49 | {d.title}
50 |
51 | ))}
52 |
53 |
54 | 标签
55 |
56 | {tagList.map((tag, i) => (
57 |
58 | {tag.name}
59 |
60 | ))}
61 |
62 |
63 | )
64 | }
65 |
66 | export default SideBar
67 |
--------------------------------------------------------------------------------
/src/layout/admin/sidebar/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { NavLink, withRouter } from 'react-router-dom'
3 | import { Menu, Icon } from 'antd'
4 | import menu from './menu'
5 | const SubMenu = Menu.SubMenu
6 |
7 | function getMenuOpenKeys(menu) {
8 | const list = []
9 | menu.forEach(item => {
10 | if (item.children) {
11 | item.children.forEach(child => {
12 | list.push({
13 | pathname: child.path,
14 | openKey: item.path
15 | })
16 | })
17 | }
18 | })
19 | return list
20 | }
21 | const menuMenuOpenKeys = getMenuOpenKeys(menu)
22 |
23 | function AdminSidebar(props) {
24 | // 菜单渲染
25 | function renderMenu(list) {
26 | const renderRoute = item => {
27 | if (item.hidden) return null
28 | if (item.children) {
29 | return (
30 |
34 | {item.icon && }
35 | {item.name}
36 |
37 | }>
38 | {item.children.map(r => renderRoute(r))}
39 |
40 | )
41 | } else {
42 | return (
43 | item.name && (
44 |
45 |
46 | {item.icon && }
47 | {item.name}
48 |
49 |
50 | )
51 | )
52 | }
53 | }
54 | return list.map(l => renderRoute(l))
55 | }
56 |
57 | const target = menuMenuOpenKeys.find(d => d.pathname === props.selectedKeys[0])
58 | const openKeys = target ? [target.openKey] : []
59 | return (
60 |
66 | {renderMenu(menu)}
67 |
68 | )
69 | }
70 |
71 | export default withRouter(AdminSidebar)
72 |
--------------------------------------------------------------------------------
/src/layout/web/header/right/UserInfo.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect, useSelector, useDispatch } from 'react-redux'
3 | import { withRouter } from 'react-router-dom'
4 |
5 | // methods
6 | import { loginout } from '@/redux/user/actions'
7 |
8 | // components
9 | import { Button, Dropdown, Menu, Avatar } from 'antd'
10 | import AppAvatar from '@/components/Avatar'
11 |
12 | // hooks
13 | import useBus from '@/hooks/useBus'
14 |
15 | function UserInfo(props) {
16 | const dispatch = useDispatch()
17 | const bus = useBus()
18 | const userInfo = useSelector(state => state.user)
19 | const { username, github, role } = userInfo
20 |
21 | const MenuOverLay = (
22 |
23 | {role === 1 && (
24 |
25 | bus.emit('openUploadModal')}>导入文章
26 |
27 | )}
28 | {role === 1 && (
29 |
30 | props.history.push('/admin')}>后台管理
31 |
32 | )}
33 |
34 | dispatch(loginout())}>
35 | 退出登录
36 |
37 |
38 |
39 | )
40 | return (
41 |
42 | {username ? (
43 |
44 |
47 |
48 | )
49 | : (
50 | <>
51 |
bus.emit('openSignModal', 'login')}>
57 | 登录
58 |
59 |
bus.emit('openSignModal', 'register')}>
60 | 注册
61 |
62 | >
63 | )}
64 |
65 | )
66 | }
67 |
68 | export default withRouter(UserInfo)
69 |
--------------------------------------------------------------------------------
/src/views/admin/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './index.less'
3 |
4 | const Minions = () => (
5 |
6 | {/* */}
7 |
8 | {/* */}
9 |
10 | {/* */}
11 |
12 | {/* */}
13 |
17 | {/* */}
18 |
19 | {/* */}
20 |
21 | {/* */}
22 |
23 |
24 |
25 |
26 |
27 | {/* */}
28 |
29 |
30 |
31 |
32 | {/* */}
33 |
34 | {/* */}
35 |
40 | {/* */}
41 |
46 |
47 | {/* */}
48 |
51 | {/* */}
52 |
56 | {/* */}
57 |
61 | {/* */}
62 |
63 |
64 |
65 | )
66 |
67 | export default Minions
68 |
--------------------------------------------------------------------------------
/src/views/web/archives/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, Fragment } from 'react'
2 | import './index.less'
3 |
4 | import { ARCHIVES_PAGESIZE } from '@/utils/config'
5 |
6 | // methods
7 | import { groupBy } from '@/utils'
8 |
9 | // components
10 | import { Timeline, Icon, Spin } from 'antd'
11 | import { Link } from 'react-router-dom'
12 | import Pagination from '@/components/Pagination'
13 |
14 | // hooks
15 | import useFetchList from '@/hooks/useFetchList'
16 |
17 | function Archives(props) {
18 | const { dataList, loading, pagination } = useFetchList({
19 | requestUrl: '/article/list',
20 | queryParams: {
21 | pageSize: ARCHIVES_PAGESIZE
22 | },
23 | fetchDependence: [props.location.pathname, props.location.search]
24 | })
25 |
26 | const list = groupBy(dataList, item => item.createdAt.slice(0, 4)) // 按年份排序
27 |
28 | return (
29 |
30 |
31 |
32 | {list.map((d, i) => (
33 |
34 | {i === 0 && (
35 |
36 | {`Nice! ${pagination.total} posts in total. Keep on posting.`}
37 |
38 |
39 |
40 | )}
41 |
42 | } color='red'>
43 |
44 | {d[0]['createdAt'].slice(0, 4)}
45 | ...
46 |
47 |
48 |
49 |
50 | {d.map(item => (
51 |
52 | {item.createdAt.slice(5, 10)}
53 | {item.title}
54 |
55 | ))}
56 |
57 | ))}
58 |
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default Archives
67 |
--------------------------------------------------------------------------------
/src/layout/web/header/left/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Icon, Dropdown, Menu, Input, message } from 'antd'
3 | import { Link } from 'react-router-dom'
4 | import { useHistory, useLocation } from 'react-router-dom'
5 |
6 | // import config
7 | import { HEADER_BLOG_NAME } from '@/config'
8 | import navList from '../right/navList'
9 |
10 | // icon
11 | import SvgIcon from '@/components/SvgIcon'
12 |
13 | const HeaderLeft = props => {
14 | const [keyword, setKeyword] = useState('')
15 | const history = useHistory()
16 |
17 | function handleChange(e) {
18 | e.preventDefault()
19 | setKeyword(e.target.value)
20 | }
21 |
22 | function onPressEnter(e) {
23 | e.target.blur()
24 | }
25 |
26 | function onSubmit() {
27 | history.push(`/?page=1&keyword=${keyword}`)
28 | setKeyword('')
29 | }
30 |
31 | function clickSearch(e) {
32 | e.stopPropagation()
33 | }
34 |
35 | const menu = (
36 |
37 | {navList.map(nav => (
38 |
39 |
40 | {nav.icon && }
41 | {nav.title}
42 |
43 |
44 | ))}
45 |
46 |
47 |
55 |
56 |
57 | )
58 |
59 | return (
60 |
61 |
62 | {HEADER_BLOG_NAME}
63 | document.querySelector('.app-header .header-left')}>
68 |
69 |
70 |
71 | )
72 | }
73 |
74 | export default HeaderLeft
75 |
--------------------------------------------------------------------------------
/server/config/index.js:
--------------------------------------------------------------------------------
1 | const devMode = process.env.NODE_ENV === 'development'
2 |
3 | const config = {
4 | PORT: 6060, // 启动端口
5 | ADMIN_GITHUB_LOGIN_NAME: 'gershonv', // 博主的 github 登录的账户名 user
6 | GITHUB: {
7 | client_id: 'c6a96a84105bb0be1fe5',
8 | client_secret: '463f3994ab5687544b2cddbb6cf44920bf179ad9',
9 | access_token_url: 'https://github.com/login/oauth/access_token',
10 | fetch_user_url: 'https://api.github.com/user', // 用于 oauth2
11 | fetch_user: 'https://api.github.com/users/' // fetch user url https://api.github.com/users/gershonv
12 | },
13 | EMAIL_NOTICE: {
14 | // 邮件通知服务
15 | // detail: https://nodemailer.com/
16 | enable: true, // 开关
17 | transporterConfig: {
18 | host: 'smtp.163.com',
19 | port: 465,
20 | secure: true, // true for 465, false for other ports
21 | auth: {
22 | user: 'guodadablog@163.com', // generated ethereal user
23 | pass: '123456' // generated ethereal password 授权码 而非 密码
24 | }
25 | },
26 | subject: '郭大大的博客 - 您的评论获得新的回复!', // 主题
27 | text: '您的评论获得新的回复!',
28 | WEB_HOST: 'http://127.0.0.1:3000' // email callback url
29 | },
30 | TOKEN: {
31 | secret: 'guo-test', // secret is very important!
32 | expiresIn: '720h' // token 有效期
33 | },
34 | DATABASE: {
35 | database: 'test',
36 | user: 'root',
37 | password: '123456',
38 | options: {
39 | host: 'localhost', // 连接的 host 地址
40 | dialect: 'mysql', // 连接到 mysql
41 | pool: {
42 | max: 5,
43 | min: 0,
44 | acquire: 30000,
45 | idle: 10000
46 | },
47 | define: {
48 | timestamps: false, // 默认不加时间戳
49 | freezeTableName: true // 表名默认不加 s
50 | },
51 | timezone: '+08:00'
52 | }
53 | }
54 | }
55 |
56 | // 部署的环境变量设置
57 | if (!devMode) {
58 | console.log('env production....')
59 |
60 | // ==== 配置数据库
61 | config.DATABASE = {
62 | ...config.DATABASE,
63 | database: '', // 数据库名
64 | user: '', // 账号
65 | password: '' // 密码
66 | }
67 |
68 | // 配置 github 授权
69 | config.GITHUB.client_id = ''
70 | config.GITHUB.client_secret = ''
71 |
72 | // ==== 配置 token 密钥
73 | config.TOKEN.secret = ''
74 |
75 | // ==== 配置邮箱
76 |
77 | // config.EMAIL_NOTICE.enable = true
78 | config.EMAIL_NOTICE.transporterConfig.auth = {
79 | user: 'guodadablog@163.com', // generated ethereal user
80 | pass: '123456XXX' // generated ethereal password 授权码 而非 密码
81 | }
82 | config.EMAIL_NOTICE.WEB_HOST = 'https://guodada.fun'
83 | }
84 |
85 | module.exports = config
86 |
--------------------------------------------------------------------------------
/src/views/admin/article/edit/Tag.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import { Input, Tooltip, Icon, Tag } from 'antd'
4 |
5 | const { CheckableTag } = Tag
6 |
7 | function AppTag(props) {
8 | const { list, setList } = props
9 | const [inputVisible, setInputVisible] = useState(false)
10 | const [inputValue, setInputValue] = useState('')
11 | const { selectedList, setSelectedList } = props
12 | let inputRef = null
13 |
14 | function removeItem(item) {
15 | const newList = list.filter(l => l !== item)
16 | setList(newList)
17 | }
18 |
19 | function addItem() {
20 | if (inputValue && !list.find(d => d === inputValue)) {
21 | setList([...list, inputValue])
22 | setSelectedList([...selectedList, inputValue])
23 | setInputValue('')
24 | }
25 |
26 | setInputVisible(false)
27 | }
28 |
29 | function showInput() {
30 | setInputVisible(true)
31 | inputRef && inputRef.focus()
32 | }
33 |
34 | // 行点击选中事件
35 | function handleSelect(value, checked) {
36 | const newList = checked ? [...selectedList, value] : selectedList.filter(t => t !== value)
37 | setSelectedList(newList)
38 | }
39 |
40 | return (
41 | <>
42 | {list.map((item, index) => {
43 | const isLongTag = item.length > 20
44 | const tagElem = (
45 | removeItem(item)}
49 | checked={selectedList.includes(item)}
50 | onChange={checked => handleSelect(item, checked)}
51 | color='#1890ff'>
52 | {isLongTag ? `${item.slice(0, 20)}...` : item}
53 |
54 | )
55 | return isLongTag ? (
56 |
57 | {tagElem}
58 |
59 | ) : (
60 | tagElem
61 | )
62 | })}
63 |
64 | {
67 | inputRef = el
68 | }}
69 | type='text'
70 | size='small'
71 | value={inputValue}
72 | onChange={e => setInputValue(e.target.value)}
73 | onBlur={addItem}
74 | onPressEnter={addItem}
75 | />
76 |
77 | {!inputVisible && (
78 |
79 | New Tag
80 |
81 | )}
82 | >
83 | )
84 | }
85 |
86 | export default AppTag
87 |
--------------------------------------------------------------------------------
/src/views/web/about/MyInfo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // components
4 | import { Divider, Rate, Icon, Avatar } from 'antd'
5 | import Href from '@/components/Href'
6 | import SvgIcon from '@/components/SvgIcon'
7 |
8 | const skills = [
9 | {
10 | label: '具备扎实的 Javascript 基础,熟练使用 ES6+ 语法。',
11 | rate: 3
12 | },
13 | {
14 | label: '熟悉 React 并理解其原理,熟悉 Vue 框架及其用法。',
15 | rate: 3
16 | },
17 | {
18 | label: '熟练使用 Webpack 打包工具,熟悉常用工程化和模块化方案。',
19 | rate: 3
20 | },
21 | {
22 | label: '熟悉 Koa、Mysql,针对需求可以做到简单的数据库设计、接口的开发与设计!',
23 | rate: 2
24 | },
25 | {
26 | label: '熟悉 HTTP 协议,缓存、性能优化、安全等,了解浏览器原理。',
27 | rate: 2
28 | },
29 | {
30 | label: '熟悉常用的算法与数据结构',
31 | rate: 2
32 | }
33 | ]
34 |
35 | const MyInfo = () => {
36 | return (
37 | <>
38 | 博客简述
39 | 本博客使用的技术为 react hooks + antd + koa2 + mysql
40 |
41 | 源码地址为 github
42 | ,仅供参考,不做商业用途!
43 |
44 |
45 | 关于我
46 |
47 |
48 | 姓名:Guodada
49 | 学历专业:本科 软件工程
50 |
51 | 联系方式:
52 | {/* 434358603
53 | */}
54 |
55 | alvin00216@163.com
56 |
57 | 坐标:广州市
58 |
59 | 其他博客地址:
60 | alvin's note
61 |
62 | 掘金主页
63 |
64 |
65 | 技能
66 |
67 | {skills.map((item, i) => (
68 |
69 | {item.label}
70 |
71 |
72 | ))}
73 |
74 |
75 |
76 | 其他
77 |
78 | 常用开发工具: vscode、webstorm、git
79 | 熟悉的 UI 框架: antd、element-ui、vux
80 | 具备良好的编码风格和习惯,团队规范意识,乐于分享
81 |
82 |
83 |
84 | 个人
85 |
88 |
89 |
90 | >
91 | )
92 | }
93 |
94 | export default MyInfo
95 |
--------------------------------------------------------------------------------
/LICENSE.996ICU:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Chersquwn
2 |
3 | 996 License Version 1.0 (Draft)
4 |
5 | Permission is hereby granted to any individual or legal entity
6 | obtaining a copy of this licensed work (including the source code,
7 | documentation and/or related items, hereinafter collectively referred
8 | to as the "licensed work"), free of charge, to deal with the licensed
9 | work for any purpose, including without limitation, the rights to use,
10 | reproduce, modify, prepare derivative works of, distribute, publish
11 | and sublicense the licensed work, subject to the following conditions:
12 |
13 | 1. The individual or the legal entity must conspicuously display,
14 | without modification, this License and the notice on each redistributed
15 | or derivative copy of the Licensed Work.
16 |
17 | 2. The individual or the legal entity must strictly comply with all
18 | applicable laws, regulations, rules and standards of the jurisdiction
19 | relating to labor and employment where the individual is physically
20 | located or where the individual was born or naturalized; or where the
21 | legal entity is registered or is operating (whichever is stricter). In
22 | case that the jurisdiction has no such laws, regulations, rules and
23 | standards or its laws, regulations, rules and standards are
24 | unenforceable, the individual or the legal entity are required to
25 | comply with Core International Labor Standards.
26 |
27 | 3. The individual or the legal entity shall not induce or force its
28 | employee(s), whether full-time or part-time, or its independent
29 | contractor(s), in any methods, to agree in oral or written form, to
30 | directly or indirectly restrict, weaken or relinquish his or her
31 | rights or remedies under such laws, regulations, rules and standards
32 | relating to labor and employment as mentioned above, no matter whether
33 | such written or oral agreement are enforceable under the laws of the
34 | said jurisdiction, nor shall such individual or the legal entity
35 | limit, in any methods, the rights of its employee(s) or independent
36 | contractor(s) from reporting or complaining to the copyright holder or
37 | relevant authorities monitoring the compliance of the license about
38 | its violation(s) of the said license.
39 |
40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
43 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
44 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
45 | OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE
46 | LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
47 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
25 | 郭大大的博客
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | You need to enable JavaScript to run this app.
51 |
52 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/config/modules.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 | const chalk = require('react-dev-utils/chalk');
7 | const resolve = require('resolve');
8 |
9 | /**
10 | * Get the baseUrl of a compilerOptions object.
11 | *
12 | * @param {Object} options
13 | */
14 | function getAdditionalModulePaths(options = {}) {
15 | const baseUrl = options.baseUrl;
16 |
17 | // We need to explicitly check for null and undefined (and not a falsy value) because
18 | // TypeScript treats an empty string as `.`.
19 | if (baseUrl == null) {
20 | // If there's no baseUrl set we respect NODE_PATH
21 | // Note that NODE_PATH is deprecated and will be removed
22 | // in the next major release of create-react-app.
23 |
24 | const nodePath = process.env.NODE_PATH || '';
25 | return nodePath.split(path.delimiter).filter(Boolean);
26 | }
27 |
28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
29 |
30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
31 | // the default behavior.
32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
33 | return null;
34 | }
35 |
36 | // Allow the user set the `baseUrl` to `appSrc`.
37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') {
38 | return [paths.appSrc];
39 | }
40 |
41 | // Otherwise, throw an error.
42 | throw new Error(
43 | chalk.red.bold(
44 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
45 | ' Create React App does not support other values at this time.'
46 | )
47 | );
48 | }
49 |
50 | function getModules() {
51 | // Check if TypeScript is setup
52 | const hasTsConfig = fs.existsSync(paths.appTsConfig);
53 | const hasJsConfig = fs.existsSync(paths.appJsConfig);
54 |
55 | if (hasTsConfig && hasJsConfig) {
56 | throw new Error(
57 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
58 | );
59 | }
60 |
61 | let config;
62 |
63 | // If there's a tsconfig.json we assume it's a
64 | // TypeScript project and set up the config
65 | // based on tsconfig.json
66 | if (hasTsConfig) {
67 | const ts = require(resolve.sync('typescript', {
68 | basedir: paths.appNodeModules,
69 | }));
70 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
71 | // Otherwise we'll check if there is jsconfig.json
72 | // for non TS projects.
73 | } else if (hasJsConfig) {
74 | config = require(paths.appJsConfig);
75 | }
76 |
77 | config = config || {};
78 | const options = config.compilerOptions || {};
79 |
80 | const additionalModulePaths = getAdditionalModulePaths(options);
81 |
82 | return {
83 | additionalModulePaths: additionalModulePaths,
84 | hasTsConfig,
85 | };
86 | }
87 |
88 | module.exports = getModules();
89 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import './index.less'
4 | // config
5 | import { DISCUSS_AVATAR } from '@/config'
6 |
7 | // components
8 | import Href from '@/components/Href'
9 | import { Avatar, Popover, Icon, Typography } from 'antd'
10 | import SvgIcon from '@/components/SvgIcon'
11 |
12 | const { Text, Title } = Typography
13 |
14 | function AvatarComponent({ username, github, role }) {
15 | let avatarSrc = ''
16 | if (github && github.avatar_url) avatarSrc = github.avatar_url
17 | if (role === 1) avatarSrc = DISCUSS_AVATAR
18 | return {username}
19 | }
20 | //
21 | function AppAvatar(props) {
22 | const { role, username, github } = props.userInfo
23 | if (github && props.popoverVisible) {
24 | return (
25 |
34 |
35 | {github.bio}
36 | >
37 | ) : null
38 | }
39 | content={
40 |
41 |
42 |
43 |
44 |
45 |
46 | {github.name ? (
47 | <>
48 | {github.name}
49 | {github.login}
50 | >
51 | ) : (
52 | {github.login}
53 | )}
54 |
55 |
56 | {github.blog && (
57 |
58 |
59 |
60 | {github.blog}
61 |
62 |
63 | )}
64 |
65 | {github.location && (
66 |
67 |
68 | {github.location}
69 |
70 | )}
71 |
72 |
73 | }>
74 |
75 |
76 |
77 | )
78 | } else {
79 | return
80 | }
81 | }
82 |
83 | AppAvatar.propTypes = {
84 | userInfo: PropTypes.object.isRequired,
85 | popoverVisible: PropTypes.bool
86 | }
87 |
88 | AppAvatar.defaultProps = {
89 | popoverVisible: true
90 | }
91 |
92 | export default AppAvatar
93 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import marked from 'marked'
2 | import { COLOR_LIST } from '@/utils/config'
3 | import xss from 'xss'
4 | import { clear, get } from '@/utils/storage'
5 |
6 | // 转化 md 语法为 html
7 | export const translateMarkdown = (plainText, isGuardXss = false) => {
8 | return marked(isGuardXss ? xss(plainText) : plainText, {
9 | renderer: new marked.Renderer(),
10 | gfm: true,
11 | pedantic: false,
12 | sanitize: false,
13 | tables: true,
14 | breaks: true,
15 | smartLists: true,
16 | smartypants: true,
17 | highlight: function(code) {
18 | /*eslint no-undef: "off"*/
19 | return hljs.highlightAuto(code).value
20 | }
21 | })
22 | }
23 |
24 | // 获取 url query 参数
25 | export const decodeQuery = url => {
26 | const params = {}
27 | const paramsStr = url.replace(/\.*\?/, '') // a=1&b=2&c=&d=xxx&e
28 | paramsStr.split('&').forEach(v => {
29 | const d = v.split('=')
30 | if (d[1] && d[0]) params[d[0]] = d[1]
31 | })
32 | return params
33 | }
34 |
35 | // 计算 评论数
36 | export const calcCommentsCount = commentList => {
37 | let count = commentList.length
38 | commentList.forEach(item => {
39 | count += item.replies.length
40 | })
41 | return count
42 | }
43 |
44 | // 取数组中的随机数
45 | export const randomIndex = arr => Math.floor(Math.random() * arr.length)
46 |
47 | /**
48 | * 对数组进行分组
49 | * @param {Array} arr - 分组对象
50 | * @param {Function} f
51 | * @returns 数组分组后的新数组
52 | */
53 | export const groupBy = (arr, f) => {
54 | const groups = {}
55 | arr.forEach(item => {
56 | const group = JSON.stringify(f(item))
57 | groups[group] = groups[group] || []
58 | groups[group].push(item)
59 | })
60 | return Object.keys(groups).map(group => groups[group])
61 | }
62 |
63 | /**
64 | * @param {string} path
65 | * @returns {Boolean}
66 | */
67 | export function isExternal(path) {
68 | return /^(https?:|mailto:|tel:|http:)/.test(path)
69 | }
70 |
71 | // 获取 token
72 | export function getToken() {
73 | let token = ''
74 | const userInfo = get('userInfo')
75 |
76 | if (userInfo && userInfo.token) {
77 | token = 'Bearer ' + userInfo.token
78 | }
79 |
80 | return token
81 | }
82 |
83 | /**
84 | * 生成随机 ID
85 | * @param {Number} len - 长度
86 | */
87 | export function RandomId(len) {
88 | return Math.random()
89 | .toString(36)
90 | .substr(3, len)
91 | }
92 |
93 | /**
94 | * debounce
95 | */
96 | export function debounce(func, wait) {
97 | let timer = null
98 | return function() {
99 | const context = this
100 | const args = arguments
101 | clearTimeout(timer)
102 | timer = setTimeout(function() {
103 | func.apply(context, args)
104 | }, wait)
105 | }
106 | }
107 |
108 | // 生成 color
109 | export function genertorColor(list = [], colorList = COLOR_LIST) {
110 | const _list = [...list]
111 | _list.forEach((l, i) => {
112 | l.color = colorList[i] || colorList[randomIndex(colorList)]
113 | })
114 | return _list
115 | }
116 |
--------------------------------------------------------------------------------
/src/hooks/useFetchList.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from 'react'
2 | import axios from '@/utils/axios'
3 |
4 | import { useLocation, useHistory } from 'react-router-dom'
5 | import { decodeQuery } from '@/utils'
6 | import useMount from './useMount'
7 |
8 | /**
9 | * fetchList
10 | * requestUrl 请求地址
11 | * queryParams 请求参数
12 | * withLoading 是否携带 loading
13 | * fetchDependence 依赖 => 可以根据地址栏解析拉取列表
14 | */
15 | export default function useFetchList({
16 | requestUrl = '',
17 | queryParams = null,
18 | withLoading = true,
19 | fetchDependence = []
20 | }) {
21 | const [dataList, setDataList] = useState([])
22 | const [loading, setLoading] = useState(false)
23 | const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 })
24 |
25 | const location = useLocation()
26 | const history = useHistory()
27 |
28 | useMount(() => {
29 | if (fetchDependence.length === 0) {
30 | fetchWithLoading()
31 | }
32 | })
33 |
34 | useEffect(() => {
35 | if (fetchDependence.length > 0) {
36 | const params = decodeQuery(location.search)
37 | fetchWithLoading(params)
38 | }
39 | }, fetchDependence)
40 |
41 | function fetchWithLoading(params) {
42 | withLoading && setLoading(true)
43 | fetchDataList(params)
44 | }
45 |
46 | function fetchDataList(params) {
47 | const requestParams = {
48 | page: pagination.current,
49 | pageSize: pagination.pageSize,
50 | ...queryParams,
51 | ...params
52 | }
53 |
54 | requestParams.page = parseInt(requestParams.page)
55 | requestParams.pageSize = parseInt(requestParams.pageSize)
56 | axios
57 | .get(requestUrl, { params: requestParams })
58 | .then(response => {
59 | pagination.total = response.count
60 | pagination.current = parseInt(requestParams.page)
61 | pagination.pageSize = parseInt(requestParams.pageSize)
62 | setPagination({ ...pagination })
63 | setDataList(response.rows)
64 | // console.log('%c useFetchList: ', 'background: yellow', requestParams, response)
65 | withLoading && setLoading(false)
66 | })
67 | .catch(e => withLoading && setLoading(false))
68 | }
69 |
70 | const onFetch = useCallback(
71 | params => {
72 | withLoading && setLoading(true)
73 | fetchDataList(params)
74 | },
75 | [queryParams]
76 | )
77 |
78 | const handlePageChange = useCallback(
79 | page => {
80 | // return
81 | const search = location.search.includes('page=')
82 | ? location.search.replace(/(page=)(\d+)/, `$1${page}`)
83 | : `?page=${page}`
84 | const jumpUrl = location.pathname + search
85 |
86 | history.push(jumpUrl)
87 | },
88 | [queryParams, location.pathname]
89 | )
90 |
91 | return {
92 | dataList,
93 | loading,
94 | pagination: {
95 | ...pagination,
96 | onChange: handlePageChange
97 | },
98 | onFetch
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(inputPath, needsSlash) {
15 | const hasSlash = inputPath.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return inputPath.substr(0, inputPath.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${inputPath}/`;
20 | } else {
21 | return inputPath;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right