133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { cn } from '@/lib';
|
|
import React, { useState } from 'react';
|
|
import rightIcon from '@/assets/components/go_right.svg';
|
|
import Image from 'next/image';
|
|
import { Select as SelectPrimitive } from 'radix-ui';
|
|
import { useControllableValue } from 'ahooks';
|
|
import { InputLeft } from '.';
|
|
|
|
const { Root, Trigger, Portal, Content, Viewport, Item, ItemText } =
|
|
SelectPrimitive;
|
|
|
|
const View: React.FC<
|
|
{
|
|
icon?: any;
|
|
text?: string;
|
|
expand?: boolean;
|
|
} & React.HTMLAttributes<HTMLDivElement>
|
|
> = (props) => {
|
|
const { icon, text, expand, ...rest } = props;
|
|
return (
|
|
<div
|
|
{...rest}
|
|
className={cn('input-view justify-between', props.className)}
|
|
>
|
|
<InputLeft icon={icon} text={text} />
|
|
<Image
|
|
className={cn('transition-transform', expand ? 'rotate-90' : '')}
|
|
src={rightIcon}
|
|
width={14}
|
|
height={14}
|
|
alt="right"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type SelectProps = {
|
|
options?: {
|
|
label: string;
|
|
value: string;
|
|
}[];
|
|
value?: string;
|
|
defaultValue?: string;
|
|
onChange?: (value: string) => void;
|
|
render?: (option: { label: string; value: string }) => React.ReactNode;
|
|
placeholder?: string;
|
|
icon?: any;
|
|
contentClassName?: string; // 自定义下拉框样式
|
|
defaultOpen?: boolean; // 默认是否打开
|
|
onOpenChange?: (open: boolean) => void; // 打开/关闭回调
|
|
zIndex?: number;
|
|
} & React.HTMLAttributes<HTMLDivElement>;
|
|
|
|
function Select(props: SelectProps) {
|
|
const {
|
|
options = [],
|
|
render,
|
|
placeholder = '请选择',
|
|
icon,
|
|
className,
|
|
contentClassName,
|
|
zIndex,
|
|
} = props;
|
|
|
|
// 使用 useControllableValue 管理状态,支持受控和非受控模式
|
|
const [value, setValue] = useControllableValue<string | undefined>(props);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
// 找到当前选中项的 label
|
|
const selectedLabel = options.find((opt) => opt.value === value)?.label;
|
|
|
|
return (
|
|
<Root
|
|
value={value}
|
|
onValueChange={setValue}
|
|
open={open}
|
|
onOpenChange={() => setOpen(!open)}
|
|
>
|
|
{/* Trigger - 使用 View 组件 */}
|
|
<Trigger asChild>
|
|
<View
|
|
icon={icon}
|
|
expand={open}
|
|
text={selectedLabel || placeholder}
|
|
className={className}
|
|
/>
|
|
</Trigger>
|
|
|
|
{/* 下拉内容 */}
|
|
<Portal>
|
|
<Content
|
|
className={cn(
|
|
'rounded-[20px] border border-white/10',
|
|
'overflow-hidden',
|
|
contentClassName
|
|
)}
|
|
position="popper"
|
|
side="bottom"
|
|
align="start"
|
|
style={{
|
|
width: 'var(--radix-select-trigger-width)', // 默认跟随 Trigger 宽度
|
|
backgroundColor: 'rgba(26, 23, 34, 1)',
|
|
zIndex: zIndex || 'var(--z-select)',
|
|
}}
|
|
>
|
|
<Viewport className="p-2">
|
|
{options.map((option) => (
|
|
<Item
|
|
key={option.value}
|
|
value={option.value}
|
|
className={cn(
|
|
'cursor-pointer outline-none',
|
|
'rounded-lg px-5 py-3', // 添加内边距和圆角
|
|
'hover:bg-white/10', // 悬停效果
|
|
'transition-colors', // 平滑过渡
|
|
'data-[highlighted]:bg-white/10' // Radix UI 高亮状态
|
|
)}
|
|
>
|
|
<ItemText>{render ? render(option) : option.label}</ItemText>
|
|
</Item>
|
|
))}
|
|
</Viewport>
|
|
</Content>
|
|
</Portal>
|
|
</Root>
|
|
);
|
|
}
|
|
|
|
Select.View = View;
|
|
export default Select;
|