React组件 -- 手把手教你构建一个Form表

本文旨在记录React开发过程中通用表格的构造,本文的目的是为了手把手的教会React入门的同学构建自己的Form表单,也为了以后自己复用起来方便。如果你觉得不错或者有所收获的话,还望不惜赐赞!

1. Form表单组件架构搭建

0. 引入react依赖

tsx 复制代码
import React from "react";

1. 引入antd组件库

tsx 复制代码
import { Button, Form, Input } from "antd";

2. 引入@ant-design/icons图标

tsx 复制代码
import { LockOutlined, UserOutlined } from "@ant-design/icons";

3. 引入axios发送表单数据

tsx 复制代码
import axios from 'axios';

4. 引入css文件

tsx 复制代码
import "./FormDemo.css";

5. 写函数式组件的Props接口

tsx 复制代码
export interface FormProps {}

6. 创建函数式组件FormDemo并默认向外暴露出去

tsx 复制代码
const FormDemo = (props: FormProps) => {
  return (<></>)
}
export default FormDemo;

7. 创建Form表单组件的ref

tsx 复制代码
const formRef = React.useRef<typeof Form>();

8. 管理提交按钮的状态

tsx 复制代码
const [okState, setOkState] = React.useState(false);

9. 记录提交按钮的点击时间--节流用

tsx 复制代码
const [lastOperationTime, setLastOperationTime] = React.useState(0);

10. 创建提交失败的回调函数--失败信息提示

tsx 复制代码
const onSubmitFailed = React.useCallback(() => { }, [formRef] );

11. 创建表单内容发生变化的回调函数

tsx 复制代码
const onFormValuesChange = React.useCallback((changeValues: any, values: any) => { }, []);

12. 创建提交表单的回调函数

tsx 复制代码
const onFinish = React.useCallback((values: any) => { }, []);

13. 构造Form表单

tsx 复制代码
  return (
    <div id="my-form">
      <Form // 结构为Form -> Form.Item
        ref={formRef} // 表单标签的句柄
        name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
        layout={"vertical"} // 排列的方向是纵向放置的
        style={{ width: "100%" }} // 充满包裹容器
        size={"large"}
        onFinish={onFinish} // submit发生的回调
        onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
      >
      </Form>
    </div>
  )

14. 构造用户名表单元素:Form.Item>Input

tsx 复制代码
        {/* 用户名输入框 */}
        <Form.Item
          name={"userName"} // 用户名
          label={null} // 要么使用label要么使用input的placeHolder
          rules={[
            { required: true, message: 'Please input your username!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input
            style={{
              height: 40,
            }}
            prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
            placeholder={"请输入用户名..."} // 文字占位符
          />
        </Form.Item>

15. 构造输入密码表单元素:Form.Item>Input.Password

tsx 复制代码
        {/* 密码输入框 */}
        <Form.Item 
          name={"password"} 
          label={null} 
          rules={[
            { required: true, message: 'Please input your password!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input.Password
            style={{ height: 40 }}
            prefix={<LockOutlined />} // input输入框前面的小图标
            placeholder={"请输入密码..."} // 文字占位符
          />
        </Form.Item>

16. 构造登录按钮:Form.Item>Button

tsx 复制代码
        {/* 确认登录按钮 */}
        <Form.Item style={{ marginTop: 58 }}>
          <Button
            type={"primary"}
            style={{ width: "100%", height: 40 }}
            htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
            disabled={okState}
          >
            登录
          </Button>
        </Form.Item>

表单架构完整代码

tsx 复制代码
import React from "react";
import { Button, Form, Input, FormInstance } from "antd";
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import axios from 'axios';
import "./Form.css";

export interface FormProps { }


const FormDemo = (props: FormProps) => {
  const formRef = React.useRef<FormInstance<any> | null>();
  const [okState, setOkState] = React.useState(false);
  const [lastOperationTime, setLastOperationTime] = React.useState(0);
  const onSubmitFailed = React.useCallback(() => { }, [formRef]);
  const onFormValuesChange = React.useCallback((changeValues: any, values: any) => { }, []);
  const onFinish = React.useCallback((values: any) => { }, []);
  return (
    <div id="my-form">
      <Form // 结构为Form -> Form.Item
        ref={formRef} // 表单标签的句柄
        name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
        layout={"vertical"} // 排列的方向是纵向放置的
        style={{ width: "100%" }} // 充满包裹容器
        size={"large"}
        onFinish={onFinish} // submit发生的回调
        onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
      >
        {/* 用户名输入框 */}
        <Form.Item
          name={"userName"} // 用户名
          label={null} // 要么使用label要么使用input的placeHolder
          rules={[
            { required: true, message: 'Please input your username!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input
            style={{
              height: 40,
            }}
            prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
            placeholder={"请输入用户名..."} // 文字占位符
          />
        </Form.Item>

        {/* 密码输入框 */}
        <Form.Item 
          name={"password"} 
          label={null} 
          rules={[
            { required: true, message: 'Please input your password!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input.Password
            style={{ height: 40 }}
            prefix={<LockOutlined />} // input输入框前面的小图标
            placeholder={"请输入密码..."} // 文字占位符
          />
        </Form.Item>

        {/* 确认登录按钮 */}
        <Form.Item style={{ marginTop: 58 }}>
          <Button
            type={"primary"}
            style={{ width: "100%", height: 40 }}
            htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
          >
            登录
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

export default FormDemo;

2. Form表单组件细节构造

2.1 父组件调用此表单组件的时候应该传入一个回调函数,此回调函数接受一个布尔值state作为入参;state表示当前表单的内容是否符合rules, 通过这个回调函数可以将当前表单是否满足预置规则传递出去。

tsx 复制代码
// 补充接口的内容
export interface FormProps {
  formStateChangeCallback: (state: boolean) => void;
}
...
// 补充从props中解析出回调
const { formStateChangeCallback } = props;
...
// 表单的内容发生变化的时候,先校验,根据校验的结果修改状态,然后通过回调函数传递到父组件中
  const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
    formRef.current?.validateFields()
      .then(values => {
        // 这种情况下表示校验通过了
        formStateChangeCallback(true);
      })
      .catch(errors => {
        // 这种也算是校验通过了
        if(!errors.errorFields.length) {
          formStateChangeCallback(true);
        } else {
          formStateChangeCallback(false);
        }
      })
  }, [formStateChangeCallback, formRef]);

2.2 当表单的内容发生变化的时候,管理提交按钮的状态的变量okState的值也应该发生变化

tsx 复制代码
  // 表单内容改变回调
  const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
    formRef.current?.validateFields()
      .then(values => {
        // 这种情况下表示校验通过了
        formStateChangeCallback(true);
        setOkState(true);
      })
      .catch(errors => {
        // 这种也算是校验通过了
        if(!errors.errorFields.length) {
          formStateChangeCallback(true);
          setOkState(true);
        } else {
          formStateChangeCallback(false);
          setOkState(false);
        }
      })
  }, [formStateChangeCallback, formRef]);
...
// 使用okState控制提交按钮的状态
        {/* 确认登录按钮 */}
        <Form.Item style={{ marginTop: 58 }}>
          <Button
            type={"primary"}
            style={{ width: "100%", height: 40 }}
            htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
            disabled={!okState}
          >
            登录
          </Button>
        </Form.Item>

2.3 解决formRef类型报错的问题

tsx 复制代码
// 引入类型
import { FormInstance } from 'antd/lib/form';
...
// 修改useRef的初值
const formRef = React.useRef<FormInstance>(null);

2.4 构造表单提交失败的回调函数

此例中,如果表单能够提交,则说明输入是符合规则要求的;如果提交失败了,则证明提交到后端的数据被服务器拒绝了,这种情况只有一种可能,那就是:密码或者用户名错误:

tsx 复制代码
  const onSubmitFailed = React.useCallback(
    () => {
      // 使用Form实例对象的setFields方法可以根据name和对应的FormItem关联起来
      if (formRef?.current) formRef.current.setFields([
        {
          name: "password",
          errors: ["userName or password is wrong!"],
        },
      ]);
    }, [formRef]
  );

2.5 构造发送http请求的函数

发送给后端服务器使用的是axios,发送的内容除了表单数据还有操作日志

tsx 复制代码
// 日志接口
type SubmitLog = {
  topic: string;
  decs: string;
  user: string;
  timer: number;
}
...
// 表单数据发送函数 -- 注意此方法需要在组件开发完毕之后分离出去
const login = (formData:Record<PropertyKey, any>, operationLog:SubmitLog) => {
  const postData = {
    ...formData,
    operationLog,
  };
  const response = axios.post('http://localhost:6666/login', postData, {
    headers: {
      'Content-Type': 'application/json',
    },
  });

  return response;
}

其中,formData是表单数据,而operationLog则是根据提交时候的环境生成的操作日志,操作日志中具有时间戳等信息。

2.6 对表单的密码进行加密--使用第三方库crypto-js完成

tsx 复制代码
// 引入crypto-js
import CryptoJS from "crypto-js";
// 字符串加密函数
// 密码加密函数 -- 注意此方法需要在组件开发完毕之后分离出去
const encrypt = (content: string) => {
  if (!content) {
    content = "";
  }

  const sKey = CryptoJS.enc.Utf8.parse("crypto5c870991230ad");
  const iv = CryptoJS.enc.Utf8.parse("cryptoa3ebc56458ff7");
  const rawBytes = CryptoJS.enc.Utf8.parse(content);
  const encrypted = CryptoJS.AES.encrypt(rawBytes, sKey, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}

2.7 构件函数,对服务端返回的信息做持久化存储

tsx 复制代码
// 持久化存储函数 -- 注意此方法需要在组件开发完毕之后分离出去
const localDataStorage = (data4Storage: any) => void data4Storage.forEach(
  (pair: any) => void localStorage.setItem(pair.key, pair.value || "")
)

2.8 对登录操作做节流--防止登陆操作频繁触发

对登录事件做节流的本质是对onFinish函数做节流,实现原理就是记录每一次onFinish调用的时间,等到下次调用的时候比较当前时间和记录时间,如果小于gap值则不执行。

tsx 复制代码
  const onFinish = React.useCallback((values: any) => {
    // 对于提交事件做代码上的节流,规定一秒之内最多点击一次
    const _now = Date.now();
    const _gap = _now - lastOperationTime;

    if ( _gap < 1000) return;
    // 更新操作时间
    setLastOperationTime(_now);
  }, [lastOperationTime]);

2.9 完善提交表单的回调函数onFinish

  • 对密码进行加密:调用encrypt方法即可
  • 构造提交日志:话题、描述、用户名、时间戳
  • 发送请求:调用login方法即可
  • 处理请求返回值:1. 如果服务器返回失败的结果,需要调用onSubmitFailed方法将失败结果反馈给表示层; 2.如果提交成功,需要调用父组件可能传入的回调函数。

2.9.1 密码进行加密

tsx 复制代码
    // 使用encrypt方法对密码进行加密
    values.password = values.password ? encrypt(values.password) : "";

2.9.2 构造提交日志

tsx 复制代码
    // 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
    const submitLog = {
      topic: "login",
      desc: "login",
      user: values.userName,
      time: _now,
    };

2.9.3 登录

tsx 复制代码
   login({ ...values }, submitLog)

2.9.4 提交成功之后执行父组件的回调

tsx 复制代码
// 更新接口
export interface FormProps {
  formStateChangeCallback: (state: boolean) => void;
  callback?: (params?:any) => void;
}
...
// 从props解析出来
const { formStateChangeCallback, callback } = props;
...
// 提交成功之后调用
    // 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
    login({ ...values }, submitLog)
      .then(({data}) => {
        const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;

        if (code === 200) {
          
          // 执行父组件可能传递的登陆成功的回调
          callback?.();

        } else {
          throw new Error();
        }
      })
      .catch((e) => {
        // 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
        onLoginFailed();
      });

2.9.4 解析服务器返回的数据

tsx 复制代码
    login({ ...values }, submitLog)
      .then(({data}) => {
        const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;

        if (code === 200) {
          // 登录成功之后需要将一些信息做本地化存储
          localDataStorage([
            { key: 'LOGIN_TIME', value: lastLoginTime },
            { key: 'LOCAL_TOKEN_KEY', value: token },
            { key: 'USER_NAME', value: values.userName || "" },
          ]);
          // 执行父组件可能传递的登陆成功的回调
          callback.();

        } else {
          throw new Error();
        }
      })
      .catch((e) => {
        // 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
        onLoginFailed();
      });

onFinish的全部代码

tsx 复制代码
  const onFinish = React.useCallback((values: any) => {
    // 对于提交事件做代码上的节流,规定一秒之内最多点击一次
    const _now = Date.now();
    const _gap = _now - lastOperationTime;

    if ( _gap < 1000) return;
    // 更新操作时间
    setLastOperationTime(_now);

    // 使用encrypt方法对密码进行加密
    values.password = values.password ? encrypt(values.password) : "";
    // 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
    const submitLog = {
      topic: "login",
      desc: "login",
      user: values.userName as string,
      time: _now,
    };

    // 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
    login({ ...values }, submitLog)
      .then(({data}) => {
        const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;

        if (code === 200) {
          // 登录成功之后需要将一些信息做本地化存储
          localDataStorage([
            { key: 'LOGIN_TIME', value: lastLoginTime },
            { key: 'LOCAL_TOKEN_KEY', value: token },
            { key: 'USER_NAME', value: values.userName || "" },
          ]);

          // 使用前端路由跳转至主页面
          // navigate("/home/main");
          // 执行父组件可能传递的登陆成功的回调
          callback(true);

        } else {
          throw new Error();
        }
      })
      .catch((e) => {
        // 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
        onSubmitFailed();
      });
  }, [lastOperationTime, callback, onSubmitFailed]);

2.10 设置一些样式

// 在Form.css中。

css 复制代码
#my-form{
    width: 300px;
    height: 450px;
    overflow: hidden;
    box-sizing: border-box;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: auto;
    padding: 180px 20px 0 20px;
    border: 1px solid grey;
}

3. Form表单的全部代码

tsx 复制代码
import React from "react";
import { Button, Form, Input } from "antd";
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import { FormInstance } from 'antd/lib/form';
import axios from 'axios';
import CryptoJS from "crypto-js"; // @4.1.2
import "./Form.css";

export interface FormProps {
  formStateChangeCallback: (state: boolean) => void;
  callback: (params: any) => void;
}

type SubmitLog = {
  topic: string;
  desc: string;
  user: string;
  time: number;
}

const encrypt = (content: string) => {
  if (!content) {
    content = "";
  }

  const sKey = CryptoJS.enc.Utf8.parse("crypto5c870991230ad");
  const iv = CryptoJS.enc.Utf8.parse("cryptoa3ebc56458ff7");
  const rawBytes = CryptoJS.enc.Utf8.parse(content);
  const encrypted = CryptoJS.AES.encrypt(rawBytes, sKey, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}

// 表单数据发送函数 -- 注意此方法需要在组件开发完毕之后分离出去
const login = (formData: Record<PropertyKey, any>, operationLog: SubmitLog) => {
  const postData = {
    ...formData,
    operationLog,
  };
  const response = axios.post('http://localhost:6666/login', postData, {
    headers: {
      'Content-Type': 'application/json',
    },
  });

  return response;
}

const localDataStorage = (data4Storage: any) => void data4Storage.forEach(
  (pair: any) => void localStorage.setItem(pair.key, pair.value || "")
)


const FormDemo = (props: FormProps) => {
  const { formStateChangeCallback, callback } = props;
  const formRef = React.useRef<FormInstance>(null);
  const [okState, setOkState] = React.useState(false);
  const [lastOperationTime, setLastOperationTime] = React.useState(0);

  const onSubmitFailed = React.useCallback(
    () => {
      // 使用Form实例对象的setFields方法可以根据name和对应的FormItem关联起来
      if (formRef?.current) formRef.current.setFields([
        {
          name: "password",
          errors: ["userName or password is wrong!"],
        },
      ]);
    }, [formRef]
  );

  // 表单内容改变回调
  const onFormValuesChange = React.useCallback((changeValues: any, values: any) => {
    formRef.current?.validateFields()
      .then(values => {
        // 这种情况下表示校验通过了
        formStateChangeCallback(true);
        setOkState(true);
      })
      .catch(errors => {
        // 这种也算是校验通过了
        if(!errors.errorFields.length) {
          formStateChangeCallback(true);
          setOkState(true);
        } else {
          formStateChangeCallback(false);
          setOkState(false);
        }
      })
  }, [formStateChangeCallback, formRef]);

  // 表单submit回调函数
  const onFinish = React.useCallback((values: any) => {
    // 对于提交事件做代码上的节流,规定一秒之内最多点击一次
    const _now = Date.now();
    const _gap = _now - lastOperationTime;

    if ( _gap < 1000) return;
    // 更新操作时间
    setLastOperationTime(_now);

    // 使用encrypt方法对密码进行加密
    values.password = values.password ? encrypt(values.password) : "";
    // 构造登录日志,登录日志作为http请求报文的一部分发送给服务器,作为服务器更新状态的依据
    const submitLog = {
      topic: "login",
      desc: "login",
      user: values.userName as string,
      time: _now,
    };

    // 调用账户登录方法,方法返回promise对象,根据promise对象的状态可以判断出登录是否成功
    login({ ...values }, submitLog)
      .then(({data}) => {
        const { code, result: { data: { lastLoginTime, token } } = { data: { lastLoginTime: _now, token: '' } } } = data;

        if (code === 200) {
          // 登录成功之后需要将一些信息做本地化存储
          localDataStorage([
            { key: 'LOGIN_TIME', value: lastLoginTime },
            { key: 'LOCAL_TOKEN_KEY', value: token },
            { key: 'USER_NAME', value: values.userName || "" },
          ]);

          // 使用前端路由跳转至主页面
          // navigate("/home/main");
          // 执行父组件可能传递的登陆成功的回调
          callback(true);

        } else {
          throw new Error();
        }
      })
      .catch((e) => {
        // 走到这一步说明http发送失败了,这种情况下直接表示登录失败,调用登录失败处理方法
        onSubmitFailed();
      });
  }, [lastOperationTime, callback, onSubmitFailed]);

  return (
    <div id="my-form">
      <Form // 结构为Form -> Form.Item
        ref={formRef} // 表单标签的句柄
        name={"form-demo"} // 渲染到文档上的form标签的id是form-demo <form id="form-demo">...
        layout={"vertical"} // 排列的方向是纵向放置的
        style={{ width: "100%" }} // 充满包裹容器
        size={"large"}
        onFinish={onFinish} // submit发生的回调
        onValuesChange={onFormValuesChange} // 表单中的内容发生改变的时候发生的回调
      >
        {/* 用户名输入框 */}
        <Form.Item
          name={"userName"} // 用户名
          label={null} // 要么使用label要么使用input的placeHolder
          rules={[
            { required: true, message: 'Please input your username!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Username can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input
            style={{
              height: 40,
            }}
            prefix={<UserOutlined />} // input输入框前面的小图标,使用的是ant-design/icons中的图标
            placeholder={"请输入用户名..."} // 文字占位符
          />
        </Form.Item>

        {/* 密码输入框 */}
        <Form.Item 
          name={"password"} 
          label={null} 
          rules={[
            { required: true, message: 'Please input your password!' },
            { min: 3, message: 'Username must be at least 3 characters long!' },
            { pattern: /^[a-zA-Z0-9]+$/, message: 'Password can only contain letters and numbers!' },
          ]} // 校验规则
        >
          <Input.Password
            style={{ height: 40 }}
            prefix={<LockOutlined />} // input输入框前面的小图标
            placeholder={"请输入密码..."} // 文字占位符
          />
        </Form.Item>

        {/* 确认登录按钮 */}
        <Form.Item style={{ marginTop: 58 }}>
          <Button
            type={"primary"}
            style={{ width: "100%", height: 40 }}
            htmlType={"submit"} // 这个属性决定了此按钮被点击的时候,间接调用了onFinish方法
            disabled={!okState}
          >
            登录
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

export default FormDemo;

4. 使用express写一个后端验证

js 复制代码
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());
app.use(express.json()); // 使用内置的 JSON 解析中间件

app.post('/login', (req, res) => {
  const lastLoginTime = new Date().getTime().toString();
  const token = Math.random().toString(16);

  const responseData = {
    code: 200,
    result: {
      data: {
        lastLoginTime,
        token,
      },
    },
  };

  res.json(responseData);
});

const port = 6666;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

5. 后续优化

5.1 国际化

tsx 复制代码
import { useIntl } from "react-intl";
import { SOME_KEY} from "@/locales/I18nKeys";
const intl = useIntl();
intl.formatMessage({ id: SOME_KEY} }),

5.2 前端路由跳转

tsx 复制代码
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
navigate("/home");
相关推荐
GIS程序媛—椰子23 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00129 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端32 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x35 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟100936 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习