Browse Source

feat: 完成了文章列表界面、部分汉堡菜单、路由

Shellmiao 3 years ago
parent
commit
7f28d72d61

File diff suppressed because it is too large
+ 611 - 56
package-lock.json


+ 8 - 1
package.json

@@ -3,8 +3,10 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@ant-design/icons": "^4.7.0",
     "@babel/core": "^7.16.0",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
+    "@reduxjs/toolkit": "^1.8.1",
     "@svgr/webpack": "^5.5.0",
     "@testing-library/jest-dom": "^5.14.1",
     "@testing-library/react": "^13.0.0",
@@ -13,6 +15,7 @@
     "@types/node": "^16.7.13",
     "@types/react": "^18.0.0",
     "@types/react-dom": "^18.0.0",
+    "axios": "^0.27.2",
     "babel-jest": "^27.4.2",
     "babel-loader": "^8.2.3",
     "babel-plugin-named-asset-import": "^0.3.8",
@@ -46,7 +49,10 @@
     "react-app-polyfill": "^3.0.0",
     "react-dev-utils": "^12.0.1",
     "react-dom": "^18.1.0",
+    "react-photo-view": "^1.1.2",
+    "react-redux": "^8.0.2",
     "react-refresh": "^0.11.0",
+    "react-router-dom": "^6.3.0",
     "resolve": "^1.20.0",
     "resolve-url-loader": "^4.0.0",
     "sass-loader": "^12.3.0",
