react【实战】自定义下拉框、单选、多选、输入框

效果预览

完整代码

ts 复制代码
import { FiChevronDown, FiCheck } from "react-icons/fi";
import { useState } from "react";

function CustomSelect() {
  const [selected, setSelected] = useState("");
  const [isOpen, setIsOpen] = useState(false);

  const options = [
    { value: "apple", label: "苹果" },
    { value: "banana", label: "香蕉" },
    { value: "orange", label: "橙子" },
  ];

  return (
    <div className="relative w-52">
      <div
        className={`border border-gray-300 rounded-lg px-4 py-2 cursor-pointer flex items-center justify-between transition-all duration-200 text-sm
          ${isOpen ? "border-blue-500 ring-2 ring-blue-500" : "hover:border-gray-400"}`}
        onClick={() => setIsOpen(!isOpen)}
      >
        <span className={selected ? "text-black" : "text-gray-400"}>
          {selected
            ? options.find((opt) => opt.value === selected)?.label
            : "请选择水果"}
        </span>
        <FiChevronDown
          size={16}
          className={`text-gray-500 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
        />
      </div>

      {isOpen && (
        <div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10 overflow-hidden">
          {options.map((option) => (
            <div
              key={option.value}
              className={`px-4 py-2 cursor-pointer transition-colors duration-150 text-sm
                ${selected === option.value ? "bg-blue-50 text-blue-600" : "hover:bg-gray-50 text-gray-700"}`}
              onClick={(e) => {
                e.stopPropagation();
                setSelected(option.value);
                setIsOpen(false);
              }}
            >
              {option.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function CustomCheckbox() {
  const [checked, setChecked] = useState(false);

  return (
    <label className="flex items-center gap-2 cursor-pointer">
      <div className="relative">
        <input
          type="checkbox"
          className="sr-only"
          checked={checked}
          onChange={() => setChecked(!checked)}
        />
        <div
          className={`w-4 h-4 rounded border-2 transition-all duration-200 flex items-center justify-center
            ${checked ? "bg-blue-500 border-blue-500" : "border-gray-300 bg-white"}`}
        >
          {checked && <FiCheck size={10} className="text-white" />}
        </div>
      </div>
      <span className="text-gray-700 text-sm">同意协议</span>
    </label>
  );
}

function CustomRadio() {
  const [selected, setSelected] = useState("");

  const options = [
    { value: "option1", label: "选项一" },
    { value: "option2", label: "选项二" },
    { value: "option3", label: "选项三" },
  ];

  return (
    <div className="flex flex-col gap-2">
      {options.map((option) => (
        <label
          key={option.value}
          className="flex items-center gap-2 cursor-pointer"
        >
          <div className="relative">
            <input
              type="radio"
              className="sr-only"
              name="custom-radio-group"
              checked={selected === option.value}
              onChange={() => setSelected(option.value)}
            />
            <div
              className={`w-4 h-4 rounded-full border-2 transition-all duration-200
                ${selected === option.value ? "border-blue-500" : "border-gray-300"}`}
            >
              {selected === option.value && (
                <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-blue-500" />
              )}
            </div>
          </div>
          <span className="text-gray-700 text-sm">{option.label}</span>
        </label>
      ))}
    </div>
  );
}

function CustomInput() {
  const [value, setValue] = useState("");

  return (
    <div className="w-52">
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="请输入内容"
        className="w-full px-4 py-1.5 bg-gray-50 border-0 rounded-lg text-sm
          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white
          placeholder:text-gray-400 transition-all duration-200"
      />
    </div>
  );
}

function FormExamples() {
  const [nativeSelect, setNativeSelect] = useState("");
  const [nativeCheckbox, setNativeCheckbox] = useState(false);
  const [nativeRadio, setNativeRadio] = useState("");
  const [nativeInput, setNativeInput] = useState("");

  return (
    <div className="py-8 px-4">
      <h2 className="text-2xl font-bold mb-8 text-center">表单组件对比</h2>
      <div className="flex justify-center">
        <table className="border-collapse">
          <thead>
            <tr>
              <th className="text-left p-4 w-32 text-gray-600 font-medium border-b border-gray-200">
                组件类型
              </th>
              <th className="text-center p-4 w-64 text-red-500 font-semibold border-b border-gray-200">
                原生效果
              </th>
              <th className="text-center p-4 w-64 text-green-500 font-semibold border-b border-gray-200">
                自定义效果
              </th>
            </tr>
          </thead>
          <tbody>
            {/* 下拉框对比 */}
            <tr className="hover:bg-gray-50">
              <td className="p-4 text-gray-700 font-medium border-b border-gray-100">
                下拉框
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <select
                  value={nativeSelect}
                  onChange={(e) => setNativeSelect(e.target.value)}
                  className="border border-gray-300 px-3 py-1.5 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  <option value="">请选择</option>
                  <option value="apple">苹果</option>
                  <option value="banana">香蕉</option>
                  <option value="orange">橙子</option>
                </select>
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <CustomSelect />
              </td>
            </tr>

            {/* 复选框对比 */}
            <tr className="hover:bg-gray-50">
              <td className="p-4 text-gray-700 font-medium border-b border-gray-100">
                复选框
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <label className="flex items-center justify-center gap-2 cursor-pointer">
                  <input
                    type="checkbox"
                    checked={nativeCheckbox}
                    onChange={() => setNativeCheckbox(!nativeCheckbox)}
                    className="w-4 h-4"
                  />
                  <span className="text-gray-700 text-sm">同意协议</span>
                </label>
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <CustomCheckbox />
              </td>
            </tr>

            {/* 单选框对比 */}
            <tr className="hover:bg-gray-50">
              <td className="p-4 text-gray-700 font-medium border-b border-gray-100">
                单选框
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <div className="flex flex-col gap-2 items-center">
                  {["选项一", "选项二", "选项三"].map((label, i) => (
                    <label
                      key={i}
                      className="flex items-center gap-2 cursor-pointer"
                    >
                      <input
                        type="radio"
                        name="native-radio"
                        value={label}
                        checked={nativeRadio === label}
                        onChange={() => setNativeRadio(label)}
                        className="w-4 h-4"
                      />
                      <span className="text-gray-700 text-sm">{label}</span>
                    </label>
                  ))}
                </div>
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <CustomRadio />
              </td>
            </tr>

            {/* 输入框对比 */}
            <tr className="hover:bg-gray-50">
              <td className="p-4 text-gray-700 font-medium border-b border-gray-100">
                输入框
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <input
                  type="text"
                  value={nativeInput}
                  onChange={(e) => setNativeInput(e.target.value)}
                  placeholder="请输入内容"
                  className="w-52 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </td>
              <td className="p-4 text-center border-b border-gray-100">
                <CustomInput />
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

function App() {
  return <FormExamples />;
}

export default App;
相关推荐
涵涵(互关)7 小时前
GoView各项目文件中的相关语法3
前端·vue.js·typescript
李白的天不白7 小时前
vs code -- uniapp gets
前端
吴声子夜歌7 小时前
Vue3——网络框架Axios的应用
javascript·vue3·axios
lifewange7 小时前
CNode API v1 完整接口文档(JSON 规范整理)
java·前端·json
QQ1__81151751516 小时前
Spring boot名城小区物业管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
钛态16 小时前
前端微前端架构:大项目的救命稻草还是自找麻烦?
前端·vue·react·web
一粒黑子16 小时前
【实战解析】阿里开源 PageAgent:纯前端 GUI Agent,一行JS让网页支持自然语言操控
前端·javascript·开源
独角鲸网络安全实验室16 小时前
2026微信小程序抓包全解析:从实操落地到合规风控,解锁前端调试新范式
前端·微信小程序·小程序·抓包·系统代理绕过·https证书严格校验·进程隔离
紫微AI16 小时前
前端文本测量成了卡死一切创新的最后瓶颈,pretext实现突破了
前端·人工智能·typescript