react-问卷星项目(7)

实战

React表单组件

入门

重点在于change的时候改变state的值,类似vue的双向数据绑定v-model,即数据更新的时候页面同步更新,页面数据更新时数据源也能获得最新的值,只是Vue中设置在data中的属性默认绑定,React中需要state触发页面更新

使用原先较多测试组件的项目写基础

input组件

TypeScript 复制代码
import React, { useState, ChangeEvent } from "react";
// 上下两种引入方式都可以
// import type { ChangeEvent } from "react";

function App() {
  const [text, setText] = useState<string>("hello");
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    // event.target.value就是当前input的值
    setText(event.target.value);
  }

  return (
    <div>
      <input defaultValue={text} onChange={handleChange} />
      <button onClick={() => console.log(text)}>打印</button>
    </div>
  );
}

export default App;
  • 受控组件:值同步到state,使用value属性,可获可控
  • 非受控组件:值不同步到state,使用defaultValue属性,能够设置默认值,但是无法获得更改后最新的数值
  • React推荐使用受控组件,看似繁琐(没有规律),但更加可控(有规律,可读)

textarea组件

TypeScript 复制代码
function App() {
  const [text, setText] = useState<string>("hello");
  function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {
    // event.target.value就是当前input的值
    setText(event.target.value);
  }

  function genHtml() {
    return { __html: text.replaceAll("\n", "<br>") };
  }

  return (
    <div>
      {/* <input defaultValue={text} onChange={handleChange} /> */}
      <textarea value={text} onChange={handleChange}></textarea>
      {/* {text.replaceAll("\n", "<br>")} */}
      {/* 上面这个方法中为了防止XSS注入,React会将br换成明文展示在页面而不是换行,可以通过下列方式解决 */}
      <p dangerouslySetInnerHTML={genHtml()}></p>
    </div>
  );
}

export default App;

radio单选框

TypeScript 复制代码
function App() {
  const [gender, setGender] = useState("male");
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    // event.target.value就是当前input的值
    setGender(event.target.value);
  }

  return (
    <div>
      <label htmlFor="radio1">男</label>
      <input
        type="radio"
        id="radio1"
        name="gender"
        value="male"
        checked={gender == "male"}
        onChange={handleChange}
      />
      <label htmlFor="radio2">女</label>
      <input
        type="radio"
        id="radio2"
        name="gender"
        value="female"
        checked={gender === "female"}
        onChange={handleChange}
      />
      <button onClick={() => console.log(gender)}>打印</button>
    </div>
  );
}

export default App;

checkbox复选

TypeScript 复制代码
function App() {
  const [selectedList, setSelectedList] = useState<string[]>([]);

  function handleCityChange(event: ChangeEvent<HTMLInputElement>) {
    // event.target.value就是当前input的值
    const city = event.target.value;
    if (selectedList.includes(city)) {
      setSelectedList(
        selectedList.filter((c) => {
          if (c == city) return false;
          return true;
        }),
      );
    } else {
      // 添加
      setSelectedList(selectedList.concat(city));
    }
  }

  return (
    <>
      {/* htmlFor 点击的时候也会触发切换 */}
      <label htmlFor="checkbox1">北京</label>
      <input
        type="checkbox"
        id="checkbox1"
        value="beijing"
        checked={selectedList.includes("beijing")}
        onChange={handleCityChange}
      />
      <label htmlFor="checkbox2">上海</label>
      <input
        type="checkbox"
        id="checkbox2"
        value="shanghai"
        checked={selectedList.includes("shanghai")}
        onChange={handleCityChange}
      />
      <label htmlFor="checkbox3">深圳</label>
      <input
        type="checkbox"
        id="checkbox3"
        value="shenzhen"
        checked={selectedList.includes("shenzhen")}
        onChange={handleCityChange}
      />
      {/* 方便表单获取和提交 */}
      <input
        type="hidden"
        name="cityInput"
        value={JSON.stringify(selectedList)}
      />
    </>
  );
}

export default App;

select下拉框

TypeScript 复制代码
function App() {
  const [lang, setLang] = useState("js");

  function handleChange(event: ChangeEvent<HTMLSelectElement>) {
    // event.target.value就是当前input的值
    setLang(event.target.value);
  }

  return (
    <>
      {/* 不设置state的话选择了没反应 */}
      <select value={lang} onChange={handleChange}>
        <option value="java">Java</option>
        <option value="C++">C++</option>
        <option value="python">Python</option>
      </select>
    </>
  );
}