@@ -92,7 +98,8 @@
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-react": "^7.29.4",
-    "eslint-plugin-react-hooks": "^4.5.0"
+    "eslint-plugin-react-hooks": "^4.5.0",
+    "node-sass": "^7.0.1"
   },
   "jest": {
     "roots": [

+ 2 - 2
public/index.html

@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="zh">
   <head>
     <meta charset="utf-8" />
     <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <title>React App</title>
+    <title>Shellmiao's Blog</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 11 - 1
src/App.tsx

@@ -1,9 +1,19 @@
 import React from "react";
+import "./Global/Global.sass";
+import "react-photo-view/dist/react-photo-view.css";
+import HeaderComponent from "./Components/HeaderComponent";
+import ArticleList from "./Components/ArticleList";
+import MenuComponent from "./Components/MenuComponent";
+import { Routes, Route } from "react-router-dom";
 
 function App() {
   return (
     <div className="App">
-      <h1>This is React App.</h1>
+      <HeaderComponent />
+      <MenuComponent />
+      <Routes>
+        <Route path="/" element={<ArticleList />}></Route>
+      </Routes>
     </div>
   );
 }

+ 0 - 0
src/Components/ArticleList.sass


+ 49 - 0
src/Components/ArticleList.tsx

@@ -0,0 +1,49 @@
+import "./ArticleList.sass";
+import { useDispatch, useSelector } from "react-redux";
+import {
+  fetchCodimd,
+  selectAllCodimd,
+  selectCodimdState,
+} from "../Redux/CodimdReducer";
+import ArticleListUnitComponent from "./ArticleListUnitComponent";
+import { useEffect } from "react";
+import { Status } from "../Redux/CodimdReducer";
+import { AnyAction, Dispatch } from "@reduxjs/toolkit";
+
+function getList(codimdStatus: Status, dispatch: Dispatch<AnyAction>): void {
+  if (codimdStatus === "idle") {
+    dispatch<any>(fetchCodimd());
+  }
+}
+
+function ArticleList() {
+  const dispatch = useDispatch();
+
+  const codimd = useSelector(selectAllCodimd);
+
+  const codimdStatus = useSelector(selectCodimdState);
+
+  useEffect(() => {
+    console.log(codimdStatus);
+    getList(codimdStatus, dispatch);
+  }, [codimdStatus, dispatch]);
+
+  const articleList = codimd.map((ele, index) => (
+    <div key={index}>
+      <ArticleListUnitComponent {...ele} />
+    </div>
+  ));
+
+  let content;
+  if (codimdStatus === "loading") {
+    content = <div>{articleList}加载中</div>;
+  } else if (codimdStatus === "failed") {
+    content = <div>{articleList}加载失败,请稍后重试</div>;
+  } else {
+    content = <div>{articleList}</div>;
+  }
+
+  return <div>{content}</div>;
+}
+
+export default ArticleList;

+ 27 - 0
src/Components/ArticleListUnitComponent.sass

@@ -0,0 +1,27 @@
+@import "../Global/GlobalVar"
+
+.article-unit
+    padding-bottom: 32px
+
+.article-unit-h2
+    color: $global-color
+    font-size: 24px
+    padding: 12px 0
+    font-weight: 700
+    letter-spacing: 0.05em
+    text-align: left
+.article-unit-a
+    color: rgba(0, 0, 0, 0.98)
+    text-decoration: none
+    transition: all 0.3s
+.article-unit-info
+    font-size: 12px
+    padding-bottom: 24px
+    text-align: left
+    >span
+        color: #5e5e5e7b
+    >span:not(:first-child):before
+        content: "/ "
+        font-size: 10px
+        color: rgba(0, 0, 0, 0.1)
+        margin: 0 4px

+ 38 - 0
src/Components/ArticleListUnitComponent.tsx

@@ -0,0 +1,38 @@
+import "./ArticleListUnitComponent.sass";
+import { EyeOutlined, FieldTimeOutlined } from "@ant-design/icons";
+
+export type ArticleEle = {
+  title: string;
+  short_id: string;
+  create: string;
+  view_count: string;
+};
+
+function ArticleListUnitComponent(articleEle: ArticleEle) {
+  return (
+    <article className="article-unit">
+      <a
+        className="article-unit-a"
+        href={"https://md.shellmiao.com/s/" + articleEle.short_id}
+        target="view_window"
+      >
+        <h2 className="article-unit-h2">{articleEle.title}</h2>
+      </a>
+      <div className="article-unit-info">
+        <span>
+          <FieldTimeOutlined />
+          &nbsp;
+          {articleEle.create.slice(4, 16)}
+        </span>
+        &nbsp;
+        <span>
+          <EyeOutlined />
+          &nbsp;
+          {articleEle.view_count}
+        </span>
+      </div>
+    </article>
+  );
+}
+
+export default ArticleListUnitComponent;

+ 15 - 0
src/Components/HeaderComponent.sass

@@ -0,0 +1,15 @@
+.header-div
+    padding: 32px 0px
+    .avatar
+        width: 120px
+        height: 120px
+        border-radius: 25%
+        margin-bottom: 24px
+    .header-id-h1
+        font-size: 32px
+        font-weight: bold
+    .header-p
+        font-size: 16px
+        color: rgb(73, 80, 87)
+        font-weight: lighter
+        padding: 24px

+ 21 - 0
src/Components/HeaderComponent.tsx

@@ -0,0 +1,21 @@
+import "./HeaderComponent.sass";
+
+function HeaderComponent() {
+  return (
+    <div className="header-div">
+      <a href="https://www.shellmiao.com">
+        <img
+          className="avatar"
+          src="https://pic.shellmiao.com/2022/05/25/628df4743b4b8.jpeg"
+          alt="avatar"
+        />
+      </a>
+      <h1 className="header-id-h1">Shellmiao</h1>
+      <p className="header-p">
+        🇨🇳 <b>China Beijing</b>
+      </p>
+    </div>
+  );
+}
+
+export default HeaderComponent;

+ 62 - 0
src/Components/MenuComponent.sass

@@ -0,0 +1,62 @@
+$color: #cbcbcb
+$height-icon: 16px
+$width-line: 25px
+$height-line: 2px
+
+$transition-time: 0.4s
+$rotation: 45deg
+$translateY: ($height-icon / 2)
+$translateX: 0
+
+#menu-component-div
+    background: #dddddd
+    position: absolute
+    right: 24px
+    bottom: 64px
+    padding: 10px 6px
+    border-radius: 5px
+
+#hamburger-icon
+    width: $width-line
+    height: $height-icon
+    position: relative
+    display: block
+    cursor: pointer
+    padding:
+    .line
+        display: block
+        background: $color
+        width: $width-line
+        height: $height-line
+        position: absolute
+        left: 0
+        border-radius: ($height-line / 2)
+        transition: all $transition-time
+        -webkit-transition: all $transition-time
+        -moz-transition: all $transition-time
+        &.line-1
+            top: 0
+        &.line-2
+            top: 50%
+        &.line-3
+            top: 100%
+    &:hover, &:focus
+        .line-1
+            transform: translateY($height-line * -1)
+            -webkit-transform: translateY($height-line * -1)
+            -moz-transform: translateY($height-line * -1)
+        .line-3
+            transform: translateY($height-line)
+            -webkit-transform: translateY($height-line)
+            -moz-transform: translateY($height-line)
+    &.active
+        .line-1
+            transform: translateY($translateY) translateX($translateX) rotate($rotation)
+            -webkit-transform: translateY($translateY) translateX($translateX) rotate($rotation)
+            -moz-transform: translateY($translateY) translateX($translateX) rotate($rotation)
+        .line-2
+            opacity: 0
+        .line-3
+            transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1)
+            -webkit-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1)
+            -moz-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1)

+ 24 - 0
src/Components/MenuComponent.tsx

@@ -0,0 +1,24 @@
+import { useState } from "react";
+import "./MenuComponent.sass";
+
+function MenuComponent() {
+  const [ifActive, setIfAvtive] = useState<Boolean>(false);
+
+  const activeFlag = ifActive ? " active" : "";
+  return (
+    <div id="menu-component-div">
+      <div
+        id="hamburger-icon"
+        onClick={() => {
+          setIfAvtive(!activeFlag);
+        }}
+        className={activeFlag}
+      >
+        <span className="line line-1"></span>
+        <span className="line line-2"></span>
+        <span className="line line-3"></span>
+      </div>
+    </div>
+  );
+}
+export default MenuComponent;

+ 1 - 0
src/DataBase/codimd.ts

@@ -0,0 +1 @@
+export const getNodes = () => {};

