visual-novel-web/src/components/ui/inputs/select.tsx

131 lines
3.5 KiB
TypeScript
Raw Normal View History

2025-10-28 07:59:26 +00:00
'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; // 打开/关闭回调
} & React.HTMLAttributes<HTMLDivElement>;
function Select(props: SelectProps) {
const {
options = [],
render,
placeholder = '请选择',
icon,
className,
contentClassName,
} = 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: 9999,
}}
>
<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;