筛选组件开发

This commit is contained in:
juguohong
2025-08-17 18:36:43 +08:00
parent 4f6ca73148
commit db48e55b05
17 changed files with 406 additions and 314 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { BubbleOption } from './index';
import './bubbleItem.scss';
import styles from './bubbleItem.module.scss';
export interface BubbleItemProps {
option: BubbleOption;
@@ -8,6 +8,7 @@ export interface BubbleItemProps {
size: 'small' | 'medium' | 'large';
disabled: boolean;
onClick: (option: BubbleOption) => void;
itemClassName?: string;
}
const BubbleItem: React.FC<BubbleItemProps> = ({
@@ -15,19 +16,20 @@ const BubbleItem: React.FC<BubbleItemProps> = ({
isSelected,
size,
disabled,
onClick
onClick,
itemClassName
}) => {
return (
<button
className={`bubble-option ${size} ${isSelected ? 'selected' : ''} ${
option.disabled || disabled ? 'disabled' : ''
}`}
className={`${styles.bubbleOption} ${size} ${isSelected ? styles.selected : ''} ${
option.disabled || disabled ? styles.disabled : ''
} ${itemClassName ? itemClassName : ''}`}
onClick={() => onClick(option)}
disabled={option.disabled || disabled}
>
{option.icon && <span className="bubble-icon">{option.icon}</span>}
<span className="bubble-label">{option.label}</span>
{option.description && <span className="bubble-description">{option.description}</span>}
{option.icon && <span className={ styles.bubbleIcon}>{option.icon}</span>}
<span className={styles.bubbleLabel}>{option.label}</span>
{option.description && <span className={ styles.bubbleDescription}>{option.description}</span>}
</button>
);
};

View File

@@ -1,4 +1,4 @@
.bubble-option {
.bubbleOption {
position: relative;
border: 1px solid #e5e5e5;
outline: none; // 移除浏览器默认的outline
@@ -14,11 +14,11 @@
justify-content: center;
gap: 8px;
white-space: nowrap;
padding: 0;
font-size: 14px;
border-radius: 28px;
margin: 0;
height: 36px;
width: 116px;
height: 28px;
// 移除浏览器默认样式
&:focus {
@@ -38,8 +38,6 @@
// 尺寸变体
&.small {
padding: 8px 12px;
min-height: 32px;
font-size: 12px;
}
@@ -71,7 +69,7 @@
}
// 图标样式
.bubble-icon {
.bubbleIcon {
display: flex;
align-items: center;
justify-content: center;
@@ -83,12 +81,12 @@
}
// 标签样式
.bubble-label {
.bubbleLabel {
font-weight: 500;
}
// 描述样式
.bubble-description {
.bubbleDescription {
font-size: 12px;
opacity: 0.7;
font-weight: 400;

View File

@@ -1,34 +1,23 @@
.bubble-container {
.bubbleContainer {
width: 100%;
// 水平布局
.bubble-horizontal {
.bubbleHorizontal {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: space-between;
.bubble-option {
$gap: 8px;
$count: 3;
flex-basis: calc(100% / $count - $gap * 2);
gap: $gap;
}
}
// 垂直布局
.bubble-vertical {
.bubbleVertical {
display: flex;
flex-direction: column;
gap: 12px;
.bubble-option {
width: 100%;
}
}
// 网格布局
.bubble-grid {
.bubbleGrid {
display: grid;
width: 100%;
}

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import './index.scss';
import BubbleItem from './BubbleItem';
import React, { useState, useEffect } from "react";
import styles from "./index.module.scss";
import BubbleItem from "./BubbleItem";
export interface BubbleOption {
id: string | number;
@@ -14,12 +14,16 @@ export interface BubbleOption {
export interface BubbleProps {
options: BubbleOption[];
value?: string | number | (string | number)[];
onChange?: (value: string | number | (string | number)[], option: BubbleOption | BubbleOption[]) => void;
onChange?: (
value: string | number | (string | number)[],
option: BubbleOption | BubbleOption[]
) => void;
multiple?: boolean;
layout?: 'horizontal' | 'vertical' | 'grid';
layout?: "horizontal" | "vertical" | "grid";
columns?: number;
size?: 'small' | 'medium' | 'large';
size?: "small" | "medium" | "large";
className?: string;
itemClassName?: string;
style?: React.CSSProperties;
disabled?: boolean;
}
@@ -29,12 +33,13 @@ const Bubble: React.FC<BubbleProps> = ({
value,
onChange,
multiple = false,
layout = 'horizontal',
layout = "horizontal",
columns = 3,
size = 'small',
className = '',
size = "small",
className = "",
itemClassName = "",
style = {},
disabled = false
disabled = false,
}) => {
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
@@ -53,7 +58,7 @@ const Bubble: React.FC<BubbleProps> = ({
if (multiple) {
if (selectedValues.includes(option.value)) {
newSelectedValues = selectedValues.filter(v => v !== option.value);
newSelectedValues = selectedValues.filter((v) => v !== option.value);
} else {
newSelectedValues = [...selectedValues, option.value];
}
@@ -62,11 +67,13 @@ const Bubble: React.FC<BubbleProps> = ({
}
setSelectedValues(newSelectedValues);
// 调用onChange回调传递选中的值和对应的选项
if (onChange) {
if (multiple) {
const selectedOptions = options.filter(opt => newSelectedValues.includes(opt.value));
const selectedOptions = options.filter((opt) =>
newSelectedValues.includes(opt.value)
);
onChange(newSelectedValues, selectedOptions);
} else {
onChange(option.value, option);
@@ -79,7 +86,7 @@ const Bubble: React.FC<BubbleProps> = ({
};
const renderHorizontalLayout = () => (
<div className="bubble-horizontal">
<div className={styles.bubbleHorizontal}>
{options.map((option) => (
<BubbleItem
key={option.id}
@@ -88,13 +95,14 @@ const Bubble: React.FC<BubbleProps> = ({
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
);
const renderVerticalLayout = () => (
<div className="bubble-vertical">
<div className={styles.bubbleVertical}>
{options.map((option) => (
<BubbleItem
key={option.id}
@@ -103,17 +111,18 @@ const Bubble: React.FC<BubbleProps> = ({
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
);
const renderGridLayout = () => (
<div
className="bubble-grid"
style={{
<div
className={styles.bubbleGrid}
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: size === 'small' ? '8px' : size === 'large' ? '16px' : '12px'
gap: size === "small" ? "8px" : size === "large" ? "16px" : "12px",
}}
>
{options.map((option) => (
@@ -124,6 +133,7 @@ const Bubble: React.FC<BubbleProps> = ({
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
@@ -131,11 +141,11 @@ const Bubble: React.FC<BubbleProps> = ({
const renderLayout = () => {
switch (layout) {
case 'horizontal':
case "horizontal":
return renderHorizontalLayout();
case 'vertical':
case "vertical":
return renderVerticalLayout();
case 'grid':
case "grid":
return renderGridLayout();
default:
return renderHorizontalLayout();
@@ -143,7 +153,7 @@ const Bubble: React.FC<BubbleProps> = ({
};
return (
<div className={`bubble-container ${className}`} style={style}>
<div className={`${styles.bubbleContainer} ${className}`} style={style}>
{renderLayout()}
</div>
);