Files
mini-programs/src/components/Bubble/index.tsx
2025-08-17 18:36:43 +08:00

163 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from "react";
import styles from "./index.module.scss";
import BubbleItem from "./BubbleItem";
export interface BubbleOption {
id: string | number;
label: string;
value: string | number;
disabled?: boolean;
icon?: React.ReactNode;
description?: string;
}
export interface BubbleProps {
options: BubbleOption[];
value?: string | number | (string | number)[];
onChange?: (
value: string | number | (string | number)[],
option: BubbleOption | BubbleOption[]
) => void;
multiple?: boolean;
layout?: "horizontal" | "vertical" | "grid";
columns?: number;
size?: "small" | "medium" | "large";
className?: string;
itemClassName?: string;
style?: React.CSSProperties;
disabled?: boolean;
}
const Bubble: React.FC<BubbleProps> = ({
options,
value,
onChange,
multiple = false,
layout = "horizontal",
columns = 3,
size = "small",
className = "",
itemClassName = "",
style = {},
disabled = false,
}) => {
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
// 同步外部传入的value
useEffect(() => {
if (value !== undefined) {
const newValues = Array.isArray(value) ? value : [value];
setSelectedValues(newValues);
}
}, [value]);
const handleOptionClick = (option: BubbleOption) => {
if (disabled || option.disabled) return;
let newSelectedValues: (string | number)[];
if (multiple) {
if (selectedValues.includes(option.value)) {
newSelectedValues = selectedValues.filter((v) => v !== option.value);
} else {
newSelectedValues = [...selectedValues, option.value];
}
} else {
newSelectedValues = [option.value];
}
setSelectedValues(newSelectedValues);
// 调用onChange回调传递选中的值和对应的选项
if (onChange) {
if (multiple) {
const selectedOptions = options.filter((opt) =>
newSelectedValues.includes(opt.value)
);
onChange(newSelectedValues, selectedOptions);
} else {
onChange(option.value, option);
}
}
};
const isSelected = (option: BubbleOption) => {
return selectedValues.includes(option.value);
};
const renderHorizontalLayout = () => (
<div className={styles.bubbleHorizontal}>
{options.map((option) => (
<BubbleItem
key={option.id}
option={option}
isSelected={isSelected(option)}
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
);
const renderVerticalLayout = () => (
<div className={styles.bubbleVertical}>
{options.map((option) => (
<BubbleItem
key={option.id}
option={option}
isSelected={isSelected(option)}
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
);
const renderGridLayout = () => (
<div
className={styles.bubbleGrid}
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: size === "small" ? "8px" : size === "large" ? "16px" : "12px",
}}
>
{options.map((option) => (
<BubbleItem
key={option.id}
option={option}
isSelected={isSelected(option)}
size={size}
disabled={disabled}
onClick={handleOptionClick}
itemClassName={itemClassName}
/>
))}
</div>
);
const renderLayout = () => {
switch (layout) {
case "horizontal":
return renderHorizontalLayout();
case "vertical":
return renderVerticalLayout();
case "grid":
return renderGridLayout();
default:
return renderHorizontalLayout();
}
};
return (
<div className={`${styles.bubbleContainer} ${className}`} style={style}>
{renderLayout()}
</div>
);
};
export default Bubble;