mirror of
https://github.com/HackPlan/uui-admin-template.git
synced 2026-04-29 20:16:10 +08:00
table demo page
This commit is contained in:
13
src/api/DataApi.ts
Normal file
13
src/api/DataApi.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
70
src/hooks/usePagination.ts
Normal file
70
src/hooks/usePagination.ts
Normal 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
44
src/hooks/usePromise.ts
Normal 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
53
src/hooks/useRequest.ts
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
128
src/pages/table/TableAdvanced.tsx
Normal file
128
src/pages/table/TableAdvanced.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
src/pages/table/TableBasic.tsx
Normal file
39
src/pages/table/TableBasic.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user