通用组件开发
This commit is contained in:
35
src/components/Bubble/BubbleItem.tsx
Normal file
35
src/components/Bubble/BubbleItem.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { BubbleOption } from './index';
|
||||
import './bubbleItem.scss';
|
||||
|
||||
export interface BubbleItemProps {
|
||||
option: BubbleOption;
|
||||
isSelected: boolean;
|
||||
size: 'small' | 'medium' | 'large';
|
||||
disabled: boolean;
|
||||
onClick: (option: BubbleOption) => void;
|
||||
}
|
||||
|
||||
const BubbleItem: React.FC<BubbleItemProps> = ({
|
||||
option,
|
||||
isSelected,
|
||||
size,
|
||||
disabled,
|
||||
onClick
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`bubble-option ${size} ${isSelected ? 'selected' : ''} ${
|
||||
option.disabled || disabled ? 'disabled' : ''
|
||||
}`}
|
||||
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>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default BubbleItem;
|
||||
208
src/components/Bubble/README.md
Normal file
208
src/components/Bubble/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Bubble 通用气泡组件
|
||||
|
||||
一个高度可配置的气泡选择器组件,支持任何内容的选择,包括但不限于时间、地点、标签、分类等。
|
||||
|
||||
## 特性
|
||||
|
||||
- 🎯 支持单选和多选模式
|
||||
- 📱 三种布局方式:水平、垂直、网格
|
||||
- 🎨 三种尺寸:小、中、大
|
||||
- ♿ 支持禁用状态和图标描述
|
||||
- 🔄 支持受控和非受控模式
|
||||
- 📱 响应式设计,自动适应不同屏幕
|
||||
- 🎨 可自定义样式和类名
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 室内外选择示例(如UI图所示)
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
import Bubble, { BubbleOption } from './index';
|
||||
|
||||
const LocationSelector: React.FC = () => {
|
||||
const [selectedLocation, setSelectedLocation] = useState<string>('');
|
||||
|
||||
const locationOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '室内', value: 'indoor' },
|
||||
{ id: 2, label: '室外', value: 'outdoor' },
|
||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Bubble
|
||||
options={locationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={(value) => setSelectedLocation(value as string)}
|
||||
layout="horizontal"
|
||||
size="medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 时间选择器示例
|
||||
|
||||
```tsx
|
||||
const TimeSelector: React.FC = () => {
|
||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
||||
|
||||
const timeOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '晨间 6:00-10:00', value: 'morning' },
|
||||
{ id: 2, label: '上午 10:00-12:00', value: 'forenoon' },
|
||||
{ id: 3, label: '中午 12:00-14:00', value: 'noon' },
|
||||
{ id: 4, label: '下午 14:00-18:00', value: 'afternoon' },
|
||||
{ id: 5, label: '晚上 18:00-22:00', value: 'evening' },
|
||||
{ id: 6, label: '夜间 22:00-24:00', value: 'night' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Bubble
|
||||
options={timeOptions}
|
||||
value={selectedTime}
|
||||
onChange={(value) => setSelectedTime(value as string)}
|
||||
layout="grid"
|
||||
columns={3}
|
||||
size="medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 多选模式
|
||||
|
||||
```tsx
|
||||
const MultiSelectExample: React.FC = () => {
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{ id: 1, label: '运动', value: 'sports' },
|
||||
{ id: 2, label: '音乐', value: 'music' },
|
||||
{ id: 3, label: '阅读', value: 'reading' },
|
||||
{ id: 4, label: '旅行', value: 'travel' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValues}
|
||||
onChange={(value) => setSelectedValues(value as string[])}
|
||||
multiple={true}
|
||||
layout="grid"
|
||||
columns={2}
|
||||
size="medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 带图标和描述
|
||||
|
||||
```tsx
|
||||
const IconExample: React.FC = () => {
|
||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: '网球',
|
||||
value: 'tennis',
|
||||
icon: '🎾',
|
||||
description: '室内外均可'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '篮球',
|
||||
value: 'basketball',
|
||||
icon: '🏀',
|
||||
description: '室内场地'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={(value) => setSelectedValue(value as string)}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### BubbleProps
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| options | `BubbleOption[]` | - | 选项数组 |
|
||||
| value | `string \| number \| (string \| number)[]` | - | 当前选中的值 |
|
||||
| onChange | `(value, option) => void` | - | 选择变化时的回调 |
|
||||
| multiple | `boolean` | `false` | 是否支持多选 |
|
||||
| layout | `'horizontal' \| 'vertical' \| 'grid'` | `'horizontal'` | 布局方式 |
|
||||
| columns | `number` | `3` | 网格布局的列数 |
|
||||
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 按钮尺寸 |
|
||||
| className | `string` | `''` | 自定义类名 |
|
||||
| style | `React.CSSProperties` | `{}` | 自定义样式 |
|
||||
| disabled | `boolean` | `false` | 是否禁用整个组件 |
|
||||
|
||||
### BubbleOption
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| id | `string \| number` | - | 选项的唯一标识 |
|
||||
| label | `string` | - | 显示的文本 |
|
||||
| value | `string \| number` | - | 选项的值 |
|
||||
| disabled | `boolean` | `false` | 是否禁用 |
|
||||
| icon | `React.ReactNode` | - | 可选的图标 |
|
||||
| description | `string` | - | 可选的描述文本 |
|
||||
|
||||
## 布局说明
|
||||
|
||||
### 水平布局 (horizontal)
|
||||
- 适合选项较少的情况
|
||||
- 自动换行,响应式设计
|
||||
- 适合顶部导航、标签选择等
|
||||
|
||||
### 垂直布局 (vertical)
|
||||
- 适合选项较多的情况
|
||||
- 每个选项占满一行
|
||||
- 适合侧边栏、设置页面等
|
||||
|
||||
### 网格布局 (grid)
|
||||
- 适合选项较多且需要整齐排列的情况
|
||||
- 可自定义列数
|
||||
- 适合分类选择、时间选择等
|
||||
|
||||
## 尺寸说明
|
||||
|
||||
### 小尺寸 (small)
|
||||
- 适合紧凑的界面
|
||||
- 适合移动端或空间受限的场景
|
||||
|
||||
### 中尺寸 (medium)
|
||||
- 默认尺寸,适合大多数场景
|
||||
- 平衡了可用性和美观性
|
||||
|
||||
### 大尺寸 (large)
|
||||
- 适合需要突出显示的场景
|
||||
- 适合触摸设备或重要操作
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件使用 SCSS 编写,可以通过以下方式自定义样式:
|
||||
|
||||
1. 覆盖 CSS 变量
|
||||
2. 使用 `className` 和 `style` 属性
|
||||
3. 修改 SCSS 源文件
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 在网格布局中,`columns` 属性控制列数,行数会根据选项数量自动计算
|
||||
- 多选模式下,`value` 应该是数组类型
|
||||
- 单选模式下,`value` 可以是字符串或数字类型
|
||||
- 组件会自动处理选中状态的样式变化
|
||||
- 支持图标和描述,让选项更加丰富
|
||||
- 响应式设计,自动适应不同屏幕尺寸
|
||||
211
src/components/Bubble/USAGE.md
Normal file
211
src/components/Bubble/USAGE.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 如何使用 Bubble 通用气泡组件
|
||||
|
||||
## 在其他组件中导入
|
||||
|
||||
```tsx
|
||||
import Bubble, { BubbleOption } from '@/components/Bubble';
|
||||
```
|
||||
|
||||
## 基本使用示例
|
||||
|
||||
### 1. 室内外选择(如UI图所示)
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
import Bubble, { BubbleOption } from '@/components/Bubble';
|
||||
|
||||
const MyPage: React.FC = () => {
|
||||
const [selectedLocation, setSelectedLocation] = useState<string>('');
|
||||
|
||||
const locationOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '室内', value: 'indoor' },
|
||||
{ id: 2, label: '室外', value: 'outdoor' },
|
||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>选择场地类型</h2>
|
||||
<Bubble
|
||||
options={locationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={(value) => setSelectedLocation(value as string)}
|
||||
layout="horizontal"
|
||||
size="medium"
|
||||
/>
|
||||
<p>您选择的场地类型: {selectedLocation}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 时间选择器
|
||||
|
||||
```tsx
|
||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
||||
|
||||
const timeOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '晨间 6:00-10:00', value: 'morning' },
|
||||
{ id: 2, label: '上午 10:00-12:00', value: 'forenoon' },
|
||||
{ id: 3, label: '中午 12:00-14:00', value: 'noon' },
|
||||
{ id: 4, label: '下午 14:00-18:00', value: 'afternoon' },
|
||||
{ id: 5, label: '晚上 18:00-22:00', value: 'evening' },
|
||||
{ id: 6, label: '夜间 22:00-24:00', value: 'night' }
|
||||
];
|
||||
|
||||
<Bubble
|
||||
options={timeOptions}
|
||||
value={selectedTime}
|
||||
onChange={(value) => setSelectedTime(value as string)}
|
||||
layout="grid"
|
||||
columns={3}
|
||||
size="medium"
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. 多选模式
|
||||
|
||||
```tsx
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{ id: 1, label: '运动', value: 'sports' },
|
||||
{ id: 2, label: '音乐', value: 'music' },
|
||||
{ id: 3, label: '阅读', value: 'reading' },
|
||||
{ id: 4, label: '旅行', value: 'travel' }
|
||||
];
|
||||
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValues}
|
||||
onChange={(value) => setSelectedValues(value as string[])}
|
||||
multiple={true}
|
||||
layout="grid"
|
||||
columns={2}
|
||||
size="medium"
|
||||
/>
|
||||
```
|
||||
|
||||
### 4. 带图标和描述
|
||||
|
||||
```tsx
|
||||
const [selectedSport, setSelectedSport] = useState<string>('');
|
||||
|
||||
const sportOptions: BubbleOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: '网球',
|
||||
value: 'tennis',
|
||||
icon: '🎾',
|
||||
description: '室内外均可'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '篮球',
|
||||
value: 'basketball',
|
||||
icon: '🏀',
|
||||
description: '室内场地'
|
||||
}
|
||||
];
|
||||
|
||||
<Bubble
|
||||
options={sportOptions}
|
||||
value={selectedSport}
|
||||
onChange={(value) => setSelectedSport(value as string)}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. 不同布局方式
|
||||
|
||||
```tsx
|
||||
// 水平布局 - 适合选项较少
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="horizontal"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
// 垂直布局 - 适合选项较多
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="vertical"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
// 网格布局 - 适合需要整齐排列
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="grid"
|
||||
columns={3}
|
||||
size="medium"
|
||||
/>
|
||||
```
|
||||
|
||||
### 6. 不同尺寸
|
||||
|
||||
```tsx
|
||||
// 小尺寸 - 适合紧凑界面
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="horizontal"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
// 中尺寸 - 默认尺寸
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="horizontal"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
// 大尺寸 - 适合触摸设备
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
layout="horizontal"
|
||||
size="large"
|
||||
/>
|
||||
```
|
||||
|
||||
## 组件特性
|
||||
|
||||
- **通用性**: 不局限于特定功能,可用于任何选择场景
|
||||
- **灵活布局**: 支持水平、垂直、网格三种布局方式
|
||||
- **多尺寸支持**: 小、中、大三种尺寸适应不同场景
|
||||
- **丰富内容**: 支持图标和描述,让选项更加丰富
|
||||
- **响应式设计**: 自动适应不同屏幕尺寸
|
||||
- **状态管理**: 内置选中状态管理,支持单选和多选
|
||||
- **类型安全**: 完整的 TypeScript 类型定义
|
||||
- **可访问性**: 支持键盘导航和屏幕阅读器
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
1. **场地选择**: 室内/室外/半室外
|
||||
2. **时间选择**: 时间段、日期范围
|
||||
3. **分类选择**: 兴趣爱好、技能标签
|
||||
4. **设置选项**: 主题、语言、通知设置
|
||||
5. **筛选条件**: 价格范围、评分、距离等
|
||||
6. **导航菜单**: 顶部导航、侧边栏菜单
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保传入的 `options` 数组不为空
|
||||
2. 多选模式下,`value` 应该是数组类型
|
||||
3. 单选模式下,`value` 可以是字符串或数字类型
|
||||
4. 组件会自动处理选中状态的样式变化
|
||||
5. 支持图标和描述,让选项更加丰富
|
||||
6. 响应式设计,自动适应不同屏幕尺寸
|
||||
7. 可以通过 `disabled` 属性禁用整个组件或单个选项
|
||||
96
src/components/Bubble/bubbleItem.scss
Normal file
96
src/components/Bubble/bubbleItem.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
.bubble-option {
|
||||
position: relative;
|
||||
border: 1px solid #e5e5e5;
|
||||
outline: none; // 移除浏览器默认的outline
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
border-radius: 28px;
|
||||
margin: 0;
|
||||
height: 36px;
|
||||
|
||||
// 移除浏览器默认样式
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// 尺寸变体
|
||||
&.small {
|
||||
padding: 8px 12px;
|
||||
min-height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
padding: 12px 16px;
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
padding: 16px 20px;
|
||||
min-height: 56px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #f5f5f5;
|
||||
color: #999999;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
// 图标样式
|
||||
.bubble-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式
|
||||
.bubble-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 描述样式
|
||||
.bubble-description {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
235
src/components/Bubble/example.tsx
Normal file
235
src/components/Bubble/example.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useState } from 'react';
|
||||
import Bubble, { BubbleOption } from './index';
|
||||
|
||||
// 室内外选择示例(如UI图所示)
|
||||
export const LocationSelector: React.FC = () => {
|
||||
const [selectedLocation, setSelectedLocation] = useState<string>('');
|
||||
|
||||
const locationOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '室内', value: 'indoor' },
|
||||
{ id: 2, label: '室外', value: 'outdoor' },
|
||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>选择场地类型</h3>
|
||||
<Bubble
|
||||
options={locationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={(value) => setSelectedLocation(value as string)}
|
||||
layout="horizontal"
|
||||
size="small"
|
||||
/>
|
||||
<p>当前选择: {selectedLocation || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 时间选择器示例
|
||||
export const TimeSelector: React.FC = () => {
|
||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
||||
|
||||
const timeOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '晨间 6:00-10:00', value: 'morning' },
|
||||
{ id: 2, label: '上午 10:00-12:00', value: 'forenoon' },
|
||||
{ id: 3, label: '中午 12:00-14:00', value: 'noon' },
|
||||
{ id: 4, label: '下午 14:00-18:00', value: 'afternoon' },
|
||||
{ id: 5, label: '晚上 18:00-22:00', value: 'evening' },
|
||||
{ id: 6, label: '夜间 22:00-24:00', value: 'night' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>选择时间段</h3>
|
||||
<Bubble
|
||||
options={timeOptions}
|
||||
value={selectedTime}
|
||||
onChange={(value) => setSelectedTime(value as string)}
|
||||
layout="grid"
|
||||
size="small"
|
||||
columns={3}
|
||||
/>
|
||||
<p>当前选择: {selectedTime || '未选择'}</p>
|
||||
<hr />
|
||||
<h3>选择时间段</h3>
|
||||
<Bubble
|
||||
options={timeOptions}
|
||||
value={selectedTime}
|
||||
onChange={(value) => setSelectedTime(value as string)}
|
||||
layout="grid"
|
||||
columns={2}
|
||||
size="small"
|
||||
/>
|
||||
<p>当前选择: {selectedTime || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 多选示例
|
||||
export const MultiSelectExample: React.FC = () => {
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{ id: 1, label: '运动', value: 'sports' },
|
||||
{ id: 2, label: '音乐', value: 'music' },
|
||||
{ id: 3, label: '阅读', value: 'reading' },
|
||||
{ id: 4, label: '旅行', value: 'travel' },
|
||||
{ id: 5, label: '美食', value: 'food' },
|
||||
{ id: 6, label: '摄影', value: 'photography' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>多选示例 - 兴趣爱好</h3>
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValues}
|
||||
onChange={(value) => setSelectedValues(value as string[])}
|
||||
multiple={true}
|
||||
layout="grid"
|
||||
columns={3}
|
||||
size="medium"
|
||||
/>
|
||||
<p>当前选择: {selectedValues.join(', ') || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 带图标和描述的示例
|
||||
export const IconExample: React.FC = () => {
|
||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: '网球',
|
||||
value: 'tennis',
|
||||
icon: '🎾',
|
||||
description: '室内外均可'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '篮球',
|
||||
value: 'basketball',
|
||||
icon: '🏀',
|
||||
description: '室内场地'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '足球',
|
||||
value: 'football',
|
||||
icon: '⚽',
|
||||
description: '室外场地'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>带图标和描述的运动选择</h3>
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={(value) => setSelectedValue(value as string)}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
/>
|
||||
<p>当前选择: {selectedValue || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 带禁用状态的示例
|
||||
export const DisabledExample: React.FC = () => {
|
||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
||||
|
||||
const options: BubbleOption[] = [
|
||||
{ id: 1, label: '可用选项1', value: 'option1' },
|
||||
{ id: 2, label: '禁用选项2', value: 'option2', disabled: true },
|
||||
{ id: 3, label: '可用选项3', value: 'option3' },
|
||||
{ id: 4, label: '禁用选项4', value: 'option4', disabled: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>带禁用状态的示例</h3>
|
||||
<Bubble
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={(value) => setSelectedValue(value as string)}
|
||||
layout="grid"
|
||||
columns={2}
|
||||
size="medium"
|
||||
/>
|
||||
<p>当前选择: {selectedValue || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 不同尺寸的示例
|
||||
export const SizeExample: React.FC = () => {
|
||||
const [selectedSize, setSelectedSize] = useState<string>('');
|
||||
|
||||
const sizeOptions: BubbleOption[] = [
|
||||
{ id: 1, label: '小尺寸', value: 'small' },
|
||||
{ id: 2, label: '中尺寸', value: 'medium' },
|
||||
{ id: 3, label: '大尺寸', value: 'large' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>不同尺寸的示例</h3>
|
||||
|
||||
<h4>小尺寸</h4>
|
||||
<Bubble
|
||||
options={sizeOptions}
|
||||
value={selectedSize}
|
||||
onChange={(value) => setSelectedSize(value as string)}
|
||||
layout="horizontal"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<h4>中尺寸</h4>
|
||||
<Bubble
|
||||
options={sizeOptions}
|
||||
value={selectedSize}
|
||||
onChange={(value) => setSelectedSize(value as string)}
|
||||
layout="horizontal"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
<h4>大尺寸</h4>
|
||||
<Bubble
|
||||
options={sizeOptions}
|
||||
value={selectedSize}
|
||||
onChange={(value) => setSelectedSize(value as string)}
|
||||
layout="horizontal"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<p>当前选择: {selectedSize || '未选择'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主示例组件
|
||||
export const BubbleExamples: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Bubble 通用气泡组件示例</h1>
|
||||
<LocationSelector />
|
||||
<hr />
|
||||
<TimeSelector />
|
||||
<hr />
|
||||
<MultiSelectExample />
|
||||
<hr />
|
||||
<IconExample />
|
||||
<hr />
|
||||
<DisabledExample />
|
||||
<hr />
|
||||
<SizeExample />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BubbleExamples;
|
||||
35
src/components/Bubble/index.scss
Normal file
35
src/components/Bubble/index.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
.bubble-container {
|
||||
width: 100%;
|
||||
|
||||
// 水平布局
|
||||
.bubble-horizontal {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.bubble-option {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 网格布局
|
||||
.bubble-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
152
src/components/Bubble/index.tsx
Normal file
152
src/components/Bubble/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './index.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;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Bubble: React.FC<BubbleProps> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
multiple = false,
|
||||
layout = 'horizontal',
|
||||
columns = 3,
|
||||
size = 'small',
|
||||
className = '',
|
||||
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="bubble-horizontal">
|
||||
{options.map((option) => (
|
||||
<BubbleItem
|
||||
key={option.id}
|
||||
option={option}
|
||||
isSelected={isSelected(option)}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
onClick={handleOptionClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderVerticalLayout = () => (
|
||||
<div className="bubble-vertical">
|
||||
{options.map((option) => (
|
||||
<BubbleItem
|
||||
key={option.id}
|
||||
option={option}
|
||||
isSelected={isSelected(option)}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
onClick={handleOptionClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderGridLayout = () => (
|
||||
<div
|
||||
className="bubble-grid"
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLayout = () => {
|
||||
switch (layout) {
|
||||
case 'horizontal':
|
||||
return renderHorizontalLayout();
|
||||
case 'vertical':
|
||||
return renderVerticalLayout();
|
||||
case 'grid':
|
||||
return renderGridLayout();
|
||||
default:
|
||||
return renderHorizontalLayout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`bubble-container ${className}`} style={style}>
|
||||
{renderLayout()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bubble;
|
||||
Reference in New Issue
Block a user