index.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import * as React from "react"
  2. import { cname } from "../utils"
  3. import { SearchIcon } from "../../bs-icons/search"
  4. import { generateUUID } from "../utils"
  5. import { MinusCircledIcon } from "@radix-ui/react-icons"
  6. import sousuo from "../../../assets/npc/sousuo1.png"
  7. export interface InputProps
  8. extends React.InputHTMLAttributes<HTMLInputElement> { }
  9. const Input = React.forwardRef<HTMLInputElement, InputProps>(
  10. ({ className, type, ...props }, ref) => {
  11. return (
  12. <input
  13. type={type}
  14. className={cname(
  15. "flex h-9 w-full rounded-md border border-input bg-[#FAFBFC] px-3 py-1 text-sm text-[#111] shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ",
  16. className
  17. )}
  18. ref={ref}
  19. {...props}
  20. />
  21. )
  22. }
  23. )
  24. Input.displayName = "Input"
  25. const SearchInput = React.forwardRef<HTMLInputElement, InputProps & { inputClassName?: string, iconClassName?: string }>(
  26. ({ className, inputClassName, iconClassName, ...props }, ref) => {
  27. return <div className={cname("relative", className)}>
  28. {/* <SearchIcon className={cname("h-5 w-5 absolute left-2 top-2 text-[#666666]", iconClassName)} /> */}
  29. <img src={sousuo} alt="" className="w-[14px] absolute left-[14px] top-[10px]" />
  30. <Input type="text" ref={ref} className={cname("w-[244px] h-[34px] pl-[40px] npcInput3", inputClassName)} {...props}></Input>
  31. </div>
  32. }
  33. )
  34. SearchInput.displayName = "SearchInput"
  35. /**
  36. * 多行文本
  37. */
  38. export interface TextareaProps
  39. extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
  40. const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  41. ({ className, ...props }, ref) => {
  42. return (
  43. <textarea
  44. className={cname(
  45. "flex min-h-[80px] w-full rounded-md border border-input bg-[#FAFBFC] px-3 py-2 text-sm text-[#111] shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
  46. className
  47. )}
  48. ref={ref}
  49. {...props}
  50. />
  51. )
  52. }
  53. )
  54. Textarea.displayName = "Textarea"
  55. /**
  56. * input list
  57. */
  58. const InputList = React.forwardRef<HTMLDivElement, InputProps & {
  59. rules: any[],
  60. value?: string[],
  61. inputClassName?: string,
  62. defaultValue?: string[],
  63. onChange?: (values: string[]) => void
  64. }>(
  65. ({ rules, className, inputClassName, value = [], defaultValue = [], ...props }, ref) => {
  66. // 初始化 inputs 状态,为每个值分配唯一 ID
  67. const [inputs, setInputs] = React.useState(() =>
  68. value.map(val => ({ id: generateUUID(8), value: val }))
  69. );
  70. React.useEffect(() => {
  71. // 仅为新增的值分配新的 ID
  72. const updatedInputs = value.map((val, index) => {
  73. return inputs[index] && inputs[index].value === val
  74. ? inputs[index] // 如果当前输入项与外部值相同,则保持不变
  75. : { id: generateUUID(8), value: val }; // 否则,创建新的输入项
  76. });
  77. setInputs(updatedInputs);
  78. }, [value]); // 依赖项中包含 value,确保外部 value 更新时同步更新
  79. const handleChange = (newValue, id, index) => {
  80. let newInputs = inputs.map(input =>
  81. input.id === id ? { ...input, value: newValue } : input
  82. );
  83. // push
  84. if (index === newInputs.length - 1) {
  85. newInputs = ([...newInputs, { id: generateUUID(8), value: '' }]);
  86. }
  87. setInputs(newInputs);
  88. props.onChange(newInputs.map(input => input.value));
  89. };
  90. // delete input
  91. const handleRemoveInput = (id) => {
  92. const newInputs = inputs.filter(input => input.id !== id);
  93. setInputs(newInputs);
  94. props.onChange(newInputs.map(input => input.value));
  95. };
  96. return <div className={cname('', className)}>
  97. {
  98. inputs.map((item, index) => (
  99. <div className="relative mt-2">
  100. <Input
  101. key={item.id}
  102. defaultValue={item.value}
  103. className={cname('pr-8', inputClassName)}
  104. placeholder={props.placeholder || ''}
  105. onChange={(e) => handleChange(e.target.value, item.id, index)}
  106. onInput={(e) => {
  107. rules.some(rule => {
  108. if (rule.maxLength && e.target.value.length > rule.maxLength) {
  109. e.target.nextSibling.textContent = rule.message;
  110. e.target.nextSibling.style.display = '';
  111. return true;
  112. }
  113. e.target.nextSibling.style.display = 'none';
  114. })
  115. }}
  116. // onFocus={(e) => {
  117. // if (e.target.value && index === inputs.length - 1) {
  118. // setInputs([...inputs, { id: generateUUID(8), value: '' }]);
  119. // }
  120. // }}
  121. ></Input>
  122. <p className="text-sm text-red-500" style={{ display: 'none' }}></p>
  123. {index !== inputs.length - 1 && <MinusCircledIcon onClick={(e) => {
  124. e.target.previousSibling.style.display = 'none';
  125. handleRemoveInput(item.id)
  126. }} className="absolute top-2.5 right-2 text-gray-500 hover:text-gray-700 cursor-pointer" />}
  127. </div>
  128. ))
  129. }
  130. </div>
  131. }
  132. )
  133. export { Input, SearchInput, Textarea, InputList }