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

TypeScript 复制代码
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

TypeScript 复制代码
.container{
  width: 200px;
 
  h1{
    font-size: 32px;
    color: #f7f7f7;
  }
}

QuestionCard.tsx

TypeScript 复制代码
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

TypeScript 复制代码
.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

TypeScript 复制代码
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

css 复制代码
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

css 复制代码
.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

css 复制代码
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

css 复制代码
.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

TypeScript 复制代码
// 首页
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设置,颜色可以参考渐变颜色,直接复制即可

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,可以自己命名

css 复制代码
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

css 复制代码
.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

TypeScript 复制代码
// 收藏问卷
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

TypeScript 复制代码
// 回收站页面
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

TypeScript 复制代码
// 未找到页面
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;
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax