美文网首页
从零学习React+TS项目搭建(二)

从零学习React+TS项目搭建(二)

作者: WhoJun | 来源:发表于2024-04-18 15:57 被阅读0次

本文接着上篇文章,继续路由配置+登录拦截+ant样例+redux样例。

添加多个目录结构

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>) => {
  // 相当于给 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;
}

// 设置路由的地方
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结合的页面 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;

相关文章

网友评论

      本文标题:从零学习React+TS项目搭建(二)

      本文链接:https://www.haomeiwen.com/subject/drtexjtx.html