@gdjiami/rc-components

mygzb.com React Components

Usage no npm install needed!

<script type="module">
  import gdjiamiRcComponents from 'https://cdn.skypack.dev/@gdjiami/rc-components';
</script>

README

React Components

React 组件库, 收集了工作宝中后台应用的常用组件或套件. 致力于减少应用开发的代码重复,提高维护效率

DEMO


Installation

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 插件, 例如:

// 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 插件

使用示例

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
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 | 头部右侧内容

    // 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 一般用于业务子页面,里面直接添加页面内容

    <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 | 操作表格的统一方法

// 直接用就好啦
<FatTable
  enableSelect
  columns="{column}"
  header="{renderHeader}"
  headerExtra="{renderHeaderExtra}"
  onRemove="{handleRemove}"
  onFetch="{handleFetch}"
  onAction="{handleAction}"
/>

onAction 使用方法 和表格交互的重要途径

// 示例内容
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,为有需要使用的路由提供组件服务。
import { UserSelectProvider } from '@gdjiami/rc-components/es/user-select'

;<UserSelectProvider adaptor={adaptor}>
  <Route path="/static" exact component={Comp} />
</UserSelectProvider>

随后需定义 UserSelectAdaptor.tsx(一般和 Route.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.