form表单组件

TypeScript 复制代码
function App() {
  function handleSubmit(event: ChangeEvent<HTMLFormElement>) {
    event.preventDefault(); // 阻止默认行为,即不会提交到action
    // 可以自行处理提交的逻辑
  }

  return (
    <>
      {/* 点击提交后会将相应的数据发送到action中填写的接口。提交的就是name和value */}
      {/* 没有name,没有value的话会导致提交无法识别 */}
      {/* 隐藏input(type:hidden)的价值就在于可以把想要提交的数据偷偷提交上去 */}
      {/* onSubmit提交前调用的钩子函数,可以阻止默认行为,不然点击就直接提交了 */}
      <form action="/api/post" onSubmit={handleSubmit}>
        <input />
        <br />
        <textarea />
        <input type="hidden" />
        <button type="submit">提交</button>
      </form>
    </>
  );
}

Ant Design 实现

为什么地址要添加参数,避免刷新的时候搜索数据丢失,不保存的话一刷新页面内容就重置了,保存了之后就算是刷新,由于地址附有参数,刷新时地址不会改变,所以仍能展示出搜索后的数据

同时也是为了避免组件之间的耦合,最好不要一搜索就更新列表组件或者一分页就更新列表组件,而是通过一个更加公共的比如地址栏进行搜索或者分页信息的传递,也能避免刷新后数据无法保存,比如搜索栏一刷新原来的搜索词就没有了,但是可以通过从路径获取参数来达到"保存"的效果

新建constants文件夹,其中设置index.tsx文件保存常用变量,跟router中导出常用变量类似

// 存储所有的常量
export const LIST_SEARCH_PARAM_KEY = "keyword";
搜索栏组件

ListSearch.tsx

TypeScript 复制代码
import React, { FC, useEffect, useState } from "react";
import { Input } from "antd";
import type { ChangeEvent } from "react";
import { useNavigate, useLocation, useSearchParams } from "react-router-dom";
import { LIST_SEARCH_PARAM_KEY } from "../constants";

const { Search } = Input;

const ListSearch: FC = () => {
  const [val, setVal] = useState("");
  const nav = useNavigate();
  const { pathname } = useLocation();

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setVal(event.target.value);
  }
  function handleSearch(value: string) {
    // 跳转页面增加URL参数
    nav({
      pathname,
      search: `${LIST_SEARCH_PARAM_KEY}=${value}`,
    });
  }
  // 获取url参数,并设置到input value
  const [searchParams] = useSearchParams();
  useEffect(() => {
    // 每当searchParams有变化就执行函数
    // serchParams用来获得上面nav中设置的参数
    const newVal = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";
    setVal(newVal);
  }, [searchParams]);
  return (
    <Search
      allowClear
      placeholder="请输入关键字"
      value={val}
      onChange={handleChange}
      onSearch={handleSearch}
      style={{ width: "260px" }}
    />
  );
};
export default ListSearch;
开发注册页

Register.module.scss

css 复制代码
.contain{
  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%);
}

Register.tsx

TypeScript 复制代码
import React, { FC } from "react";
import { Typography, Space, Form, Input, Button } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import styled from "./Register.module.scss";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router";

const { Title } = Typography;

const Register: FC = () => {
  // any表示任意类型都可
  const onFinish = (values: any) => {
    console.log(values);
  };

  return (
    <div className={styled.contain}>
      <div>
        <Space>
          <Title level={2}>
            <UserAddOutlined />
          </Title>
          <Title level={2}>注册新用户</Title>
        </Space>
      </div>
      <div>
        <Form
          labelCol={{ span: 6 }}
          wrapperCol={{ span: 16 }}
          onFinish={onFinish}
        >
          <Form.Item label="用户名" name="userName">
            <Input />
          </Form.Item>
          <Form.Item label="密码" name="passWord">
            <Input.Password />
          </Form.Item>
          <Form.Item label="确认密码" name="conFirm">
            <Input.Password />
          </Form.Item>
          <Form.Item label="昵称" name="nickName">
            <Input />
          </Form.Item>
          <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
            <Space>
              {/* htmlType就是之前html里的type,只是前面设置属性被占用了,用这个一样的效果都是为了触发方法 */}
              <Button type="primary" htmlType="submit">
                注册
              </Button>
            </Space>
            <Link to={LOGIN_PATHNAME}>已有账户,登录</Link>
          </Form.Item>
        </Form>
      </div>
    </div>
  );
};
export default Register;
开发登录页

Login.module.scss

css 复制代码
.contain{
  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%);
}

Login.tsx

TypeScript 复制代码
// 登陆页面
import React, { FC, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Space, Typography, Form, Input, Button, Checkbox } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { REGISTER_PATHNAME } from "../router";
import styled from "./Login.module.scss";

const { Title } = Typography;

const USERNAME_KEY = "USERNAME";
const PASSWORD_KEY = "PASSWORD";

function rememberUser(username: string, password: string) {
  localStorage.setItem(USERNAME_KEY, username);
  localStorage.setItem(PASSWORD_KEY, password);
}

function deleteUser() {
  localStorage.removeItem(USERNAME_KEY);
  localStorage.removeItem(PASSWORD_KEY);
}

function getUser() {
  return {
    username: localStorage.getItem(USERNAME_KEY),
    password: localStorage.getItem(PASSWORD_KEY),
  };
}

const Login: FC = () => {
  const nav = useNavigate();

  // 第三方hook,即第三方提供的组件
  const [form] = Form.useForm();

  const onFinish = (values: any) => {
    const { username, password, remember } = values;
    if (remember) {
      rememberUser(username, password);
    } else {
      deleteUser();
    }
  };

  // 依赖不填写,默认在组件渲染完成后执行
  useEffect(() => {
    const { username, password } = getUser();
    // 这里如果不小心写成了setFieldValue会报错,差了个s,这个只能输入一个参数,有s的才能输入多个
    form.setFieldsValue({ username, password });
  }, []);

  return (
    <div className={styled.contain}>
      <div>
        <Space>
          <Title level={2}>
            <UserAddOutlined />
          </Title>
          <Title level={2}>用户登录</Title>
        </Space>
      </div>
      <div>
        <Form
          labelCol={{ span: 6 }}
          wrapperCol={{ span: 16 }}
          onFinish={onFinish}
          // 默认remember设置为true
          initialValues={{ remember: true }}
          // 将返回值关联起来,即使其变成受控组件
          form={form}
        >
          <Form.Item label="用户名" name="username">
            <Input />
          </Form.Item>
          <Form.Item label="密码" name="password">
            <Input.Password />
          </Form.Item>
          <Form.Item
            name="remember"
            valuePropName="checked"
            wrapperCol={{ offset: 6, span: 16 }}
          >
            {/* 表单需要有name和value才能提交,valuePropname就是将Checkbox的checked属性,即选中的属性(true || false)当作值 */}
            <Checkbox>记住我</Checkbox>
          </Form.Item>
          <Form.Item wrapperCol={{ offset: 6, span: 16 }}>
            <Space>
              <Button type="primary" htmlType="submit">
                登录
              </Button>
              <Link to={REGISTER_PATHNAME}>注册新用户</Link>
            </Space>
          </Form.Item>
        </Form>
      </div>
    </div>
  );
};
export default Login;
表单校验

先在注册里进行校验,使用的都是antd里form的功能,然后复制到登录就行

Register.tsx

TypeScript 复制代码
// 注册界面
import React, { FC } from "react";
import { Typography, Space, Form, Input, Button, message } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import styled from "./Register.module.scss";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router";

const { Title } = Typography;

