├── .dockerignore ├── .github └── workflows │ └── aliyun.yml ├── .gitignore ├── .helm ├── README.md └── config │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ └── ingress.yaml │ └── values.yaml ├── .prettierrc ├── Dockerfile ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── nginx └── default.conf ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo180.png ├── manifest.json ├── mock │ └── homeData │ │ ├── articleList.json │ │ └── home.json └── robots.txt ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── Api │ ├── account.ts │ ├── article.ts │ ├── comment.ts │ ├── file.ts │ ├── follow.ts │ ├── index.d.ts │ ├── like.ts │ ├── request.ts │ ├── url.ts │ └── user.ts ├── App.test.tsx ├── App.tsx ├── components │ ├── Advertising │ │ ├── index.tsx │ │ └── style.ts │ ├── AppDownload │ │ ├── index.tsx │ │ └── style.ts │ └── SpinCenter │ │ ├── index.tsx │ │ └── style.ts ├── containers │ ├── AuthRoute │ │ ├── index.less │ │ └── index.tsx │ ├── Frame │ │ ├── Header │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Login │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Main │ │ │ └── index.tsx │ │ ├── Register │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ └── index.tsx │ └── RouteErrorBoundary │ │ ├── index.less │ │ └── index.tsx ├── index.tsx ├── lib │ ├── hooks │ │ ├── useAuthLogin.tsx │ │ ├── useDocumentTitle.ts │ │ ├── useEventFetch.ts │ │ ├── useFetch.ts │ │ ├── useFlag.ts │ │ ├── useInputEvent.ts │ │ ├── usePersist.ts │ │ ├── useQuery.ts │ │ ├── useRedux.ts │ │ └── useToggle.ts │ └── utils │ │ └── markdown.ts ├── modal │ ├── dtos │ │ ├── account.dto.ts │ │ ├── article.dto.ts │ │ ├── comment.dto.ts │ │ ├── signIn.dto.ts │ │ ├── signUp.dto.ts │ │ └── userUpdate.dto.ts │ ├── entities │ │ ├── article.entity.ts │ │ └── common.entity.ts │ └── interfaces │ │ ├── auth.interface.ts │ │ └── common.interface.ts ├── pages │ ├── editor │ │ ├── Menu │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Publish │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── home │ │ ├── Article │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ArticleList │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── mobiPost │ │ ├── Article │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── post │ │ ├── Article │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Author │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Catalog │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Comment │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── SuspendedPanel │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── settings │ │ ├── InfoGroup │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Navigation │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Password │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── Profile │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ └── user │ │ ├── FallowBlock │ │ ├── index.tsx │ │ └── style.ts │ │ ├── InfoBlock │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBlock │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBodyFollow │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBodyLike │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBodyLikes │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBodyPost │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListBodyPosts │ │ ├── index.tsx │ │ └── style.ts │ │ ├── ListHeader │ │ ├── index.tsx │ │ └── style.ts │ │ ├── MoreBLock │ │ ├── index.tsx │ │ └── style.ts │ │ ├── StatBlock │ │ ├── index.tsx │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts ├── react-app-env.d.ts ├── redux │ ├── context.ts │ └── reducer.ts ├── routes │ ├── index.tsx │ ├── lazyComponents.tsx │ ├── renderRoutes.tsx │ └── routes.ts ├── serviceWorker.ts ├── statics │ ├── arrow-down.svg │ ├── avatar.png │ ├── close-black.png │ ├── close-gray.png │ ├── dot-hover.svg │ ├── dot.svg │ ├── edit.svg │ ├── logo.svg │ ├── logout.svg │ ├── person.svg │ └── setting.svg ├── style.ts └── styles │ ├── atom-one-light.css │ ├── index.less │ └── markdown.less ├── tsconfig.json ├── tslint.json ├── typings └── global.d.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | logs/ 3 | .github/ 4 | .vscode/ 5 | test/ 6 | build/ -------------------------------------------------------------------------------- /.github/workflows/aliyun.yml: -------------------------------------------------------------------------------- 1 | name: Helm CI 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | env: 10 | NAMESPACE: namespace 11 | TAG: 0.0.${{ github.run_number }} 12 | REPO: https://anthhub.github.io/react-mini-blog 13 | REQ: $(curl -s https://anthhub.github.io/react-mini-blog/latest?q=$RANDOM) 14 | APP: react-mini-blog 15 | HOST: ${{ secrets.ALI_HOST }} 16 | DOMAIN: anthhub/react-mini-blog 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout Dockerfile 24 | uses: actions/checkout@v2 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v1 28 | 29 | - name: Cache Docker layers 30 | uses: actions/cache@v2 31 | with: 32 | path: /tmp/.buildx-cache 33 | key: ${{ runner.os }}-buildx-${{ github.sha }} 34 | restore-keys: | 35 | ${{ runner.os }}-buildx- 36 | 37 | - name: Cache NPM Dependences 38 | uses: actions/cache@v2 39 | with: 40 | path: ~/.npm 41 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 42 | restore-keys: | 43 | ${{ runner.os }}-node- 44 | 45 | - name: Build and Push Docker Image 46 | uses: docker/build-push-action@v1 47 | with: 48 | registry: ${{ secrets.ALI_DOCKER_HUB_REGISTRY }} 49 | username: ${{ secrets.ALI_DOCKER_HUB_USN }} 50 | password: ${{ secrets.ALI_DOCKER_HUB_PWD }} 51 | repository: ${{ secrets.HOST }} 52 | tags: ${{ env.TAG }} 53 | cache-from: type=local,src=/tmp/.buildx-cache 54 | cache-to: type=local,dest=/tmp/.buildx-cache 55 | 56 | - name: Install Helm 57 | uses: azure/setup-helm@v1 58 | with: 59 | version: v3.4.0 60 | 61 | - name: Package Helm 62 | run: cd ./.helm && helm package ./config --version=${{ env.TAG }} && helm repo index . && ls > index.html && echo ${{ env.TAG }} > latest 63 | 64 | - name: Deploy Helm Repo 65 | uses: peaceiris/actions-gh-pages@v3 66 | with: 67 | github_token: ${{ secrets.GITHUB_TOKEN }} 68 | publish_dir: ./.helm 69 | keep_files: true 70 | commit_message: "helm repo: ${{ env.REPO }}?v=${{ env.TAG }} deployed!" 71 | 72 | - name: Setup Helm 73 | timeout-minutes: 10 74 | uses: JimCronqvist/action-ssh@master 75 | with: 76 | hosts: ${{ env.HOST }} 77 | privateKey: ${{ secrets.PRIVATE_KEY }} 78 | command: | 79 | function error_exit { 80 | echo "$1" 1>&2 81 | exit 1 82 | } 83 | 84 | until [ ${{ env.REQ }} = ${{ env.TAG }} ] 85 | do 86 | sleep 1 87 | echo "sleeping" 88 | echo ${{ env.REQ }} -- ${{ env.TAG }} 89 | done 90 | echo "pass" 91 | 92 | helm repo add ${{ env.APP }} ${{ env.REPO }} || error_exit "$LINENO failed: helm repo add ${{ env.APP }} " 93 | helm repo update || error_exit "$LINENO failed: helm repo update" 94 | helm search repo ${{ env.APP }} || error_exit "$LINENO failed: helm search repo ${{ env.APP }}" 95 | 96 | helm upgrade -i ${{ env.APP }} ${{ env.APP }}/mychart --version=${{ env.TAG }} --set image.tag=${{ env.TAG }} || error_exit "$LINENO failed: helm upgrade ${{ env.APP }} ${{ env.APP }}/mychart" 97 | helm ls 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.helm/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | [Helm](https://helm.sh) must be installed to use the charts. Please refer to 4 | Helm's [documentation](https://helm.sh/docs) to get started. 5 | 6 | Once Helm has been set up correctly, add the repo as follows: 7 | 8 | helm repo add https://.github.io/helm-charts 9 | 10 | If you had already added this repo earlier, run `helm repo update` to retrieve 11 | the latest versions of the packages. You can then run `helm search repo 12 | ` to see the charts. 13 | 14 | To install the chart: 15 | 16 | helm install my- / 17 | 18 | To uninstall the chart: 19 | 20 | helm delete my- -------------------------------------------------------------------------------- /.helm/config/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /.helm/config/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: mychart 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /.helm/config/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "notebook.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "notebook.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "notebook.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /.helm/config/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.service.app }} 5 | spec: 6 | ports: 7 | - protocol: TCP 8 | name: web 9 | port: {{ .Values.service.port }} 10 | selector: 11 | app: {{ .Values.service.app }} 12 | --- 13 | kind: Deployment 14 | apiVersion: apps/v1 15 | metadata: 16 | name: {{ .Values.service.app }} 17 | labels: 18 | app: {{ .Values.service.app }} 19 | spec: 20 | replicas: {{ .Values.pod.replicas }} 21 | selector: 22 | matchLabels: 23 | app: {{ .Values.service.app }} 24 | template: 25 | metadata: 26 | labels: 27 | app: {{ .Values.service.app }} 28 | spec: 29 | imagePullSecrets: 30 | - name: liuma-registry 31 | containers: 32 | - name: {{ .Values.service.app }} 33 | image: {{ .Values.image.repository }}:{{ .Values.image.tag }} 34 | # resources: 35 | # requests: 36 | # memory: {{ .Values.resources.requests.memory }} 37 | # cpu: {{ .Values.resources.requests.cpu }} 38 | # limits: 39 | # memory: {{ .Values.resources.limits.memory }} 40 | # cpu: {{ .Values.resources.limits.cpu }} 41 | ports: 42 | - name: web 43 | containerPort: {{ .Values.service.port }} 44 | -------------------------------------------------------------------------------- /.helm/config/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRoute 3 | metadata: 4 | name: {{ .Values.service.app }}-route 5 | spec: 6 | entryPoints: 7 | - web 8 | routes: 9 | - match: Host(`{{ .Values.route.host }}`) 10 | kind: Rule 11 | services: 12 | - name: {{ .Values.service.app }} 13 | port: {{ .Values.service.port }} 14 | 15 | --- 16 | apiVersion: traefik.containo.us/v1alpha1 17 | kind: IngressRoute 18 | metadata: 19 | name: {{ .Values.service.app }}-route-secure 20 | spec: 21 | entryPoints: 22 | - websecure 23 | routes: 24 | - match: Host(`{{ .Values.route.host }}`) 25 | kind: Rule 26 | services: 27 | - name: {{ .Values.service.app }} 28 | port: {{ .Values.service.port }} 29 | tls: 30 | secretName: who-tls 31 | -------------------------------------------------------------------------------- /.helm/config/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for discovery. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | image: 6 | rigistry: registry.cn-shanghai.aliyuncs.com 7 | repository: registry.cn-shanghai.aliyuncs.com/anthhub/react-mini-blog 8 | tag: latest 9 | # pullPolicy: IfNotPresent 10 | 11 | pod: 12 | replicas: 1 13 | 14 | service: 15 | # type: NodePort 16 | # nodePort: 30002 17 | port: 80 18 | app: react-mini-blog 19 | 20 | route: 21 | host: rjj.liuma.top 22 | 23 | resources: 24 | requests: 25 | cpu: "50m" 26 | memory: "16Mi" 27 | limits: 28 | memory: "32Mi" 29 | cpu: "100m" 30 | # We usually recommend not to specify default resources and to leave this as a conscious 31 | # choice for the user. This also increases chances charts run on environments with little 32 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 33 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 34 | # limits: 35 | # cpu: 100m 36 | # memory: 128Mi 37 | # requests: 38 | # cpu: 100m 39 | # memory: 128Mi 40 | 41 | nodeSelector: {} 42 | 43 | tolerations: [] 44 | 45 | affinity: {} 46 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 180, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "avoid", 10 | "requirePragma": false, 11 | "proseWrap": "preserve" 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3 AS builder 2 | RUN npm install -g yarn --force 3 | 4 | WORKDIR /code 5 | ADD . /code 6 | 7 | RUN yarn 8 | 9 | ENV NODE_ENV production 10 | RUN yarn build:prod 11 | 12 | FROM nginx:alpine 13 | COPY --from=builder /code/build /usr/share/nginx/html 14 | COPY nginx/default.conf /etc/nginx/conf.d/default.conf 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 一直以来都想写个自己的博客, 本来想简单做个博客就好, 后面就想干脆写个掘金好啦, 哈哈哈..., 于是和徒弟开始动手~ ●ω● ~ 3 | 4 | [在线地址](http://101.132.79.152/) 5 | 6 | [GitHub 地址](https://github.com/anthhub/react-mini-blog) 7 | 8 | 9 | ## 简介 10 | 11 | > react + nestjs 掘金全栈! 12 | 13 | > 前端技术栈 14 | 15 | - react -- 全家桶 16 | - react hooks -- 到处都是, 基本操作了吧... 17 | - ant design -- 只用了一些 18 | - styled-components -- 必须啊, react的好基友=.= 19 | - typescript -- 基本操作啊 pro 20 | 21 | 22 | > 后端技术栈 23 | - nestjs --node界的spring, 也是因为 typescript 写太久, 所以选用; 阿里 midway 也用过 24 | - mongoose -- 为了用的爽, 选择的比较新的 typegoose, 暂时没坑... 25 | 26 | > 后端详细打算放在另一篇, 小伙伴们 clone 前端项目的话可以直接调我的接口, 应该扛得住 ~ ●ω● ~ 27 | 28 | 29 | ## 运行项目 30 | ```javascript 31 | git clone https://github.com/anthhub/react-mini-blog 32 | 33 | cd react-mini-blog 34 | 35 | yarn 36 | 37 | yarn start:test 38 | ``` 39 | 40 | 注:此项目与 [掘金](https://juejin.im/timeline) 无任何关系, 如有侵权, 敬请告知 41 | 42 | ## 部分功能截图 43 | 44 | > 登录 45 | 46 | 47 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee60cfd0e782f0~tplv-t2oaga2asx-image.image) 48 | 49 | > 搜索 50 | 51 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee613608c334b6~tplv-t2oaga2asx-image.image) 52 | 53 | > 发帖 54 | 55 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee614c7245f992~tplv-t2oaga2asx-image.image) 56 | 57 | > 点赞、评论、关注 58 | 59 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee616d5abcbe31~tplv-t2oaga2asx-image.image) 60 | 61 | > 个人主页 62 | 63 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee617c40aa5e6c~tplv-t2oaga2asx-image.image) 64 | 65 | 66 | > 修改个人信息 67 | 68 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/8/16ee6189b2d6338d~tplv-t2oaga2asx-image.image) 69 | 70 | 71 | 72 | ## 目标功能 73 | 74 | - [x] 登录、注册 -- 完成 75 | - [x] 修改个人信息 --完成 76 | - [x] 个人主页 --完成 77 | - [x] 关注 -- 完成 78 | - [x] 评论 -- 完成 79 | - [x] 点赞 -- 完成 80 | - [x] 搜索帖子 -- 完成 81 | - [x] 上传头像 -- 完成 82 | - [x] 发帖 -- 完成 83 | 84 | 只完成了主要功能,代码可能有点乱, 没办法, 时间太紧了😂 85 | 86 | 小伙伴们,如果觉得文章有点东西,记得点个赞或者给个 star! 87 | 88 | [在线地址](http://101.132.79.152/) 89 | 90 | [GitHub 地址](https://github.com/anthhub/react-mini-blog) 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const paths = require('./paths') 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')] 9 | 10 | const NODE_ENV = process.env.NODE_ENV 11 | if (!NODE_ENV) { 12 | throw new Error('The NODE_ENV environment variable is required but was not specified.') 13 | } 14 | 15 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 16 | const dotenvFiles = [ 17 | `${paths.dotenv}.${NODE_ENV}.local`, 18 | `${paths.dotenv}.${NODE_ENV}`, 19 | // Don't include `.env.local` for `test` environment 20 | // since normally you expect tests to produce the same 21 | // results for everyone 22 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 23 | paths.dotenv, 24 | ].filter(Boolean) 25 | 26 | // Load environment variables from .env* files. Suppress warnings using silent 27 | // if this file is missing. dotenv will never modify any environment variables 28 | // that have already been set. Variable expansion is supported in .env files. 29 | // https://github.com/motdotla/dotenv 30 | // https://github.com/motdotla/dotenv-expand 31 | dotenvFiles.forEach(dotenvFile => { 32 | if (fs.existsSync(dotenvFile)) { 33 | require('dotenv-expand')( 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }) 37 | ) 38 | } 39 | }) 40 | 41 | // We support resolving modules according to `NODE_PATH`. 42 | // This lets you use absolute paths in imports inside large monorepos: 43 | // https://github.com/facebook/create-react-app/issues/253. 44 | // It works similar to `NODE_PATH` in Node itself: 45 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 46 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 47 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 48 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 49 | // We also resolve them to make sure all tools using them work consistently. 50 | const appDirectory = fs.realpathSync(process.cwd()) 51 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 52 | .split(path.delimiter) 53 | .filter(folder => folder && !path.isAbsolute(folder)) 54 | .map(folder => path.resolve(appDirectory, folder)) 55 | .join(path.delimiter) 56 | 57 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 58 | // injected into the application via DefinePlugin in Webpack configuration. 59 | const REACT_APP = /^REACT_APP_/i 60 | 61 | function getClientEnvironment(publicUrl) { 62 | const raw = Object.keys(process.env) 63 | .filter(key => REACT_APP.test(key)) 64 | .reduce( 65 | (env, key) => { 66 | env[key] = process.env[key] 67 | return env 68 | }, 69 | { 70 | // Useful for determining whether we’re running in production mode. 71 | // Most importantly, it switches React into the correct mode. 72 | NODE_ENV: process.env.NODE_ENV || 'development', 73 | // Useful for resolving the correct path to static assets in `public`. 74 | // For example, . 75 | // This should only be used as an escape hatch. Normally you would put 76 | // images into the `src` and `import` them in code to get their paths. 77 | PUBLIC_URL: publicUrl, 78 | 79 | API_ENV: process.env.API_ENV || 'development', 80 | } 81 | ) 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]) 86 | return env 87 | }, {}), 88 | } 89 | 90 | return { raw, stringified } 91 | } 92 | 93 | module.exports = getClientEnvironment 94 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /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 additional module paths based on 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 | // If the path is equal to the root directory we ignore it here. 42 | // We don't want to allow importing from the root directly as source files are 43 | // not transpiled outside of `src`. We do allow importing them with the 44 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 45 | // an alias. 46 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 47 | return null; 48 | } 49 | 50 | // Otherwise, throw an error. 51 | throw new Error( 52 | chalk.red.bold( 53 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 54 | ' Create React App does not support other values at this time.' 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 61 | * 62 | * @param {*} options 63 | */ 64 | function getWebpackAliases(options = {}) { 65 | const baseUrl = options.baseUrl; 66 | 67 | if (!baseUrl) { 68 | return {}; 69 | } 70 | 71 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 72 | 73 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 74 | return { 75 | src: paths.appSrc, 76 | }; 77 | } 78 | } 79 | 80 | /** 81 | * Get jest aliases based on the baseUrl of a compilerOptions object. 82 | * 83 | * @param {*} options 84 | */ 85 | function getJestAliases(options = {}) { 86 | const baseUrl = options.baseUrl; 87 | 88 | if (!baseUrl) { 89 | return {}; 90 | } 91 | 92 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 93 | 94 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 95 | return { 96 | 'src/(.*)$': '/src/$1', 97 | }; 98 | } 99 | } 100 | 101 | function getModules() { 102 | // Check if TypeScript is setup 103 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 104 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 105 | 106 | if (hasTsConfig && hasJsConfig) { 107 | throw new Error( 108 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 109 | ); 110 | } 111 | 112 | let config; 113 | 114 | // If there's a tsconfig.json we assume it's a 115 | // TypeScript project and set up the config 116 | // based on tsconfig.json 117 | if (hasTsConfig) { 118 | const ts = require(resolve.sync('typescript', { 119 | basedir: paths.appNodeModules, 120 | })); 121 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 122 | // Otherwise we'll check if there is jsconfig.json 123 | // for non TS projects. 124 | } else if (hasJsConfig) { 125 | config = require(paths.appJsConfig); 126 | } 127 | 128 | config = config || {}; 129 | const options = config.compilerOptions || {}; 130 | 131 | const additionalModulePaths = getAdditionalModulePaths(options); 132 | 133 | return { 134 | additionalModulePaths: additionalModulePaths, 135 | webpackAliases: getWebpackAliases(options), 136 | jestAliases: getJestAliases(options), 137 | hasTsConfig, 138 | }; 139 | } 140 | 141 | module.exports = getModules(); 142 | -------------------------------------------------------------------------------- /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 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/logo180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/public/logo180.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/mock/homeData/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags":[ 3 | { 4 | "path":"recommended", 5 | "text":"推荐" 6 | }, 7 | { 8 | "path":"following", 9 | "text":"关注" 10 | } 11 | ], 12 | "goodAuthor":[ 13 | { 14 | "title": "程序员1", 15 | "desc":"vue.js领域贡献者", 16 | "userImage":"//user-gold-cdn.xitu.io/2019/2/23/169198b85bfbec1d?imageView2/1/w/100/h/100/q/85/format/webp/interlace/1", 17 | "id":"0001" 18 | }, 19 | { 20 | "title": "程序员2", 21 | "desc":"css领域贡献者", 22 | "userImage":"//mirror-gold-cdn.xitu.io/168e09474c7f9ebe902?imageView2/1/w/100/h/100/q/85/format/webp/interlace/1", 23 | "id":"0002" 24 | }, 25 | { 26 | "title": "程序员3", 27 | "desc":"javascript领域贡献者", 28 | "userImage":"//user-gold-cdn.xitu.io/2019/2/14/168ec55aba592628?imageView2/1/w/100/h/100/q/85/format/webp/interlace/1", 29 | "id":"0003" 30 | } 31 | ], 32 | "recommendBooks":[ 33 | { 34 | "id":"book0001", 35 | "bookImage":"//user-gold-cdn.xitu.io/2018/6/11/163ee322e6d2c827?imageView2/1/w/200/h/280/q/95/format/webp/interlace/1", 36 | "title":"深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务", 37 | "sellNum":100 38 | }, 39 | { 40 | "id":"book0002", 41 | "bookImage":"//user-gold-cdn.xitu.io/2018/7/30/164ea7de07b7f79e?imageView2/1/w/200/h/280/q/95/format/webp/interlace/1", 42 | "title":"Redis 深度历险:核心原理与应用实践", 43 | "sellNum":50 44 | } 45 | ], 46 | "linkList":[ 47 | { 48 | "id":"link001", 49 | "linkImage":"//b-gold-cdn.xitu.io/v3/static/img/repos.28d0802.png", 50 | "title":"开源库" 51 | }, 52 | { 53 | "id":"link002", 54 | "linkImage":"//b-gold-cdn.xitu.io/v3/static/img/collections.945b9ae.png", 55 | "title":"收藏集" 56 | }, 57 | { 58 | "id":"link003", 59 | "linkImage":"//b-gold-cdn.xitu.io/v3/static/img/juejin-extension-icon.4b79fb4.png", 60 | "title":"下载掘金浏览器插件" 61 | }, 62 | { 63 | "id":"link004", 64 | "linkImage":"//b-gold-cdn.xitu.io/v3/static/img/juejin-miner.b78347c.png", 65 | "title":"前往掘金翻译计划" 66 | }, 67 | { 68 | "id":"link005", 69 | "linkImage":"//b-gold-cdn.xitu.io/v3/static/img/juejin-partner.4dd2d8c.png", 70 | "title":"商务合作" 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/Api/account.ts: -------------------------------------------------------------------------------- 1 | import { SignInDto } from '@/modal/dtos/signIn.dto' 2 | import { SignUpDto } from '@/modal/dtos/signUp.dto' 3 | 4 | import { baseUrl } from './url' 5 | import http from './request' 6 | 7 | // console.log({ baseUrl }) 8 | 9 | // const account = { 10 | // signUp: `​/signUp`, 11 | // signIn: `/signIn` 12 | // } 13 | 14 | export const signUp = (data: SignUpDto) => { 15 | return http.post(`${baseUrl}/signUp`, data).then(res => { 16 | return res 17 | }) 18 | } 19 | 20 | export const signIn = (data: SignInDto) => { 21 | return http.post(`${baseUrl}/signIn`, data).then(res => { 22 | return res 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/Api/article.ts: -------------------------------------------------------------------------------- 1 | import { CreateArticleDto } from '@/modal/dtos/article.dto' 2 | import { ArticleEntity } from '@/modal/entities/article.entity' 3 | import { IPage } from '@/modal/interfaces/common.interface' 4 | 5 | import http from './request' 6 | import { baseUrl } from './url' 7 | 8 | const article = { 9 | query: `/article/query`, 10 | detail: `/article/`, 11 | create: `/article`, 12 | reedit: `/article/`, 13 | delete: `/article`, 14 | viewCount: `/article/`, 15 | } 16 | 17 | export const getArticles = (data?: any) => { 18 | return http.get(baseUrl + article.query, data || {}).then(res => { 19 | return res as IPage 20 | }) 21 | } 22 | 23 | export const getArticle = (articleId: string) => { 24 | return http.get(baseUrl + article.detail + articleId).then(res => { 25 | return res 26 | }) 27 | } 28 | 29 | export const createArticle = (data: CreateArticleDto) => { 30 | return http.post(baseUrl + article.create, data).then(res => { 31 | return res 32 | }) 33 | } 34 | 35 | export const reeditArticle = (articleId: string, data: CreateArticleDto) => { 36 | return http.patch(baseUrl + article.reedit + articleId, data).then(res => { 37 | return res 38 | }) 39 | } 40 | 41 | export const deleteArticle = (articleId: string) => { 42 | return http.delete(baseUrl + article.delete + `?id=` + articleId).then(res => { 43 | return res 44 | }) 45 | } 46 | 47 | export const putViewCount = (articleId: string) => { 48 | return http.put(baseUrl + article.viewCount + articleId + '/putViewCount').then(res => { 49 | return res 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/Api/comment.ts: -------------------------------------------------------------------------------- 1 | import http from './request' 2 | import { baseUrl } from './url' 3 | import { CreateCommentDto } from '@/modal/dtos/comment.dto' 4 | 5 | // const article = { 6 | // query: `/article/query`, 7 | // detail: `/article/`, 8 | // create: `/article`, 9 | // reedit: `/article/`, 10 | // delete: `/article`, 11 | // viewCount: `/article/` 12 | // } 13 | 14 | const comment = { 15 | create: `/comment`, 16 | query: `/comment/`, 17 | } 18 | 19 | // export const getArticles = (data?: any) => { 20 | // return http.get(baseUrl + article.query, data || {}).then((res) => { 21 | // return res as IPage 22 | // }) 23 | // } 24 | 25 | export const getCommentList = (articleId: string) => { 26 | return http.get(baseUrl + comment.query + articleId).then(res => { 27 | return res 28 | }) 29 | } 30 | 31 | export const createComment = (data: CreateCommentDto) => { 32 | return http.post(baseUrl + comment.create, data).then(res => { 33 | return res 34 | }) 35 | } 36 | 37 | // export const reeditArticle = (articleId: string, data: CreateArticleDto) => { 38 | // return http.patch(baseUrl + article.reedit + articleId, data).then((res) => { 39 | // return res 40 | // }) 41 | // } 42 | 43 | // export const deleteArticle = (articleId: string) => { 44 | // return http.delete(baseUrl + article.delete + `?id=` + articleId).then((res) => { 45 | // return res 46 | // }) 47 | // } 48 | 49 | // export const putViewCount = (articleId: string) => { 50 | // return http.put(baseUrl + article.viewCount + articleId + '/putViewCount').then((res) => { 51 | // return res 52 | // }) 53 | // } 54 | -------------------------------------------------------------------------------- /src/Api/file.ts: -------------------------------------------------------------------------------- 1 | import http from './request' 2 | import { baseUrl } from './url' 3 | 4 | // console.log({ baseUrl }) 5 | const file = { 6 | update: `/file/upload`, 7 | } 8 | // 注意:不要直接从 Swagger 复制 url,会包含特殊字符 9 | export const uploadFile = (formData: any) => { 10 | return http.post(baseUrl + file.update, formData).then(res => { 11 | // console.log(res, '文件上传成功') 12 | return res 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/Api/follow.ts: -------------------------------------------------------------------------------- 1 | import http from './request' 2 | import { baseUrl } from './url' 3 | 4 | const follow = { 5 | add: `/follow/`, 6 | delete: `/follow/`, 7 | } 8 | 9 | export const addFollow = (id: string) => { 10 | return http.put(baseUrl + follow.add + id).then(res => { 11 | return res 12 | }) 13 | } 14 | 15 | export const deleteFollow = (id: string) => { 16 | return http.delete(baseUrl + follow.delete + id).then(res => { 17 | return res 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/Api/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | readonly NODE_ENV: 'development' | 'production' | 'test' 4 | readonly API_ENV: 'development' | 'production' | 'test' 5 | readonly PUBLIC_URL: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Api/like.ts: -------------------------------------------------------------------------------- 1 | import http from './request' 2 | import { baseUrl } from './url' 3 | 4 | // console.log({ baseUrl }) 5 | 6 | // 赞某篇文章 7 | export const addLike = (articleId: string) => { 8 | return http.put(baseUrl + '/like/' + articleId).then(res => { 9 | // console.log('addLike', res) 10 | return res 11 | }) 12 | } 13 | 14 | export const deleteLike = (articleId: string) => { 15 | return http.delete(baseUrl + '/like/' + articleId).then(res => { 16 | // console.log('addLike', res) 17 | return res 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/Api/request.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | import axios, { AxiosRequestConfig as _AxiosRequestConfig } from 'axios' 3 | import * as qs from 'qs' 4 | 5 | import { bridge } from '@/App' 6 | 7 | export interface IAxiosRequestConfig extends _AxiosRequestConfig { 8 | startTime?: Date 9 | } 10 | 11 | export interface IHttpRequest { 12 | get(url: string, data?: any, baseUrl?: string): Promise 13 | post(url: string, data?: any, baseUrl?: string): Promise 14 | delete(url: string, data?: any, baseUrl?: string): Promise 15 | put(url: string, data?: any, baseUrl?: string): Promise 16 | patch(url: string, data?: any, baseUrl?: string): Promise 17 | } 18 | 19 | const DEFAULTCONFIG = { 20 | baseURL: process.env.BASEURL, 21 | } 22 | 23 | const http = {} as IHttpRequest 24 | const methods = ['get', 'post', 'put', 'delete', 'patch'] as Array<'get' | 'post' | 'put' | 'delete' | 'patch'> 25 | 26 | methods.forEach(v => { 27 | http[v] = (url: string, data?: any, baseUrl?: string) => { 28 | const axiosConfig: IAxiosRequestConfig = { 29 | method: v, 30 | url, 31 | baseURL: baseUrl || DEFAULTCONFIG.baseURL, 32 | } 33 | const instance = axios.create(DEFAULTCONFIG) 34 | // Add a request interceptor 35 | // instance.interceptors.request.use( 36 | // cfg => { 37 | // cfg.params = { ...cfg.params } 38 | // return cfg 39 | // }, 40 | // error => Promise.reject(error) 41 | // ) 42 | 43 | if (v === 'get') { 44 | axiosConfig.params = data 45 | } else if (data instanceof FormData) { 46 | axiosConfig.data = data 47 | } else { 48 | axiosConfig.data = qs.stringify(data) 49 | } 50 | axiosConfig.startTime = new Date() 51 | 52 | return instance 53 | .request(axiosConfig) 54 | .then(res => { 55 | const rs = res.data 56 | const { message: msg, status: code } = rs 57 | if (msg) { 58 | message.destroy() 59 | if (code === 0) { 60 | // message.success(msg) 61 | } else { 62 | message.error(msg) 63 | } 64 | } 65 | 66 | return Promise.resolve(res.data.data) 67 | }) 68 | .catch(err => { 69 | const status = err && err.response && err.response.status 70 | 71 | if (status === 401) { 72 | console.log('%c%s', 'color: #20bd08;font-size:15px', '===TQY===: status', status) 73 | bridge.dispatch({ type: 'CHANGE_SHOW_LOGIN', payload: { showLogin: true } }) 74 | 75 | return Promise.reject({}) 76 | } 77 | 78 | message.destroy() 79 | message.error(err.msg || err.message || err.stack || '未知错误') 80 | 81 | return Promise.reject({ err, stack: err.msg || err.stack || '' }) 82 | }) 83 | } 84 | }) 85 | 86 | export default http 87 | -------------------------------------------------------------------------------- /src/Api/url.ts: -------------------------------------------------------------------------------- 1 | const VERSION = '/api/v1' 2 | 3 | function getBaseUrl() { 4 | if (process.env.API_ENV === 'development') { 5 | return 'http://localhost:3003/blog' + VERSION 6 | } 7 | return 'http://njj.liuma.top/blog' + VERSION 8 | } 9 | 10 | export const baseUrl = getBaseUrl() 11 | -------------------------------------------------------------------------------- /src/Api/user.ts: -------------------------------------------------------------------------------- 1 | import { UserUpdateDto } from '@/modal/dtos/userUpdate.dto' 2 | 3 | import http from './request' 4 | import { baseUrl } from './url' 5 | 6 | const user = { 7 | update: '/user/update', 8 | info: '/user/', 9 | // article: '/user/${id}/article' 10 | } 11 | 12 | export const userUpdate = (data: UserUpdateDto) => { 13 | return http.patch(baseUrl + user.update, data).then(res => { 14 | return res 15 | }) 16 | } 17 | 18 | // 拿到当前登录用户的信息 19 | export const getUserInfo = (id: string) => { 20 | return http.get(baseUrl + user.info + id + '/info').then(res => { 21 | return res 22 | }) 23 | } 24 | 25 | // 拿到指定 id 的用户的文章 26 | export const getUserArticles = (data?: any) => { 27 | return http.get(baseUrl + '/user/' + (data.id || data) + '/articles', data || {}).then(res => { 28 | return res 29 | }) 30 | } 31 | 32 | // 拿到指定 id 的用户的关注 33 | export const getUserFollowing = (id: string) => { 34 | return http.get(baseUrl + '/user/' + id + '/following').then(res => { 35 | return res 36 | }) 37 | } 38 | 39 | export const getUserFollowers = (id: string) => { 40 | return http.get(baseUrl + '/user/' + id + '/followers').then(res => { 41 | return res 42 | }) 43 | } 44 | 45 | // id 是查看用户, followerId是登录用户 46 | export const isFollowing = (id: string, followerId: string) => { 47 | return http.get(baseUrl + '/user/' + id + '/isFollowing/' + followerId).then(res => { 48 | return res 49 | }) 50 | } 51 | 52 | // 拿到指定 id 的用户的赞 53 | export const getUserLikes = (id: string) => { 54 | return http.get(baseUrl + '/user/' + id + '/likes').then(res => { 55 | return res 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistedContext, usePersistedReducer } from '@/lib/hooks/usePersist' 2 | import React, { useContext, useReducer } from 'react' 3 | 4 | import Store from './redux/context' 5 | import reducer from './redux/reducer' 6 | 7 | import '@/styles/index.less' 8 | import AppRouter from './routes' 9 | import { GlobalStyle } from './style' 10 | 11 | export const bridge: any = {} 12 | 13 | const App: React.FC = () => { 14 | const globalStore = usePersistedContext(useContext(Store), 'state', false) 15 | 16 | const [state, dispatch] = usePersistedReducer(useReducer(reducer, globalStore), 'state') 17 | 18 | bridge.dispatch = dispatch 19 | return ( 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default App 28 | -------------------------------------------------------------------------------- /src/components/Advertising/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React from 'react' 4 | 5 | import { Wrapper } from './style' 6 | 7 | 8 | import avatar from '@/statics/avatar.png' 9 | 10 | 11 | interface IProps {} 12 | 13 | const Advertising: React.FC = () => { 14 | return ( 15 | 16 |
17 | 打个广告{' '} 18 | 19 | 😂😂😂 20 | 21 |
22 |
23 | 24 |
25 |
26 | {'前端工程师'} 27 | 28 | 29 | {'上海在职@求挖'} 30 | 31 |
32 |
33 | 34 |
35 | {''} 36 |
37 | 38 | 44 |
45 | 邮箱 46 | {'anthhub@163.com'} 47 |
48 |
49 | 50 | ) 51 | } 52 | 53 | export default Advertising 54 | -------------------------------------------------------------------------------- /src/components/Advertising/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | width: 240px; 11 | margin-bottom: 18px; 12 | background: #fff; 13 | border-radius: 2px; 14 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 15 | 16 | .author-title { 17 | padding: 12px 16px; 18 | /* color: #333; */ 19 | color: red; 20 | font-weight: bold; 21 | font-size: 14px; 22 | border-bottom: 1px solid hsla(0, 0%, 58.8%, 0.1); 23 | } 24 | 25 | .author-info { 26 | overflow: hidden; 27 | 28 | .author-desc { 29 | display: flex; 30 | align-items: center; 31 | padding: 16px; 32 | 33 | // 左边 头像 34 | .avatar { 35 | flex: 0 0 auto; 36 | width: 50px; 37 | height: 50px; 38 | margin-right: 12px; 39 | border-radius: 50%; 40 | background: ${({ avatarLarge }: { avatarLarge: string }) => `#eee url(${avatarLarge}) no-repeat center/cover`}; 41 | } 42 | 43 | // 右边 作者名字简介 44 | .info { 45 | min-width: 0; 46 | 47 | .author-name { 48 | display: block; 49 | font-size: 16px; 50 | font-weight: 600; 51 | color: #000; 52 | white-space: pre-wrap; 53 | } 54 | 55 | .author-intro { 56 | display: block; 57 | margin-top: 10px; 58 | font-size: 15px; 59 | color: #72777b; 60 | white-space: nowrap; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | } 64 | } 65 | } 66 | 67 | .agree, 68 | .views { 69 | display: flex; 70 | align-items: center; 71 | padding: 0 16px; 72 | margin-bottom: 10px; 73 | font-size: 15px; 74 | color: #000; 75 | } 76 | 77 | .views { 78 | margin-bottom: 16px; 79 | } 80 | 81 | .count { 82 | margin: 0 5px; 83 | font-weight: 500; 84 | } 85 | 86 | .icon { 87 | width: 25px; 88 | height: 25px; 89 | margin-right: 12px; 90 | background: #eee; 91 | border-radius: 50%; 92 | } 93 | } 94 | ` 95 | -------------------------------------------------------------------------------- /src/components/AppDownload/index.tsx: -------------------------------------------------------------------------------- 1 | // 右侧 下载客户端小卡片 2 | 3 | import React from 'react' 4 | // import { connect } from 'react-redux'; 5 | import { Wrapper } from './style' 6 | 7 | const AppDownload: React.FC = props => { 8 | return ( 9 | 10 |
11 | qrcode 12 |
13 |
下载掘金客户端
14 |
一个帮助开发者成长的社区
15 |
16 |
17 |
18 | ) 19 | } 20 | 21 | export default AppDownload 22 | -------------------------------------------------------------------------------- /src/components/AppDownload/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | width: 240px; 11 | margin-bottom: 18px; 12 | background: #fff; 13 | border-radius: 2px; 14 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 15 | 16 | .app-link { 17 | display: flex; 18 | align-items: center; 19 | padding: 16px; 20 | 21 | .qr-img { 22 | width: 50px; 23 | height: 50px; 24 | margin-right: 6px; 25 | } 26 | 27 | .headline { 28 | color: #333; 29 | font-size: 14px; 30 | font-weight: 600; 31 | } 32 | 33 | .desc { 34 | color: #909090; 35 | font-size: 12px; 36 | margin-top: 6px; 37 | } 38 | } 39 | ` 40 | -------------------------------------------------------------------------------- /src/components/SpinCenter/index.tsx: -------------------------------------------------------------------------------- 1 | // 加载 ing 2 | 3 | import { Spin } from 'antd' 4 | import React from 'react' 5 | import { Wrapper } from './style' 6 | 7 | const SpinCenter: React.FC = props => { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default SpinCenter 16 | -------------------------------------------------------------------------------- /src/components/SpinCenter/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components'; 8 | 9 | export const Wrapper = styled.div` 10 | .loading { 11 | position: fixed; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/containers/AuthRoute/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/src/containers/AuthRoute/index.less -------------------------------------------------------------------------------- /src/containers/AuthRoute/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory, Redirect } from 'react-router' 3 | import { useIsLogin } from '@/redux/context' 4 | 5 | interface IProps { 6 | children?: any 7 | needLogin?: boolean 8 | } 9 | 10 | const AuthRoute: React.FC = ({ children, needLogin = false }) => { 11 | const history = useHistory() 12 | const isLogin = useIsLogin() 13 | // console.log({ needLogin }) 14 | 15 | if (needLogin && !isLogin) { 16 | return 17 | } 18 | 19 | return children 20 | } 21 | 22 | export default AuthRoute 23 | -------------------------------------------------------------------------------- /src/containers/Frame/Login/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable react-hooks/exhaustive-deps */ 3 | 4 | // 右侧 下载客户端小卡片 5 | 6 | import { signIn } from '@/Api/account' 7 | import { useDispatch } from '@/redux/context' 8 | import React, { useCallback } from 'react' 9 | import useInputEvent from '@/lib/hooks/useInputEvent' 10 | import { message } from 'antd' 11 | 12 | import { Wrapper } from './style' 13 | 14 | interface IProps { 15 | onClose(e: any): void 16 | onSwitch(e: any): void 17 | } 18 | 19 | // 全局生效(显示时长3s,最多显示1条) 20 | message.config({ 21 | duration: 3, 22 | maxCount: 1, 23 | }) 24 | 25 | const Login: React.FC = ({ onClose, onSwitch }) => { 26 | const { value: phoneNumber, onInputEvent: onChangeNumber } = useInputEvent('') 27 | const { value: password, onInputEvent: onChangePassword } = useInputEvent('') 28 | 29 | const dispatch = useDispatch() 30 | 31 | const onLogin = useCallback(() => { 32 | // 简单校验输入手机和密码后再发送请求 33 | if (phoneNumber.length === 0) { 34 | message.warning('请填写手机号') 35 | } else if (password.length === 0) { 36 | message.warning('请输入密码') 37 | } else if (password.length < 6 || password.length > 12) { 38 | message.warning('密码错误') 39 | } else if (phoneNumber.length !== 11) { 40 | message.warning('请输入正确的手机号') 41 | } else { 42 | signIn({ mobilePhoneNumber: phoneNumber, password }).then(data => { 43 | console.log('%c%s', 'color: #20bd08;font-size:15px', '===TQY===: onLogin -> data', data) 44 | dispatch({ 45 | type: 'LOGIN', 46 | payload: { user: { ...data, access_token: 'Bearer ' + data.access_token } }, 47 | }) 48 | onClose(data) 49 | }) 50 | } 51 | }, [phoneNumber, password]) 52 | 53 | // 监听回车事件 54 | // const login = useCallback((event: any) => { 55 | // let e = event || window.event 56 | // if (e && e.keyCode === 13) { 57 | // console.log('login', phoneNumber) 58 | // onLogin() 59 | // } 60 | // }, [phoneNumber, password]) 61 | 62 | // useEffect(() => { 63 | // document.addEventListener('keydown', login) 64 | // return () => document.removeEventListener('keydown', login) 65 | // }, []) 66 | 67 | return ( 68 | 69 |
70 | 71 |
72 |

