feat(components): 添加 HSegmengts 组件

- 实现了一个新的 Segments 组件,支持水平和垂直方向的选项切换
- 组件具有选择状态和动态指示器样式
- 支持通过 props 传递选项、初始值和变更事件处理函数
- 优化了组件的性能和可维护性
This commit is contained in:
Vixalie
2025-08-12 16:39:10 +08:00
parent d58035648a
commit 1022f69d19

109
src/components/Segments.tsx Normal file
View File

@@ -0,0 +1,109 @@
import cx from 'clsx';
import { isNotNil } from 'es-toolkit';
import {
Component,
createEffect,
createMemo,
createSignal,
Index,
JSX,
mergeProps,
Show,
} from 'solid-js';
interface Option {
label: string | JSX.Element;
value: string | number | symbol | boolean;
}
interface SegmentsProps {
name?: string;
options: Option[];
value?: Option['value'];
direction?: 'horizontal' | 'vertical';
disabled?: boolean;
onChange?: (value: Option['value'] | undefined) => void;
}
export const HSegmengts: Component<SegmentsProps> = (props) => {
const mProps = mergeProps<SegmentsProps[]>(
{
options: [],
disabled: false,
direction: 'horizontal',
},
props,
);
const optionRefs: HTMLDivElement[] = [];
const originalValue = createMemo(() => mProps.value);
const [selected, setSelected] = createSignal<Option['value'] | undefined>(undefined);
const [indicatorTop, setIndicatorTop] = createSignal(0);
const [indicatorLeft, setIndicatorLeft] = createSignal(0);
const [indicatorHeight, setIndicatorHeight] = createSignal(0);
const [indicatorWidth, setIndicatorWidth] = createSignal(0);
const indicatorStyle = createMemo(() => ({
top: `${indicatorTop()}px`,
left: `${indicatorLeft()}px`,
height: `${indicatorHeight()}px`,
width: `${indicatorWidth()}px`,
}));
createEffect(() => {
if (isNotNil(originalValue()) && originalValue() !== selected()) {
setSelected(originalValue());
}
});
createEffect(() => {
const selectedIndex = mProps.options?.findIndex((option) => option.value === selected());
if (isNotNil(selectedIndex) && optionRefs.length > 0 && optionRefs.length > selectedIndex) {
const optionElement = optionRefs[selectedIndex];
if (isNotNil(optionElement)) {
setIndicatorTop(optionElement.offsetTop);
setIndicatorLeft(optionElement.offsetLeft);
setIndicatorHeight(optionElement.offsetHeight);
setIndicatorWidth(optionElement.offsetWidth);
}
}
});
const handleSelect = (value: Option['value']) => {
setSelected(value);
mProps.onChange?.(value);
};
return (
<div class="inline-block overflow-hidden rounded-sm bg-neutral p-1 select-none">
<div
class={cx(
'relative flex',
mProps.direction === 'horizontal' ? 'flex-row items-center' : 'flex-col',
)}>
<Index each={mProps.options}>
{(option, index) => (
<div
ref={(el) => (optionRefs[index] = el)}
class={cx(
'z-[5] cursor-pointer rounded-sm px-2 py-1',
selected() === option().value
? 'text-on-primary-surface hover:bg-primary-surface-hover/45'
: 'text-on-surface hover:bg-surface/35',
)}
onClick={() => handleSelect(option().value)}>
{option().label}
</div>
)}
</Index>
<Show when={isNotNil(selected())}>
<div
class="pointer-events-none absolute z-[2] rounded-sm bg-primary-surface transition-[top,left,height,width] duration-200 ease-in-out"
style={indicatorStyle()}
/>
</Show>
</div>
<Show when={isNotNil(mProps.name)}>
<input type="hidden" name={mProps.name} value={selected()} />
</Show>
</div>
);
};