# Part4-React+Antd+TS开发后台界面

## 一、CMS后台管理系统创建

本项目采用React+Ant Design+TypeScript开发，React主要使用Function Component的形式做开发，结合路由与请求实现。

### 1、创建React项目

由于项目结合ts与antd开发，因此请使用以下方式创建项目并安装依赖：

```shell
# 创建名为cms-manage的项目
$ npx create-react-app cms-manage --template typescript
# 或者
$ yarn create react-app cms-manage --template typescript

# 添加antd
npm install antd --save
# 或
yarn add antd

# 添加axios
npm install axios --save
# 或
yarn add axios
```

清空src下所有的内容，并且新建App.tsx与index.tsx，代码如下：

```tsx
// App.tsx
import React from "react";

export default function App() {
  return (
    <div>
      <h2>App</h2>
    </div>
  );
}

// index.tsx
import ReactDOM from 'react-dom'
import App from './App'
import 'antd/dist/antd.css';	// 引入antd的样式

ReactDOM.render(
  <App />,
  document.getElementById("root")
)
```

先把项目跑起来：

```shell
$ npm run start
```

看到跑起来的界面即可。

[![image-20211206091130083](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfylnqtdj31hc0q13z8.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfylnqtdj31hc0q13z8.jpg)

### 2、Antd组件

打开Antd的官网：<https://ant.design/index-cn>。

在App.tsx中：

```tsx
import React, { FC } from "react";
import { Button } from 'antd';

const App: FC = () => (
  <div>
    <Button type="primary">Button</Button>
  </div>
)

export default App;
```

看到蓝色按钮代表成功：[![image-20211206092850276](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfymsgwzj302300w0sh.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfymsgwzj302300w0sh.jpg)。

### 3、解包

解包之前必须做Git提交

#### a. Git提交

```shell
$ git add .
$ git commit -m 'xxx'
$ git remote origin xxxx
$ git push origin master
```

[![image-20211206090942052](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfym9ku9j30y70i8di2.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfym9ku9j30y70i8di2.jpg)

#### b. 解包

```shell
$ npm run eject
```

解包后项目根目录出现config文件夹，代表解包成功。

### 4、配置Less

#### a. 安装Less

```shell
$ npm install less less-loader@6.0.0 --save-dev
# 或
$ yarn add less less-loader@6.0.0
```

#### b. Webpack配置

找到 `webpack.config.js` ，搜索 `sassModuleRegex` 后，在其下方添加：

```js
module: {
    ...,
    // less加载器 
    {
        test: /\.less$/,
        use: getStyleLoaders(
            {
                //暂不配置
            },
            'less-loader'
        ),
    },
}
```

修改了配置文件，记得重新 `yarn start` 哦！

#### c. Less测试

删掉 `index.tsx` 中的 `antd.css` 引入，在src下新建 `App.less`：

```less
@import '~antd/dist/antd.css';

@bg: #efefef;

body{
    background: @bg;
}

#root{
    font-size: 14px;
    font-family: NotoSansHans;
    color: #333333;
}

a{
    color: #333333;
    text-decoration: none;
}
```

> 注意：以上顺便把文字三属性等基本配置写了

然后在 `App.tsx` 中引入：

```tsx
import "./App.less";
```

打开看到页面样式发生变化，即代表Less测试成功。

### 5、tsconfig

打开 `tsconfig.json`，修改下面出现的字段：

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "baseUrl": "./src",
    "jsx": "preserve"
  }
}
```

> baseUrl配置其指向src，所有的相对路径都可以直接从src下的文件或文件夹写起，如：
>
> ```tsx
> // 原本的引入
> import App from './App'
>
> // 现在的引入
> import App from 'App'
> ```

### 6、搭建基本页面结构（直接套用）

为了节省基本的布局时间，请直接使用这个基本布局：

#### a. 布局

在 `App.tsx` 使用Layout布局：

```tsx
import React from "react";
import { Layout, Breadcrumb, Menu } from "antd";
import { EditOutlined, TeamOutlined, AccountBookOutlined, ReadOutlined, SelectOutlined } from "@ant-design/icons";
// import { useNavigate } from "react-router-dom";
import logo from "assets/images/logo.png";
import "App.less";

const { Header, Footer, Sider, Content } = Layout;
const { SubMenu } = Menu;

const App = () => {
  // const navigate = useNavigate();
    
  const menu = (
    <Menu>
      <Menu.Item key="n">退出登录</Menu.Item>
    </Menu>
  );  
  
  return (
    <Layout>
      <Header className="header">
        <img src={logo} alt="" className="logo" />
        <Dropdown overlay={menu} onVisibleChange={() => setVisible(!visible)} visible={visible}>
          <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
            <img className="avatar" src="http://codesohigh.com/images/logo.png" alt="" />
            <span>你单排吧</span>
            <CaretDownOutlined />
          </a>
        </Dropdown>
      </Header>
      <Layout>
        <Sider
          theme="dark"
          style={{
            overflow: "auto",
            height: "100vh",
            position: "fixed",
            left: 0,
          }}
        >
          <Menu theme="dark" mode="inline" defaultSelectedKeys={["1"]}>
            <SubMenu key="sub1" icon={<TeamOutlined />} title="小编">
              <Menu.Item key="1" icon={<EditOutlined />}>
                文章编辑
              </Menu.Item>
              <Menu.Item key="2" icon={<ReadOutlined />}>
                查看文章列表
              </Menu.Item>
            </SubMenu>
            <SubMenu key="sub2" icon={<TeamOutlined />} title="管理员">
              <Menu.Item key="3" icon={<EditOutlined />}>
                文章编辑
              </Menu.Item>
              <Menu.Item key="4" icon={<ReadOutlined />}>
                查看文章列表
              </Menu.Item>
              <Menu.Item key="5" icon={<SelectOutlined />}>
                小编名单
              </Menu.Item>
            </SubMenu>
            <SubMenu key="sub3" icon={<AccountBookOutlined />} title="超级管理员">
              <Menu.Item key="6" icon={<EditOutlined />}>
                文章编辑
              </Menu.Item>
              <Menu.Item key="7" icon={<ReadOutlined />}>
                查看文章列表
              </Menu.Item>
              <Menu.Item key="8" icon={<SelectOutlined />}>
                小编名单
              </Menu.Item>
              <Menu.Item key="9" icon={<TeamOutlined />}>
                管理员名单
              </Menu.Item>
            </SubMenu>
          </Menu>
        </Sider>
        <Content className="content">
          <Breadcrumb style={{ margin: "16px 0" }}>
            <Breadcrumb.Item>首页</Breadcrumb.Item>
            <Breadcrumb.Item>文章编辑</Breadcrumb.Item>
          </Breadcrumb>
          <section className="content_main">
            {/* 在此处渲染页面内容 */}
           </section>
        </Content>
      </Layout>
      <Footer className="footer">Respect | Copyright © 2022 Author 你单排吧</Footer>
    </Layout>
  );
};

export default App;
```

#### b. 样式

在 `App.less` 中进行修改：

```less
@import '~antd/dist/antd.css';

@bg: #efefef;

body {
    background: @bg;
}

#root {
    font-size: 14px;
    font-family: NotoSansHans;
    color: #333333;
}