登录

73 |
74 | 75 | 76 |
77 | 80 |
{ 83 | onClose(e) 84 | onSwitch(e) 85 | }} 86 | > 87 | 没有账号?注册 88 |
89 |
90 | 91 |
92 | ) 93 | } 94 | 95 | export default Login 96 | -------------------------------------------------------------------------------- /src/containers/Frame/Login/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | import closePicGray from '../../../statics/close-gray.png' 9 | import closePicBlack from '../../../statics/close-black.png' 10 | 11 | export const Wrapper = styled.div` 12 | position: fixed; 13 | z-index: 100; 14 | left: 0; 15 | right: 0; 16 | top: 0; 17 | bottom: 0; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | background: rgba(0, 0, 0, 0.3); 22 | .login-box { 23 | margin-top: -200px; 24 | width: 318px; 25 | /* height: 100px; */ 26 | padding: 24px; 27 | font-size: 14px; 28 | background: #fff; 29 | border-radius: 2px; 30 | 31 | .close-btn { 32 | float: right; 33 | width: 20px; 34 | height: 20px; 35 | background: url(${closePicGray}) no-repeat center/contain; 36 | cursor: pointer; 37 | 38 | :hover { 39 | background: url(${closePicBlack}) no-repeat center/contain; 40 | } 41 | } 42 | 43 | .title { 44 | margin-bottom: 24px; 45 | font-size: 18px; 46 | font-weight: 700; 47 | } 48 | 49 | .input-group { 50 | margin-bottom: 6px; 51 | .input { 52 | margin-bottom: 10px; 53 | padding: 10px; 54 | width: 100%; 55 | color: #000; 56 | border: 1px solid #e9e9e9; 57 | border-radius: 2px; 58 | box-sizing: border-box; 59 | 60 | // 兼容不同浏览器的 placeholder 61 | ::-webkit-input-placeholder { 62 | color: #666; 63 | font-size: 16px; 64 | } 65 | 66 | :-moz-placeholder { 67 | color: #666; 68 | font-size: 16px; 69 | } 70 | 71 | ::-moz-placeholder { 72 | color: #666; 73 | font-size: 16px; 74 | } 75 | 76 | :-ms-input-placeholder { 77 | color: #666; 78 | font-size: 16px; 79 | } 80 | } 81 | } 82 | .commit-btn { 83 | width: 100%; 84 | height: 40px; 85 | padding: 6px 16px; 86 | color: #fff; 87 | background: #007fff; 88 | border-radius: 2px; 89 | border: none; 90 | outline: none; 91 | box-sizing: border-box; 92 | cursor: pointer; 93 | } 94 | 95 | .switch { 96 | margin-top: 12px; 97 | font-size: 14px; 98 | color: #007fff; 99 | text-align: center; 100 | cursor: pointer; 101 | } 102 | } 103 | ` 104 | -------------------------------------------------------------------------------- /src/containers/Frame/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Main: React.FC = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Main; 8 | -------------------------------------------------------------------------------- /src/containers/Frame/Register/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | // 右侧 下载客户端小卡片 4 | 5 | import { signUp } from '@/Api/account' 6 | import useInputEvent from '@/lib/hooks/useInputEvent' 7 | import React, { useCallback } from 'react' 8 | import { message } from 'antd' 9 | 10 | import { Wrapper } from './style' 11 | 12 | interface IProps { 13 | onClose(e: any): void 14 | onSwitch(e: any): void 15 | } 16 | 17 | const Register: React.FC = ({ onClose, onSwitch }) => { 18 | const { value: username, onInputEvent: onChangeUsername } = useInputEvent('') 19 | const { value: phoneNumber, onInputEvent: onChangeNumber } = useInputEvent('') 20 | const { value: password, onInputEvent: onChangePassword } = useInputEvent('') 21 | 22 | const onRegister = useCallback(() => { 23 | // 简单校验输入手机和密码后再发送请求 24 | if (phoneNumber.length !== 11) { 25 | message.warning('请输入正确的手机号') 26 | } else if (username.length === 0) { 27 | message.warning('用户名不能为空') 28 | } else if (password.length < 6 || password.length > 12) { 29 | message.warning('密码长度不能小于6位') 30 | } else { 31 | signUp({ mobilePhoneNumber: phoneNumber, password, username }).then(data => { 32 | onClose(data) 33 | onSwitch(data) 34 | }) 35 | } 36 | }, [phoneNumber, password, username]) 37 | 38 | return ( 39 | 40 |
41 | 42 |
43 |

