# React Components

React 组件库， 收集了工作宝中后台应用的常用组件或套件. 致力于减少应用开发的代码重复，提高维护效率

[DEMO](http://demo.ejiahe.com/api/rc-components/#/)

---

## Installation

```shell
yarn add @gdjiami/rc-components

# 依赖
yarn add react react-dom tslib react-router react-router-dom
```

---

## Usage

所有组件都在`es`目录下， es 使用 ES6 模块系统，另外每目录下面都有 Typescript 声明文件，所以支持类型检查，开发者可以按需导入需要的组件

`rc-components` 支持类似于`antd`的按需加载方式，如果你使用 typescript 可以使用[`ts-import-plugin`](https://github.com/Brooooooklyn/ts-import-plugin) 插件, 例如：

```js
// webpack.config.js
const tsImportPluginFactory = require('ts-import-plugin')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(jsx|tsx|js|ts)$/,
        loader: 'ts-loader',
        options: {
          transpileOnly: true,
          getCustomTransformers: () => ({
            before: [
              tsImportPluginFactory([
                // 按需导入antd组件
                {
                  libraryName: 'antd',
                  libraryDirectory: 'es',
                  style: 'css',
                },
                // 按需导入rc-components组件
                {
                  libraryName: '@gdjiami/rc-components',
                  libraryDirectory: 'es',
                  style: 'css',
                },
              ]),
            ],
          }),
        },
        exclude: /node_modules/,
      },
    ],
  },
  // ...
}
```

> 对于`babel`可以使用[`babel-plugin-import`](https://github.com/ant-design/babel-plugin-import) 插件

使用示例

```typescript
import React from 'react'
import { Login } from '@gdjiami/rc-components'
import { message } from 'antd'
import { delay } from './utils'

export default class LoginPage extends React.Component {
  public render() {
    return (
      <Login
        title="登录页面"
        onSubmit={this.handleSubmit}
        onSuccess={this.handleSuccess}
      />
    )
  }

  private handleSubmit = async () => {
    await delay(2000)
  }

  private handleSuccess = () => {
    message.success('登录成功')
  }
}
```

---

## 定位

rc-components 是基于 antd 组件库之上的高层组件库，旨在抽象重复的业务场景， 减少代码重复。其中耦合的东西有：

- antd
- react, react-dom
- tslib
- react-router v4
- lodash

这些耦合的技术是 rc-components 的构建基础，而且在团队内的应用是比较稳定的、静态的，近期不会有大的变动。相对的，有些东西是我们
要避免耦合的：

- 状态管理库，如 mobx，redux.
- Ajax 请求库
- 前端路由类型

---

## Components

这里列举各组件的使用方法和注意事项

- ### Title
  用于修改浏览器 title

```typescript
import { Title } from '@gdjiami/rc-components'
import React, { FC } from 'react'

export const Page: FC = (props) => {
  return <Title>系统管理</Title>
}
```

- ### AdminLayout

  后台应用布局组件
  AdminLayout 为顶层父组件，其子组件分别有

  1. AdminLayout.Action 位于顶部的右边展示？ （当前用户）
  2. AdminLayout.View 次顶层视图层，全局最外层用一次
  3. AdminLayout.HeaderBar
  4. AdminLayout.Footer 底部
  5. AdminLayout.Body 内容层，当业务页面用这个组件，其内容会按 AdminLayout 布局正确展示

  AdminLayout 常用参数（包括但不限于）：
  | 参数 | 格式 | 用途|
  | ---- | ---- | ---- |
  | siteName | string | 应用名称
  | logo | string | 应用图标
  | menus | () => Promise<MenuConfig[]>) \| MenuConfig[] | 菜单列表
  | after | React.ReactNode | 头部右侧内容

  ```html
  // layout.tsx

  <AdminLayout
    siteName="后台管理系统"
    title={<Title.Display breadcrumb inline />}
    menus={[]}
    after={
        <Dropdown
          overlay={
            <Menu>
              <Menu.Item key="resetPassword">修改密码</Menu.Item>
              <Menu.Item key="logout">安全退出</Menu.Item>
            </Menu>
          }
        >
          <AdminLayout.Action>用户名</AdminLayout.Action>
        </Dropdown>
    }
  >
    <AdminLayout.View>
      {props.children}
      <AdminLayout.Footer>Version</AdminLayout.Footer>
    </AdminLayout.View>
  </AdminLayout>
  ```

  AdminLayout.Body 一般用于业务子页面，里面直接添加页面内容

  ```html
  <AdminLayout.Body>
    <title>应用管理</title>
    <FatTable
      enableSelect
      enablePersist="{false}"
      columns="{column}"
      header="{renderHeader}"
      headerExtra="{renderHeaderExtra}"
      onRemove="{handleRemove}"
      onFetch="{handleFetch}"
      onAction="{handleAction}"
    />
  </AdminLayout.Body>
  ```

- ### FatTable
  后台应用表格组件，高频组件之一，集成了翻页，搜索，多选，上移下移等基础功能。
  FatTable 子组件有
  1. FatTable.Actions 表格项功能按钮组，下为其子组件
  2. FatTable.Action 表格项功能按钮

FatTable 常用参数（仅列举了常用，更多请查看源码）：
| 参数 | 格式 | 用途|
| ---- | ---- | ---- |
| enableSelect | boolean | 是否开启可选
| enablePagination | boolean | 是否开启翻页
| onFetch | FetchHandler | 获取表格数据的方法（翻页搜索均调用此方法）
| header | HeaderRenderer | 表格头部内容 （一般为搜索功能）
| headerExtra | HeaderExtraRenderer | 表格头部额外内容 （一般表格功能按钮，导出、导出、删除、添加等）
| columns | ColumnsType | 列表数据展示
| idKey | string | 列表项的 key （如没有唯一的值可手动构造）
| className | string | 定义类名
| onShift | ShiftHandler | 顺序发生改变所调用的回调
| onRemove | RemoveHandler | 列表项删除所调用的回调
| onAction | ActionHandler | 操作表格的统一方法

```html
// 直接用就好啦
<FatTable
  enableSelect
  columns="{column}"
  header="{renderHeader}"
  headerExtra="{renderHeaderExtra}"
  onRemove="{handleRemove}"
  onFetch="{handleFetch}"
  onAction="{handleAction}"
/>
```

onAction 使用方法 和表格交互的重要途径

```jsx
// 示例内容
import { FatTable } from '@gdjiami/rc-components'
import { ColumnsType} from '@gdjiami/rc-components/es/fat-table'

const AppStore: FC = () => {
  const { getDownloadUrl } = useRootModel()
  const column: ColumnsType<T, P> = [
    {
      title: '示例内容', // 列的标题
      width: 80,
      render: r => ( // 自定义展示内容 没有则展示dataIndex字段
        <span>
          自定义的展示内容{r.logo}
        </span>
      )
    },
    {
      title: '示例内容2',
      dataIndex: 'downloadUrl',
    },
    {
      title: '操作',
      width: 180,
      render: (r, _, t) => {
        // t.triggerAction('toggleOpen', r) 来触发handleAction 可传入 action类型和数据
        // t.remove([r.id]) 来执行删除项的请求等方法
        return (
          <FatTable.Actions className="Container__TagGroup">
            <FatTable.Action onClick={() => t.remove([r.id])}>
              删除
            </FatTable.Action>
            <FatTable.Action onClick={() => t.triggerAction('actionType', r)}>
              启用
            </FatTable.Action>
          </FatTable.Actions>
        )
      }
    }
  ]

 const handleAction = (async (name, data, t) => {
    switch (name) {
      case 'actionType':
        // do something
        break
    }
  }

  return (
      <FatTable
        enableSelect
        enablePersist={false}
        columns={column}
        header={renderHeader}
        headerExtra={renderHeaderExtra}
        onRemove={handleRemove}
        onFetch={handleFetch}
        onAction={handleAction}
      />
  )
}


```

- ### UserSelect
  员工选择的组件
  首先在路由定义处使用 UserSelectProvider，为有需要使用的路由提供组件服务。

```jsx
import { UserSelectProvider } from '@gdjiami/rc-components/es/user-select'

;<UserSelectProvider adaptor={adaptor}>
  <Route path="/static" exact component={Comp} />
</UserSelectProvider>
```

随后需定义 UserSelectAdaptor.tsx（一般和 Route.tsx 同层）

```tsx
import {
  UserSelectAdaptor,
  DepartmentDesc,
  UserDesc,
  TenementDesc,
} from '@gdjiami/rc-components/es/user-select'
import { DepartmentSearchResult } from '@gdjiami/rc-components/es/user-select/Provider'
import rpc from '~/rpc'

interface DepartmentTreeItem {
  children?: DepartmentTreeItem[]
  departmentId: string
  departmentName: string
  tenementId: string
  fullPath: string
  parentIds: string[]
}

const Adaptor: UserSelectAdaptor = {
  /**
   * 获取部门树
   */
  async getDepartmentTree(tenementId: string): Promise<DepartmentDesc> {
    const res = await rpc.request<{ items: DepartmentTreeItem[] }>(
      'org.department.getTree',
      {
        tenementId,
        fetchFullPath: true,
      },
    )
    const items = res.items.map(
      ({ departmentId: id, departmentName: name, ...others }) =>
        ({ id, name, ...others } as DepartmentDesc),
    )
    return items[0]
  },

  async getDepartmentChildren(tenementId: string, departmentId: string) {
    const res = await rpc.request<{ items: DepartmentTreeItem[] }>(
      'org.department.getTree',
      {
        tenementId,
        parentId: departmentId,
        fetchFullPath: true,
      },
    )
    const items = res.items.map(
      ({ departmentId: id, departmentName: name, ...others }) =>
        ({ id, name, ...others } as DepartmentDesc),
    )
    return items
  },

  /**
   * 获取部门成员
   */
  async getDepartmentUsers(
    tenementId: string,
    departmentId: string,
    page: number,
    pageSize: number,
  ): Promise<{ items: UserDesc[]; total: number }> {
    return { items: [], total: 0 }
  },

  /**
   * 用户搜索
   * tenementId不为空时，表示企业内搜索
   */
  async searchUser(
    query: string,
    page: number,
    pageSize: number,
    tenementId?: string,
  ): Promise<{ items: UserDesc[]; total: number }> {
    const params = {
      key: query,
      startIndex: (page - 1) * pageSize,
      resultRows: pageSize,
      tenementId,
    }
    const res = await rpc.request<{
      items: Array<UserDesc & { userId: string }>
      totalItems: number
    }>('user.search', params)
    return {
      items: res.items.map((i) => ({ ...i, id: i.userId })),
      total: res.totalItems,
    }
  },

  /**
   * 企业搜索
   */
  async searchTenement(
    query: string,
    page: number,
    pageSize: number,
  ): Promise<{ items: TenementDesc[]; total: number }> {
    const params = {
      searchKey: query,
      startIndex: (page - 1) * pageSize,
      resultRows: pageSize,
    }
    const res = await rpc.request<{
      items: Array<{ tenementId: string; tenementName: string }>
      totalItems: number
    }>('tenement.lists', params)
    return {
      items: res.items.map((item) => {
        const { tenementId, tenementName } = item
        return { id: tenementId, name: tenementName, extra: item }
      }),
      total: res.totalItems,
    }
  },

  async searchDepartment(
    query: string,
    page: number,
    pageSize: number,
    tenementId?: string,
  ) {
    const res = await rpc.request<{
      items: Array<{
        userCount: string
        parentId: string
        parentIds: string[]
        leaf: boolean
        departmentId: string
        departmentName: string
      }>
      totalItems: number
    }>('org.department.search', {
      tenementId,
      key: query,
      startIndex: (page - 1) * pageSize,
      resultRows: pageSize,
      fetchFullPath: true,
    })

    return {
      items: res.items.map(
        (i) =>
          ({
            ...i,
            id: i.departmentId,
            name: i.departmentName,
          } as DepartmentSearchResult),
      ),
      total: res.totalItems,
    }
  },

  async normalizeDepartmentChecked(
    currentSelected: DepartmentDesc[],
    added: DepartmentDesc[],
    removed: DepartmentDesc[],
  ): Promise<DepartmentSearchResult[]> {
    const map = (i: DepartmentDesc) => ({
      tenementId: i.tenement!.id,
      departmentId: i.id,
    })
    const params = {
      currentItems: currentSelected.map(map),
      addItems: added.map(map),
      delItems: removed.map(map),
    }
    const res = await rpc.request<{
      currentItems: Array<{
        userCount: string
        parentId: string
        parentIds: string[]
        leaf: boolean
        departmentId: string
        departmentName: string
      }>
    }>('org.department.selectedChange', params)
    return res.currentItems.map(
      (i) =>
        ({
          ...i,
          id: i.departmentId,
          name: i.departmentName,
        } as DepartmentSearchResult),
    )
  },

  async getDepartmentDetail(
    ids: string[],
    tenementId?: string,
  ): Promise<DepartmentSearchResult[]> {
    const params = {
      items: ids.map((i) => ({ tenementId, departmentId: i })),
    }
    const res = await rpc.request<{
      items: Array<{
        userCount: string
        parentId: string
        parentIds: string[]
        leaf: boolean
        departmentId: string
        departmentName: string
      }>
    }>('org.department.getDepartmentInfo', params)
    return res.items.map(
      (i) =>
        ({
          ...i,
          id: i.departmentId,
          name: i.departmentName,
        } as DepartmentSearchResult),
    )
  },
}

export default Adaptor
```

## Demo

run: yarn parcel -- ./components/AdminLayout/example/index.html

## License

This project is licensed under the terms of the
[MIT license](LICENSE).