a {
    color: #333333;
    text-decoration: none;
}

.header {
    background: #fff;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .logo{
        width: 220px;
        height: 50px;
        cursor: pointer;
    }
    .avatar{
        width: 40px;
        height: 40px;
        margin-right: 10px;
        border-radius: 50%;
        cursor: pointer;
    }
}

.footer {
    background: #001529;
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    color: #fff;
    text-align: center;
}

.ant-menu-item {
    margin-top: 0 !important;
}

.content {
    margin-left: 200px;
    padding: 0 20px;
}

.content_main {
    background: #fff;
    // 100vh减掉header、breadcrumb、footer高度后，减掉20px作为高度
    height: calc(100vh - 188px - 20px);
    overflow-y: scroll;
    width: 100%;
    padding: 20px;
    box-sizing: border-box;
}

.content_main::-webkit-scrollbar {
    /*滚动条整体样式*/
    width: 10px;
    height: 100%;
    background: #fff;
    border-radius: 10px;
}

.content_main::-webkit-scrollbar-track {
    /*滚动条里面轨道*/
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    border-radius: 10px;
    background: #EDEDED;
}

.content_main::-webkit-scrollbar-thumb {
    /*滚动条里面小方块*/
    border-radius: 10px;
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    background: #535353;
}
```

## 二、路由

### 1、路由安装

安装最新版本的路由：

```shell
$ npm i react-router-dom
```

### 2、路由统一配置

在src下创建 `router/index.jsx` ：

```tsx
import { Routes, Route, BrowserRouter as Router } from "react-router-dom";
import React, { Suspense, lazy } from "react";
import App from "App";