注册

44 |
45 | 46 | 47 | 48 |
49 | 52 |
{ 55 | onClose(e) 56 | onSwitch(e) 57 | }} 58 | > 59 | 已有账号登录 60 |
61 |
62 | 63 |
64 | ) 65 | } 66 | 67 | export default Register 68 | -------------------------------------------------------------------------------- /src/containers/Frame/Register/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | import closePicGray from '../../../statics/close-gray.png' 9 | import closePicBlack from '../../../statics/close-black.png' 10 | 11 | export const Wrapper = styled.div` 12 | position: fixed; 13 | z-index: 100; 14 | left: 0; 15 | right: 0; 16 | top: 0; 17 | bottom: 0; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | background: rgba(0, 0, 0, .3); 22 | .login-box { 23 | // position: relative; 24 | width: 318px; 25 | /* height: 100px; */ 26 | padding: 24px; 27 | font-size: 14px; 28 | background: #fff; 29 | border-radius: 2px; 30 | 31 | .close-btn { 32 | float: right; 33 | width: 20px; 34 | height: 20px; 35 | background: url(${closePicGray}) no-repeat center/contain; 36 | cursor: pointer; 37 | 38 | :hover { 39 | background: url(${closePicBlack}) no-repeat center/contain; 40 | } 41 | } 42 | 43 | .title { 44 | margin-bottom: 24px; 45 | font-size: 18px; 46 | font-weight: 700; 47 | } 48 | 49 | .input-group { 50 | margin-bottom: 6px; 51 | .input { 52 | margin-bottom: 10px; 53 | padding: 10px; 54 | width: 100%; 55 | color: #000; 56 | border: 1px solid #e9e9e9; 57 | border-radius: 2px; 58 | box-sizing: border-box; 59 | 60 | // 兼容不同浏览器的 placeholder 61 | ::-webkit-input-placeholder { 62 | color: #666; 63 | font-size: 16px; 64 | } 65 | 66 | :-moz-placeholder { 67 | color: #666; 68 | font-size: 16px; 69 | } 70 | 71 | ::-moz-placeholder { 72 | color: #666; 73 | font-size: 16px; 74 | } 75 | 76 | :-ms-input-placeholder { 77 | color: #666; 78 | font-size: 16px; 79 | } 80 | } 81 | } 82 | .commit-btn { 83 | width: 100%; 84 | height: 40px; 85 | padding: 6px 16px; 86 | color: #fff; 87 | background: #007fff; 88 | border-radius: 2px; 89 | border: none; 90 | outline: none; 91 | box-sizing: border-box; 92 | cursor: pointer; 93 | } 94 | 95 | .switch { 96 | margin-top: 12px; 97 | font-size: 14px; 98 | color: #007fff; 99 | text-align: center; 100 | cursor: pointer; 101 | } 102 | } 103 | ` 104 | -------------------------------------------------------------------------------- /src/containers/Frame/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Main from './Main'; 4 | 5 | import Header from './Header'; 6 | 7 | const Frame: React.FC = ({ children }) => { 8 | return ( 9 |
10 |
11 |
{children}
12 |
13 | ); 14 | }; 15 | 16 | export default Frame; 17 | -------------------------------------------------------------------------------- /src/containers/RouteErrorBoundary/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/src/containers/RouteErrorBoundary/index.less -------------------------------------------------------------------------------- /src/containers/RouteErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.less' 3 | 4 | class RouteErrorBoundary extends React.Component { 5 | state = { hasError: false } 6 | static getDerivedStateFromError(error: any) { 7 | console.log('%c%s', 'color: #20bd08;font-size:15px', '===TQY===: RouteErrorBoundary -> getDerivedStateFromError -> error', error) 8 | // 更新 state 使下一次渲染能够显示降级后的 UI 9 | return { hasError: true } 10 | } 11 | 12 | componentDidCatch(error: any, errorInfo: any) { 13 | console.log('%c%s', 'color: #20bd08;font-size:15px', '===TQY===: RouteErrorBoundary -> componentDidCatch -> error, errorInfo', { error }, { errorInfo }) 14 | // 你同样可以将错误日志上报给服务器 15 | // logErrorToMyService(error, errorInfo) 16 | } 17 | 18 | render() { 19 | if (this.state.hasError) { 20 | // 你可以自定义降级后的 UI 并渲染 21 | return

Something went wrong.

22 | } 23 | 24 | return this.props.children 25 | } 26 | } 27 | 28 | export default RouteErrorBoundary 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | import * as serviceWorker from './serviceWorker' 7 | 8 | ReactDOM.render(, document.getElementById('root')) 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister() 14 | -------------------------------------------------------------------------------- /src/lib/hooks/useAuthLogin.tsx: -------------------------------------------------------------------------------- 1 | import { useHistory } from 'react-router-dom' 2 | import { useIsLogin } from '@/redux/context' 3 | 4 | export default function useAuthLogin(needLogin = true) { 5 | const isLogin = useIsLogin() 6 | const history = useHistory() 7 | // console.log('11111111111111') 8 | // debugger 9 | if (!isLogin && needLogin) { 10 | history.replace('/') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/hooks/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | 4 | import { useEffect, useRef } from 'react' 5 | 6 | export default function useDocumentTitle(title: string) { 7 | const titleRef = useRef('') 8 | 9 | useEffect(() => { 10 | titleRef.current = document.title 11 | document.title = title 12 | return () => { 13 | document.title = titleRef.current 14 | } 15 | }, []) 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/hooks/useEventFetch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import { useCallback, useEffect, useMemo, useState } from 'react' 4 | 5 | export default function useEventFetch(request: (args?: T) => Promise, deps: any[], memoCb: (arg: V) => any = a => a, initialData?: V) { 6 | const [data, setData] = useState(initialData) 7 | const [isLoading, setIsLoading] = useState(false) 8 | const [isError, setIsError] = useState(false) 9 | 10 | let didCancel = false 11 | useEffect(() => { 12 | didCancel = false 13 | return () => { 14 | didCancel = true 15 | } 16 | }, deps || []) 17 | 18 | const onEvent = useCallback(async () => { 19 | setIsLoading(true) 20 | try { 21 | const result = await request() 22 | 23 | if (!didCancel) { 24 | setData(result) 25 | } 26 | } catch (error) { 27 | if (!didCancel) { 28 | setIsError(true) 29 | } 30 | } finally { 31 | if (!didCancel) { 32 | setIsLoading(false) 33 | } 34 | } 35 | }, deps || []) 36 | 37 | const memoData = useMemo(() => { 38 | try { 39 | return memoCb(data as V) 40 | } catch (error) { 41 | console.warn(error.message) 42 | return data 43 | } 44 | }, [data]) 45 | 46 | return { data, setData, isLoading, isError, onEvent, memoData } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/hooks/useFetch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import { useCallback, useEffect, useState } from 'react' 4 | 5 | export default function useFetch(request: (args?: T) => Promise, initialRequestParam: any = [], initialData?: V) { 6 | const [data, setData] = useState(initialData) 7 | const [isLoading, setIsLoading] = useState(false) 8 | const [isError, setIsError] = useState(false) 9 | 10 | // const [param, setParam] = useState(initialRequestParam) 11 | 12 | const fetchData = useCallback(async () => { 13 | try { 14 | const result = await request() 15 | 16 | setData(result) 17 | } catch (error) { 18 | setIsError(true) 19 | } finally { 20 | setIsLoading(false) 21 | } 22 | }, [...initialRequestParam]) 23 | 24 | useEffect(() => { 25 | setIsLoading(true) 26 | fetchData() 27 | }, [...initialRequestParam]) 28 | 29 | function doFetch() { 30 | fetchData() 31 | } 32 | 33 | return { data, setData, isLoading, isError, doFetch } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/hooks/useFlag.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export default function useFlag(initiateFlag: boolean) { 4 | const [flag, setFlag] = useState(initiateFlag) 5 | 6 | const setTrue = useCallback(e => setFlag(true), []) 7 | 8 | const setFalse = useCallback(e => setFlag(false), []) 9 | 10 | return { flag, setTrue, setFalse, setFlag } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/hooks/useInputEvent.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export default function useInputEvent(initiateValue: T) { 4 | const [ value, setValue ] = useState(initiateValue) 5 | 6 | const onInputEvent = useCallback((e) => setValue(e.target.value), []) 7 | 8 | return { value, onInputEvent, setValue } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/hooks/usePersist.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | 4 | import axios from 'axios' 5 | import { useEffect } from 'react' 6 | 7 | import { defaultStore } from '@/redux/context' 8 | 9 | export function usePersistedContext(context: T, key = 'state', flag = true): T { 10 | // if (!flag) { 11 | // return context 12 | // } 13 | 14 | const persistedContext = JSON.parse(localStorage.getItem(key) || 'null') || defaultStore 15 | 16 | axios.defaults.headers.common.Authorization = persistedContext!.user && persistedContext.user.access_token 17 | 18 | return persistedContext ? persistedContext : context 19 | } 20 | 21 | export function usePersistedReducer([state, dispatch]: any[], key = 'state', flag = true) { 22 | useEffect(() => { 23 | axios.defaults.headers.common.Authorization = state.user && state.user.access_token 24 | 25 | // if (!flag) { 26 | // return 27 | // } 28 | // const tmp = state || {} 29 | 30 | // const newState = Object.keys(tmp).reduce((res, cur) => { 31 | // if (Array.isArray(tmp[cur])) { 32 | // // 避免数组过大 33 | // res[cur] = tmp[cur].slice(0, 20) 34 | // } 35 | // res[cur] = tmp[cur] 36 | // return res 37 | // }, {} as any) 38 | 39 | return localStorage.setItem(key, JSON.stringify(state)) 40 | }, [state]) 41 | return [state, dispatch] 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import { useCallback, useMemo, useRef } from 'react' 4 | import { useHistory, useLocation } from 'react-router' 5 | 6 | const queryToObject = (search: string) => { 7 | return search 8 | .replace('?', '') 9 | .split('&') 10 | .map(item => item.split('=')) 11 | .reduce((res, cur) => { 12 | if (!cur[0] || !cur[1]) { 13 | return res 14 | } 15 | 16 | res[cur[0]] = cur[1] 17 | 18 | return res 19 | }, {} as any) 20 | } 21 | 22 | const objectToQuery = (obj: any) => { 23 | return ( 24 | '?' + 25 | Object.keys(obj) 26 | .filter(item => obj[item]) 27 | // .sort((a, b) => a.localeCompare(b)) 28 | .reduce((res, cur, index, arr) => { 29 | if (index === arr.length - 1) { 30 | return `${res}${cur}=${obj[cur]}` 31 | } 32 | 33 | return `${res}${cur}=${obj[cur]}&` 34 | }, '') 35 | ) 36 | } 37 | 38 | export default function useQuery() { 39 | // search 是地址栏中 ? 开始的内容 40 | const history = useHistory() 41 | const { search } = useLocation() 42 | const queryRef = useRef({} as any) 43 | 44 | const query = useMemo(() => { 45 | const query1 = queryToObject(search) 46 | queryRef.current = query1 47 | return query1 48 | }, [search]) 49 | 50 | const setQuery = useCallback((newQuery: object) => { 51 | history.replace(objectToQuery({ ...queryRef.current, ...newQuery })) 52 | }, []) 53 | 54 | return { query, setQuery } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/hooks/useRedux.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export function usePersistedContext(context: any, key = 'state', flag = true) { 4 | if (!flag) { 5 | return context 6 | } 7 | 8 | const persistedContext = localStorage.getItem(key) 9 | return persistedContext ? JSON.parse(persistedContext) : context 10 | } 11 | 12 | export function usePersistedReducer([state, dispatch]: any[], key = 'state', flag = true) { 13 | useEffect(() => { 14 | if (!flag) { 15 | return 16 | } 17 | return localStorage.setItem(key, JSON.stringify(state)) 18 | }, [state]) 19 | return [state, dispatch] 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | export default function useToggle(initiateFlag: boolean) { 4 | const [flag, setFlag] = useState(initiateFlag) 5 | 6 | const onToggle = useCallback(() => setFlag(!flag), [flag]) 7 | 8 | return { flag, onToggle, setFlag } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import marked from 'marked' 2 | 3 | declare var hljs: any 4 | 5 | // 转化 md 语法为 html 6 | export const translateMarkdown = (plainText: string, isGuardXss = false) => { 7 | return marked(plainText, { 8 | renderer: new marked.Renderer(), 9 | gfm: true, 10 | pedantic: false, 11 | sanitize: false, 12 | tables: true, 13 | breaks: true, 14 | smartLists: true, 15 | smartypants: true, 16 | highlight(code: any) { 17 | return (hljs as any).highlightAuto(code).value 18 | }, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/modal/dtos/account.dto.ts: -------------------------------------------------------------------------------- 1 | export class AccountDto { 2 | readonly email!: string 3 | 4 | readonly password!: string 5 | } 6 | -------------------------------------------------------------------------------- /src/modal/dtos/article.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateArticleDto { 2 | readonly author!: string 3 | 4 | readonly content!: string 5 | 6 | readonly html!: string 7 | 8 | readonly title!: string 9 | 10 | readonly screenshot!: string 11 | 12 | readonly tag!: string[] 13 | 14 | readonly type!: string 15 | } 16 | -------------------------------------------------------------------------------- /src/modal/dtos/comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCommentDto { 2 | readonly firstComment?: string 3 | 4 | readonly respComment?: string 5 | 6 | readonly respUser!: string 7 | 8 | readonly articleId!: string 9 | 10 | readonly content!: string 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/modal/dtos/signIn.dto.ts: -------------------------------------------------------------------------------- 1 | export class SignInDto { 2 | readonly mobilePhoneNumber!: string 3 | 4 | readonly password!: string 5 | } 6 | -------------------------------------------------------------------------------- /src/modal/dtos/signUp.dto.ts: -------------------------------------------------------------------------------- 1 | export class SignUpDto { 2 | readonly mobilePhoneNumber!: string 3 | 4 | readonly username!: string 5 | 6 | readonly password!: string 7 | } 8 | -------------------------------------------------------------------------------- /src/modal/dtos/userUpdate.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserUpdateDto { 2 | email?: 'string' 3 | avatarHd?: 'string' 4 | avatarLarge?: 'string' 5 | blogAddress?: 'string' 6 | company?: 'string' 7 | jobTitle?: 'string' 8 | selfDescription?: 'string' 9 | username?: 'string' 10 | } 11 | -------------------------------------------------------------------------------- /src/modal/entities/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { CommonEntity } from './common.entity' 2 | 3 | export class ArticleEntity extends CommonEntity { 4 | author!: string 5 | 6 | content!: string 7 | 8 | html!: string 9 | 10 | title!: string 11 | 12 | screenshot!: string 13 | 14 | tag!: string[] 15 | 16 | type!: string 17 | 18 | user!: any 19 | 20 | _id!: string 21 | 22 | id!: string 23 | 24 | create_at!: number 25 | 26 | viewCount!: number 27 | 28 | likeCount!: number 29 | 30 | likedCount!: number 31 | 32 | commentCount!: number 33 | 34 | isLiked!: boolean 35 | 36 | isFeatured!: boolean 37 | } 38 | -------------------------------------------------------------------------------- /src/modal/entities/common.entity.ts: -------------------------------------------------------------------------------- 1 | export class CommonEntity { 2 | id!: string 3 | 4 | create_at!: number 5 | 6 | update_at!: number 7 | } 8 | -------------------------------------------------------------------------------- /src/modal/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IToken { 2 | access_token: string 3 | expires_in: number 4 | } 5 | -------------------------------------------------------------------------------- /src/modal/interfaces/common.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPage { 2 | edges: T[] 3 | pageInfo: { hasNextPage: boolean; endCursor: string } 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/editor/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React, { useCallback, useEffect, useState } from 'react' 4 | import { Link } from 'react-router-dom' 5 | 6 | import { useDispatch, useSelector } from '@/redux/context' 7 | 8 | import avatarPic from '../../../statics/avatar.png' 9 | // 引入样式 10 | import { Wrapper } from './style' 11 | 12 | const Menu: React.FC = () => { 13 | // 头像下拉菜单显隐 14 | const [showDropdown, setDropdown] = useState(false) 15 | 16 | const hideDropdown = useCallback((e: any) => { 17 | if (e.target.className !== 'avatar') { 18 | setDropdown(false) 19 | } 20 | }, []) 21 | 22 | useEffect(() => { 23 | document.addEventListener('click', hideDropdown) 24 | return () => { 25 | document.removeEventListener('click', hideDropdown) 26 | } 27 | }, []) 28 | 29 | // 登出确定框 30 | const dispatch = useDispatch() 31 | 32 | const confirmLogout = () => { 33 | if (window.confirm('确定登出吗?每一片贫瘠的土地都需要坚定的挖掘者!')) { 34 | dispatch({ type: 'LOGOUT' }) 35 | // 跳转到 home page 36 | window.location.href = '/' 37 | } 38 | } 39 | 40 | // 拿到用户头像 默认值为本地头像 41 | const { 42 | user: { avatarLarge = avatarPic, id }, 43 | } = useSelector() 44 | 45 | return ( 46 | 47 | 76 | 77 | ) 78 | } 79 | 80 | export default Menu 81 | 82 | /* 83 | // 滚动条 84 | 标签循环 写死 85 | 编辑padding 86 | 外部高度 87 | 字数 88 | */ 89 | -------------------------------------------------------------------------------- /src/pages/editor/Menu/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | // @import headPicIcon from '../../' 3 | 4 | export const Wrapper = styled.div` 5 | .with-padding { 6 | padding: 0 14px; 7 | cursor: pointer; 8 | } 9 | 10 | .navigation { 11 | padding-right: 0; 12 | 13 | .avatar { 14 | width: 32px; 15 | height: 32px; 16 | border-radius: 50%; 17 | background: ${({ avatarLarge }: { avatarLarge: string }) => 18 | `#eee url(${avatarLarge}) no-repeat center/cover`}; 19 | } 20 | 21 | .dropdown-list { 22 | position: absolute; 23 | top: 100%; 24 | right: 0; 25 | width: 158px; 26 | padding: 10px 0; 27 | background: #fff; 28 | border: 1px solid rgba(177, 180, 185, 0.45); 29 | border-radius: 4px; 30 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1); 31 | list-style: none; 32 | 33 | .menu-item { 34 | display: flex; 35 | padding: 10px 14px; 36 | font-size: 15px; 37 | color: #909090; 38 | } 39 | 40 | .menu-item:hover { 41 | color: #333; 42 | background: rgba(242, 242, 242, 0.5); 43 | } 44 | } 45 | } 46 | 47 | article { 48 | height: 100%; 49 | padding: 20px; 50 | overflow-y: auto; 51 | line-height: 1.7; 52 | } 53 | 54 | h1 { 55 | font-weight: bolder; 56 | font-size: 32px; 57 | } 58 | 59 | h2 { 60 | font-weight: bold; 61 | font-size: 24px; 62 | } 63 | 64 | h3 { 65 | font-weight: bold; 66 | font-size: 20px; 67 | } 68 | 69 | h4 { 70 | font-weight: bold; 71 | font-size: 16px; 72 | } 73 | 74 | h5 { 75 | font-weight: bold; 76 | font-size: 14px; 77 | } 78 | 79 | h6 { 80 | font-weight: bold; 81 | font-size: 12px; 82 | } 83 | 84 | ul { 85 | list-style: inherit; 86 | } 87 | 88 | ol { 89 | list-style: decimal; 90 | } 91 | 92 | pre { 93 | overflow-x: auto; 94 | color: #333; 95 | font-family: Monaco, Consolas, Courier New, monospace; 96 | background: #f8f8f8; 97 | } 98 | 99 | img { 100 | max-width: 100%; 101 | margin: 10px 0; 102 | } 103 | 104 | table { 105 | max-width: 100%; 106 | /* overflow: auto; */ 107 | font-size: 14px; 108 | border: 1px solid #f6f6f6; 109 | border-collapse: collapse; 110 | border-spacing: 0; 111 | 112 | thead { 113 | color: #000; 114 | text-align: left; 115 | background: #f6f6f6; 116 | } 117 | } 118 | 119 | td, 120 | th { 121 | min-width: 80px; 122 | padding: 10px; 123 | } 124 | 125 | tbody tr:nth-of-type(odd) { 126 | background: #fcfcfc; 127 | } 128 | 129 | tbody tr:nth-of-type(even) { 130 | background: #f6f6f6; 131 | } 132 | ` 133 | -------------------------------------------------------------------------------- /src/pages/editor/Publish/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | // @import headPicIcon from '../../' 3 | 4 | export const Wrapper = styled.div` 5 | .with-padding { 6 | padding: 0 14px; 7 | cursor: pointer; 8 | } 9 | 10 | .cover-img { 11 | display: block; 12 | width: 28px; 13 | height: 28px; 14 | cursor: pointer; 15 | } 16 | 17 | .publish { 18 | display: flex; 19 | align-items: center; 20 | flex: 0 0 auto; 21 | 22 | .publish-title { 23 | flex: 0 0 auto; 24 | font-size: 16px; 25 | color: #007fff; 26 | } 27 | 28 | .arrow-down, 29 | .arrow-up { 30 | flex: 0 0 auto; 31 | margin: 0 14px 0 6px; 32 | width: 16px; 33 | height: 16px; 34 | } 35 | 36 | .arrow-up { 37 | transform: rotate(180deg); 38 | } 39 | } 40 | 41 | .panel { 42 | // display: none; 43 | position: absolute; 44 | z-index: 100; 45 | top: 100%; 46 | right: 36px; 47 | width: 336px; 48 | padding: 24px; 49 | background: #fff; 50 | border: 1px solid #ddd; 51 | border-radius: 2px; 52 | box-shadow: 0 1px 2px #f1f1f1; 53 | 54 | .panel-title { 55 | margin-bottom: 18px; 56 | font-size: 19px; 57 | font-weight: 700; 58 | color: hsla(218, 9%, 51%, .8); 59 | } 60 | 61 | .type-box { 62 | margin-bottom: 18px; 63 | .sub-title { 64 | margin-bottom: 12px; 65 | font-size: 16px; 66 | color: #909090; 67 | } 68 | 69 | .type-list { 70 | display: flex; 71 | margin: 0; 72 | flex-wrap: wrap; 73 | list-style: none; 74 | 75 | .item { 76 | margin: 0 7px 10px 0; 77 | padding: 5px 8px; 78 | line-height: 1.2; 79 | font-size: 13px; 80 | color: #909090; 81 | border: 1px solid #f1f1f1; 82 | border-radius: 2px; 83 | cursor: pointer; 84 | } 85 | 86 | .item.active, 87 | .item:hover { 88 | color: #007fff; 89 | border-color: rgba(0, 127, 255, .15); 90 | background-color: rgba(0, 127, 255, .05); 91 | } 92 | } 93 | } 94 | 95 | .publish-btn { 96 | display: block; 97 | margin: 0 auto; 98 | padding: 7px 14px; 99 | color: #007fff; 100 | font-size: 14px; 101 | background: #fff; 102 | border: 1px solid currentColor; 103 | border-radius: 2px; 104 | outline: none; 105 | cursor: pointer; 106 | } 107 | 108 | .publish-btn:hover { 109 | background: rgba(3, 113, 223, 0.05); 110 | } 111 | } 112 | 113 | .panel::before { 114 | content: ''; 115 | position: absolute; 116 | top: -6px; 117 | right: 21%; 118 | width: 12px; 119 | height: 12px; 120 | border-top: 1px solid #ddd; 121 | border-left: 1px solid #ddd; 122 | background: #fff; 123 | transform: rotate(45deg); 124 | } 125 | 126 | article { 127 | height: 100%; 128 | padding: 20px; 129 | overflow-y: auto; 130 | line-height: 1.7; 131 | } 132 | 133 | h1 { 134 | font-weight: bolder; 135 | font-size: 32px; 136 | } 137 | 138 | h2 { 139 | font-weight: bold; 140 | font-size: 24px; 141 | } 142 | 143 | h3 { 144 | font-weight: bold; 145 | font-size: 20px; 146 | } 147 | 148 | h4 { 149 | font-weight: bold; 150 | font-size: 16px; 151 | } 152 | 153 | h5 { 154 | font-weight: bold; 155 | font-size: 14px; 156 | } 157 | 158 | h6 { 159 | font-weight: bold; 160 | font-size: 12px; 161 | } 162 | 163 | ul { 164 | list-style: inherit; 165 | } 166 | 167 | ol { 168 | list-style: decimal; 169 | } 170 | 171 | pre { 172 | overflow-x: auto; 173 | color: #333; 174 | font-family: Monaco, Consolas, Courier New, monospace; 175 | background: #f8f8f8; 176 | } 177 | 178 | img { 179 | max-width: 100%; 180 | margin: 10px 0; 181 | } 182 | 183 | table { 184 | max-width: 100%; 185 | /* overflow: auto; */ 186 | font-size: 14px; 187 | border: 1px solid #f6f6f6; 188 | border-collapse: collapse; 189 | border-spacing: 0; 190 | 191 | thead { 192 | color: #000; 193 | text-align: left; 194 | background: #f6f6f6; 195 | } 196 | } 197 | 198 | td, 199 | th { 200 | min-width: 80px; 201 | padding: 10px; 202 | } 203 | 204 | tbody tr:nth-of-type(odd) { 205 | background: #fcfcfc; 206 | } 207 | 208 | tbody tr:nth-of-type(even) { 209 | background: #f6f6f6; 210 | } 211 | ` 212 | -------------------------------------------------------------------------------- /src/pages/home/Article/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | position: relative; 5 | border-bottom: 1px solid rgba(178, 186, 194, 0.15); 6 | 7 | .content { 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: space-between; 11 | align-items: center; 12 | /* height: 116px; */ 13 | padding: 18px 24px; 14 | 15 | .info-box { 16 | flex: 1 1 auto; 17 | width: 568px; 18 | height: 100%; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | 23 | .info-row { 24 | display: flex; 25 | color: #b2bac2; 26 | 27 | .info-item { 28 | :not(:last-child)::after { 29 | content: '·'; 30 | color: #b2bac2; 31 | margin: 0 5px; 32 | } 33 | 34 | &.column { 35 | color: #b71ed7; 36 | font-weight: 500; 37 | } 38 | 39 | .user-link, 40 | .tag-link { 41 | color: #b2bac2; 42 | :hover { 43 | color: #007fff; 44 | } 45 | } 46 | } 47 | 48 | .row { 49 | display: flex; 50 | align-items: center; 51 | } 52 | 53 | .little-box { 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | height: 26px; 58 | padding: 0 10px; 59 | color: #b2bac2; 60 | font-weight: 700; 61 | border: 1px solid #edeeef; 62 | border-radius: 1px; 63 | 64 | &.comment { 65 | margin-left: -1px; 66 | } 67 | 68 | .count { 69 | margin-left: 3px; 70 | } 71 | } 72 | } 73 | 74 | .title { 75 | margin: 6px 0 12px; 76 | white-space: nowrap; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | 80 | .title-link { 81 | color: #2e3135; 82 | font-size: 17px; 83 | font-weight: 600; 84 | 85 | ::visited { 86 | color: #909090; 87 | } 88 | } 89 | } 90 | 91 | .abstract { 92 | margin-bottom: 12px; 93 | font-size: 13px; 94 | color: #5b6169; 95 | /* color: #909090; */ 96 | white-space: nowrap; 97 | overflow: hidden; 98 | text-overflow: ellipsis; 99 | } 100 | 101 | .action-row { 102 | display: flex; 103 | justify-content: space-between; 104 | align-items: center; 105 | } 106 | } 107 | 108 | .thumb { 109 | display: ${({ screenshot }: { screenshot: string }) => (screenshot ? 'block' : 'none')}; 110 | flex: 0 0 auto; 111 | margin-left: 24px; 112 | width: 60px; 113 | height: 60px; 114 | border-radius: 2px; 115 | background-color: #fff; 116 | background: ${({ screenshot }) => `url(${screenshot}) no-repeat center/cover`}; 117 | box-sizing: content-box; 118 | } 119 | } 120 | ` 121 | -------------------------------------------------------------------------------- /src/pages/home/ArticleList/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | 4 | import React, { useEffect, useState, useCallback } from 'react' 5 | import InfiniteScroll from 'react-infinite-scroll-component' 6 | import { useHistory } from 'react-router-dom' 7 | 8 | import { getArticles } from '@/Api/article' 9 | import { getUserArticles } from '@/Api/user' 10 | import useFetch from '@/lib/hooks/useFetch' 11 | import useQuery from '@/lib/hooks/useQuery' 12 | import { ArticleEntity } from '@/modal/entities/article.entity' 13 | import { useDispatch, useIsLogin, useSelector } from '@/redux/context' 14 | 15 | import Article from '../Article' 16 | import { Wrapper } from './style' 17 | 18 | const ArticleList: React.FC = () => { 19 | const [pageInfo, setPageInfo] = useState({ hasNextPage: true, endCursor: 0 }) 20 | 21 | const { setQuery, query } = useQuery() 22 | 23 | const isLogin = useIsLogin() 24 | 25 | const history = useHistory() 26 | 27 | // 未登录状态 手动输入 http://localhost:3000/?own=mine 无效 28 | useEffect(() => { 29 | const { own } = query 30 | if (!isLogin && own === 'mine') { 31 | history.replace('/') 32 | } 33 | }, []) 34 | 35 | const dispatch = useDispatch() 36 | const { 37 | user: { id }, 38 | } = useSelector() 39 | 40 | useFetch(async () => { 41 | const rs = query.own === 'mine' ? await getUserArticles({ id, endCursor: 0 }) : await getArticles({ ...query, endCursor: 0 }) 42 | const list = (rs && rs.edges) || [] 43 | 44 | dispatch({ 45 | type: 'CHANGE_ARTICLE_LIST', 46 | payload: { articleList: [...list] }, 47 | }) 48 | if (rs && rs.pageInfo) { 49 | setPageInfo(rs.pageInfo) 50 | } 51 | 52 | return list 53 | }, [query]) 54 | 55 | const nextPage = useCallback(async () => { 56 | const rs = query.own === 'mine' ? await getUserArticles({ id, endCursor: pageInfo.endCursor }) : await getArticles({ ...query, endCursor: pageInfo.endCursor }) 57 | const list = (rs && rs.edges) || [] 58 | 59 | if (rs.pageInfo.endCursor <= pageInfo.endCursor) { 60 | return [] 61 | } 62 | 63 | dispatch({ 64 | type: 'APPEND_ARTICLE_LIST', 65 | payload: { articleList: [...list] }, 66 | }) 67 | 68 | if (rs && rs.pageInfo) { 69 | setPageInfo(rs.pageInfo) 70 | } 71 | 72 | return list 73 | }, [pageInfo.endCursor, pageInfo.hasNextPage, query]) 74 | 75 | // 用 store 的数据渲染页面 76 | const { articleList } = useSelector() 77 | 78 | return ( 79 | 80 |
81 | 101 |
102 | 103 |
    104 | 加载中... 111 | ) : ( 112 |
    没有更多数据了...
    113 | ) 114 | } 115 | > 116 | {articleList.map((item: ArticleEntity) => ( 117 |
    118 | ))} 119 | 120 |