const Register: FC = () => {
  // any表示任意类型都可
  const onFinish = (values: any) => {
    console.log(values);
  };

  return (
    <div className={styled.contain}>
      <div>
        <Space>
          <Title level={2}>
            <UserAddOutlined />
          </Title>
          <Title level={2}>注册新用户</Title>
        </Space>
      </div>
      <div>
        <Form
          labelCol={{ span: 6 }}
          wrapperCol={{ span: 16 }}
          onFinish={onFinish}
        >
          <Form.Item
            label="用户名"
            name="username"
            rules={[
              { required: true, message: "请输入用户名" },
              // string表示按长度计算区间范围,不然变成数字就成最小5最大20了
              {
                type: "string",
                min: 5,
                max: 20,
                message: "字符长度在5-20之间",
              },
              {
                pattern: /^\w+$/,
                message: "只能是字母数字下划线",
              },
            ]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            label="密码"
            name="password"
            // required表示必填项,会出现红点
            rules={[{ required: true, message: "请输入密码" }]}
          >
            <Input.Password />
          </Form.Item>
          <Form.Item
            label="确认密码"
            name="confirm"
            // 依赖password属性,password变化会重新触发验证
            dependencies={["password"]}
            rules={[
              { required: true, message: "请确认输入的密码" },
              // 这个校验传递进去的是一个函数
              ({ getFieldValue }) => ({
                validator(_, value) {
                  if (getFieldValue("password") === value) {
                    return Promise.resolve();
                  } else {
                    return Promise.reject(new Error("两次密码不一致"));
                  }
                },
              }),
            ]}
          >
            <Input.Password />
          </Form.Item>
          <Form.Item label="昵称" name="nickname">
            <Input />
          </Form.Item>
          <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
            <Space>
              {/* htmlType就是之前html里的type,只是前面设置属性被占用了,用这个一样的效果都是为了触发方法 */}
              <Button type="primary" htmlType="submit">
                注册
              </Button>
              <Link to={LOGIN_PATHNAME}>已有账户,登录</Link>
            </Space>
          </Form.Item>
        </Form>
      </div>
    </div>
  );
};
export default Register;

Login.tsx

TypeScript 复制代码
// 登陆页面
import React, { FC, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Space, Typography, Form, Input, Button, Checkbox } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { REGISTER_PATHNAME } from "../router";
import styled from "./Login.module.scss";

const { Title } = Typography;

const USERNAME_KEY = "USERNAME";
const PASSWORD_KEY = "PASSWORD";

function rememberUser(username: string, password: string) {
  localStorage.setItem(USERNAME_KEY, username);
  localStorage.setItem(PASSWORD_KEY, password);
}

function deleteUser() {
  localStorage.removeItem(USERNAME_KEY);
  localStorage.removeItem(PASSWORD_KEY);
}

function getUser() {
  return {
    username: localStorage.getItem(USERNAME_KEY),
    password: localStorage.getItem(PASSWORD_KEY),
  };
}

const Login: FC = () => {
  const nav = useNavigate();

  // 第三方hook,即第三方提供的组件
  const [form] = Form.useForm();

  const onFinish = (values: any) => {
    const { username, password, remember } = values;
    if (remember) {
      rememberUser(username, password);
    } else {
      deleteUser();
    }
  };

  // 依赖不填写,默认在组件渲染完成后执行
  useEffect(() => {
    const { username, password } = getUser();
    // 这里如果不小心写成了setFieldValue会报错,差了个s,这个只能输入一个参数,有s的才能输入多个
    form.setFieldsValue({ username, password });
  }, []);

  return (
    <div className={styled.contain}>
      <div>
        <Space>
          <Title level={2}>
            <UserAddOutlined />
          </Title>
          <Title level={2}>用户登录</Title>
        </Space>
      </div>
      <div>
        <Form
          labelCol={{ span: 6 }}
          wrapperCol={{ span: 16 }}
          onFinish={onFinish}
          // 默认remember设置为true
          initialValues={{ remember: true }}
          // 将返回值关联起来,即使其变成受控组件
          form={form}
        >
          <Form.Item
            label="用户名"
            name="username"
            rules={[
              { required: true, message: "请输入用户名" },
              // string表示按长度计算区间范围,不然变成数字就成最小5最大20了
              {
                type: "string",
                min: 5,
                max: 20,
                message: "字符长度在5-20之间",
              },
              {
                pattern: /^\w+$/,
                message: "只能是字母数字下划线",
              },
            ]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            label="密码"
            name="password"
            rules={[{ required: true, message: "请输入密码" }]}
          >
            <Input.Password />
          </Form.Item>
          <Form.Item
            name="remember"
            valuePropName="checked"
            wrapperCol={{ offset: 6, span: 16 }}
          >
            {/* 表单需要有name和value才能提交,valuePropname就是将Checkbox的checked属性,即选中的属性(true || false)当作值 */}
            <Checkbox>记住我</Checkbox>
          </Form.Item>
          <Form.Item wrapperCol={{ offset: 6, span: 16 }}>
            <Space>
              <Button type="primary" htmlType="submit">
                登录
              </Button>
              <Link to={REGISTER_PATHNAME}>注册新用户</Link>
            </Space>
          </Form.Item>
        </Form>
      </div>
    </div>
  );
};
export default Login;
相关推荐
SameX7 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_34 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito37 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录2 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js