# 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，都可以直接调用。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/cms/part4react+antd+ts-kai-fa-hou-tai-jie-mian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