121 |
122 | ) 123 | } 124 | 125 | export default ArticleList 126 | -------------------------------------------------------------------------------- /src/pages/home/ArticleList/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | width: 700px; 11 | background: #fff; 12 | 13 | .header { 14 | padding: 16px 12px; 15 | border-bottom: 1px solid hsla(0, 0%, 59.2%, .1); 16 | 17 | .nav-list { 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | margin-bottom: 0; 22 | 23 | .nav-item { 24 | padding: 0 14px; 25 | line-height: 14px; 26 | font-size: 14px; 27 | color: #909090; 28 | cursor: pointer; 29 | 30 | &.active, 31 | :hover { 32 | color: #007fff; 33 | } 34 | 35 | :not(:first-child) { 36 | border-left: 1px solid hsla(0, 0%, 59.2%, .2); 37 | } 38 | } 39 | } 40 | } 41 | 42 | .item { 43 | border-bottom: 1px solid rgba(178, 186, 194, .15); 44 | .thumb { 45 | width: 60px; 46 | height: 60px; 47 | border-radius: 2px; 48 | background-color: #fff; 49 | } 50 | } 51 | ` 52 | -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { BackTop } from 'antd' 2 | import React from 'react' 3 | 4 | import Advertising from '@/components/Advertising' 5 | 6 | import AppDownload from '../../components/AppDownload' 7 | import ArticleList from './ArticleList' 8 | import { Wrapper } from './style' 9 | 10 | const Home: React.FC = props => { 11 | return ( 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | ) 24 | } 25 | 26 | export default Home 27 | -------------------------------------------------------------------------------- /src/pages/home/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | display: flex; 11 | flex-direction: row; 12 | justify-content: space-between; 13 | /* 交叉轴对齐方式默认 stretch 会纵向拉伸 AppDownload 组件(未设置高度) */ 14 | align-items: flex-start; 15 | width: 960px; 16 | margin: 76px auto 0; 17 | /* background: skyblue; */ 18 | ` 19 | -------------------------------------------------------------------------------- /src/pages/mobiPost/Article/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | 4 | import React from 'react' 5 | 6 | import { translateMarkdown } from '@/lib/utils/markdown' 7 | import { ArticleEntity } from '@/modal/entities/article.entity' 8 | 9 | import { Wrapper } from './style' 10 | 11 | interface IProps extends ArticleEntity {} 12 | 13 | const Article: React.FC = ({ update_at, content, author, title, html }) => { 14 | return ( 15 | 16 |
17 |
18 | 19 |
25 | 26 |
27 | {author} 28 |
29 | 30 | {/* 阅读 1367 */} 31 |
32 |
33 |
34 | {/* */} 35 |
36 |

{title}

