table demo page

This commit is contained in:
ShinCurry
2020-07-13 18:53:03 +08:00
parent 8e7407e962
commit 9773170696
11 changed files with 421 additions and 12 deletions

13
src/api/DataApi.ts Normal file
View File

@@ -0,0 +1,13 @@
import { injectable, inject } from "inversify";
import { HttpService } from "../services/HttpService";
@injectable()
export class DataApi {
@inject(HttpService.name) httpService!: HttpService
async userList(query?: { name?: string; gender?: string; offset?: number; limit?: number }) {
const { data } = await this.httpService.axios.get('http://localhost:12345/userList', { params: query })
return data
}
}

View File

@@ -6,7 +6,7 @@ export interface CardProps {
export function Card(props: CardProps) {
return (
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-100">
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-100 mb-4">
{props.children}
</div>
)

View File

@@ -0,0 +1,70 @@
import { useMemo, useState, useRef } from 'react';
export interface IPagination {
offset: number
limit: number
count: number
}
export const EmptyPagination = () => ({
offset: 0,
limit: 50,
count: 0,
} as IPagination)
/**
* This hook used for Lsit or Table pagination manage.
*
* There are two states inside the hooks: `pagination` and `serverPagination`.
* `pagination` used for current pagination status,
* `serverPagination` used for next pagination status returned by the server.
*
* To change page, you should call `toNextPage`, `toPrevPage` or `toNthPage` to update offset.
* To update server returned pagination, you should call `setServerPagination`.
* @param _pagination initial pagination - Default: `EmptyPagination`
*/
export default function usePagination(_pagination: IPagination = EmptyPagination()) {
const [pagination, _setPagination] = useState<IPagination>(_pagination)
const [serverPagination, _setServerPagination] = useState<IPagination>(_pagination)
const setPagination = useRef(_setPagination).current
const setServerPagination = useRef(_setServerPagination).current
const prevOffset = useMemo(() => serverPagination.offset - serverPagination.limit, [serverPagination.offset, serverPagination.limit])
const nextOffset = useMemo(() => serverPagination.offset + serverPagination.limit, [serverPagination.offset, serverPagination.limit])
const hasPrevious = useMemo(() => prevOffset >= 0, [prevOffset])
const hasNext = useMemo(() => nextOffset < serverPagination.count, [nextOffset, serverPagination.count])
const currentPage = useMemo(
() => {
if (serverPagination.offset === 0) return 1
return serverPagination.offset && serverPagination.limit ? Math.floor(serverPagination.offset / serverPagination.limit) + 1 : 0
},
[serverPagination.offset, serverPagination.limit],
)
const totalPage = useMemo(() => serverPagination.limit ? Math.ceil(serverPagination.count / serverPagination.limit) : 0, [serverPagination.count, serverPagination.limit])
const toNextPage = () => {
setPagination({ offset: nextOffset, limit: serverPagination.limit, count: serverPagination.count })
}
const toPrevPage = () => {
setPagination({ offset: Math.max(0, prevOffset), limit: serverPagination.limit, count: serverPagination.count })
}
const toNthPage = (n: number) => {
setPagination({ offset: Math.min((n-1) * serverPagination.limit, serverPagination.count), limit: serverPagination.limit, count: serverPagination.count })
}
return {
pagination,
setPagination,
serverPagination,
setServerPagination,
currentPage,
totalPage,
toNextPage,
toPrevPage,
toNthPage,
hasPrevious,
hasNext,
}
}

44
src/hooks/usePromise.ts Normal file
View File

@@ -0,0 +1,44 @@
import { useCallback, useState, useMemo } from 'react';
export type PromiseApi<A extends any[], T> = (...args: A) => Promise<T>
export type LoadingState = {
[key: string]: boolean
}
/**
* This hook used for network request status
*
* @param requestMethod network request method
*/
export function usePromise<A extends any[], D>(requestMethod: PromiseApi<A, D>) {
const [loading, setLoading] = useState<LoadingState>({})
const doRequest = useCallback((id: string = 'default') => {
return (...args: A) => {
return new Promise<D>((resolve, reject) => {
setLoading((value) => { return { ...value, [id]: true, default: true }})
requestMethod(...args)
.then((data) => {
resolve(data)
setLoading((value) => { return { ...value, [id]: false, default: false }})
}).catch((error) => {
reject(error)
setLoading((value) => { return { ...value, [id]: false, default: false }})
})
})
}
}, [requestMethod])
const defaultLoading = useMemo(() => {
return loading['default']
}, [loading])
const defaultDoRequest = useMemo(() => {
return doRequest()
}, [doRequest])
return {
state: [defaultLoading, loading],
methods: [defaultDoRequest, doRequest],
} as const
}

53
src/hooks/useRequest.ts Normal file
View File

@@ -0,0 +1,53 @@
import { useCallback, useState, useRef, useEffect } from 'react';
export type RequestApi<A extends any[], T> = (...args: A) => Promise<T>
/**
* This hook used for network request status
*
* @param requestMethod network request method
*/
export function useRequest<A extends any[], D>(requestMethod: RequestApi<A, D>, initialValue?: D) {
const [data, setData] = useState<D | undefined>(initialValue)
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<any>()
const isMounted = useRef<boolean | null>(true)
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false }
}, []);
const doRequest = useCallback((...args: A) => {
setLoading(true)
setError(undefined)
requestMethod(...args)
.then((data) => {
if (isMounted.current) {
setData(data)
}
}).catch((error) => {
if (isMounted.current) {
setError(error)
}
}).finally(() => {
if (isMounted.current) {
setLoading(false)
}
})
/**
* Deprecated.
* Since we use isMounted ref to manage hooks cleanup,
* returning cleanup function of doRequest is unnecessary.
* It's safe to remove this line when we update all useRequest.doRequest cleanup usage
*/
return () => {}
}, [requestMethod])
return {
data: [data, setData],
state: [loading, error],
methods: [doRequest]
} as const
}