interface Iroute {
  path: string;
  component: React.FC;
  children?: Iroute[];
}

let routeArr: Iroute[] = [
  {
    path: "/",
    component: App,
    children: [
      { path: "edit", component: lazy(() => import("../views/Edit")) },
      { path: "list", component: lazy(() => import("../views/List")) },
    ],
  },
  { path: "/login", component: lazy(() => import("../views/Login")) },
  { path: "/register", component: lazy(() => import("../views/Register")) },
];

const MyRouter = () => (
  <Router>
    <Suspense fallback={<div>loading...</div>}>
      <Routes>
        {
          routeArr.map((item, index) => (
            <Route key={index} path={item.path} element={<item.component />}></Route>
          ))
        }
      </Routes>
    </Suspense>
  </Router>
);

export default MyRouter;
```

### 3、入口文件配置

在 `src/index.tsx` 引入路由组件：

```tsx
import ReactDOM from 'react-dom'
import Router from 'router'

ReactDOM.render(
  <Router />,
  document.getElementById("root")
)
```

### 4、路由API

React-router-dom有很多新的API。

#### a. 获取路由地址与参数

```tsx
import { useLocation, useParams } from "react-router-dom";

const location = useLocation();   // 获取location，即可得到state中的参数
let params = useParams();		  // 获取地址栏携带的参数xxx： /list/xxx
```

#### b. 父组件展示路由对应的子组件

```tsx
import { Outlet } from "react-router-dom";

<Outlet />
```

#### c. 页面跳转

```tsx
import { useNavigate } from "react-router-dom";

let navigate = useNavigate();
navigate('/xxx')
```

## 三、富文本编辑器WangEditor

这里提供官网文档：<https://www.wangeditor.com/doc/>

#### a. 安装

```shell
$ npm i wangeditor --save
```

#### b. Editor组件封装

> 这个富文本编辑器请直接使用代码，没必要再自己手写一次。
>
> 注意：这里使用的是jsx语法。

`src/components` 目录下创建 `Editor.jsx` 文件：

```jsx
import { useEffect, useState } from 'react';
import { PageHeader, Button } from 'antd';
import E from 'wangeditor'

let editor = null
const Editor = () => {
  const [content, setContent] = useState("");

  useEffect(() => {
    // 实例化
    editor = new E("#myeditor")

    editor.config.onchange = (newHtml) => {
      setContent(newHtml);
    }

    // 创建
    editor.create()

    return () => {
      // 组件销毁时销毁编辑器
      editor.destroy()
    }
    // eslint-disable-next-line
  }, [])

  return (
    <div className="editor">
      <PageHeader
        style={{padding: 0, marginBottom: '20px'}}
        ghost={false}
        title="文章编辑"
        subTitle={`当前日期：${new Date().getFullYear()}-${new Date().getMonth()}-${new Date().getDate()}`}
        extra={[
          <Button key="3" type="primary">提交文章</Button>,
        ]}
      ></PageHeader>
      <div id="myeditor"></div>
    </div>
  );
}

export default Editor;
```

#### c. 调用

直接在父级组件中调用：

```tsx
import Editor from "components/Editor";

<Editor />
```

> 注意：父组件无论jsx或是tsx，都可以直接调用。