37 |
38 |
39 |
40 | 41 | ) 42 | } 43 | 44 | export default Article 45 | -------------------------------------------------------------------------------- /src/pages/mobiPost/Article/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | /* width: 700px; */ 11 | /* margin-bottom: 18px; */ 12 | padding: 24px 24px 36px; 13 | background: #fff; 14 | border-radius: 2px; 15 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 16 | 17 | .author { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | margin-bottom: 24px; 22 | 23 | .author-info { 24 | display: flex; 25 | align-items: center; 26 | 27 | .avatar { 28 | width: 40px; 29 | height: 40px; 30 | margin-right: 12px; 31 | border-radius: 50%; 32 | } 33 | 34 | .author-name { 35 | font-size: 16px; 36 | font-weight: 700; 37 | color: #333; 38 | } 39 | 40 | .article-info { 41 | margin-top: 4px; 42 | font-size: 13px; 43 | color: #909090; 44 | 45 | .views { 46 | margin-left: 7px; 47 | } 48 | } 49 | } 50 | 51 | .follow { 52 | width: 55px; 53 | height: 26px; 54 | font-size: 13px; 55 | color: #6cbd45; 56 | border: 1px solid #6cbd45; 57 | border-radius: 2px; 58 | background: #fff; 59 | cursor: pointer; 60 | } 61 | } 62 | 63 | .article-title { 64 | margin: 20px 0; 65 | line-height: 1.5; 66 | font-size: 30px; 67 | font-weight: 700; 68 | color: #333; 69 | } 70 | ` 71 | -------------------------------------------------------------------------------- /src/pages/mobiPost/index.tsx: -------------------------------------------------------------------------------- 1 | import { getArticle } from '@/Api/article' 2 | import useFetch from '@/lib/hooks/useFetch' 3 | import { ArticleEntity } from '@/modal/entities/article.entity' 4 | import React from 'react' 5 | import { useParams } from 'react-router' 6 | 7 | import Article from './Article' 8 | 9 | import useDocumentTitle from '@/lib/hooks/useDocumentTitle' 10 | import { Wrapper } from './style' 11 | 12 | const Post: React.FC = props => { 13 | const { id = '' } = useParams() as any 14 | useDocumentTitle('文章详情') 15 | 16 | const { data } = useFetch(() => getArticle(id)) 17 | 18 | const item: ArticleEntity = data && data[0] 19 | 20 | return ( 21 | 22 |
23 | 24 | ) 25 | } 26 | 27 | export default Post 28 | -------------------------------------------------------------------------------- /src/pages/mobiPost/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | /* display: flex; 11 | justify-content: space-between; 12 | width: 960px; */ 13 | /* margin: 82px auto 0; */ 14 | 15 | /* .left { 16 | width: 700px; 17 | // height:200px; 18 | // background:#fff; 19 | } 20 | 21 | .right { 22 | width: 240px; 23 | // height:200px; 24 | // background:#fff; 25 | } */ 26 | ` 27 | -------------------------------------------------------------------------------- /src/pages/post/Article/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | 4 | import React, { useCallback } from 'react' 5 | import { useHistory } from 'react-router' 6 | import { Link } from 'react-router-dom' 7 | 8 | import { addFollow, deleteFollow } from '@/Api/follow' 9 | import { isFollowing } from '@/Api/user' 10 | import useFetch from '@/lib/hooks/useFetch' 11 | import useToggle from '@/lib/hooks/useToggle' 12 | import { translateMarkdown } from '@/lib/utils/markdown' 13 | import { ArticleEntity } from '@/modal/entities/article.entity' 14 | import { useIsLogin, useSelector } from '@/redux/context' 15 | 16 | import { Wrapper } from './style' 17 | 18 | const formatDate = (milliseconds: number) => { 19 | const data = new Date(milliseconds) 20 | const year = data.getFullYear() 21 | const month = data.getMonth() + 1 22 | const day = data.getDate() 23 | return year + '年' + month + '月' + day + '日' 24 | } 25 | 26 | interface IProps extends ArticleEntity { 27 | // user: { avatarLarge: string } 28 | } 29 | 30 | const Article: React.FC = ({ create_at, content, title, html, screenshot, id, viewCount, user: { avatarLarge = '', id: userId, username: author } = {} }) => { 31 | const isLogin = useIsLogin() 32 | // 登录用户的用户名 33 | const { 34 | user: { username }, 35 | } = useSelector() 36 | const history = useHistory() 37 | const onReedit = useCallback(async () => { 38 | history.push('/editor/' + id) 39 | }, [id]) 40 | // const isFollow = true 41 | 42 | // 拿到当前登录用户的 id 43 | const { 44 | user: { id: loginId }, 45 | } = useSelector() 46 | 47 | const { flag, onToggle, setFlag } = useToggle(false) 48 | 49 | const onFollow = useCallback(async () => { 50 | // if (!id || !loginId) { 51 | // return 52 | // } 53 | flag ? await deleteFollow(userId) : await addFollow(userId) 54 | onToggle() 55 | }, [flag, userId]) 56 | 57 | useFetch(async () => { 58 | if (!userId || !loginId) { 59 | return 60 | } 61 | // id 是查看用户, followerId是登录用户 62 | const rs = await isFollowing(userId, loginId) 63 | setFlag(rs) 64 | }, [userId]) 65 | 66 | return ( 67 | 68 | {/* 作者及文章简介 */} 69 |
70 |
71 | 72 |
73 | 74 |
75 | 76 | {author} 77 | 78 |
79 | 80 | 阅读 {viewCount} 81 | {isLogin && author === username && ( 82 |
83 | · 84 | 85 | 编辑 86 | 87 |
88 | )} 89 |
90 |
91 |
92 | {flag ? ( 93 | 96 | ) : ( 97 | 100 | )} 101 |
102 | 103 | {/* 文章标题及内容 */} 104 |
105 |

{title}

106 |
107 |
108 |
109 | 110 | ) 111 | } 112 | 113 | export default Article 114 | -------------------------------------------------------------------------------- /src/pages/post/Article/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | width: 700px; 11 | padding: 24px 24px 0; 12 | margin-bottom: 36px; 13 | 14 | .author { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | margin-bottom: 24px; 19 | 20 | .author-info { 21 | display: flex; 22 | align-items: center; 23 | 24 | .avatar { 25 | width: 40px; 26 | height: 40px; 27 | margin-right: 12px; 28 | border-radius: 50%; 29 | background: ${({ avatarLarge }) => `#eee url(${avatarLarge}) no-repeat center/cover`}; 30 | } 31 | 32 | .author-name { 33 | font-size: 16px; 34 | font-weight: 700; 35 | color: #333; 36 | } 37 | 38 | .article-info { 39 | display: flex; 40 | margin-top: 4px; 41 | font-size: 13px; 42 | color: #909090; 43 | 44 | .views { 45 | margin-left: 7px; 46 | } 47 | 48 | .dot { 49 | margin: 0 6px; 50 | } 51 | 52 | .edit-btn { 53 | color: #1264b6; 54 | cursor: pointer; 55 | :hover { 56 | text-decoration: underline; 57 | } 58 | } 59 | } 60 | } 61 | 62 | .follow-btn { 63 | outline: none; 64 | width: 55px; 65 | height: 26px; 66 | font-size: 13px; 67 | color: #6cbd45; 68 | border: 1px solid #6cbd45; 69 | border-radius: 2px; 70 | background: #fff; 71 | cursor: pointer; 72 | 73 | :hover { 74 | opacity: 0.8; 75 | } 76 | 77 | &.followed { 78 | color: #fff; 79 | background-color: #6cbd45; 80 | } 81 | } 82 | } 83 | 84 | .cover-img { 85 | display: ${({ screenshot }: { screenshot: string; avatarLarge: string }) => (screenshot ? 'block' : 'none')}; 86 | margin-bottom: 24px; 87 | width: 100%; 88 | height: 367px; 89 | background: ${({ screenshot }) => `#fff url(${screenshot}) no-repeat center/cover`}; 90 | } 91 | 92 | .article-title { 93 | margin: 20px 0; 94 | line-height: 1.5; 95 | font-size: 30px; 96 | font-weight: 700; 97 | color: #333; 98 | } 99 | ` 100 | -------------------------------------------------------------------------------- /src/pages/post/Author/index.tsx: -------------------------------------------------------------------------------- 1 | // 详情页 右侧 作者简介卡片 2 | 3 | import React from 'react' 4 | import { Link } from 'react-router-dom' 5 | 6 | import { getUserInfo } from '@/Api/user' 7 | import useFetch from '@/lib/hooks/useFetch' 8 | import { ArticleEntity } from '@/modal/entities/article.entity' 9 | import { toThousands } from '@/pages/user/StatBlock' 10 | 11 | import { Wrapper } from './style' 12 | 13 | interface IProps extends ArticleEntity { 14 | // user: { avatarLarge: string } 15 | } 16 | 17 | const Author: React.FC = ({ user: { username = '', jobTitle = '', company = '', avatarLarge = '', id = '' } = {} }) => { 18 | const { data: author = {} } = useFetch(() => { 19 | if (!id) { 20 | return Promise.resolve({}) 21 | } 22 | return getUserInfo(id) 23 | }, [id]) 24 | 25 | const { likedCount, viewCount } = author 26 | 27 | return ( 28 | 29 |
关于作者
30 |
31 | 32 |
33 |
34 | {username} 35 | 36 | {jobTitle && company ? jobTitle + ' @ ' + company : jobTitle ? jobTitle : company ? company : '暂无简介'} 37 | 38 |
39 | 40 |
41 | 42 | 获得点赞 43 | {toThousands(likedCount)} 44 |
45 |
46 | 47 | 文章被阅读 48 | {toThousands(viewCount)} 49 |
50 |
51 | 52 | ) 53 | } 54 | 55 | export default Author 56 | -------------------------------------------------------------------------------- /src/pages/post/Author/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | // display: flex; 11 | // flex-direction: row; 12 | // justify-content: space-between; 13 | width: 240px; 14 | margin-bottom: 18px; 15 | background: #fff; 16 | border-radius: 2px; 17 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 18 | 19 | .author-title { 20 | padding: 12px 16px; 21 | color: #333; 22 | font-size: 14px; 23 | border-bottom: 1px solid hsla(0, 0%, 58.8%, .1); 24 | } 25 | 26 | .author-info { 27 | // 解决下外边距和下边框坍塌问题 28 | overflow: hidden; 29 | 30 | .author-desc { 31 | display: flex; 32 | align-items: center; 33 | padding: 16px; 34 | 35 | // 左边 头像 36 | .avatar { 37 | flex: 0 0 auto; 38 | width: 50px; 39 | height: 50px; 40 | margin-right: 12px; 41 | border-radius: 50%; 42 | background: ${({ avatarLarge }: { avatarLarge: string }) => 43 | `#eee url(${avatarLarge}) no-repeat center/cover`}; 44 | } 45 | 46 | // 右边 作者名字简介 47 | .info { 48 | min-width: 0; 49 | 50 | .author-name { 51 | display: block; 52 | font-size: 16px; 53 | font-weight: 600; 54 | color: #000; 55 | white-space: pre-wrap; 56 | } 57 | 58 | .author-intro { 59 | display: block; 60 | margin-top: 10px; 61 | font-size: 15px; 62 | color: #72777b; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | } 67 | } 68 | } 69 | 70 | .agree, 71 | .views { 72 | display: flex; 73 | align-items: center; 74 | padding: 0 16px; 75 | margin-bottom: 10px; 76 | font-size: 15px; 77 | color: #000; 78 | } 79 | 80 | .views { 81 | margin-bottom: 16px; 82 | } 83 | 84 | .count { 85 | margin: 0 5px; 86 | font-weight: 500; 87 | } 88 | 89 | .icon { 90 | width: 25px; 91 | height: 25px; 92 | margin-right: 12px; 93 | background: #eee; 94 | border-radius: 50%; 95 | } 96 | } 97 | ` 98 | -------------------------------------------------------------------------------- /src/pages/post/Catalog/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | 3 | import { ArticleEntity } from '@/modal/entities/article.entity' 4 | import { Wrapper } from './style' 5 | import { Anchor } from 'antd' 6 | import { translateMarkdown } from '@/lib/utils/markdown' 7 | const { Link } = Anchor 8 | 9 | export function matchReg(str: string) { 10 | let reg = /<\/?.+?\/?>/g 11 | return str.replace(reg, '') 12 | } 13 | 14 | // 根据 article 来生成锚点列表 15 | function getAnchorList(str: any = '') { 16 | const pattern = /<(h[1-6])[\s\S]+?(?=<\/\1>)/g 17 | const list: any[] = [] 18 | function pushItem(arr: any, item: any) { 19 | const len = arr.length 20 | const matchItem = arr[len - 1] 21 | if (matchItem && matchItem.tag !== item.tag) { 22 | pushItem(matchItem.children, item) 23 | } else { 24 | arr.push(item) 25 | } 26 | } 27 | str.replace(pattern, ($0: any, $1: any) => { 28 | const title = matchReg($0.replace(/.*?>/, '') || '') 29 | const startIndex = $0.indexOf('"') 30 | const endIndex = $0.indexOf('">') 31 | 32 | const href = `#${$0.slice(startIndex + 1, endIndex)}` 33 | const currentItem = { 34 | tag: $1, // 标签类型 35 | title, 36 | href, 37 | children: [], 38 | } 39 | pushItem(list, currentItem) 40 | }) 41 | return list 42 | } 43 | 44 | interface IProps extends ArticleEntity {} 45 | 46 | const Catalog: React.FC = ({ html, content }) => { 47 | // 根据content生成锚点列表 48 | const list = getAnchorList(html || translateMarkdown(content || '')) 49 | // console.log(list, '222') 50 | 51 | // 把锚点列表中的每项转成链接 52 | const renderLink = useCallback(({ href, title, children }: { href: string; title: string; children: any[] }) => { 53 | return ( 54 | 55 | {children.length > 0 && children.map((sub: any) => renderLink(sub))} 56 | 57 | ) 58 | }, []) 59 | 60 | return ( 61 | 62 | {/* offsetTop 是目录列表距窗口顶部距离,targetOffset 是锚点距窗口顶部距离 */} 63 | 64 |
目录
65 |
{list.map(renderLink)}
66 |
67 |
68 | ) 69 | } 70 | 71 | export default Catalog 72 | -------------------------------------------------------------------------------- /src/pages/post/Catalog/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | .ant-anchor-wrapper { 5 | /* 去掉 antd 目录背景颜色 */ 6 | background: transparent; 7 | 8 | /* 隐藏滚动条 */ 9 | /* IE 10+ */ 10 | -ms-overflow-style: none; 11 | /* Firefox */ 12 | scrollbar-width: none; 13 | /* Safari and Chrome */ 14 | ::-webkit-scrollbar { 15 | /* display: none; */ 16 | width: 0; 17 | height: 0; 18 | } 19 | 20 | /* 左侧刻度轴隐藏 */ 21 | .ant-anchor-ink { 22 | display: none; 23 | } 24 | 25 | .catalog-title { 26 | font-size: 14px; 27 | color: #000; 28 | } 29 | 30 | .catalog-body { 31 | margin: 6px 0; 32 | 33 | .ant-anchor-link { 34 | padding: 0; 35 | 36 | /* 所有链接 */ 37 | .ant-anchor-link-title { 38 | margin: 6px 0; 39 | padding: 4px 0 4px 21px; 40 | color: #000; 41 | ::before { 42 | content: ''; 43 | position: absolute; 44 | top: 50%; 45 | transform: translateY(-50%); 46 | z-index: 1000; 47 | width: 4px; 48 | height: 4px; 49 | border-radius: 50%; 50 | background: currentColor; 51 | } 52 | } 53 | } 54 | 55 | /* 二級目錄中的鏈接 */ 56 | .ant-anchor-link > .ant-anchor-link > .ant-anchor-link-title { 57 | margin: 0; 58 | padding: 4px 0 4px 36px; 59 | color: #333; 60 | 61 | ::before { 62 | left: 24px; 63 | } 64 | } 65 | 66 | /* 三级目录链接小圆点 */ 67 | .ant-anchor-link > .ant-anchor-link > .ant-anchor-link > .ant-anchor-link-title { 68 | padding: 4px 0 4px 51px; 69 | 70 | ::before { 71 | left: 39px; 72 | } 73 | } 74 | 75 | /* 被選中的一級目錄 */ 76 | .ant-anchor-link-active { 77 | background-color: #ebedef; 78 | 79 | .ant-anchor-link-title-active { 80 | color: #007fff; 81 | } 82 | } 83 | 84 | /* 被選中的二級目錄(考慮優先級) */ 85 | .ant-anchor-link > .ant-anchor-link-active > .ant-anchor-link-title-active { 86 | color: #007fff; 87 | } 88 | 89 | /* 鼠標懸停時 */ 90 | .ant-anchor-link:hover { 91 | background: #ebedef; 92 | 93 | .ant-anchor-link-title { 94 | /* 如何移除 a:hover 的效果?——把里面的代码全部去掉即可 */ 95 | /* color: inherit; */ 96 | } 97 | } 98 | } 99 | 100 | /* 一级目录中的链接 */ 101 | .catalog-body > .ant-anchor-link > .ant-anchor-link-title { 102 | font-weight: 600; 103 | 104 | ::before { 105 | left: 5px; 106 | width: 6px; 107 | height: 6px; 108 | } 109 | } 110 | } 111 | ` 112 | -------------------------------------------------------------------------------- /src/pages/post/SuspendedPanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React, { useCallback, useState, useEffect } from 'react' 4 | 5 | import { addLike, deleteLike } from '@/Api/like' 6 | import { ArticleEntity } from '@/modal/entities/article.entity' 7 | 8 | import { Wrapper } from './style' 9 | 10 | interface IProps extends ArticleEntity {} 11 | const SuspendedPanel: React.FC = ({ id, isLiked, likeCount = 0, commentCount = 0 }) => { 12 | const [likeFlag, setLikeFlag] = useState(false) 13 | // likeCount2 只控制前端显示,不会影响后台数据 14 | const [likeCountNew, setLikeCountNew] = useState(0) 15 | 16 | useEffect(() => { 17 | setLikeFlag(isLiked) 18 | }, [isLiked]) 19 | 20 | useEffect(() => { 21 | setLikeCountNew(likeCount) 22 | }, [likeCount]) 23 | 24 | const onLike = useCallback(async () => { 25 | if (likeFlag) { 26 | await deleteLike(id) 27 | setLikeCountNew(likeCountNew - 1) 28 | } else { 29 | await addLike(id) 30 | setLikeCountNew(likeCountNew + 1) 31 | } 32 | setLikeFlag(!likeFlag) 33 | }, [likeFlag]) 34 | 35 | return ( 36 | // 37 | 38 |
39 | 40 |
41 | 42 | 43 | ) 44 | } 45 | 46 | export default SuspendedPanel 47 | -------------------------------------------------------------------------------- /src/pages/post/SuspendedPanel/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | position: fixed; 5 | margin-left: -84px; 6 | top: 192px; 7 | 8 | .panel-btn { 9 | /* 右上角有个绝对定位的点赞数 */ 10 | position: relative; 11 | margin-bottom: 9px; 12 | width: 36px; 13 | height: 36px; 14 | border-radius: 50%; 15 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .04); 16 | cursor: pointer; 17 | 18 | ::after { 19 | position: absolute; 20 | top: 0; 21 | left: 75%; 22 | padding: 2px 5px; 23 | color: #fff; 24 | font-size: 12px; 25 | text-align: center; 26 | line-height: 1; 27 | white-space: nowrap; 28 | background-color: #b2bac2; 29 | border-radius: 8px; 30 | transform-origin: left top; 31 | transform: scale(0.75); 32 | } 33 | } 34 | 35 | .like-btn { 36 | background: #fff url(https://b-gold-cdn.xitu.io/v3/static/img/zan.b4bb964.svg) no-repeat 53% 46%; 37 | 38 | ::after { 39 | display: ${({ likeCount }: { likeCount: string; commentCount: string }) => 40 | likeCount !== '0' ? 'block' : 'none'}; 41 | content: ${({ likeCount }) => `'${likeCount}'`}; 42 | } 43 | 44 | :hover { 45 | background-image: url(https://b-gold-cdn.xitu.io/v3/static/img/zan-hover.91657d6.svg); 46 | } 47 | 48 | &.active { 49 | background-image: url(https://b-gold-cdn.xitu.io/v3/static/img/zan-active.337b9a0.svg); 50 | 51 | ::after { 52 | background-color: #74ca46; 53 | } 54 | } 55 | } 56 | 57 | .comment-btn { 58 | background: #fff url(https://b-gold-cdn.xitu.io/v3/static/img/comment.7fc22c2.svg) no-repeat 50% 55%; 59 | 60 | ::after { 61 | display: ${({ commentCount }) => (commentCount !== '0' ? 'block' : 'none')}; 62 | content: ${({ commentCount }) => `'${commentCount}'`}; 63 | } 64 | 65 | :hover { 66 | background-image: url(https://b-gold-cdn.xitu.io/v3/static/img/comment-hover.1074e67.svg); 67 | } 68 | } 69 | ` 70 | -------------------------------------------------------------------------------- /src/pages/post/index.tsx: -------------------------------------------------------------------------------- 1 | import { BackTop } from 'antd' 2 | import React, { useEffect } from 'react' 3 | import { useParams } from 'react-router' 4 | 5 | import { getArticle, putViewCount } from '@/Api/article' 6 | import useFetch from '@/lib/hooks/useFetch' 7 | import { ArticleEntity } from '@/modal/entities/article.entity' 8 | import { useSelector } from '@/redux/context' 9 | 10 | import AppDownload from '../../components/AppDownload' 11 | import Article from './Article' 12 | import Author from './Author' 13 | import Catalog from './Catalog' 14 | import Comment from './Comment' 15 | import { Wrapper } from './style' 16 | import SuspendedPanel from './SuspendedPanel' 17 | import Advertising from '@/components/Advertising' 18 | 19 | const Post: React.FC = props => { 20 | // 文章id 21 | const { id = '' } = useParams() 22 | 23 | const { articleList = [] } = useSelector() 24 | 25 | // 从 store 中的文章列表中找到 url 中 id 对应的文章 26 | const article = articleList.find((it: any) => id === it.id) || {} 27 | 28 | const { data = article } = useFetch(() => getArticle(id)) 29 | 30 | const item: ArticleEntity = data 31 | 32 | useEffect(() => { 33 | putViewCount(id) 34 | }, [id]) 35 | 36 | return ( 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | ) 52 | } 53 | 54 | export default Post 55 | -------------------------------------------------------------------------------- /src/pages/post/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | display: flex; 11 | justify-content: space-between; 12 | width: 960px; 13 | margin: 82px auto 24px; 14 | 15 | .left { 16 | width: 700px; 17 | background: #fff; 18 | border-radius: 2px; 19 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 20 | } 21 | 22 | .right { 23 | width: 240px; 24 | } 25 | ` 26 | -------------------------------------------------------------------------------- /src/pages/settings/InfoGroup/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React, { useState, useEffect } from 'react' 4 | import { Wrapper } from './style' 5 | import useEventFetch from '@/lib/hooks/useEventFetch' 6 | import { userUpdate, getUserInfo } from '@/Api/user' 7 | import useInputEvent from '@/lib/hooks/useInputEvent' 8 | import { useSelector, useDispatch } from '@/redux/context' 9 | 10 | interface IProps { 11 | item: { 12 | title: string 13 | field: string 14 | placeholder?: string 15 | } 16 | } 17 | 18 | const setFocus = (id: string) => { 19 | const input = document.getElementById(id) 20 | input && input.focus() 21 | } 22 | 23 | const InfoGroup: React.FC = ({ item: { field, title, placeholder } }) => { 24 | // 是否为编辑状态 25 | const { user = {} } = useSelector() 26 | // console.log('user对象', user) 27 | 28 | const [editFlag, setEditFlag] = useState(false) 29 | const { value, onInputEvent, setValue } = useInputEvent('') 30 | // console.log({ field, value }, user[field]) 31 | 32 | const dispatch = useDispatch() 33 | 34 | const { onEvent: onSave } = useEventFetch(async () => { 35 | // 第一步:请求更新用户信息 36 | await userUpdate({ 37 | [field]: value, 38 | }) 39 | // 第二步:拿到服务器用户信息 40 | const userInfo = await getUserInfo(user.id) 41 | // console.log(userInfo, '==userInfo==') 42 | // 第三步:用服务器拿到的数据覆盖 store 中的数据 43 | dispatch({ 44 | type: 'UPDATE_USER', 45 | payload: { user: { ...userInfo } }, 46 | }) 47 | return userInfo 48 | }, [value]) 49 | 50 | useEffect(() => { 51 | setValue(user[field]) 52 | }, [user[field]]) 53 | 54 | return ( 55 | 56 | {title} 57 |
58 | setEditFlag(true)} value={value} onChange={onInputEvent} /> 59 | {/* 输入框右侧根据编辑状态显示不同按钮 */} 60 | {editFlag ? ( 61 |
setEditFlag(false)}> 62 | 65 | 66 |
67 | ) : ( 68 |
{ 71 | setEditFlag(true) 72 | setFocus(field) 73 | }} 74 | > 75 | 79 |
80 | )} 81 |
82 |
83 | ) 84 | } 85 | 86 | export default InfoGroup 87 | -------------------------------------------------------------------------------- /src/pages/settings/InfoGroup/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | import editIcon from '../../../statics/edit.svg' 10 | 11 | export const Wrapper = styled.div` 12 | display: flex; 13 | align-items: center; 14 | width: 100%; 15 | 16 | .item-title { 17 | /* display:block; */ 18 | width: 120px; 19 | font-size: 14px; 20 | } 21 | 22 | .input-box { 23 | display: flex; 24 | justify-content: space-between; 25 | flex: 1; 26 | 27 | .input { 28 | flex: 1; 29 | font-size: 16px; 30 | color: #909090; 31 | border: none; 32 | outline: none; 33 | 34 | // 兼容不同浏览器的 placeholder 35 | ::-webkit-input-placeholder { 36 | color: #ccc; 37 | } 38 | 39 | :-moz-placeholder { 40 | color: #ccc; 41 | } 42 | 43 | ::-moz-placeholder { 44 | color: #ccc; 45 | } 46 | 47 | :-ms-input-placeholder { 48 | color: #ccc; 49 | } 50 | } 51 | 52 | .edit-box { 53 | margin-left: 12px; 54 | 55 | .edit-btn, 56 | .confirm-btn, 57 | .cancel-btn { 58 | font-size: 14px; 59 | color: #007fff; 60 | border: none; 61 | background: #fff; 62 | outline: none; 63 | cursor: pointer; 64 | 65 | .edit-icon { 66 | display: inline-block; 67 | width: 18px; 68 | height: 18px; 69 | margin-right: 7px; 70 | background: url(${editIcon}) no-repeat center/contain; 71 | vertical-align: bottom; 72 | } 73 | } 74 | 75 | .cancel-btn { 76 | color: #666; 77 | } 78 | } 79 | } 80 | ` 81 | -------------------------------------------------------------------------------- /src/pages/settings/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import useFlag from '@/lib/hooks/useFlag' 2 | import React from 'react' 3 | // import { connect } from 'react-redux'; 4 | import { Wrapper } from './style' 5 | 6 | const Navigation: React.FC = props => { 7 | // 被选中的导航栏标签(由父组件传入)第一项默认 false 第二项为 true 8 | const { flag, setFalse, setTrue } = useFlag(false) 9 | 10 | return ( 11 | 12 |
    13 |
  • 14 | 个人资料 15 |
  • 16 |
  • 17 | 修改密码 18 |
  • 19 |
