实战
React常用UI组件库
- Ant Design国内最常用组件库,稳定,强大
- Material UI国外流行
- TailWind UI 国外流行,收费
Ant Design
官网地址
这一章基本内容就是使用UI重构页面,也没有什么知识点,直接上代码
下载
npm install antd --save
安装icon组件包
npm install @ant-design/icons --save
router/index.tsx
// 导出常用路由
export const LOGIN_PATHNAME = "/login";
export const REGISTER_PATHNAME = "/register";
export const HOME_PATHNAME = "/home";
export const MANAGER_INDEX_PATHNAME = "/manager/list";
大致更新的顺序为组件 -> 布局 -> 页面
组件当前结构如下
Logo.tsx
import React, { FC } from "react";
import { Space, Typography } from "antd";
import { FormOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom";
import styled from "./Logo.module.scss";
const { Title } = Typography;
const Logo: FC = () => {
return (
<div className={styled.container}>
<Link to="/">
<Space>
<Title>
<FormOutlined />
</Title>
<Title>小木问卷</Title>
</Space>
</Link>
</div>
);
};
export default Logo;
Logo.module.scss
.container{
width: 200px;
h1{
font-size: 32px;
color: #f7f7f7;
}
}
QuestionCard.tsx
import React, { FC, useEffect } from "react";
// import "./QuestionCard.css";
import styled from "./QuestionCard.module.scss";
import { Button, Space, Divider, Tag, Popconfirm, Modal, message } from "antd";
import { useNavigate, Link } from "react-router-dom";
// import classnames from "classnames";
const { confirm } = Modal;
import {
EditOutlined,
LineChartOutlined,
StarOutlined,
CopyOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons";
type PropsType = {
_id: string;
title: string;
isPublished: boolean;
isStar: boolean;
answerCount: number;
createAt: string;
// 问号是可写可不写,跟flutter语法相似
deletQuestion?: (id: string) => void;
pubQuestion?: (id: string) => void;
};
const QuestionCard: FC<PropsType> = (props: PropsType) => {
const { _id, title, createAt, answerCount, isPublished, isStar } = props;
const nav = useNavigate();
// const {confi} = Modal();
function duplicate() {
message.success("执行复制");
// alert("执行复制");
}
function del() {
confirm({
title: "确定删除该问卷?",
icon: <ExclamationCircleOutlined />,
onOk: () => message.success("删除成功!"),
});
}
return (
<div className={styled.container}>
<div className={styled.title}>
<div className={styled.left}>
<Link
to={
isPublished ? `/question/static/${_id}` : `/question/edit/${_id}`
}
>
<Space>
{isStar && <StarOutlined style={{ color: "red" }} />}
{title}
</Space>
</Link>
</div>
<div className={styled.right}>
<Space>
{isPublished ? (
<Tag color="processing">已发布</Tag>
) : (
<Tag>未发布</Tag>
)}
<span>答卷:{answerCount}</span>
<span>{createAt}</span>
</Space>
</div>
</div>
<Divider style={{ margin: "12px" }} />
<div className={styled["button-container"]}>
<div className={styled.left}>
<Space>
<Button
icon={<EditOutlined />}
type="text"
size="small"
onClick={() => nav(`/question/edit/${_id}`)}
>
统计问卷
</Button>
<Button
icon={<LineChartOutlined />}
type="text"
size="small"
onClick={() => nav(`/question/static/${_id}`)}
disabled={!isPublished}
>
问卷统计
</Button>
</Space>
</div>
<div className={styled.right}>
<Space>
<Button type="text" icon={<StarOutlined />} size="small">
{isStar ? "取消标星" : "标星"}
</Button>
<Popconfirm
title="确定复制该问卷?"
okText="确定"
cancelText="取消"
onConfirm={duplicate}
>
<Button type="text" icon={<CopyOutlined />} size="small">
复制
</Button>
</Popconfirm>
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
onClick={del}
>
删除
</Button>
</Space>
</div>
</div>
</div>
);
};
export default QuestionCard;
QuestionCard.module.scss
.container{
margin-bottom: 20px;
padding: 12px;
border-radius: 3px;
background-color: white;
&:hover{
box-shadow: 0 4px 10px lightgray;
}
}
.title{
display: flex;
.left{
flex: 1;
}
.right{
flex: 1;
text-align: right;
}
}
.button-container{
display: flex;
.left{
flex: 1;
}
.right{
flex: 1;
text-align: right;
button{
color: #999;
}
}
}
UserInfo.tsx
import React, { FC } from "react";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router";
const UserInfo: FC = () => {
return (
<>
<Link to={LOGIN_PATHNAME}>登录</Link>
</>
);
};
export default UserInfo;
布局这一块的样式我和视频里老师的写法不一致,能实现效果就行
MainLayout.tsx
import React, { FC } from "react";
import { Outlet } from "react-router-dom";
import { Layout } from "antd";
import styled from "./MainLayout.module.scss";
import Logo from "../components/Logo";
import UserInfo from "../components/UserInfo";
const { Header, Content, Footer } = Layout;
const MainLayout: FC = () => {
return (
<Layout>
<Header className={styled.header}>
<div className={styled.left}>
<Logo />
</div>
<div className={styled.right}>
<UserInfo />
</div>
</Header>
<Layout className={styled.main}>
<Content>
<Outlet />
</Content>
</Layout>
{/* <Content className={styled.main}>
<Outlet />
</Content> */}
<Footer className={styled.footer}>
小木问卷 © 2023-present. Created by 双
</Footer>
</Layout>
);
};
export default MainLayout;
MainLayout.module.scss
.header{
height: auto;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
.left{
float: left;
}
.right{
float: right;
}
}
.main{
// 减去的分别是header和footer的高度
min-height: calc(100vh - 64px - 71px);
}
.footer{
text-align: center;
background-color: #f7f7f7;
border-top: 1px solid #e8e8e8;
}
ManagerLayout.tsx
import React, { FC } from "react";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import styled from "./MangerLayout.module.scss";
import { Button, Space, Divider } from "antd";
import {
PlusOutlined,
BarsOutlined,
StarOutlined,
DeleteOutlined,
} from "@ant-design/icons";
const MangerLayout: FC = () => {
const nav = useNavigate();
const { pathname } = useLocation();
console.log(pathname);
return (
<div className={styled.container}>
<div className={styled.left}>
<Space direction="vertical">
<Button type="primary" size="large" icon={<PlusOutlined />}>
新建问卷
</Button>
<Divider style={{ border: "none" }} />
<Button
type={pathname.startsWith("/manager/list") ? "default" : "text"}
size="large"
icon={<BarsOutlined />}
onClick={() => nav("/manager/list")}
>
我的问卷
</Button>
<Button
type={pathname.startsWith("/manager/star") ? "default" : "text"}
size="large"
icon={<StarOutlined />}
onClick={() => nav("/manager/star")}
>
星标问卷
</Button>
<Button
type={pathname.startsWith("/manager/trash") ? "default" : "text"}
size="large"
icon={<DeleteOutlined />}
onClick={() => nav("/manager/trash")}
>
回收站
</Button>
</Space>
</div>
<div className={styled.right}>
<Outlet />
</div>
</div>
);
};
export default MangerLayout;
ManagerLayout.module.scss
.container{
display: flex;
padding: 24px 0;
width: 1200px;
margin: 0 auto; // 水平居中
.left{
width: 120px;
}
.right{
flex: 1;
margin-left: 60px;
}
}
接下来就是零散的各个页面的布局,下面的图片是管理问卷的大概样式设置,可以按照登录时看到的页面顺序进行样式更新,比如Home -> List -> Star -> Trash,大部分从antd导入的都是文档中能看到的组件,直接用了看效果就行,想更进一步了解的可以去翻阅相关的文档
Home.tsx
// 首页
import React, { FC } from "react";
import { useNavigate, Link } from "react-router-dom";
import { Button, Typography } from "antd";
// router中导出的设置好的路径
import { MANAGER_INDEX_PATHNAME } from "../router";
import styled from "./Home.module.scss";
// Typography可以理解为文章组件,可以分解出标题段落等,经常使用
const { Title, Paragraph } = Typography;
const Home: FC = () => {
const nav = useNavigate();
// function clickHandler() {
// nav({
// pathname: "/login", // 路径
// search: "b=21", // 路径附加参数,类似get
// });
// }
return (
<div className={styled.contain}>
<div className={styled.info}>
<Title>问卷调查|在线投票</Title>
<Paragraph>已累计创建问卷100份,发布问卷90份,收到答卷989份</Paragraph>
<div>
<Button type="primary" onClick={() => nav(MANAGER_INDEX_PATHNAME)}>
开始使用
</Button>
</div>
</div>
</div>
);
};
export default Home;
Home.module.scss
其中background-image就是一个渐变的css设置,颜色可以参考渐变颜色,直接复制即可
.contain{
background-color: aqua;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-image: linear-gradient(to top, #5ee7df 0%, #b490ca 100%);
// background-image: linear-gradient(to top, #9890e3 0%, #b1f4cf 100%);
.info{
text-align: center;
button{
height: 60px;
font-size: 24px;
}
}
}
Manager/list.tsx
注意Home界面的跳转导入的路径就是这个list,可以自己命名
import React, { FC, useState } from "react";
import { useSearchParams } from "react-router-dom";
import QuestionCard from "../../components/QuestionCard";
import styled from "./Common.module.scss";
import { Typography } from "antd";
import { useTitle } from "ahooks";
const { Title } = Typography;
const rawQuestionList = [
{
_id: "q1",
title: "问卷1",
isPublished: true,
isStar: false,
answerCount: 5,
createAt: "3月10日 13:23",
},
{
_id: "q2",
title: "问卷2",
isPublished: false,
isStar: true,
answerCount: 15,
createAt: "3月22日 13:23",
},
{
_id: "q3",
title: "问卷3",
isPublished: true,
isStar: true,
answerCount: 100,
createAt: "4月10日 13:23",
},
{
_id: "q4",
title: "问卷4",
isPublished: false,
isStar: false,
answerCount: 98,
createAt: "3月23日 13:23",
},
];
const List: FC = () => {
// const [searchParams] = useSearchParams();
// console.log("keyword", searchParams.get("keyword"));
useTitle("小木问卷-我的问卷");
const [questionList, setQuestionList] = useState(rawQuestionList);
return (
<>
<div className={styled.header}>
<div className={styled.left}>
// level 3表示h3
<Title level={3}>我的问卷</Title>
</div>
<div className={styled.right}>搜索</div>
</div>
<div className={styled.content}>
{/* {问卷列表} */}
{questionList.length > 0 &&
questionList.map((q) => {
const { _id } = q;
return <QuestionCard key={_id} {...q} />;
})}
</div>
<div className={styled.footer}>loadMore 上划加载更多</div>
</>
);
};
export default List;
原先的List.module.scss改为Common.module.scss
.header{
display: flex;
.left{
flex: 1;
}
.right{
flex: 1;
text-align: right;
}
}
.content{
margin-bottom: 20px;
}
.footer{
text-align: center;
}
body{
background-color: #f1f1f1;
}
Star.tsx
// 收藏问卷
import React, { FC, useState } from "react";
import QuestionCard from "../../components/QuestionCard";
import styled from "./Common.module.scss";
import { Typography, Empty } from "antd";
import { useTitle } from "ahooks";
const { Title } = Typography;
const rawQuestionList = [
{
_id: "q1",
title: "问卷1",
isPublished: true,
isStar: true,
answerCount: 5,
createAt: "3月10日 13:23",
},
{
_id: "q2",
title: "问卷2",
isPublished: false,
isStar: true,
answerCount: 15,
createAt: "3月22日 13:23",
},
{
_id: "q3",
title: "问卷3",
isPublished: true,
isStar: true,
answerCount: 100,
createAt: "4月10日 13:23",
},
];
const Star: FC = () => {
useTitle("小木问卷-星标问卷");
const [questionList, setQuestionList] = useState(rawQuestionList);
return (
<>
<div className={styled.header}>
<div className={styled.left}>
<Title level={3}>星标问卷</Title>
</div>
<div className={styled.right}>搜索</div>
</div>
<div className={styled.content}>
{/* {问卷列表} */}
{questionList.length === 0 && <Empty description="暂无数据" />}
{questionList.length > 0 &&
questionList.map((q) => {
const { _id } = q;
return <QuestionCard key={_id} {...q} />;
})}
</div>
<div className={styled.footer}>分页</div>
</>
);
};
export default Star;
Trash.tsx
// 回收站页面
import React, { FC, useState } from "react";
import QuestionCard from "../../components/QuestionCard";
import styled from "./Common.module.scss";
import { Typography, Empty, Table, Tag, Button, Space, Modal } from "antd";
import { useTitle } from "ahooks";
import { ExclamationOutlined } from "@ant-design/icons";
const { Title } = Typography;
const { confirm } = Modal;
const rawQuestionList = [
{
_id: "q1",
title: "问卷1",
isPublished: true,
isStar: false,
answerCount: 5,
createAt: "3月10日 13:23",
},
{
_id: "q2",
title: "问卷2",
isPublished: false,
isStar: true,
answerCount: 15,
createAt: "3月22日 13:23",
},
{
_id: "q3",
title: "问卷3",
isPublished: true,
isStar: true,
answerCount: 100,
createAt: "4月10日 13:23",
},
{
_id: "q4",
title: "问卷4",
isPublished: false,
isStar: false,
answerCount: 98,
createAt: "3月23日 13:23",
},
];
// 表格列元素
const tableColumn = [
{
title: "标题",
dataIndex: "title",
// key:'title' // 循环列的key,会默认取dataIndex的值,dataIndex的值不重复可以不使用key
},
{
title: "是否发布",
dataIndex: "isPublished",
// 根据当前这一列根据数据源进行筛选,返回自定义的JSX片段
render: (isPublished: boolean) => {
return isPublished ? (
<Tag color="processing">已发布</Tag>
) : (
<Tag>未发布</Tag>
);
},
},
{
title: "答卷",
dataIndex: "answerCount",
},
{
title: "创建时间",
dataIndex: "createAt",
},
];
const Trash: FC = () => {
useTitle("小木问卷-回收站");
const [questionList, setQuestionList] = useState(rawQuestionList);
// 泛形定义数组类型
const [selectedId, setSelectedId] = useState<string[]>([]);
function del() {
confirm({
title: "确认彻底删除该问卷?",
icon: <ExclamationOutlined />,
content: "删除以后不可以找回,请谨慎操作!",
onOk: () => alert(JSON.stringify(selectedId)),
});
}
const TableElement = (
<>
<div style={{ marginBottom: "15px" }}>
<Space>
<Button type="primary" disabled={selectedId.length == 0}>
恢复
</Button>
<Button danger onClick={del}>
彻底删除
</Button>
</Space>
</div>
<Table
dataSource={questionList}
columns={tableColumn}
pagination={false}
// 告诉表格用什么属性作为key
rowKey={(q) => q._id}
// 多选框的设置,打印出来的是选择的条数id,将选择条数id赋值给当前选择变量
rowSelection={{
type: "checkbox",
onChange: (selectedRowKey) => {
// selectedRowKey打印出的选中的key
// as强制认为是数组类型
setSelectedId(selectedRowKey as string[]);
},
}}
></Table>
</>
);
return (
<>
<div className={styled.header}>
<div className={styled.left}>
<Title level={3}>回收站</Title>
</div>
<div className={styled.right}>搜索{selectedId}</div>
</div>
<div className={styled.content}>
{/* {问卷列表} */}
{questionList.length === 0 && <Empty description="暂无数据" />}
{questionList.length > 0 && TableElement}
</div>
</>
);
};
export default Trash;
NotFound.tsx
// 未找到页面
import React, { FC } from "react";
import { Result, Button } from "antd";
import { useNavigate } from "react-router-dom";
import { MANAGER_INDEX_PATHNAME } from "../router";
const NotFound: FC = () => {
const nav = useNavigate();
return (
<Result
status="404"
title="404"
subTitle="抱歉,您访问的界面不存在"
extra={
<Button type="primary" onClick={() => nav(MANAGER_INDEX_PATHNAME)}>
返回标签页
</Button>
}
></Result>
);
};
export default NotFound;