本文接著上篇文章栗精,繼續(xù)路由配置+登錄攔截+ant
樣例+redux
樣例。
添加多個目錄結(jié)構(gòu)
src
├─ components
│ └─ withRouter.tsx
├─ router
│ └─ index.tsx
├─ pages
│ ├─ layout
│ │ ├─ index.scss
│ │ └─ index.tsx
│ ├─ home
│ │ └─ index.tsx
│ ├─ 404
│ │ └─ index.tsx
│ ├─ login
│ │ └─ index.tsx
│ ├─ table
│ │ └─ index.tsx
安裝路由
安裝依賴react-router-dom
這里用最新的v6版本
npm i react-router-dom
在src目錄添加自定義路由高階組件 withRouter.tsx
import { useParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import React, { ComponentType, PropsWithChildren } from 'react';
export interface WithRouterProps<T = ReturnType<typeof useParams>> extends PropsWithChildren {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
};
location: ReturnType<typeof useLocation>;
params: T;
searchParams: T;
navigate: ReturnType<typeof useNavigate>;
}
const withRouter = <P extends object>(Component: ComponentType<P>) => {
// 相當(dāng)于給 MyCard組件添加各種props屬性瞻鹏,還添加三個重要的屬性params,location,navigate
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const params = useParams();
const searchParams = useSearchParams();
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) =>
navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
navigate={navigate}
params={params}
searchParams={searchParams}
{...(props as P)}
/>
);
};
};
export default withRouter;
路由攔截和路由配置 router/index.tsx
import Login from '../pages/login/index';
import Layout from '../pages/layout/index';
import Home from '../pages/home/index';
import Other from '../pages/other/index';
import NotPage from '../pages/404/index';
import { useRoutes, Navigate, RouteObject } from 'react-router-dom';
import React from 'react';
function getItem(
path: RouteObject['path'],
element: RouteObject['element'],
children?: RouteObject['children']
): RouteObject {
return {
path,
element,
children
} as RouteObject;
}
// 設(shè)置路由的地方
const routers: RouteObject[] = [
getItem('/login', <Login></Login>),
getItem('/', <Layout></Layout>, [
getItem('/', <Home></Home>),
getItem('/other', <Other></Other>)
]),
getItem('*', <NotPage></NotPage>)
];
const isUserAuthenticated = () => {
const token = localStorage.getItem('token');
return token && token.length > 0;
};
const AuthRoute: React.FC<React.PropsWithChildren> = (props: React.PropsWithChildren) => {
if (!isUserAuthenticated()) {
return (
<div>
{props.children}
<Navigate to="/login" />
</div>
);
}
return <div>{props.children}</div>;
};
AuthRoute.displayName = 'AuthRoute';
const RouterInterceptor: React.FC = (props: React.PropsWithChildren) => {
const routerEls = useRoutes(routers);
return <AuthRoute>{routerEls}</AuthRoute>;
};
RouterInterceptor.displayName = 'RouterInterceptor';
export default RouterInterceptor;
登錄頁面login/index.tsx
import React from 'react';
import { Input, Form, Button, Space } from 'antd';
import withRouter, { WithRouterProps } from '@/components/withRouter';
const Login: React.FC<WithRouterProps> = (props: WithRouterProps) => {
const [form] = Form.useForm();
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
style: { maxWidth: 200 }
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 }
};
const onFinish = (values: { token: string }) => {
localStorage.setItem('token', values.token);
props.history.push('/');
};
const onReset = () => {
form.resetFields();
};
return (
<div>
<Form {...layout} form={form} onFinish={onFinish}>
<Form.Item name="token" label="Token" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item {...tailLayout}>
<Space>
<Button type="primary" htmlType="submit">
登錄
</Button>
<Button htmlType="button" onClick={onReset}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</div>
);
};
export default withRouter(Login);
主布局頁面layout/index.tsx
import React from 'react';
import type { MenuProps } from 'antd';
import { Layout, Menu } from 'antd';
import { Outlet } from 'react-router-dom';
import withRouter, { WithRouterProps } from '@/components/withRouter';
import { HomeOutlined, UnorderedListOutlined } from '@ant-design/icons';
import './index.scss';
const { Header, Content, Footer, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number];
function getItem(
label: React.ReactNode,
key: React.Key,
icon?: React.ReactNode,
children?: MenuItem[],
type?: 'group'
): MenuItem {
return {
key,
icon,
children,
label,
type
} as MenuItem;
}
class LayoutContent extends React.Component<WithRouterProps> {
state: Readonly<{
menuIndex: string;
items: MenuItem[];
collapsed: boolean;
contentMarginLeft: number;
}>;
constructor(props: WithRouterProps) {
super(props);
const { location } = this.props;
const menuItems: MenuItem[] = [
getItem('Home', '/', <HomeOutlined />),
getItem('Table', '/table', <UnorderedListOutlined />)
];
this.state = {
menuIndex: location.pathname,
items: menuItems,
collapsed: false,
contentMarginLeft: 200
};
}
onClick(index: { key: string }) {
console.log(index);
this.props.navigate(index.key);
}
onCollapse(value: boolean) {
this.setState({ collapsed: value });
this.setState({ contentMarginLeft: value ? 80 : 200 });
}
componentDidUpdate(
prevProps: Readonly<WithRouterProps>,
prevState: Readonly<object>,
snapshot?: any
): void {
if (this.props.location.pathname !== this.state.menuIndex) {
this.setState({ menuIndex: this.props.location.pathname });
}
}
render() {
return (
<Layout style={{ minHeight: '100vh' }} hasSider>
<Sider
collapsible
collapsed={this.state.collapsed}
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
bottom: 0
}}
onCollapse={(value) => this.onCollapse(value)}
>
<div className="demo-logo-vertical" />
<Menu
theme="dark"
onClick={this.onClick.bind(this)}
selectedKeys={[this.state.menuIndex]}
mode="inline"
items={this.state.items}
/>
</Sider>
<Layout style={{ marginLeft: this.state.contentMarginLeft }}>
<Header style={{ padding: 0, backgroundColor: '#fff' }} />
<Content style={{ margin: '0 16px' }}>
<Outlet />
</Content>
<Footer style={{ textAlign: 'center' }}>
Demo ?{new Date().getFullYear()} Created by Demo
</Footer>
</Layout>
</Layout>
);
}
}
export default withRouter(LayoutContent);
// index.scss
.demo-logo-vertical {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 6px;
}
首頁redux
樣例home/index.tsx
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, CounterState } from '@/features/counter/counterSlice';
import { Button } from 'antd';
const Home: React.FC = () => {
const count = useSelector((state: { counter: CounterState }) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h3>這個是redux的例子:</h3>
<div style={{ display: 'flex' }}>
<Button aria-label="Increment value" onClick={() => dispatch(increment())}>
Increment
</Button>
<div style={{ minWidth: '20px', lineHeight: '30px', textAlign: 'center' }}>{count}</div>
<Button aria-label="Decrement value" onClick={() => dispatch(decrement())}>
Decrement
</Button>
</div>
</div>
);
};
export default Home;
404/index.tsx
import React from 'react';
const NotPage: React.FC = () => {
return <div>404</div>;
};
export default NotPage;
ant Form + Table
結(jié)合的頁面 table/index.tsx
import React, { useState } from 'react';
import { Button, Table, Form, Row, Col, Space, Input } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType, TablePaginationConfig } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name'
},
{
title: 'Age',
dataIndex: 'age'
},
{
title: 'Address',
dataIndex: 'address'
}
];
const data: DataType[] = [];
for (let i = 0; i < 46; i++) {
data.push({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`
});
}
interface SearchFormFields {
label: string;
name: string;
element: any;
rule?: object[];
}
const fields: SearchFormFields[] = [
{
label: 'Name',
name: 'name',
element: <Input placeholder="請輸入" />
},
{
label: 'Age',
name: 'age',
element: <Input placeholder="請輸入" />
},
{
label: 'Address',
name: 'address',
element: <Input placeholder="請輸入" />
}
];
const TableList: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [loading, setLoading] = useState(false);
const [expand, setExpand] = useState(false);
const [form] = Form.useForm();
const getFields = () => {
const count = expand ? fields.length : 3;
const children = [];
for (let i = 0; i < count; i++) {
const element = fields[i];
children.push(
<Col span={8} key={i}>
<Form.Item name={element.name} label={element.label} rules={element.rule}>
{element.element}
</Form.Item>
</Col>
);
}
return children;
};
const pageSetting: TablePaginationConfig = {
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `Total ${total} items`
};
const start = () => {
setLoading(true);
// ajax request after empty completing
setTimeout(() => {
setSelectedRowKeys([]);
setLoading(false);
}, 1000);
};
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange
};
const formStyle: React.CSSProperties = {
maxWidth: 'none',
padding: 24
};
const onFinish = (values: any) => {
console.log('Received values of form: ', values);
};
const hasSelected = selectedRowKeys.length > 0;
return (
<div>
<Form form={form} name="advanced_search" style={formStyle} onFinish={onFinish}>
<Row gutter={24}>{getFields()}</Row>
<div style={{ textAlign: 'right' }}>
<Space size="small">
<Button type="primary" htmlType="submit">
Search
</Button>
<Button
onClick={() => {
form.resetFields();
}}
>
Clear
</Button>
{fields.length > 3 ? (
<a
style={{ fontSize: 12 }}
onClick={() => {
setExpand(!expand);
}}
>
<DownOutlined rotate={expand ? 180 : 0} /> Collapse
</a>
) : (
<span></span>
)}
</Space>
</div>
</Form>
<Table
pagination={pageSetting}
rowSelection={rowSelection}
columns={columns}
dataSource={data}
/>
</div>
);
};
export default TableList;