react-问卷星项目(6)

实战

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}>
        小木问卷 &copy; 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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值