20 |
21 | ) 22 | } 23 | 24 | export default Navigation 25 | -------------------------------------------------------------------------------- /src/pages/settings/Navigation/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components'; 8 | 9 | export const Wrapper = styled.div` 10 | position: fixed; 11 | top: 60px; 12 | left:0; 13 | z-index: 1; 14 | width: 100%; 15 | height: 46px; 16 | background: #fff; 17 | border-top: 1px solid #f1f1f1; 18 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 19 | 20 | .nav-list { 21 | display: flex; 22 | align-items: center; 23 | max-width: 960px; 24 | height: 100%; 25 | margin: 0 auto; 26 | 27 | .nav-item { 28 | padding: 0 12px; 29 | font-size: 14px; 30 | color: #71777c; 31 | cursor: pointer; 32 | 33 | &.profile { 34 | padding-left: 0; 35 | } 36 | 37 | &.active, 38 | :hover { 39 | color: #007fff; 40 | } 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/pages/settings/Password/index.tsx: -------------------------------------------------------------------------------- 1 | // 从 Profile 复制来的 第一版暂时没有修改密码功能 2 | 3 | import React from 'react'; 4 | // import { connect } from 'react-redux'; 5 | import { Wrapper } from './style'; 6 | 7 | // 是否处于被编辑状态 8 | const editStatus: boolean = true; 9 | // const editStatus: boolean = false; 10 | 11 | const getBtn = (status: boolean) => { 12 | return status ? ( 13 |
14 | 15 | 16 |
17 | ) : ( 18 |
19 | 23 |
24 | ); 25 | }; 26 | 27 | const Password: React.FC = (props) => { 28 | return ( 29 | 30 |
31 | Password 32 |

个人资料

33 |
    34 |
  • 35 | 头像 36 |
    37 |
    38 |
    39 |
    支持 jpg、png 格式大小 5M 以内的图片
    40 | 41 |
    42 |
    43 |
  • 44 |
  • 45 | 用户名 46 |
    47 | 48 | {getBtn(editStatus)} 49 |
    50 |
  • 51 |
  • 52 | 邮箱 53 |
    54 | 55 | {getBtn(editStatus)} 56 |
    57 |
  • 58 |
  • 59 | GitHub 60 |
    61 | 62 | {getBtn(editStatus)} 63 |
    64 |
  • 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Password; 72 | -------------------------------------------------------------------------------- /src/pages/settings/Password/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components'; 8 | import avatarPic from '../../../statics/avatar.png'; 9 | import editIcon from '../../../statics/edit.svg'; 10 | 11 | export const Wrapper = styled.div` 12 | width: 696px; 13 | padding: 32px 48px 84px; 14 | margin: 128px 0 24px; 15 | background: #fff; 16 | border-radius: 2px; 17 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 18 | 19 | .title { 20 | margin: 16px 0; 21 | font-size: 24px; 22 | font-weight: 700; 23 | color: #333; 24 | } 25 | 26 | .item { 27 | display: flex; 28 | align-items: center; 29 | padding: 24px 0; 30 | border-top: 1px solid #f1f1f1; 31 | 32 | :first-child { 33 | padding: 12px 0; 34 | } 35 | 36 | :last-child { 37 | border-bottom: 1px solid #f1f1f1; 38 | } 39 | 40 | .item-title { 41 | /* display:block; */ 42 | width: 120px; 43 | font-size: 14px; 44 | } 45 | 46 | .avatar-uploader { 47 | display: flex; 48 | 49 | .avatar { 50 | width: 72px; 51 | height: 72px; 52 | margin-right: 12px; 53 | background: #eee url(${avatarPic}) no-repeat center/cover; 54 | } 55 | 56 | .upload { 57 | margin-left: 12px; 58 | 59 | .hint { 60 | margin-bottom: 18px; 61 | color: #909090; 62 | font-size: 12px; 63 | } 64 | 65 | .upload-btn { 66 | padding: 6px 16px; 67 | color: #fff; 68 | background: #007fff; 69 | border-radius: 2px; 70 | border: none; 71 | outline: none; 72 | cursor: pointer; 73 | } 74 | } 75 | } 76 | 77 | .input-box { 78 | display: flex; 79 | justify-content: space-between; 80 | flex: 1; 81 | 82 | .input { 83 | flex: 1; 84 | font-size: 16px; 85 | color: #909090; 86 | border: none; 87 | outline: none; 88 | 89 | // 兼容不同浏览器的 placeholder 90 | ::-webkit-input-placeholder { 91 | color: #ccc; 92 | } 93 | 94 | :-moz-placeholder { 95 | color: #ccc; 96 | } 97 | 98 | ::-moz-placeholder { 99 | color: #ccc; 100 | } 101 | 102 | :-ms-input-placeholder { 103 | color: #ccc; 104 | } 105 | } 106 | 107 | .edit-box { 108 | margin-left: 12px; 109 | 110 | .edit-btn, 111 | .confirm-btn, 112 | .cancel-btn { 113 | font-size: 14px; 114 | color: #007fff; 115 | border: none; 116 | background: #fff; 117 | outline: none; 118 | cursor: pointer; 119 | 120 | .edit-icon { 121 | display: inline-block; 122 | width: 18px; 123 | height: 18px; 124 | margin-right: 7px; 125 | background: url(${editIcon}) no-repeat center/contain; 126 | vertical-align: bottom; 127 | } 128 | } 129 | 130 | .cancel-btn { 131 | color: #666; 132 | } 133 | } 134 | } 135 | } 136 | `; 137 | -------------------------------------------------------------------------------- /src/pages/settings/Profile/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React, { useCallback } from 'react' 4 | import { Wrapper } from './style' 5 | import InfoGroup from '../InfoGroup' 6 | import { useSelector, useDispatch } from '@/redux/context' 7 | import { uploadFile } from '@/Api/file' 8 | import avatarPic from '../../../statics/avatar.png' 9 | import { userUpdate, getUserInfo } from '@/Api/user' 10 | import { message } from 'antd' 11 | 12 | const infoList: Array<{ 13 | title: string 14 | field: string 15 | placeholder?: string 16 | }> = [ 17 | { title: '用户名', field: 'username' }, 18 | { title: '职位', field: 'jobTitle' }, 19 | { title: '公司', field: 'company' }, 20 | { 21 | title: '个人介绍', 22 | field: 'selfDescription', 23 | placeholder: '填写职业技能、擅长的事情、喜欢的事情等', 24 | }, 25 | { title: '个人主页', field: 'blogAddress' }, 26 | ] 27 | 28 | const Profile: React.FC = props => { 29 | const dispatch = useDispatch() 30 | 31 | // 拿到用户头像 没有设置则使用默认头像 32 | const { 33 | user: { avatarLarge = avatarPic, id = '' }, 34 | } = useSelector() 35 | 36 | const onUpload = useCallback(async (e: any) => { 37 | const formData = new FormData() 38 | const file = e.target.files[0] 39 | // console.log(file) 40 | // 上传文件不大于 5M 41 | if (file.size > 5 * Math.pow(1024, 2)) { 42 | return message.warning('图片过大') 43 | } 44 | formData.append('file', file) 45 | // console.log(formData.get('file')) 46 | // 上传文件并拿到 url 47 | const { url } = await uploadFile(formData) 48 | // console.log(url, '==url==') 49 | // 更新用户信息中的头像路径 50 | await userUpdate({ 51 | avatarLarge: url, 52 | }) 53 | // 拿到服务器用户信息 54 | const userInfo = await getUserInfo(id) 55 | // console.log(userInfo, '==userInfo==') 56 | // 用服务器数据覆盖 store 的用户信息(本地与服务器同步) 57 | dispatch({ 58 | type: 'UPDATE_USER', 59 | payload: { user: { ...userInfo } }, 60 | }) 61 | }, []) 62 | 63 | return ( 64 | 65 |
66 |

个人资料

67 |
    68 |
  • 69 | 头像 70 |
    71 |
    72 |
    73 |
    支持 jpg、png 格式大小 5M 以内的图片
    74 | 75 | 76 |
    77 |
    78 |
  • 79 | {infoList.map(item => ( 80 |
  • 81 | 82 |
  • 83 | ))} 84 |
85 |
86 |
87 | ) 88 | } 89 | 90 | export default Profile 91 | -------------------------------------------------------------------------------- /src/pages/settings/Profile/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | import editIcon from '../../../statics/edit.svg' 9 | 10 | export const Wrapper = styled.div` 11 | width: 696px; 12 | padding: 32px 48px 84px; 13 | /* 加导航栏(46px)后 margin-top: 128px; */ 14 | margin: 82px 0 24px; 15 | background: #fff; 16 | border-radius: 2px; 17 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 18 | 19 | .title { 20 | margin: 16px 0; 21 | font-size: 24px; 22 | font-weight: 700; 23 | color: #333; 24 | } 25 | 26 | .setting-list { 27 | margin-bottom: 0; 28 | } 29 | 30 | .item { 31 | display: flex; 32 | align-items: center; 33 | padding: 24px 0; 34 | border-top: 1px solid #f1f1f1; 35 | 36 | :first-child { 37 | padding: 12px 0; 38 | } 39 | 40 | :last-child { 41 | border-bottom: 1px solid #f1f1f1; 42 | } 43 | 44 | .item-title { 45 | /* display:block; */ 46 | width: 120px; 47 | font-size: 14px; 48 | } 49 | 50 | .avatar-uploader { 51 | display: flex; 52 | 53 | .avatar { 54 | width: 72px; 55 | height: 72px; 56 | margin-right: 12px; 57 | background: ${({ avatarLarge }: { avatarLarge: string }) => 58 | `#eee url(${avatarLarge}) no-repeat center/cover`}; 59 | } 60 | 61 | .upload { 62 | position: relative; 63 | margin-left: 12px; 64 | 65 | .hint { 66 | margin-bottom: 18px; 67 | color: #909090; 68 | font-size: 12px; 69 | } 70 | 71 | .upload-btn { 72 | padding: 6px 16px; 73 | line-height: 1.2; 74 | font-size: 12px; 75 | color: #fff; 76 | background: #007fff; 77 | border-radius: 2px; 78 | border: none; 79 | outline: none; 80 | cursor: pointer; 81 | } 82 | 83 | .hidden-input { 84 | position: absolute; 85 | left: 0; 86 | z-index: 1; 87 | width: 80px; 88 | height: 26px; 89 | /* font-size: 0; 才能使得 cursor 生效 */ 90 | font-size: 0; 91 | cursor: pointer; 92 | opacity: 0; 93 | } 94 | } 95 | } 96 | 97 | .input-box { 98 | display: flex; 99 | justify-content: space-between; 100 | flex: 1; 101 | 102 | .input { 103 | flex: 1; 104 | font-size: 16px; 105 | color: #909090; 106 | border: none; 107 | outline: none; 108 | 109 | // 兼容不同浏览器的 placeholder 110 | ::-webkit-input-placeholder { 111 | color: #ccc; 112 | } 113 | 114 | :-moz-placeholder { 115 | color: #ccc; 116 | } 117 | 118 | ::-moz-placeholder { 119 | color: #ccc; 120 | } 121 | 122 | :-ms-input-placeholder { 123 | color: #ccc; 124 | } 125 | } 126 | 127 | .edit-box { 128 | margin-left: 12px; 129 | 130 | .edit-btn, 131 | .confirm-btn, 132 | .cancel-btn { 133 | font-size: 14px; 134 | color: #007fff; 135 | border: none; 136 | background: #fff; 137 | outline: none; 138 | cursor: pointer; 139 | 140 | .edit-icon { 141 | display: inline-block; 142 | width: 18px; 143 | height: 18px; 144 | margin-right: 7px; 145 | background: url(${editIcon}) no-repeat center/contain; 146 | vertical-align: bottom; 147 | } 148 | } 149 | 150 | .cancel-btn { 151 | color: #666; 152 | } 153 | } 154 | } 155 | } 156 | ` 157 | -------------------------------------------------------------------------------- /src/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // import Navigation from './Navigation'; 3 | import Profile from './Profile' 4 | import Password from './Password' 5 | import { Wrapper } from './style' 6 | import useAuthLogin from '@/lib/hooks/useAuthLogin' 7 | 8 | // 被选中的导航栏标签序号(从 0 开始)默认为 0 (暂时写死) 9 | const activeTag: number = 0 10 | 11 | const getView = (tag: number) => { 12 | switch (tag) { 13 | case 0: 14 | return 15 | case 1: 16 | return 17 | } 18 | } 19 | 20 | const Settings: React.FC = (props) => { 21 | useAuthLogin() 22 | return ( 23 | 24 | {/* 第一版只做个人资料页面 不显示导航栏 若要加导航栏 记得改 Profile 组件的 margin-top */} 25 | {/* */} 26 | {getView(activeTag)} 27 | 28 | ) 29 | } 30 | 31 | export default Settings 32 | -------------------------------------------------------------------------------- /src/pages/settings/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | max-width: 960px; 11 | margin: 0 auto; 12 | ` 13 | -------------------------------------------------------------------------------- /src/pages/user/FallowBlock/index.tsx: -------------------------------------------------------------------------------- 1 | // 详情页 右侧 作者简介卡片 2 | 3 | import React from 'react' 4 | import { Link, useParams } from 'react-router-dom' 5 | 6 | import { Wrapper } from './style' 7 | 8 | interface IProps { 9 | user: { 10 | followersCount: number 11 | followingCount: number 12 | } 13 | } 14 | 15 | const FallowBlock: React.FC = ({ user: { followersCount, followingCount } = {} }) => { 16 | const { id = '' } = useParams() as any 17 | 18 | return ( 19 | 20 | 21 |
关注了
22 |
{followingCount}
23 | 24 | 25 |
关注者
26 |
{followersCount}
27 | 28 |
29 | ) 30 | } 31 | 32 | export default FallowBlock 33 | -------------------------------------------------------------------------------- /src/pages/user/FallowBlock/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | position: relative; 6 | width: 240px; 7 | margin-bottom: 12px; 8 | background: #fff; 9 | border-radius: 2px; 10 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 11 | 12 | .follow-item { 13 | flex: 1 1 auto; 14 | padding: 16px 0; 15 | text-align: center; 16 | font-size: 16px; 17 | font-weight: 500; 18 | color: #31445b; 19 | cursor: pointer; 20 | 21 | :hover { 22 | color: #5a697c; 23 | } 24 | 25 | .item-count { 26 | margin-top: 6px; 27 | font-size: 15px; 28 | font-weight: 600; 29 | } 30 | } 31 | 32 | :after { 33 | content: ''; 34 | position: absolute; 35 | top: 50%; 36 | left: 50%; 37 | width: 1px; 38 | height: 50%; 39 | background-color: #f3f3f4; 40 | transform: translate(-50%, -50%); 41 | } 42 | ` 43 | -------------------------------------------------------------------------------- /src/pages/user/InfoBlock/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | // 详情页 右侧 作者简介卡片 4 | 5 | import React, { useCallback } from 'react' 6 | import { useHistory } from 'react-router' 7 | 8 | import { addFollow, deleteFollow } from '@/Api/follow' 9 | import { isFollowing } from '@/Api/user' 10 | import useFetch from '@/lib/hooks/useFetch' 11 | import useToggle from '@/lib/hooks/useToggle' 12 | import { useSelector } from '@/redux/context' 13 | 14 | import { Wrapper } from './style' 15 | 16 | interface IProps { 17 | user: { 18 | id: string 19 | avatarLarge: string 20 | username: string 21 | jobTitle: string 22 | company: string 23 | selfDescription: string 24 | } 25 | } 26 | 27 | const InfoBlock: React.FC = ({ user: { id = '', avatarLarge = '', username = '', jobTitle = '', company = '', selfDescription = '' } = {} }) => { 28 | // 因为要保证所有用户的都能拿到,不应该从 store 中拿 29 | // const { user: { avatarLarge = avatarPic } } = useSelector() 30 | const history = useHistory() 31 | const onSetting = useCallback(async () => { 32 | history.push('/settings') 33 | }, []) 34 | 35 | // 拿到当前登录用户的 id 36 | const { 37 | user: { id: loginId }, 38 | } = useSelector() 39 | 40 | const { flag, onToggle, setFlag } = useToggle(false) 41 | 42 | const onFollow = useCallback(async () => { 43 | // if (!id || !loginId) { 44 | // return 45 | // } 46 | flag ? await deleteFollow(id) : await addFollow(id) 47 | onToggle() 48 | }, [flag, id]) 49 | 50 | useFetch(async () => { 51 | if (!id || !loginId) { 52 | return 53 | } 54 | // id 是查看用户, followerId是登录用户 55 | const rs = await isFollowing(id, loginId) 56 | setFlag(rs) 57 | }, [id]) 58 | 59 | return ( 60 | 61 |
62 |
63 |
64 | {/* 第一行:用户名 */} 65 | {username} 66 | 67 | {/* 第二行:职位和公司 */} 68 | {/* 都沒填 */} 69 | {!jobTitle && !company ? ( 70 | loginId === id ? ( 71 | 72 | + 你从事什么职业? 73 | 74 | ) : null 75 | ) : jobTitle && company ? ( 76 | // 都填了 77 |
78 | 79 | {jobTitle} 80 | 81 | {company} 82 |
83 | ) : ( 84 | // 只填了一個 85 |
86 | 87 | {jobTitle ? jobTitle : company} 88 |
89 | )} 90 | 91 | {/* 第三行:个人介绍 */} 92 | {!selfDescription ? ( 93 | loginId === id ? ( 94 | 95 | + 你的信仰是什么? 96 | 97 | ) : null 98 | ) : ( 99 |
100 | 101 | {selfDescription} 102 |
103 | )} 104 |
105 | {loginId === id ? ( 106 | 109 | ) : flag ? ( 110 | 113 | ) : ( 114 | 117 | )} 118 |
119 | 120 | ) 121 | } 122 | 123 | export default InfoBlock 124 | -------------------------------------------------------------------------------- /src/pages/user/InfoBlock/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | padding: 30px; 11 | background: #fff; 12 | border-radius: 2px; 13 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 14 | 15 | .user-info { 16 | display: flex; 17 | 18 | .avatar { 19 | flex: 0 0 auto; 20 | margin-right: 29px; 21 | width: 90px; 22 | height: 90px; 23 | border-radius: 50%; 24 | /* background: #f9f9f9 no-repeat center/cover; */ 25 | background: ${({ avatarLarge }: { avatarLarge: string }) => 26 | `#f9f9f9 url(${avatarLarge}) no-repeat center/cover`}; 27 | } 28 | 29 | .info-box { 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | flex: 1 1 auto; 34 | margin-right: 24px; 35 | 36 | .user-name { 37 | font-size: 26px; 38 | font-weight: 600; 39 | color: #000; 40 | } 41 | 42 | .user-position, 43 | .user-intro { 44 | margin-top: 5px; 45 | font-size: 15px; 46 | color: #4a68ad; 47 | cursor: pointer; 48 | 49 | &.filled { 50 | display: flex; 51 | align-items: center; 52 | font-size: 14px; 53 | color: #72777b; 54 | overflow: hidden; 55 | cursor: auto; 56 | 57 | .icon { 58 | width: 21px; 59 | height: 21px; 60 | margin-right: 7px; 61 | background: #eee; 62 | } 63 | 64 | .split { 65 | margin: 0 7px; 66 | width: 1px; 67 | height: 10px; 68 | background-color: #72777b; 69 | opacity: 0.5; 70 | } 71 | } 72 | } 73 | 74 | .user-position { 75 | margin-top: 12px; 76 | } 77 | 78 | .user-intro { 79 | margin-top: 5px; 80 | } 81 | } 82 | 83 | .setting-btn { 84 | flex: 0 0 auto; 85 | align-self: flex-end; 86 | width: 118px; 87 | height: 34px; 88 | font-size: 16px; 89 | font-weight: 500; 90 | color: #3780f7; 91 | background: #fff; 92 | border: 1px solid; 93 | border-radius: 4px; 94 | cursor: pointer; 95 | 96 | :hover { 97 | opacity: 0.8; 98 | } 99 | } 100 | 101 | .follow-btn { 102 | flex: 0 0 auto; 103 | align-self: flex-end; 104 | width: 106px; 105 | height: 32px; 106 | font-size: 16px; 107 | font-weight: 500; 108 | color: #6cbd45; 109 | background: #fff; 110 | border: 1px solid; 111 | border-radius: 4px; 112 | outline: none; 113 | cursor: pointer; 114 | 115 | :hover { 116 | opacity: 0.8; 117 | } 118 | 119 | &.followed { 120 | color: #fff; 121 | background-color: #6cbd45; 122 | border-color: #6cbd45; 123 | } 124 | } 125 | } 126 | ` 127 | -------------------------------------------------------------------------------- /src/pages/user/ListBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useParams } from 'react-router-dom' 3 | 4 | import ListBodyFollow from '../ListBodyFollow' 5 | import ListBodyLikes from '../ListBodyLikes' 6 | import ListBodyPosts from '../ListBodyPosts' 7 | import ListHeader from '../ListHeader' 8 | import { Wrapper } from './style' 9 | 10 | const ListBlock: React.FC = props => { 11 | const { item = '' } = useParams() as any 12 | 13 | return ( 14 | 15 | 16 | {item === 'following' || item === 'followers' ? : item === 'likes' ? : } 17 | 18 | ) 19 | } 20 | 21 | export default ListBlock 22 | -------------------------------------------------------------------------------- /src/pages/user/ListBlock/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div`margin-top: 12px;` 10 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyFollow/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | background: #fff; 5 | 6 | .list-item { 7 | :not(:last-child) { 8 | border-bottom: 1px solid hsla(0, 0%, 59.2%, 0.1); 9 | } 10 | 11 | .user-link { 12 | display: flex; 13 | align-items: center; 14 | padding: 6px 29px; 15 | min-height: 84px; 16 | 17 | .avatar { 18 | flex: 0 0 auto; 19 | margin-right: 20px; 20 | width: 45px; 21 | height: 45px; 22 | border-radius: 50%; 23 | background: #eee; 24 | } 25 | 26 | .info-box { 27 | flex: 1 1 auto; 28 | min-width: 0; 29 | 30 | .username { 31 | font-size: 16px; 32 | font-weight: 600; 33 | color: #2e3135; 34 | } 35 | 36 | .detail { 37 | margin-top: 7px; 38 | font-size: 12px; 39 | font-weight: 500; 40 | color: #b9c0c8; 41 | white-space: nowrap; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | } 45 | } 46 | 47 | .follow-btn { 48 | outline: none; 49 | flex: 0 0 auto; 50 | margin-left: 12px; 51 | padding: 0; 52 | width: 90px; 53 | height: 30px; 54 | font-size: 12px; 55 | color: #92c452; 56 | background-color: #fff; 57 | border: 1px solid #92c452; 58 | border-radius: 2px; 59 | 60 | :hover { 61 | opacity: 0.8; 62 | } 63 | 64 | &.followed { 65 | color: #fff; 66 | background-color: #92c452; 67 | } 68 | } 69 | } 70 | } 71 | ` 72 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyLike/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | .list-item { 11 | position: relative; 12 | background: #fff; 13 | border-bottom: 1px solid rgba(178, 186, 194, 0.15); 14 | 15 | .content { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | padding: 18px 28px; 20 | 21 | .info-box { 22 | flex: 1 1 auto; 23 | width: 568px; 24 | height: 100%; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | 29 | .info-row { 30 | display: flex; 31 | color: #b2bac2; 32 | 33 | .info-item { 34 | :not(:last-child)::after { 35 | content: '·'; 36 | color: #b2bac2; 37 | margin: 0 5px; 38 | } 39 | 40 | &.column { 41 | color: #b71ed7; 42 | font-weight: 500; 43 | } 44 | 45 | .user-link, 46 | .tag-link { 47 | color: #b2bac2; 48 | } 49 | } 50 | .row { 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .little-box { 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | height: 26px; 60 | padding: 0 10px; 61 | color: #b2bac2; 62 | font-weight: 700; 63 | border: 1px solid #edeeef; 64 | border-radius: 1px; 65 | 66 | &.comment { 67 | margin-left: -1px; 68 | } 69 | 70 | .count { 71 | margin-left: 3px; 72 | } 73 | } 74 | } 75 | 76 | .title { 77 | margin: 6px 0 12px; 78 | white-space: nowrap; 79 | overflow: hidden; 80 | text-overflow: ellipsis; 81 | 82 | .title-link { 83 | color: #2e3135; 84 | font-size: 17px; 85 | font-weight: 600; 86 | 87 | ::visited { 88 | color: #909090; 89 | } 90 | } 91 | } 92 | 93 | .abstract { 94 | margin-bottom: 12px; 95 | font-size: 13px; 96 | color: #5b6169; 97 | /* color: #909090; */ 98 | white-space: nowrap; 99 | overflow: hidden; 100 | text-overflow: ellipsis; 101 | } 102 | 103 | .action-row { 104 | display: flex; 105 | justify-content: space-between; 106 | align-items: center; 107 | } 108 | } 109 | 110 | .thumb { 111 | flex: 0 0 auto; 112 | margin-left: 24px; 113 | width: 60px; 114 | height: 60px; 115 | border-radius: 2px; 116 | box-sizing: content-box; 117 | } 118 | } 119 | } 120 | ` 121 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyLikes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useParams } from 'react-router-dom' 3 | 4 | import { getUserLikes } from '@/Api/user' 5 | import useFetch from '@/lib/hooks/useFetch' 6 | 7 | import { ArticleEntity } from '@/modal/entities/article.entity' 8 | 9 | import { useDispatch, useSelector } from '@/redux/context' 10 | 11 | import { Wrapper } from './style' 12 | import ListBodyLike from '../ListBodyLike' 13 | 14 | interface IProps extends ArticleEntity {} 15 | 16 | const ListBodyLikes: React.FC = () => { 17 | const { id = '' } = useParams() as any 18 | 19 | const dispatch = useDispatch() 20 | // 根据 id 拿用户点赞的文章列表 21 | useFetch(async () => { 22 | const rs = await getUserLikes(id) 23 | 24 | const list = (rs && rs.edges) || [] 25 | 26 | dispatch({ 27 | type: 'CHANGE_LIKE_LIST', 28 | payload: { likeList: [...list] }, 29 | }) 30 | return list 31 | }, [id]) 32 | 33 | const { likeList = [] } = useSelector() 34 | 35 | // const toPost = () => { 36 | // window.open(`/post/${id}`, '_blank') 37 | // } 38 | 39 | return ( 40 | 41 | {/*
    {articleList.map((item: ArticleEntity) =>
    )}