+ 28 - 0
src/Global/Global.sass

@@ -0,0 +1,28 @@
+@import "./GlobalVar"
+
+body
+    font-family: $global-font-family
+    font-feature-settings: $global-font-feature-settings
+    color: $global-color
+    letter-spacing: $global-letter-spacing
+    background: #ededed
+div
+    text-align: center
+body, div, a, p, ul, li, ol, h1, h2, h3, h4, h5, h6, table, tr, td
+    box-sizing: border-box
+    margin: 0px
+    padding: 0px
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video
+    vertical-align: baseline
+    border-width: 0px
+    border-style: initial
+    border-color: initial
+    border-image: initial
+.App
+    flex: 1
+    display: flex
+    min-height: 100vh
+    flex-direction: column
+    padding: 0 24px
+    max-width: 600px
+    margin: 0 auto

+ 7 - 0
src/Global/GlobalVar.sass

@@ -0,0 +1,7 @@
+$global-font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue',Arial,sans-serif
+
+$global-font-feature-settings: "case", "ss01", "ss02"
+
+$global-color: rgba(0, 0, 0, 0.86)
+
+$global-letter-spacing: 0.05em

+ 60 - 0
src/Redux/CodimdReducer.ts

@@ -0,0 +1,60 @@
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import { ArticleEle } from "../Components/ArticleListUnitComponent";
+import getArticleList from "../Utils/Axios";
+import { state } from "./Store";
+
+export type Status = "idle" | "loading" | "succeeded" | "failed";
+
+export type CodimdState = {
+  data: [ArticleEle];
+  status: Status;
+  error: string | null;
+};
+
+export const codimdState = {
+  data: [] as unknown as [ArticleEle],
+  status: "idle",
+  error: "",
+};
+
+export const fetchCodimd = createAsyncThunk("posts/fetchCodimd", async () => {
+  const response = await getArticleList();
+  return response.data;
+});
+
+export const codimdSlice = createSlice({
+  name: "codimd",
+  initialState: codimdState,
+  reducers: {},
+  extraReducers(builder) {
+    builder
+      .addCase(fetchCodimd.pending, (state, action) => {
+        state.status = "loading";
+      })
+      .addCase(fetchCodimd.fulfilled, (state, action) => {
+        state.status = "succeeded";
+        (action.payload as [ArticleEle]).forEach((elePayload) => {
+          let flag = false;
+          for (let i = 0; i < state.data.length; i++) {
+            if (state.data[i].short_id === elePayload.short_id) {
+              flag = true;
+              break;
+            }
+          }
+          if (!flag) {
+            state.data.push(elePayload);
+          }
+        });
+      })
+      .addCase(fetchCodimd.rejected, (state, action) => {
+        state.status = "failed";
+        state.error = action.error.message as string;
+      });
+  },
+});
+
+export const selectAllCodimd = (state: state) => state.codimd.data;
+
+export const selectCodimdState = (state: state) => state.codimd.status;
+
+export default codimdSlice.reducer;

+ 12 - 0
src/Redux/Store.ts

@@ -0,0 +1,12 @@
+import { configureStore } from "@reduxjs/toolkit";
+import codimdReducer, { CodimdState } from "./CodimdReducer";
+
+export type state = {
+  codimd: CodimdState;
+};
+
+export default configureStore({
+  reducer: {
+    codimd: codimdReducer,
+  },
+});

+ 25 - 0
src/Utils/Axios.ts

@@ -0,0 +1,25 @@
+import axios from "axios";
+
+axios.defaults.baseURL = "http://127.0.0.1:5000";
+
+type DefaultRes = {
+  // `data` 由服务器提供的响应
+  data: {};
+  // `status` 来自服务器响应的 HTTP 状态码
+  status: 200;
+  // `statusText` 来自服务器响应的 HTTP 状态信息
+  statusText: "OK";
+  // `headers` 服务器响应的头
+  headers: {};
+  // `config` 是为请求提供的配置信息
+  config: {};
+  // 'request'
+  // `request` is the request that generated this response
+  // It is the last ClientRequest instance in node.js (in redirects)
+  // and an XMLHttpRequest instance the browser
+  request: {};
+};
+
+export default async function getCodimdList(): Promise<DefaultRes> {
+  return axios.get("/get_my_notes");
+}

+ 16 - 2
src/index.tsx

@@ -1,5 +1,19 @@
 import React from "react";
-import ReactDOM from "react-dom";
+import ReactDOM from "react-dom/client";
+import { Provider } from "react-redux";
 import App from "./App";
+import Store from "./Redux/Store";
+import { BrowserRouter } from "react-router-dom";
 
-ReactDOM.render(<App />, document.getElementById("root"));
+const root = ReactDOM.createRoot(
+  document.getElementById("root") as HTMLElement
+);
+root.render(
+  <React.StrictMode>
+    <Provider store={Store}>
+      <BrowserRouter>
+        <App />
+      </BrowserRouter>
+    </Provider>
+  </React.StrictMode>
+);

File diff suppressed because it is too large
+ 513 - 29
yarn.lock


Some files were not shown because too many files changed in this diff