mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
fix(frontend): resolve scroll, diagnosis layout, and multi-select regressions (#147)
* fix(frontend): restore modal scroll and bridged multi-select UI * fix(frontend): collapse multi-select panel and clean diagnosis labels
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "failed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
@@ -4,11 +4,16 @@ import { ChevronDownIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
className={cn("border-b", className)}
|
className={cn("border-b", className)}
|
||||||
@@ -27,7 +32,7 @@ function AccordionTrigger({
|
|||||||
<AccordionPrimitive.Header className="flex" data-slot="accordion-header">
|
<AccordionPrimitive.Header className="flex" data-slot="accordion-header">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
"flex flex-1 items-center justify-between gap-3 px-3 py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-slot="accordion-trigger"
|
data-slot="accordion-trigger"
|
||||||
|
|||||||
@@ -2638,7 +2638,7 @@ export default function ForwardPage() {
|
|||||||
<td className="px-3 py-2 text-center">
|
<td className="px-3 py-2 text-center">
|
||||||
{result.success && quality ? (
|
{result.success && quality ? (
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-xs whitespace-nowrap"
|
||||||
color={quality.color as any}
|
color={quality.color as any}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -2803,7 +2803,7 @@ export default function ForwardPage() {
|
|||||||
{quality && (
|
{quality && (
|
||||||
<>
|
<>
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-xs whitespace-nowrap"
|
||||||
color={quality.color as any}
|
color={quality.color as any}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
|
|||||||
@@ -2118,7 +2118,7 @@ export default function TunnelPage() {
|
|||||||
<td className="px-3 py-2 text-center">
|
<td className="px-3 py-2 text-center">
|
||||||
{result.success && quality ? (
|
{result.success && quality ? (
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-xs whitespace-nowrap"
|
||||||
color={quality.color as any}
|
color={quality.color as any}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -2282,7 +2282,7 @@ export default function TunnelPage() {
|
|||||||
{quality && (
|
{quality && (
|
||||||
<>
|
<>
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-xs whitespace-nowrap"
|
||||||
color={quality.color as any}
|
color={quality.color as any}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
|
|||||||
@@ -626,7 +626,6 @@ export default function UserPage() {
|
|||||||
"bg-default-100 border-2 border-default-200 hover:border-default-300 data-[hover=true]:border-default-300",
|
"bg-default-100 border-2 border-default-200 hover:border-default-300 data-[hover=true]:border-default-300",
|
||||||
}}
|
}}
|
||||||
placeholder="搜索用户名"
|
placeholder="搜索用户名"
|
||||||
startContent={<SearchIcon className="w-4 h-4 text-default-400" />}
|
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface AccordionProps extends Omit<React.ComponentProps<"div">, "children"> {
|
export interface AccordionProps
|
||||||
|
extends Omit<React.ComponentProps<"div">, "children"> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: "bordered" | "light" | "splitted";
|
variant?: "bordered" | "light" | "splitted";
|
||||||
}
|
}
|
||||||
@@ -21,9 +22,27 @@ export interface AccordionItemProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Accordion({ children, className }: AccordionProps) {
|
function variantClass(variant: AccordionProps["variant"]) {
|
||||||
|
if (variant === "splitted") {
|
||||||
|
return "[&>[data-slot=accordion-item]]:mb-3 [&>[data-slot=accordion-item]]:rounded-xl [&>[data-slot=accordion-item]]:border [&>[data-slot=accordion-item]]:border-divider [&>[data-slot=accordion-item]]:border-b-0 [&>[data-slot=accordion-item]]:bg-content1 [&>[data-slot=accordion-item]]:shadow-sm [&>[data-slot=accordion-item]]:overflow-hidden [&>[data-slot=accordion-item]:last-child]:mb-0";
|
||||||
|
}
|
||||||
|
if (variant === "bordered") {
|
||||||
|
return "rounded-xl border border-divider bg-content1 overflow-hidden [&>[data-slot=accordion-item]]:border-b [&>[data-slot=accordion-item]:last-child]:border-b-0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Accordion({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = "light",
|
||||||
|
}: AccordionProps) {
|
||||||
return (
|
return (
|
||||||
<BaseAccordion className={cn("w-full", className)} type="multiple">
|
<BaseAccordion
|
||||||
|
className={cn("w-full", variantClass(variant), className)}
|
||||||
|
type="multiple"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</BaseAccordion>
|
</BaseAccordion>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Dialog, DialogContent as BaseDialogContent } from "@/components/ui/dialog";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent as BaseDialogContent,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface DisclosureOptions {
|
interface DisclosureOptions {
|
||||||
@@ -35,6 +38,8 @@ export function useDisclosure(options: DisclosureOptions = {}) {
|
|||||||
interface ModalContextValue {
|
interface ModalContextValue {
|
||||||
classNames?: Record<string, string>;
|
classNames?: Record<string, string>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
scrollBehavior?: "inside" | "outside";
|
||||||
|
size?: ModalSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalContext = React.createContext<ModalContextValue | null>(null);
|
const ModalContext = React.createContext<ModalContextValue | null>(null);
|
||||||
@@ -88,6 +93,8 @@ export function Modal({
|
|||||||
isOpen = false,
|
isOpen = false,
|
||||||
onClose,
|
onClose,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
scrollBehavior,
|
||||||
|
size,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
onOpenChange?.(open);
|
onOpenChange?.(open);
|
||||||
@@ -101,16 +108,21 @@ export function Modal({
|
|||||||
onClose: () => {
|
onClose: () => {
|
||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
},
|
},
|
||||||
|
scrollBehavior,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
<ModalContext.Provider value={contextValue}>{children}</ModalContext.Provider>
|
<ModalContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModalContentProps extends Omit<React.ComponentProps<"div">, "children"> {
|
interface ModalContentProps
|
||||||
|
extends Omit<React.ComponentProps<"div">, "children"> {
|
||||||
children: React.ReactNode | ((onClose: () => void) => React.ReactNode);
|
children: React.ReactNode | ((onClose: () => void) => React.ReactNode);
|
||||||
scrollBehavior?: "inside" | "outside";
|
scrollBehavior?: "inside" | "outside";
|
||||||
size?: ModalSize;
|
size?: ModalSize;
|
||||||
@@ -124,15 +136,24 @@ export function ModalContent({
|
|||||||
...props
|
...props
|
||||||
}: ModalContentProps) {
|
}: ModalContentProps) {
|
||||||
const context = useModalContext();
|
const context = useModalContext();
|
||||||
|
const resolvedScrollBehavior = scrollBehavior ?? context?.scrollBehavior;
|
||||||
|
const resolvedSize = size ?? context?.size;
|
||||||
const renderedChildren =
|
const renderedChildren =
|
||||||
typeof children === "function" ? children(() => context?.onClose()) : children;
|
typeof children === "function"
|
||||||
|
? children(() => context?.onClose())
|
||||||
|
: children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialogContent
|
<BaseDialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
mapSize(size),
|
mapSize(resolvedSize),
|
||||||
context?.classNames?.base,
|
context?.classNames?.base,
|
||||||
scrollBehavior === "outside" ? "max-h-[90vh] overflow-y-auto" : "",
|
resolvedScrollBehavior === "outside"
|
||||||
|
? "max-h-[90vh] overflow-y-auto"
|
||||||
|
: "",
|
||||||
|
resolvedScrollBehavior === "inside"
|
||||||
|
? "max-h-[90vh] flex flex-col overflow-hidden [&>[data-slot=modal-body]]:min-h-0 [&>[data-slot=modal-body]]:flex-1 [&>[data-slot=modal-body]]:overflow-y-auto"
|
||||||
|
: "",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
@@ -143,18 +164,54 @@ export function ModalContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalHeader({ className, ...props }: React.ComponentProps<"div">) {
|
export function ModalHeader({
|
||||||
return <div className={cn("text-lg font-semibold", className)} {...props} />;
|
className,
|
||||||
}
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
const context = useModalContext();
|
||||||
|
|
||||||
export function ModalBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return <div className={cn("space-y-4", className)} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModalFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
className={cn(
|
||||||
|
"text-lg font-semibold",
|
||||||
|
context?.classNames?.header,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="modal-header"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalBody({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
const context = useModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("space-y-4", context?.classNames?.body, className)}
|
||||||
|
data-slot="modal-body"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
const context = useModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
context?.classNames?.footer,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="modal-footer"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
import { FieldContainer, extractText, type FieldMetaProps } from "./shared";
|
import { FieldContainer, extractText, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
|
import { Checkbox as BaseCheckbox } from "@/components/ui/checkbox";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SelectionMode = "single" | "multiple";
|
type SelectionMode = "single" | "multiple";
|
||||||
|
|
||||||
type SelectionValue = Iterable<React.Key> | Set<React.Key> | Array<React.Key>;
|
type SelectionValue = Iterable<React.Key> | Set<React.Key> | Array<React.Key>;
|
||||||
@@ -131,6 +133,17 @@ function sizeClass(size: SelectProps["size"]) {
|
|||||||
return "h-9 text-sm";
|
return "h-9 text-sm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textSizeClass(size: SelectProps["size"]) {
|
||||||
|
if (size === "sm") {
|
||||||
|
return "text-xs";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "text-base";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "text-sm";
|
||||||
|
}
|
||||||
|
|
||||||
export function Select<T>({
|
export function Select<T>({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
@@ -152,12 +165,41 @@ export function Select<T>({
|
|||||||
size,
|
size,
|
||||||
}: SelectProps<T>) {
|
}: SelectProps<T>) {
|
||||||
const generatedId = React.useId();
|
const generatedId = React.useId();
|
||||||
const options = React.useMemo(() => getOptions(children, items), [children, items]);
|
const options = React.useMemo(
|
||||||
|
() => getOptions(children, items),
|
||||||
|
[children, items],
|
||||||
|
);
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
|
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
|
||||||
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
|
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
|
||||||
|
|
||||||
const selectedArray = Array.from(selected);
|
const selectedArray = Array.from(selected);
|
||||||
const singleValue = selectedArray[0] ?? "";
|
const singleValue = selectedArray[0] ?? "";
|
||||||
|
const selectedText =
|
||||||
|
selectedArray.length > 0
|
||||||
|
? options
|
||||||
|
.filter((option) => selected.has(option.key))
|
||||||
|
.map((option) => option.label)
|
||||||
|
.join("、")
|
||||||
|
: (placeholder ?? "请选择");
|
||||||
|
|
||||||
|
const updateMultipleSelection = (key: string, checked?: boolean) => {
|
||||||
|
if (isDisabled || disabled.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = new Set(selected);
|
||||||
|
const shouldSelect =
|
||||||
|
typeof checked === "boolean" ? checked : !next.has(key);
|
||||||
|
|
||||||
|
if (shouldSelect) {
|
||||||
|
next.add(key);
|
||||||
|
} else {
|
||||||
|
next.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
onChange?.(event);
|
onChange?.(event);
|
||||||
@@ -167,7 +209,9 @@ export function Select<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectionMode === "multiple") {
|
if (selectionMode === "multiple") {
|
||||||
const values = Array.from(event.target.selectedOptions).map((option) => option.value);
|
const values = Array.from(event.target.selectedOptions).map(
|
||||||
|
(option) => option.value,
|
||||||
|
);
|
||||||
|
|
||||||
onSelectionChange(new Set(values));
|
onSelectionChange(new Set(values));
|
||||||
|
|
||||||
@@ -193,31 +237,127 @@ export function Select<T>({
|
|||||||
isRequired={isRequired}
|
isRequired={isRequired}
|
||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
<select
|
{selectionMode === "multiple" ? (
|
||||||
className={cn(
|
<div
|
||||||
"w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
className={cn(
|
||||||
sizeClass(size),
|
"w-full rounded-md border border-input bg-background shadow-sm",
|
||||||
selectionMode === "multiple" ? "min-h-32" : "",
|
isDisabled ? "cursor-not-allowed opacity-60" : "",
|
||||||
classNames?.trigger,
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
id={generatedId}
|
||||||
disabled={isDisabled}
|
>
|
||||||
id={generatedId}
|
<button
|
||||||
multiple={selectionMode === "multiple"}
|
aria-controls={`${generatedId}-listbox`}
|
||||||
required={isRequired}
|
aria-expanded={isExpanded}
|
||||||
value={selectionMode === "multiple" ? selectedArray : singleValue}
|
className={cn(
|
||||||
onClick={onClick}
|
"flex w-full items-center gap-2 px-3 py-2 text-left",
|
||||||
onChange={handleChange}
|
classNames?.trigger,
|
||||||
>
|
)}
|
||||||
{selectionMode === "single" ? (
|
disabled={isDisabled}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"block flex-1 truncate",
|
||||||
|
textSizeClass(size),
|
||||||
|
selectedArray.length > 0 ? "text-foreground" : "text-default-500",
|
||||||
|
)}
|
||||||
|
title={selectedText}
|
||||||
|
>
|
||||||
|
{selectedText}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 flex-shrink-0 text-default-500 transition-transform",
|
||||||
|
isExpanded ? "rotate-180" : "rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isExpanded ? (
|
||||||
|
<div
|
||||||
|
className="max-h-56 space-y-1 overflow-y-auto border-t border-divider p-2"
|
||||||
|
id={`${generatedId}-listbox`}
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
{options.length === 0 ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1 text-default-500",
|
||||||
|
textSizeClass(size),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
暂无可选项
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
options.map((option) => {
|
||||||
|
const optionDisabled = isDisabled || disabled.has(option.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.key}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md px-2 py-1.5",
|
||||||
|
optionDisabled
|
||||||
|
? "cursor-not-allowed opacity-60"
|
||||||
|
: "hover:bg-default-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BaseCheckbox
|
||||||
|
checked={selected.has(option.key)}
|
||||||
|
disabled={optionDisabled}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
updateMultipleSelection(option.key, value === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 flex-1 truncate text-left text-foreground",
|
||||||
|
textSizeClass(size),
|
||||||
|
optionDisabled
|
||||||
|
? "cursor-not-allowed"
|
||||||
|
: "cursor-pointer",
|
||||||
|
)}
|
||||||
|
disabled={optionDisabled}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateMultipleSelection(option.key)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
|
sizeClass(size),
|
||||||
|
classNames?.trigger,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={generatedId}
|
||||||
|
required={isRequired}
|
||||||
|
value={singleValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<option value="">{placeholder ?? "请选择"}</option>
|
<option value="">{placeholder ?? "请选择"}</option>
|
||||||
) : null}
|
{options.map((option) => (
|
||||||
{options.map((option) => (
|
<option
|
||||||
<option key={option.key} disabled={disabled.has(option.key)} value={option.key}>
|
key={option.key}
|
||||||
{option.label}
|
disabled={disabled.has(option.key)}
|
||||||
</option>
|
value={option.key}
|
||||||
))}
|
>
|
||||||
</select>
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
</FieldContainer>
|
</FieldContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user