*/} 42 | 43 |
    44 | {likeList.map((item: ArticleEntity) => ( 45 | 46 | //
  • 47 | //
    48 | //
    49 | //
    50 | // 60 | 61 | //
    62 | // 68 | //
    69 | 70 | // {/* 摘抄 */} 71 | // {/* 去掉 html 中的标签 */} 72 | //
    73 | // 78 | //
    79 | 80 | //
    81 | // 95 | //
    96 | //
    97 | 98 | //
    105 | //
    106 | //
    107 | //
  • 108 | ))} 109 |
110 |
111 | ) 112 | } 113 | 114 | export default ListBodyLikes 115 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyLikes/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | .list-item { 11 | position: relative; 12 | background: #fff; 13 | border-bottom: 1px solid rgba(178, 186, 194, 0.15); 14 | 15 | .content { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | padding: 18px 28px; 20 | 21 | .info-box { 22 | flex: 1 1 auto; 23 | width: 568px; 24 | height: 100%; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | 29 | .info-row { 30 | display: flex; 31 | color: #b2bac2; 32 | 33 | .info-item { 34 | :not(:last-child)::after { 35 | content: '·'; 36 | color: #b2bac2; 37 | margin: 0 5px; 38 | } 39 | 40 | &.column { 41 | color: #b71ed7; 42 | font-weight: 500; 43 | } 44 | 45 | .user-link, 46 | .tag-link { 47 | color: #b2bac2; 48 | } 49 | } 50 | 51 | .little-box { 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | height: 26px; 56 | padding: 0 10px; 57 | color: #b2bac2; 58 | font-weight: 700; 59 | border: 1px solid #edeeef; 60 | border-radius: 1px; 61 | 62 | &.comment { 63 | margin-left: -1px; 64 | } 65 | 66 | .count { 67 | margin-left: 3px; 68 | } 69 | } 70 | } 71 | 72 | .title { 73 | margin: 6px 0 12px; 74 | white-space: nowrap; 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | 78 | .title-link { 79 | color: #2e3135; 80 | font-size: 17px; 81 | font-weight: 600; 82 | 83 | ::visited { 84 | color: #909090; 85 | } 86 | } 87 | } 88 | 89 | .abstract { 90 | margin-bottom: 12px; 91 | font-size: 13px; 92 | color: #5b6169; 93 | /* color: #909090; */ 94 | white-space: nowrap; 95 | overflow: hidden; 96 | text-overflow: ellipsis; 97 | } 98 | 99 | .action-row { 100 | display: flex; 101 | justify-content: space-between; 102 | align-items: center; 103 | } 104 | } 105 | 106 | .thumb { 107 | flex: 0 0 auto; 108 | margin-left: 24px; 109 | width: 60px; 110 | height: 60px; 111 | border-radius: 2px; 112 | box-sizing: content-box; 113 | } 114 | } 115 | } 116 | ` 117 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyPost/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import dotPic from '../../../statics/dot.svg' 3 | import dotHoverPic from '../../../statics/dot-hover.svg' 4 | 5 | export const Wrapper = styled.div` 6 | background: #fff; 7 | 8 | .list-item { 9 | position: relative; 10 | padding: 30px; 11 | border-bottom: 1px solid hsla(0, 0%, 59.2%, .1); 12 | 13 | /* 第一行 */ 14 | .user-info-row { 15 | display: flex; 16 | align-items: center; 17 | padding: 4px 0 16px; 18 | font-size: 14px; 19 | color: #8b8b8b; 20 | 21 | .user-info { 22 | display: flex; 23 | align-items: center; 24 | font-size: 14px; 25 | color: #8b8b8b; 26 | 27 | .avatar { 28 | margin-right: 12px; 29 | width: 30px; 30 | height: 30px; 31 | border-radius: 50%; 32 | background: #eee; 33 | } 34 | 35 | .author-name { 36 | ::after { 37 | content: "·"; 38 | margin: 0 6px; 39 | } 40 | } 41 | } 42 | } 43 | 44 | /* 第二行 */ 45 | .thumb-row { 46 | margin-bottom: 24px; 47 | 48 | .cover-img { 49 | width: 100%; 50 | height: 216px; 51 | border-radius: 3px; 52 | } 53 | } 54 | 55 | /* 第三行 */ 56 | .abstract-row { 57 | display: flex; 58 | flex-direction: column; 59 | align-items: flex-start; 60 | cursor: pointer; 61 | 62 | .title { 63 | width: 100%; 64 | margin-bottom: 10px; 65 | font-size: 24px; 66 | font-weight: 600; 67 | color: #000; 68 | word-break: break-word; 69 | word-wrap: break-word; 70 | letter-spacing: 0.5px; 71 | 72 | :visited { 73 | color: #909090; 74 | } 75 | 76 | :hover { 77 | color: #007fff; 78 | } 79 | } 80 | 81 | .abstract { 82 | width: 100%; 83 | max-height: 94px; 84 | font-size: 16px; 85 | line-height: 1.5; 86 | color: #8b8b8b; 87 | letter-spacing: 0.5px; 88 | display: -webkit-box; 89 | overflow: hidden; 90 | text-overflow: ellipsis; 91 | -webkit-line-clamp: 4; 92 | -webkit-box-orient: vertical; 93 | 94 | &.shot { 95 | text-overflow: clip; 96 | -webkit-line-clamp: 1; 97 | -webkit-box-orient: vertical; 98 | } 99 | } 100 | } 101 | 102 | /* 第四行 */ 103 | .action-row { 104 | display: flex; 105 | justify-content: space-between; 106 | align-items: center; 107 | margin-top: 18px; 108 | 109 | .action-left { 110 | display: flex; 111 | 112 | .action { 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | height: 25px; 117 | padding: 0 12px; 118 | color: #b2bac2; 119 | font-weight: 700; 120 | white-space: nowrap; 121 | border: 1px solid #f1f1f1; 122 | border-radius: 1px; 123 | cursor: pointer; 124 | transition: color, background 0.3s; 125 | 126 | &.comment { 127 | margin-left: -1px; 128 | } 129 | 130 | :hover { 131 | color: #9f9f9f; 132 | background: hsla(0, 0%, 94.5%, 0.3); 133 | } 134 | 135 | .count { 136 | margin-left: 3px; 137 | } 138 | } 139 | } 140 | 141 | .action-right { 142 | display: flex; 143 | align-items: center; 144 | font-size: 12px; 145 | color: rgba(24, 37, 50, .3); 146 | 147 | .read-action, 148 | .more-action { 149 | margin-left: 24px; 150 | } 151 | 152 | .read-action:hover { 153 | color: #8b8b8b; 154 | } 155 | 156 | .more-action { 157 | .more-icon { 158 | display: block; 159 | width: 24px; 160 | height: 24px; 161 | background: #fff url(${dotPic}) no-repeat center/contain; 162 | background-size: 60%; 163 | cursor: pointer; 164 | 165 | :hover { 166 | background: #fff url(${dotHoverPic}) no-repeat center/contain; 167 | background-size: 60%; 168 | } 169 | } 170 | 171 | .menu { 172 | position: absolute; 173 | z-index: 100; 174 | bottom: 60px; 175 | right: 0; 176 | width: 120px; 177 | padding: 12px 0; 178 | background: #fff; 179 | border: 1px solid #f1f1f1; 180 | border-radius: 2px; 181 | box-shadow: 0 1px 2px 1px hsla(0, 0%, 94.5%, .5); 182 | list-style: none; 183 | 184 | .menu-item { 185 | display: flex; 186 | padding: 7px 24px; 187 | font-size: 12px; 188 | color: #8b8b8b; 189 | cursor: pointer; 190 | } 191 | 192 | .menu-item:hover { 193 | background: #f8f9fa; 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | ` 201 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyPosts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useParams } from 'react-router-dom' 3 | 4 | import { getUserArticles } from '@/Api/user' 5 | import useFetch from '@/lib/hooks/useFetch' 6 | 7 | import { ArticleEntity } from '@/modal/entities/article.entity' 8 | 9 | import { useDispatch, useSelector } from '@/redux/context' 10 | 11 | import ListBodyPost from '../ListBodyPost' 12 | import { Wrapper } from './style' 13 | 14 | interface IProps extends ArticleEntity {} 15 | 16 | const ListBodyPosts: React.FC = () => { 17 | const { id = '' } = useParams() 18 | 19 | const dispatch = useDispatch() 20 | // 根据 id 拿用户文章列表 21 | useFetch(async () => { 22 | const rs = await getUserArticles(id) 23 | const list = (rs && rs.edges) || [] 24 | dispatch({ 25 | type: 'CHANGE_ARTICLE_LIST', 26 | payload: { articleList: [...list] }, 27 | }) 28 | return list 29 | }, [id]) 30 | 31 | const { articleList = [] } = useSelector() 32 | 33 | return ( 34 | 35 |
    36 | {articleList.map((item: ArticleEntity) => ( 37 | 38 | ))} 39 |
40 |
41 | ) 42 | } 43 | 44 | export default ListBodyPosts 45 | -------------------------------------------------------------------------------- /src/pages/user/ListBodyPosts/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div`background: #fff;` 4 | -------------------------------------------------------------------------------- /src/pages/user/ListHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useParams } from 'react-router-dom' 3 | 4 | import { getUserArticles, getUserLikes } from '@/Api/user' 5 | import useFetch from '@/lib/hooks/useFetch' 6 | 7 | import { useDispatch, useSelector } from '@/redux/context' 8 | 9 | import { Wrapper } from './style' 10 | 11 | const ListHeader: React.FC = () => { 12 | const { id = '', item = '' } = useParams() as any 13 | 14 | const dispatch = useDispatch() 15 | // 分页部分的数据统一在 header 中拿到(因爲 header 一定會渲染),再放入 store 中供分页组件使用 16 | // 根据 id 拿用户文章列表 17 | useFetch(async () => { 18 | const rs = await getUserArticles(id) 19 | const list = (rs && rs.edges) || [] 20 | dispatch({ 21 | type: 'CHANGE_ARTICLE_LIST', 22 | payload: { articleList: [...list] }, 23 | }) 24 | return list 25 | }, [id]) 26 | 27 | // 根据 id 拿用户点赞的文章列表 28 | useFetch(async () => { 29 | const rs = await getUserLikes(id) 30 | const list = (rs && rs.edges) || [] 31 | dispatch({ 32 | type: 'CHANGE_LIKE_LIST', 33 | payload: { likeList: [...list] }, 34 | }) 35 | return list 36 | }, [id]) 37 | 38 | const { likeList = [], articleList = [] } = useSelector() 39 | 40 | return ( 41 | 42 |
43 | {/* 标题栏 */} 44 | 66 | 67 | {/* 子标题栏 */} 68 | {item === 'posts' || item === '' ? ( 69 |
70 |
专栏
71 |
72 | ) : item === 'following' ? ( 73 |
74 |
关注
75 |
76 | 77 | 关注了 78 | 79 | 80 | 关注者 81 | 82 |
83 |
84 | ) : item === 'followers' ? ( 85 |
86 |
关注
87 |
88 | 89 | 关注了 90 | 91 | 92 | 关注者 93 | 94 |
95 |
96 | ) : item === 'likes' ? ( 97 |
98 |
赞过的
99 |
100 | ) : null} 101 |
102 |
103 | ) 104 | } 105 | 106 | export default ListHeader 107 | -------------------------------------------------------------------------------- /src/pages/user/ListHeader/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | .list-header { 11 | .nav-list { 12 | display: flex; 13 | height: 50px; 14 | border-radius: 2px 2px 0 0; 15 | border-bottom: 1px solid #ebebeb; 16 | background: #fff; 17 | box-sizing: content-box; 18 | 19 | .nav-item { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | width: 90px; 24 | height: 50px; 25 | font-size: 16px; 26 | font-weight: 500; 27 | color: #31445b; 28 | 29 | &.active { 30 | box-shadow: inset 0 -2px 0 #3780f7; 31 | } 32 | 33 | .item-count { 34 | margin-left: 5px; 35 | font-size: 15px; 36 | color: #b2bac2; 37 | } 38 | } 39 | } 40 | 41 | .sub-header { 42 | display: flex; 43 | align-items: center; 44 | flex-wrap: wrap; 45 | height: 50px; 46 | padding: 0 29px; 47 | white-space: nowrap; 48 | background: #fff; 49 | border-bottom: 1px solid rgba(230, 230, 231, .5); 50 | 51 | .sub-header-left { 52 | margin-right: 12px; 53 | color: #000; 54 | font-size: 15px; 55 | font-weight: 600; 56 | } 57 | 58 | .sub-header-right { 59 | margin-left: auto; 60 | 61 | .following, 62 | .followers { 63 | font-size: 14px; 64 | color: #72777b; 65 | } 66 | 67 | .following { 68 | position: relative; 69 | margin-right: 24px; 70 | 71 | ::after { 72 | content: ""; 73 | position: absolute; 74 | top: 50%; 75 | right: -12px; 76 | margin-top: -6px; 77 | width: 1px; 78 | height: 12px; 79 | background-color: #b2bac2; 80 | opacity: 0.5; 81 | } 82 | } 83 | 84 | .active { 85 | color: #000; 86 | } 87 | } 88 | } 89 | } 90 | ` 91 | -------------------------------------------------------------------------------- /src/pages/user/MoreBLock/index.tsx: -------------------------------------------------------------------------------- 1 | // 详情页 右侧 作者简介卡片 2 | 3 | import React from 'react' 4 | 5 | import { Wrapper } from './style' 6 | 7 | interface IProps { 8 | user: { 9 | create_at: string 10 | } 11 | } 12 | 13 | const formatDate = (milliseconds: string) => { 14 | const data = new Date(milliseconds) 15 | const year = data.getFullYear() 16 | const month = data.getMonth() + 1 17 | const day = data.getDate() 18 | return year + '-' + month + '-' + day 19 | } 20 | 21 | const MoreBLock: React.FC = ({ user: { create_at = '' } }) => { 22 | // console.log('create_at', create_at) 23 | return ( 24 | 25 |
26 |
加入于
27 |
28 | 29 |
30 |
31 |
32 | ) 33 | } 34 | 35 | export default MoreBLock 36 | -------------------------------------------------------------------------------- /src/pages/user/MoreBLock/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | width: 240px; 11 | 12 | .more-item { 13 | display: flex; 14 | padding: 15px 5px; 15 | font-size: 15px; 16 | color: #000; 17 | border-top: 1px solid rgba(230, 230, 231, .5); 18 | 19 | :last-child { 20 | border-bottom: 1px solid rgba(230, 230, 231, .5); 21 | } 22 | 23 | .item-title { 24 | margin-right: auto; 25 | } 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /src/pages/user/StatBlock/index.tsx: -------------------------------------------------------------------------------- 1 | // 详情页 右侧 作者简介卡片 2 | 3 | import React from 'react' 4 | 5 | import { Wrapper } from './style' 6 | 7 | interface IProps { 8 | user: { 9 | likedCount: number 10 | viewCount: number 11 | } 12 | } 13 | 14 | // 每三位数字逗号分隔 15 | export const toThousands = (number: string | number) => { 16 | let result = '' 17 | let counter = 0 18 | const num = (number || 0).toString() 19 | for (let i = num.length - 1; i >= 0; i--) { 20 | counter++ 21 | result = num.charAt(i) + result 22 | // console.log('charat res' + result) 23 | if (!(counter % 3) && i !== 0) { 24 | result = ',' + result 25 | } 26 | } 27 | return result 28 | } 29 | 30 | const StatBlock: React.FC = ({ user: { likedCount, viewCount } = {} }) => { 31 | return ( 32 | 33 |
个人成就
34 |
35 |
36 | 37 |
38 | 获得点赞 39 | {toThousands(likedCount || 0)} 40 |
41 |
42 |
43 | 44 |
45 | 文章被阅读 46 | {toThousands(viewCount || 0)} 47 |
48 |
49 |
50 |
51 | ) 52 | } 53 | 54 | export default StatBlock 55 | -------------------------------------------------------------------------------- /src/pages/user/StatBlock/style.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | width: 240px; 5 | margin-bottom: 12px; 6 | background: #fff; 7 | border-radius: 2px; 8 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 9 | 10 | .block-title { 11 | padding: 16px; 12 | color: #31445b; 13 | font-size: 16px; 14 | font-weight: 600; 15 | border-bottom: 1px solid rgba(230, 230, 231, .5); 16 | } 17 | 18 | .block-body { 19 | padding: 16px; 20 | font-size: 15px; 21 | color: #000; 22 | 23 | .stat-item { 24 | display: flex; 25 | align-items: center; 26 | 27 | :not(:last-child) { 28 | margin-bottom: 10px; 29 | } 30 | 31 | .icon { 32 | margin-right: 14px; 33 | width: 25px; 34 | height: 25px; 35 | background: #eee; 36 | border-radius: 25%; 37 | } 38 | 39 | .count { 40 | margin: 0 5px; 41 | font-weight: 500; 42 | } 43 | } 44 | } 45 | ` 46 | -------------------------------------------------------------------------------- /src/pages/user/index.tsx: -------------------------------------------------------------------------------- 1 | import { BackTop } from 'antd' 2 | import React from 'react' 3 | import { useParams } from 'react-router' 4 | 5 | import { getUserInfo } from '@/Api/user' 6 | import Advertising from '@/components/Advertising' 7 | import useFetch from '@/lib/hooks/useFetch' 8 | 9 | import { useDispatch, useSelector } from '@/redux/context' 10 | 11 | import FallowBlock from './FallowBlock' 12 | import InfoBlock from './InfoBlock' 13 | import ListBlock from './ListBlock' 14 | import MoreBLock from './MoreBLock' 15 | import StatBlock from './StatBlock' 16 | import { Wrapper } from './style' 17 | 18 | const User: React.FC = props => { 19 | const { id = '' } = useParams() 20 | 21 | const dispatch = useDispatch() 22 | const { checkUser: info = {} } = useSelector() 23 | 24 | useFetch(async () => { 25 | const userInfo = await getUserInfo(id) 26 | 27 | dispatch({ 28 | type: 'UPDATE_CHECK_USER', 29 | payload: { checkUser: userInfo }, 30 | }) 31 | return userInfo 32 | }, [id]) 33 | 34 | return ( 35 | 36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 | 49 |
50 | ) 51 | } 52 | 53 | export default User 54 | -------------------------------------------------------------------------------- /src/pages/user/style.ts: -------------------------------------------------------------------------------- 1 | // import styled from 'styled-components'; 2 | 3 | // export const Wrapper = styled.div` 4 | 5 | // ` 6 | 7 | import styled from 'styled-components' 8 | 9 | export const Wrapper = styled.div` 10 | display: flex; 11 | justify-content: space-between; 12 | width: 960px; 13 | /* height: 1000px; */ 14 | margin: 82px auto 72px; 15 | 16 | .left { 17 | flex: 1 1 auto; 18 | } 19 | 20 | .right { 21 | flex: 0 0 auto; 22 | width: 240px; 23 | margin-left: 12px; 24 | 25 | .sticky-wrap { 26 | position: fixed; 27 | top: 82px; 28 | } 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react'; 44 | 45 | export const ReactComponent: React.FunctionComponent>; 46 | 47 | const src: string; 48 | export default src; 49 | } 50 | 51 | declare module '*.module.css' { 52 | const classes: { readonly [key: string]: string }; 53 | export default classes; 54 | } 55 | 56 | declare module '*.module.scss' { 57 | const classes: { readonly [key: string]: string }; 58 | export default classes; 59 | } 60 | 61 | declare module '*.module.sass' { 62 | const classes: { readonly [key: string]: string }; 63 | export default classes; 64 | } 65 | -------------------------------------------------------------------------------- /src/redux/context.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | // import { UserDto } from './../modal/dtos/user.dto' 3 | 4 | // Store Context is the global context that is managed by reducers. 5 | 6 | interface IStore { 7 | showLogin: boolean 8 | checkUser: any 9 | user: any 10 | query: { 11 | search: string 12 | sort: string 13 | } 14 | articleList: any[] 15 | likeList: any[] 16 | followingList: any[] 17 | followersList: any[] 18 | dispatch(action: { type: string; payload?: any }): void 19 | } 20 | 21 | export const defaultStore = { 22 | showLogin: false, 23 | checkUser: {}, 24 | user: {}, 25 | query: { search: '', sort: '' }, 26 | articleList: [], 27 | likeList: [], 28 | followingList: [], 29 | followersList: [], 30 | dispatch: (arg: any) => arg, 31 | } 32 | 33 | const Store = React.createContext(defaultStore) 34 | 35 | export const useRedux = () => useContext(Store) 36 | 37 | export const useDispatch = () => { 38 | const dispatch = useContext(Store).dispatch 39 | return dispatch 40 | } 41 | 42 | export const useSelector = (cb: (store: IStore) => T | IStore = arg => arg) => cb(useContext(Store)) 43 | 44 | export const useIsLogin = () => { 45 | const store = useRedux() 46 | return !!Object.keys(store.user || {}).length 47 | } 48 | export default Store 49 | -------------------------------------------------------------------------------- /src/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | export default function reducer(state: any, action: any) { 2 | const { payload } = action 3 | console.log('%c%s', 'color: #20bd08;font-size:15px', '===TQY===: reducer -> action', action) 4 | switch (action.type) { 5 | case 'CHANGE_SHOW_LOGIN': 6 | return { ...state, ...payload } 7 | 8 | case 'LOGIN': 9 | return { ...state, ...payload } 10 | 11 | case 'CHANGE_ARTICLE_LIST': 12 | return { ...state, ...payload } 13 | 14 | case 'APPEND_ARTICLE_LIST': 15 | return { ...state, articleList: [...state.articleList, ...payload.articleList] } 16 | 17 | case 'DELETE_ARTICLE': 18 | return { ...state, articleList: state.articleList.filter((item: any) => item.id !== payload.id) } 19 | 20 | case 'UPDATE_ARTICLE': 21 | return { 22 | ...state, 23 | articleList: state.articleList.filter((item: any) => item.id === payload.id).screenshot = payload.screenshot, 24 | } 25 | 26 | case 'CHANGE_LIKE_LIST': 27 | return { ...state, ...payload } 28 | 29 | case 'CHANG_FOLLOWING_LIST': 30 | return { ...state, ...payload } 31 | 32 | case 'CHANGE_FOLLOWERS_LIST': 33 | return { ...state, ...payload } 34 | 35 | case 'UPDATE_USER': 36 | return { ...state, user: { ...state.user, ...payload.user } } 37 | 38 | case 'UPDATE_CHECK_USER': 39 | return { ...state, checkUser: { ...state.checkUser, ...payload.checkUser } } 40 | 41 | case 'LOGOUT': 42 | return { ...state, user: {} } 43 | default: 44 | return state 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import RouteErrorBoundary from '@/containers/RouteErrorBoundary' 2 | import React from 'react' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import renderRoutes from './renderRoutes' 5 | import routes from './routes' 6 | 7 | const AppRouter: React.FC = () => { 8 | return ( 9 | 10 | {renderRoutes(routes)} 11 | 12 | ) 13 | } 14 | 15 | export default AppRouter 16 | -------------------------------------------------------------------------------- /src/routes/lazyComponents.tsx: -------------------------------------------------------------------------------- 1 | import SpinCenter from '@/components/SpinCenter' 2 | import React, { lazy, Suspense } from 'react' 3 | 4 | export type TLazyComponentsKeys = keyof typeof lazyComponents 5 | 6 | const withSuspense = (Component: any) => { 7 | return (props: any) => ( 8 | }> 9 | 10 | 11 | ) 12 | } 13 | 14 | export const lazyComponents = { 15 | Frame: withSuspense(lazy(() => import('../containers/Frame'))), 16 | Home: withSuspense(lazy(() => import('../pages/home'))), 17 | Post: withSuspense(lazy(() => import('../pages/post'))), 18 | MobilePost: withSuspense(lazy(() => import('../pages/mobiPost'))), 19 | Settings: withSuspense(lazy(() => import('../pages/settings'))), 20 | Editor: withSuspense(lazy(() => import('../pages/editor'))), 21 | User: withSuspense(lazy(() => import('../pages/user'))), 22 | // EditMarkdown: withSuspense(lazy(() => import('../components/EditMarkdown'))), 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/renderRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { Redirect, Route, Switch } from 'react-router-dom' 3 | import { lazyComponents } from './lazyComponents' 4 | import { IRoute } from './routes' 5 | 6 | const renderRoutes = (routesTree: IRoute[]) => ( 7 | 8 | {routesTree.map((route) => { 9 | const Comp = lazyComponents[route.component] 10 | return route.childRoutes ? ( 11 | ( 15 | 16 | Loading...
}> 17 | 18 | {renderRoutes(route.childRoutes as IRoute[])} 19 | 20 | 21 | 22 | )} 23 | /> 24 | ) : ( 25 | 26 | ) 27 | })} 28 | 29 | 30 | ) 31 | 32 | export default renderRoutes 33 | -------------------------------------------------------------------------------- /src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import { TLazyComponentsKeys } from './lazyComponents' 2 | 3 | const routes: IRoute[] = [ 4 | { 5 | path: '/mobi/post/:id', 6 | exact: true, 7 | component: 'MobilePost' 8 | }, 9 | { 10 | path: '/editor', 11 | exact: true, 12 | component: 'Editor' 13 | }, 14 | { 15 | path: '/', 16 | exact: true, 17 | component: 'Frame', 18 | childRoutes: [ 19 | { path: '/', component: 'Home' }, 20 | { path: '/timeline', component: 'Home' }, 21 | { path: '/post/:id', component: 'Post' }, 22 | { path: '/settings', component: 'Settings' }, 23 | { path: '/editor/:id', component: 'Editor' }, 24 | { path: '/user/:id', component: 'User' }, 25 | // { path: '/user/:id/posts', component: 'User' }, 26 | // { path: '/user/:id/following', component: 'User' }, 27 | // { path: '/user/:id/likes', component: 'User' } 28 | { path: '/user/:id/:item', component: 'User' } 29 | // { path: '/editor', component: 'Editor' } 30 | // { path: 'article/:id', component: Article } 31 | ] 32 | } 33 | ] 34 | 35 | export default routes 36 | 37 | export interface IRoute { 38 | path: string 39 | exact?: boolean 40 | component: TLazyComponentsKeys 41 | childRoutes?: IRoute[] 42 | } 43 | -------------------------------------------------------------------------------- /src/statics/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/statics/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/src/statics/avatar.png -------------------------------------------------------------------------------- /src/statics/close-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/src/statics/close-black.png -------------------------------------------------------------------------------- /src/statics/close-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthhub/react-mini-blog/56b366163d930e4e17cdac65dc28f26848cd9545/src/statics/close-gray.png -------------------------------------------------------------------------------- /src/statics/dot-hover.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/statics/dot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/statics/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon_edit 5 | Created with sketchtool. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/statics/logout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/statics/person.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/statics/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/style.ts: -------------------------------------------------------------------------------- 1 | // 全局样式 2 | import { createGlobalStyle } from 'styled-components' 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | /* http://meyerweb.com/eric/tools/css/reset/ 6 | v2.0 | 20110126 7 | License: none (public domain) 8 | */ 9 | 10 | html, body, div, span, applet, object, iframe, 11 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 12 | a, abbr, acronym, address, big, cite, code, 13 | del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, 15 | b, u, i, center, 16 | dl, dt, dd, ol, ul, li, 17 | fieldset, form, label, legend, 18 | table, caption, tbody, tfoot, thead, tr, th, td, 19 | article, aside, canvas, details, embed, 20 | figure, figcaption, footer, header, hgroup, 21 | menu, nav, output, ruby, section, summary, 22 | time, mark, audio, video { 23 | margin: 0; 24 | padding: 0; 25 | border: 0; 26 | font-size: 100%; 27 | font: inherit; 28 | vertical-align: baseline; 29 | } 30 | html::-webkit-scrollbar { 31 | display: none; 32 | } 33 | /* HTML5 display-role reset for older browsers */ 34 | article, aside, details, figcaption, figure, 35 | footer, header, hgroup, menu, nav, section { 36 | display: block; 37 | } 38 | body { 39 | font-size: 12px; 40 | font-family: -apple-system, SF UI Text, PingFang SC, Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, Helvetica Neue, Helvetica,Arial, sans-serif; 41 | color: #333; 42 | background-color: #f4f5f5 !important; 43 | } 44 | ol, ul { 45 | list-style: none; 46 | } 47 | blockquote, q { 48 | quotes: none; 49 | } 50 | blockquote:before, blockquote:after, 51 | q:before, q:after { 52 | content: ''; 53 | content: none; 54 | } 55 | table { 56 | border-collapse: collapse; 57 | border-spacing: 0; 58 | } 59 | /* 搜索结果被高亮的文字 */ 60 | em { 61 | color: #e8001c; 62 | } 63 | 64 | ` 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './markdown.less'; 2 | 3 | .svg-icon { 4 | width: 1em; 5 | height: 1em; 6 | vertical-align: -0.15em; 7 | fill: currentColor; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | position: relative; 13 | font-family: 'Chinese Quote', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 14 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 15 | line-height: 2; 16 | color: #555; 17 | } 18 | 19 | ol, 20 | ul { 21 | list-style: none; 22 | padding: 0; 23 | } 24 | 25 | .async-com-loading { 26 | float: right; 27 | margin-top: 10px; 28 | margin-right: 10px; 29 | } 30 | 31 | .ant-back-top-icon { 32 | background-size: contain; 33 | } 34 | 35 | .github-loading-container { 36 | margin-top: 20vh; 37 | display: flex; 38 | flex-direction: column; 39 | text-align: center; 40 | .text { 41 | margin-top: 10px; 42 | } 43 | } 44 | 45 | .confirm-modal { 46 | } 47 | 48 | .result-modal { 49 | min-width: 500px; 50 | .ant-modal-confirm-body > .anticon + .ant-modal-confirm-title + .ant-modal-confirm-content { 51 | margin: 0; 52 | } 53 | .ant-modal-body { 54 | .ant-result { 55 | padding-bottom: 12px; 56 | } 57 | 58 | .list { 59 | li { 60 | display: flex; 61 | align-items: center; 62 | .anticon-check-circle { 63 | margin-right: 8px; 64 | } 65 | a { 66 | flex: 1; 67 | overflow: hidden; 68 | white-space: nowrap; 69 | text-overflow: ellipsis; 70 | font-size: 14px; 71 | line-height: 32px; 72 | color: #8590a6; 73 | cursor: pointer; 74 | } 75 | &:hover { 76 | background: #f0f2f5; 77 | border-radius: 4px; 78 | a { 79 | color: #40a9ff; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/styles/markdown.less: -------------------------------------------------------------------------------- 1 | @import './atom-one-light.css'; 2 | 3 | // ide 4 | .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) { 5 | background: none !important; 6 | } 7 | 8 | .article-detail { 9 | margin: 0 auto; 10 | font-family: 'Lato', 'PingFang SC', 'Microsoft YaHei', sans-serif; 11 | color: #555; 12 | line-height: 2; 13 | img { 14 | max-width: 100%; 15 | margin: 0 auto 25px; 16 | box-sizing: border-box; 17 | padding: 3px; 18 | border: 1px solid #ddd; 19 | } 20 | 21 | pre, 22 | code { 23 | font-family: consolas, Menlo, 'PingFang SC', 'Microsoft YaHei', monospace; 24 | } 25 | code { 26 | padding: 2px 4px; 27 | word-wrap: break-word; 28 | color: #ff502c; 29 | background: #fff5f5; 30 | border-radius: 3px; 31 | font-size: 13px; 32 | } 33 | pre { 34 | padding: 10px; 35 | overflow: auto; 36 | margin: 20px 0; 37 | font-size: 13px; 38 | color: #4d4d4c; 39 | background: #f7f7f7; 40 | line-height: 1.6; 41 | } 42 | pre code { 43 | padding: 0; 44 | color: #555; 45 | background: none; 46 | text-shadow: none; 47 | font-family: consolas, Menlo, 'PingFang SC', 'Microsoft YaHei', monospace; 48 | } 49 | h2, 50 | h3, 51 | h4, 52 | h5, 53 | h6 { 54 | margin: 20px 0 15px 0; 55 | padding-top: 10px; 56 | border-bottom: 1px solid #eee; 57 | margin-bottom: 10px; 58 | font-weight: bold; 59 | line-height: 1.5; 60 | font-family: 'Lato', 'PingFang SC', 'Microsoft YaHei', sans-serif; 61 | color: #555; 62 | } 63 | h2 { 64 | font-size: 1.4em; 65 | } 66 | h3 { 67 | font-size: 1.3em; 68 | border-bottom: 1px dotted #eee; 69 | } 70 | h4 { 71 | font-size: 1.2em; 72 | } 73 | ul { 74 | padding-left: 20px; 75 | li { 76 | line-height: 2; 77 | list-style: circle; 78 | } 79 | } 80 | .hljs-comment, 81 | .hljs-quote { 82 | color: #575f6a; 83 | } 84 | blockquote { 85 | margin: 1em 0; 86 | border-left: 4px solid #ddd; 87 | padding: 0 1em; 88 | color: #666; 89 | p { 90 | margin: 0.5em 0; 91 | line-height: 1.7em; 92 | } 93 | } 94 | 95 | table { 96 | font-size: 0.8em; 97 | max-width: 100%; 98 | overflow: auto; 99 | border: 1px solid #f6f6f6; 100 | border-collapse: collapse; 101 | border-spacing: 0; 102 | thead { 103 | background: #f6f6f6; 104 | color: #000; 105 | text-align: left; 106 | } 107 | tr { 108 | display: table-row; 109 | vertical-align: inherit; 110 | border-color: inherit; 111 | &:nth-child(2n) { 112 | background-color: #fcfcfc; 113 | } 114 | } 115 | th { 116 | padding: 0.8em 0.5em; 117 | line-height: 1.5em; 118 | } 119 | tbody { 120 | display: table-row-group; 121 | vertical-align: middle; 122 | border-color: inherit; 123 | td { 124 | min-width: 7.5em; 125 | padding: 0.8em 0.5em; 126 | line-height: 1.5em; 127 | } 128 | } 129 | } 130 | 131 | ol { 132 | list-style: decimal; 133 | margin: 5px 0 5px 15px; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "experimentalDecorators": true, 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "rootDirs": ["./src"], 19 | "baseUrl": "./", 20 | "paths": { 21 | "@/*": ["src/*"] 22 | }, 23 | "typeRoots": ["node", "node_modules/@types", "./typings"] 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:latest", 5 | "custom-tslint-rules-collection", 6 | "tslint-config-prettier" 7 | ], 8 | "jsRules": { 9 | "max-line-length": { 10 | "options": [ 11 | 200 12 | ] 13 | }, 14 | "max-file-line-count": { 15 | "options": [ 16 | 300 17 | ] 18 | } 19 | }, 20 | "rules": { 21 | "'no-restricted-globals'": false, 22 | "ban-comma-operator": true, 23 | "trailing-comma": [ 24 | true 25 | ], 26 | "max-file-line-count": [ 27 | true, 28 | 300 29 | ], 30 | "max-classes-per-file": true, 31 | "match-default-export-name": false, 32 | "no-increment-decrement": false, 33 | "import-name": false, 34 | "no-duplicate-imports": true, 35 | "interface-name": true, 36 | "import-spacing": true, 37 | "max-line-length": [ 38 | true, 39 | 300 40 | ], 41 | "semicolon": [ 42 | true, 43 | "never" 44 | ], 45 | "only-arrow-functions": [ 46 | true, 47 | "allow-declarations", 48 | "allow-named-functions" 49 | ], 50 | "object-literal-sort-keys": false, 51 | "object-shorthand-properties-first": false, 52 | "no-console": false, 53 | "no-empty": true, 54 | "no-empty-interface": false, 55 | "no-object-literal-type-assertion": false, 56 | "jsx-no-multiline-js": true, 57 | "no-unused-expression": true, 58 | "align": [ 59 | true, 60 | "parameters", 61 | "statements" 62 | ], 63 | "space-before-function-paren": [ 64 | true, 65 | { 66 | "anonymous": "always", 67 | "named": "never", 68 | "asyncArrow": "always" 69 | } 70 | ], 71 | "member-access": [ 72 | true, 73 | "no-public" 74 | ], 75 | "no-submodule-imports": false, 76 | "no-implicit-dependencies": false, 77 | "one-variable-per-declaration": true, 78 | "prefer-object-spread": true, 79 | "prefer-array-literal": false, 80 | "no-unsafe-any": false, 81 | "strict-type-predicates": true, 82 | "arrow-parens": false, 83 | "arrow-return-shorthand": [ 84 | false 85 | ], 86 | "comment-format": [ 87 | true, 88 | "check-space" 89 | ], 90 | "import-blacklist": [ 91 | true, 92 | "rxjs" 93 | ], 94 | "interface-over-type-literal": false, 95 | "member-ordering": [ 96 | true, 97 | { 98 | "order": "fields-first" 99 | } 100 | ], 101 | "newline-before-return": false, 102 | "no-import-side-effect": false, 103 | "no-namespace": true, 104 | "no-inferrable-types": [ 105 | true, 106 | "ignore-params", 107 | "ignore-properties" 108 | ], 109 | "no-invalid-this": [ 110 | true, 111 | "check-function-in-method" 112 | ], 113 | "prefer-const": true, 114 | "prefer-readonly": true, 115 | "curly": true, 116 | "no-switch-case-fall-through": true, 117 | "no-null-keyword": false, 118 | "no-construct": true, 119 | "no-debugger": true, 120 | "ter-arrow-parens": false, 121 | "no-for-in-array": true, 122 | "no-unbound-method": false, 123 | "no-duplicate-super": true, 124 | "no-reference-import": true, 125 | "no-require-imports": true, 126 | "no-this-assignment": [ 127 | true, 128 | { 129 | "allow-destructuring": true 130 | } 131 | ], 132 | "no-trailing-whitespace": true, 133 | "object-literal-shorthand": true, 134 | "ordered-imports": true, 135 | "prefer-method-signature": true, 136 | "prefer-template": [ 137 | false, 138 | "allow-single-concat" 139 | ], 140 | "quotemark": [ 141 | true, 142 | "single", 143 | "jsx-double" 144 | ], 145 | "triple-equals": [ 146 | true, 147 | "allow-null-check" 148 | ], 149 | "type-literal-delimiter": false, 150 | "typedef": [ 151 | true, 152 | "property-declaration" 153 | ], 154 | "variable-name": [ 155 | false, 156 | "ban-keywords", 157 | "check-format", 158 | "allow-pascal-case", 159 | "allow-leading-underscore" 160 | ], 161 | "no-shadowed-variable": true, 162 | "jsx-no-lambda": true, 163 | "jsx-curly-spacing": [ 164 | true, 165 | { 166 | "when": "never", 167 | "allowMultiline": false 168 | } 169 | ], 170 | "jsx-boolean-value": true, 171 | "import-regular-blank-list": true, 172 | "tsx-no-any-props": false, 173 | "no-unused-vars": false, 174 | "react-hooks/exhaustive-deps": false 175 | }, 176 | "linterOptions": { 177 | "exclude": [ 178 | "node_modules", 179 | "**/*.spec.ts", 180 | "src/@types/*.ts", 181 | "config", 182 | "dist", 183 | ".temp", 184 | "test" 185 | ] 186 | } 187 | } -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var hljs: any 2 | 3 | declare interface Window { 4 | [method: string]: () => void 5 | } 6 | 7 | type PromiseState = 'pending' | 'fulfilled' | 'rejected' 8 | --------------------------------------------------------------------------------