View File

@@ -1,8 +1,10 @@
import { Container } from "inversify";
import { AuthApi } from "./api/AuthApi";
import { HttpService } from "./services/HttpService";
import { DataApi } from "./api/DataApi";
const container = new Container();
container.bind<AuthApi>(AuthApi.name).to(AuthApi);
container.bind<HttpService>(HttpService.name).to(HttpService);
container.bind<DataApi>(DataApi.name).to(DataApi);
export default container;

View File

@@ -1,12 +1,30 @@
import Mock from 'mockjs';
import MockAdapter from 'axios-mock-adapter';
import { AxiosInstance } from 'axios';
import { range, random } from 'lodash';
export default Mock.mock('http://localhost:12345/login', 'post', {
'token': Mock.Random.word(16),
const userListCount = 123
const userListData = Mock.mock({
rows: range(1, userListCount).map(() => {
return Mock.mock({
name: Mock.Random.cname(),
gender: ['男', '女'][random(0, 1)],
age: Mock.Random.integer(12, 65),
province: Mock.Random.province(),
city: Mock.Random.city(),
website: Mock.Random.url('http', 'uui.cool')
})
}),
userListCount,
})
export const setupMock = (axios: AxiosInstance) => {
Mock.setup({
timeout: '200-600',
})
const mock = new MockAdapter(axios)
mock.onPost('/login').reply(200, Mock.mock({
@@ -17,4 +35,26 @@ export const setupMock = (axios: AxiosInstance) => {
email: Mock.Random.email(),
}
}))
mock.onGet('/userList').reply((config) => {
const offset = config.params?.offset || 0
const limit = config.params?.limit || 30
const name = config.params?.name
const gender = config.params?.gender
const rows = userListData.rows.filter((i: any) => {
if (name && !i.name.includes(name)) return false
if (gender && i.gender !== gender) return false
return true
})
return [
200,
{
...userListData,
rows: rows.slice(offset, offset + limit),
offset, limit,
count: rows.length,
},
]
})
}

View File

@@ -30,20 +30,20 @@ export const navigations: Navigation[] = [
]
},
{
key: 'list',
label: '表页',
key: 'table',
label: '表页',
icon: <Icons.List />,
subs: [
{
key: 'listBasic',
label: '基础表',
path: '/list/basic',
key: 'tableBasic',
label: '基础表',
path: '/table/basic',
},
{
key: 'listAdvanced',
label: '高级表',
path: '/list/advanced',
}
key: 'tableAdvanced',
label: '高级表',
path: '/table/advanced',
},
]
},
{

View File

@@ -0,0 +1,128 @@
import { Table, TableColumn, TextField, Select, Button, Pagination } from '@hackplan/uui';
import React, { useState, useEffect } from 'react';
import { Card } from '../../components/Card';
import { Page } from '../../components/Page';
import { useInject } from '../../hooks/useInject';
import { DataApi } from '../../api/DataApi';
import { omit } from 'lodash';
export function TableAdvanced() {
const [name, setName] = useState('')
const [gender, setGender] = useState<'男' | '女' | null>(null)
const [selectedIndexes, setSelectedIndexes] = useState<number[]>([])
const [pagination, setPagination] = useState({
offset: 0,
limit: 10,
count: 0,
})
const dataApi = useInject(DataApi)
const [users, setUsers] = useState<any[]>([])
const getUserList = async () => {
dataApi.userList({
...pagination,
name: name || undefined,
gender: gender || undefined,
}).then((data) => {
console.log(data)
setUsers(data.rows)
setPagination(omit(data, 'rows') as any)
})
}
useEffect(() => {
getUserList()
}, [pagination.offset])
const columns: TableColumn[] = [
{ title: '姓名' },
{ title: '性别' },
{ title: '年龄' },
{ title: '省份' },
{ title: '城市' },
{ title: '网站' },
{ title: '操作' },
]
const rows = users.map((i: any) => {
return [
i.name,
i.gender,
i.age,
i.province,
i.city,
i.website,
<div key="action">
<Button></Button>
</div>
]
})
return (
<Page>
<Card>
<div className="flex flex-row items-center">
<LabeledControl>
<Label></Label>
<TextField className="w-64" value={name} onChange={(value) => setName(value)} />
</LabeledControl>
<LabeledControl>
<Label></Label>
<Select
value={gender}
onChange={(value) => { setGender(value) }}
options={[
{ label: '男', value: '男' },
{ label: '女', value: '女' },
]}
/>
</LabeledControl>
<div className="ml-4">
<Button onClick={() => { getUserList() }}></Button>
</div>
</div>
</Card>
<Card>
<Table
columns={columns}
rows={rows}
selectedIndexes={selectedIndexes}
onSelected={(value) => {
setSelectedIndexes(value)
}}
></Table>
<div className="mt-4 flex flex-row justify-end">
<Pagination
value={pagination}
onChange={(value) => {
setPagination(value);
}}
>
<Pagination.PageInfo />
<Pagination.PagePrevButton />
<Pagination.PageList />
<Pagination.PageNextButton />
</Pagination>
</div>
</Card>
</Page>
)
}
const LabeledControl = (props: any) => {
return (
<div className="flex row items-center">
{props.children}
</div>
)
}
const Label = (props: any) => {
return (
<label className="flex justify-end items-center mr-2 w-24 h-8">
{props.children}
</label>
)
}

View File

@@ -0,0 +1,39 @@
import { Table, TableColumn } from '@hackplan/uui';
import React, { useState } from 'react';
import { Card } from '../../components/Card';
import { Page } from '../../components/Page';
export function TableBasic() {
const [data] = useState([
{ title: '新小科技招聘会', type: '线下活动', time: '2020-03-13 13:00', location: '苏州市姑苏区' },
{ title: '多会体验活动', type: '线上活动', time: '2020-04-11 11:00', location: '' },
{ title: '有客测试活动', type: '线上活动', time: '2020-05-17 17:00', location: '' },
])
const columns: TableColumn[] = [
{ title: '活动名称' },
{ title: '活动类型' },
{ title: '活动时间' },
{ title: '活动地点' },
]
const rows = data.map((i) => {
return [
i.title,
i.type,
i.time,
i.location,
]
})
return (
<Page>
<Card>
<Table
columns={columns}
rows={rows}
></Table>
</Card>
</Page>
)
}

View File

@@ -10,6 +10,8 @@ import { RouterBreadcrumbRoutes } from './hooks/useRouterBreadcrumb';
import { NotFound } from './pages/error/NotFound';
import { Forbidden } from './pages/error/Forbidden';
import { ServerError } from './pages/error/ServerError';
import { TableBasic } from './pages/table/TableBasic';
import { TableAdvanced } from './pages/table/TableAdvanced';
export interface Route {
key: string;
@@ -49,6 +51,7 @@ export const routes: Route[] = [
</div>
),
},
// Form
{
key: 'FormBasic',
path: '/form/basic',
@@ -57,6 +60,23 @@ export const routes: Route[] = [
content: <FormBasic />,
breadcrumb: '基础表单',
},
// Table
{
key: 'TableBasic',
path: '/table/basic',
layout: MainLayout,
route: AuthenticatedRoute,
content: <TableBasic />,
breadcrumb: '基础表格',
},
{
key: 'TableAdvanced',
path: '/table/advanced',
layout: MainLayout,
route: AuthenticatedRoute,
content: <TableAdvanced />,
breadcrumb: '高级表格',
},
{
key: 'Forbidden',
path: '/error/403',