從零學(xué)習(xí)React+TS項目搭建(二)

本文接著上篇文章栗精,繼續(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;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悲立,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子新博,更是在濱河造成了極大的恐慌薪夕,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赫悄,死亡現(xiàn)場離奇詭異原献,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)埂淮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門姑隅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人同诫,你說我怎么就攤上這事粤策。” “怎么了误窖?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秩贰。 經(jīng)常有香客問我霹俺,道長,這世上最難降的妖魔是什么毒费? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任丙唧,我火速辦了婚禮,結(jié)果婚禮上觅玻,老公的妹妹穿的比我還像新娘想际。我一直安慰自己,他們只是感情好溪厘,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布胡本。 她就那樣靜靜地躺著,像睡著了一般畸悬。 火紅的嫁衣襯著肌膚如雪侧甫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音披粟,去河邊找鬼咒锻。 笑死,一個胖子當(dāng)著我的面吹牛守屉,可吹牛的內(nèi)容都是我干的惑艇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼拇泛,長吁一口氣:“原來是場噩夢啊……” “哼敦捧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碰镜,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤兢卵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绪颖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秽荤,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年柠横,在試婚紗的時候發(fā)現(xiàn)自己被綠了窃款。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡牍氛,死狀恐怖晨继,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搬俊,我是刑警寧澤紊扬,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站唉擂,受9級特大地震影響餐屎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜玩祟,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一腹缩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧空扎,春花似錦藏鹊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至黑忱,卻和暖如春宴抚,著一層夾襖步出監(jiān)牢的瞬間勒魔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工菇曲, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留冠绢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓常潮,卻偏偏與公主長得像弟胀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喊式,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容