列表综合筛选
This commit is contained in:
@@ -1,16 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BubbleOption } from './index';
|
import { BubbleItemProps } from '../../../types/list/types';
|
||||||
import styles from './bubbleItem.module.scss';
|
import styles from './bubbleItem.module.scss';
|
||||||
|
|
||||||
export interface BubbleItemProps {
|
|
||||||
option: BubbleOption;
|
|
||||||
isSelected: boolean;
|
|
||||||
size: 'small' | 'medium' | 'large';
|
|
||||||
disabled: boolean;
|
|
||||||
onClick: (option: BubbleOption) => void;
|
|
||||||
itemClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BubbleItem: React.FC<BubbleItemProps> = ({
|
const BubbleItem: React.FC<BubbleItemProps> = ({
|
||||||
option,
|
option,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
# 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` 可以是字符串或数字类型
|
|
||||||
- 组件会自动处理选中状态的样式变化
|
|
||||||
- 支持图标和描述,让选项更加丰富
|
|
||||||
- 响应式设计,自动适应不同屏幕尺寸
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
# 如何使用 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` 属性禁用整个组件或单个选项
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,34 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import BubbleItem from "./BubbleItem";
|
import BubbleItem from "./BubbleItem";
|
||||||
|
import {BubbleProps} from '../../../types/list/types'
|
||||||
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?: (
|
|
||||||
name: string,
|
|
||||||
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;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bubble: React.FC<BubbleProps> = ({
|
const Bubble: React.FC<BubbleProps> = ({
|
||||||
options,
|
options,
|
||||||
@@ -79,6 +52,7 @@ const Bubble: React.FC<BubbleProps> = ({
|
|||||||
);
|
);
|
||||||
onChange(name, newSelectedValues, selectedOptions);
|
onChange(name, newSelectedValues, selectedOptions);
|
||||||
} else {
|
} else {
|
||||||
|
console.log('===111', name, option.value)
|
||||||
onChange(name, option.value, option);
|
onChange(name, option.value, option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import MenuComponent from "./index";
|
|
||||||
|
|
||||||
export default function Example() {
|
|
||||||
const [value, setValue] = useState("a");
|
|
||||||
const options = [
|
|
||||||
{ text: "默认排序", value: "a" },
|
|
||||||
{ text: "好评排序", value: "b" },
|
|
||||||
{ text: "销量排序", value: "c" },
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<MenuComponent
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => setValue(val)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
.menuWrap {
|
.menuWrap {
|
||||||
padding: 5px 20px 10px;
|
padding: 5px 20px 10px;
|
||||||
|
$height: 26px;
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
&.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menuItem {
|
.menuItem {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -13,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
.nut-menu-bar {
|
:global(.nut-menu-bar) {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -21,24 +31,23 @@
|
|||||||
|
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
line-height: 1;
|
width: 66px;
|
||||||
|
height: $height;
|
||||||
|
border-radius: $height;
|
||||||
|
line-height: $height;
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
min-height: 28px;
|
|
||||||
min-width: 80px;
|
|
||||||
border-radius: 28px;
|
|
||||||
border: 1px solid #e5e5e5;
|
border: 1px solid #e5e5e5;
|
||||||
line-height: 28px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: max-content;
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.nut-menu-title {
|
:global(.nut-menu-title) {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nut-menu-title-text {
|
:global(.nut-menu-title-text) {
|
||||||
padding-left: 0;
|
--nutui-menu-title-padding: 0 6px 0 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.positionWrap {
|
.positionWrap {
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
import { Menu } from "@nutui/nutui-react-taro";
|
import { Menu } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import Bubble, { BubbleOption } from "../Bubble";
|
import Bubble from "../Bubble";
|
||||||
|
import { Image } from "@tarojs/components";
|
||||||
|
import img from "../../config/images";
|
||||||
|
import {DistanceFilterProps} from '../../../types/list/types'
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
options: BubbleOption[];
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
wrapperClassName?: string;
|
|
||||||
itemClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MenuComponent = (props: IProps) => {
|
|
||||||
const { value, onChange, wrapperClassName, itemClassName, options } = props;
|
const MenuComponent = (props: DistanceFilterProps) => {
|
||||||
|
const { value, onChange, wrapperClassName, itemClassName, options, name } =
|
||||||
|
props;
|
||||||
const [isChange, setIsChange] = useState(false);
|
const [isChange, setIsChange] = useState(false);
|
||||||
|
const [iOpen, setIsOpen] = useState(false);
|
||||||
const itemRef = useRef(null);
|
const itemRef = useRef(null);
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (name: string, value: string) => {
|
||||||
console.log("===value", value);
|
|
||||||
setIsChange(true);
|
setIsChange(true);
|
||||||
onChange && onChange(value);
|
onChange && onChange(name, value);
|
||||||
|
(itemRef.current as any)?.toggle(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -30,11 +35,25 @@ const MenuComponent = (props: IProps) => {
|
|||||||
isChange ? styles.active : ""
|
isChange ? styles.active : ""
|
||||||
}`}
|
}`}
|
||||||
activeColor="#000"
|
activeColor="#000"
|
||||||
|
onOpen={handleOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={`${styles.menuIcon} ${iOpen ? styles.rotate : ""}`}
|
||||||
|
src={isChange ? img.ICON_ARROW_DOWN_WHITE : img.ICON_ARROW_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
title="全城"
|
title={value}
|
||||||
className={`${styles.menuItem} ${itemClassName ? itemClassName : ''}`}
|
className={`${styles.menuItem} ${itemClassName ? itemClassName : ""}`}
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={styles.itemIcon}
|
||||||
|
src={img.ICON_MENU_ITEM_SELECTED}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.positionWrap}>
|
<div className={styles.positionWrap}>
|
||||||
<p className={styles.title}>当前位置</p>
|
<p className={styles.title}>当前位置</p>
|
||||||
@@ -49,6 +68,7 @@ const MenuComponent = (props: IProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
columns={4}
|
columns={4}
|
||||||
itemClassName={styles.distanceBubbleItem}
|
itemClassName={styles.distanceBubbleItem}
|
||||||
|
name={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -10,26 +10,32 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333333;
|
color: #000000;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-time {
|
.title-right-arrow {
|
||||||
font-size: 14px;
|
width: 16px;
|
||||||
color: #666666;
|
height: 16px;
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time,
|
||||||
.location {
|
.location {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #666666;
|
color: #3C3C4399;
|
||||||
line-height: 1.3;
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-info {
|
.bottom-info {
|
||||||
@@ -58,6 +64,8 @@
|
|||||||
border: 2px solid #ffffff;
|
border: 2px solid #ffffff;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.avatar-image {
|
.avatar-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -89,8 +97,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
padding: 3px;
|
box-sizing: border-box;
|
||||||
border: 1px solid #f5f5f5;
|
padding: 0 6px;
|
||||||
|
border: 0.5px solid #00000029;
|
||||||
|
height: 20px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
min-width: 38px;
|
min-width: 38px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -98,7 +108,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-text-max {
|
.tag-text-max {
|
||||||
@@ -112,16 +122,20 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-basis: 100px;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 2px;
|
border: 1.5px solid #ffffff;
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -132,7 +146,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 88px;
|
width: 88px;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
transform: rotate(-10deg);
|
transform: rotate(-10deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,13 +168,13 @@
|
|||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transform: rotate(-10deg);
|
transform: translateX(4px) rotate(-10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transform: rotate(10deg);
|
transform: translateX(-4px) rotate(10deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +194,7 @@
|
|||||||
width: 55px;
|
width: 55px;
|
||||||
height: 55px;
|
height: 55px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
transform: rotate(-10deg);
|
transform: translateX(4px) rotate(-10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { View, Text, Image } from '@tarojs/components'
|
import { View, Text, Image } from "@tarojs/components";
|
||||||
import './index.scss'
|
import img from "../../config/images";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
title: string
|
title: string;
|
||||||
dateTime: string
|
dateTime: string;
|
||||||
location: string
|
location: string;
|
||||||
distance: string
|
distance: string;
|
||||||
registeredCount: number
|
registeredCount: number;
|
||||||
maxCount: number
|
maxCount: number;
|
||||||
skillLevel: string
|
skillLevel: string;
|
||||||
matchType: string
|
matchType: string;
|
||||||
images: string[]
|
images: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem: React.FC<ListItemProps> = ({
|
const ListItem: React.FC<ListItemProps> = ({
|
||||||
@@ -22,11 +23,11 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
maxCount,
|
maxCount,
|
||||||
skillLevel,
|
skillLevel,
|
||||||
matchType,
|
matchType,
|
||||||
images
|
images,
|
||||||
}) => {
|
}) => {
|
||||||
// 根据图片数量决定展示样式
|
// 根据图片数量决定展示样式
|
||||||
const renderImages = () => {
|
const renderImages = () => {
|
||||||
if (images.length === 0) return null
|
if (images.length === 0) return null;
|
||||||
|
|
||||||
if (images.length === 1) {
|
if (images.length === 1) {
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +36,7 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
<Image src={images[0]} className="image" mode="aspectFill" />
|
<Image src={images[0]} className="image" mode="aspectFill" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images.length === 2) {
|
if (images.length === 2) {
|
||||||
@@ -48,7 +49,7 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
<Image src={images[1]} className="image" mode="aspectFill" />
|
<Image src={images[1]} className="image" mode="aspectFill" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3张或更多图片
|
// 3张或更多图片
|
||||||
@@ -64,40 +65,54 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
<Image src={images[2]} className="image" mode="aspectFill" />
|
<Image src={images[2]} className="image" mode="aspectFill" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="list-item">
|
<View className="list-item">
|
||||||
{/* 左侧内容区域 */}
|
{/* 左侧内容区域 */}
|
||||||
<View className="content">
|
<View className="content">
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
|
<View className="titleWrapper">
|
||||||
<Text className="title">{title}</Text>
|
<Text className="title">{title}</Text>
|
||||||
|
<Image
|
||||||
|
src={img.ICON_LIST_RIGHT_ARROW}
|
||||||
|
className="title-right-arrow"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* 时间信息 */}
|
{/* 时间信息 */}
|
||||||
<Text className="date-time">{dateTime}</Text>
|
<Text className="date-time">{dateTime}</Text>
|
||||||
|
|
||||||
{/* 地点和距离 */}
|
{/* 地点和距离 */}
|
||||||
<Text className="location">{location}・{distance}</Text>
|
<Text className="location">
|
||||||
|
{location}・{distance}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
||||||
<View className="bottom-info">
|
<View className="bottom-info">
|
||||||
<View className="left-section">
|
<View className="left-section">
|
||||||
<View className="avatar-group">
|
<View className="avatar-group">
|
||||||
{Array.from({ length: Math.min(registeredCount, 3) }).map((_, index) => (
|
{Array.from({ length: Math.min(registeredCount, 3) }).map(
|
||||||
|
(_, index) => (
|
||||||
<View key={index} className="avatar">
|
<View key={index} className="avatar">
|
||||||
<Image
|
<Image
|
||||||
className="avatar-image"
|
className="avatar-image"
|
||||||
src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center" className="avatar-image" mode="aspectFill" />
|
src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="tags">
|
<View className="tags">
|
||||||
<View className="tag">
|
<View className="tag">
|
||||||
<Text className="tag-text">
|
<Text className="tag-text">
|
||||||
报名人数 {registeredCount}/<Text className="tag-text-max">{maxCount}</Text>
|
报名人数 {registeredCount}/
|
||||||
|
<Text className="tag-text-max">{maxCount}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="tag">
|
<View className="tag">
|
||||||
@@ -111,11 +126,9 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 右侧图片区域 */}
|
{/* 右侧图片区域 */}
|
||||||
<View className="image-section">
|
<View className="image-section">{renderImages()}</View>
|
||||||
{renderImages()}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default ListItem
|
export default ListItem;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import MenuComponent from "./index";
|
|
||||||
|
|
||||||
export default function Example() {
|
|
||||||
const [value, setValue] = useState("a");
|
|
||||||
const options = [
|
|
||||||
{ text: "默认排序", value: "a" },
|
|
||||||
{ text: "好评排序", value: "b" },
|
|
||||||
{ text: "销量排序", value: "c" },
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<MenuComponent
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => setValue(val)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,46 @@
|
|||||||
.menuWrap {
|
.menuWrap {
|
||||||
position: static;
|
position: static;
|
||||||
padding: 5px 20px 10px;
|
padding: 5px 20px 10px;
|
||||||
|
--nutui-menu-title-padding: 0 6px 0 0;
|
||||||
|
$height: 26px;
|
||||||
|
|
||||||
.menuItem {
|
.menuItem {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-bottom-left-radius: 30px;
|
border-bottom-left-radius: 30px;
|
||||||
border-bottom-right-radius: 30px;
|
border-bottom-right-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
&.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
line-height: 1;
|
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
min-height: 28px;
|
border-radius: $height;
|
||||||
min-width: 94px;
|
line-height: $height;
|
||||||
border-radius: 28px;
|
height: $height;
|
||||||
border: 1px solid #e5e5e5;
|
border: 1px solid #e5e5e5;
|
||||||
line-height: 28px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: max-content;
|
width: 94px;
|
||||||
|
|
||||||
.nut-menu-title-text {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-title) {
|
:global(.nut-menu-title) {
|
||||||
@@ -35,11 +48,16 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.nut-menu-title-text) {
|
||||||
|
--nutui-menu-title-padding: 0 6px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.nut-menu-container-item) {
|
:global(.nut-menu-container-item) {
|
||||||
color: #3c3c43;
|
color: #3c3c43;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-container-item.active) {
|
:global(.nut-menu-container-item.active) {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -1,34 +1,55 @@
|
|||||||
import { Menu } from "@nutui/nutui-react-taro";
|
import { Menu } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Image } from "@tarojs/components";
|
||||||
|
import img from "../../config/images";
|
||||||
|
import { MenuFilterProps } from "../../../types/list/types";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
interface IProps {
|
const MenuComponent = (props: MenuFilterProps) => {
|
||||||
options: { text: string; value: string }[];
|
const { options, value, onChange, wrapperClassName, itemClassName, name } =
|
||||||
value: string;
|
props;
|
||||||
onChange: (value: string) => void;
|
|
||||||
wrapperClassName?: string;
|
|
||||||
itemClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MenuComponent = (props: IProps) => {
|
|
||||||
const { options, value, onChange, wrapperClassName, itemClassName } = props;
|
|
||||||
const [isChange, setIsChange] = useState(false);
|
const [isChange, setIsChange] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (val: Record<string, string>) => {
|
||||||
setIsChange(true);
|
setIsChange(true);
|
||||||
onChange && onChange(value);
|
onChange && onChange(name, val.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
className={`${styles.menuWrap} ${wrapperClassName} ${isChange ? styles.active : ""}`}
|
className={`${styles.menuWrap} ${wrapperClassName} ${
|
||||||
|
isChange ? styles.active : ""
|
||||||
|
}`}
|
||||||
activeColor="#000"
|
activeColor="#000"
|
||||||
|
onOpen={handleOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={`${styles.menuIcon} ${isOpen ? styles.rotate : ""}`}
|
||||||
|
src={isChange ? img.ICON_ARROW_DOWN_WHITE : img.ICON_ARROW_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
className={`${styles.menuItem} ${itemClassName}`}
|
className={`${styles.menuItem} ${itemClassName}`}
|
||||||
options={options}
|
options={options}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={styles.itemIcon}
|
||||||
|
src={img.ICON_MENU_ITEM_SELECTED}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
# NtrpRange 范围选择器组件
|
|
||||||
|
|
||||||
基于NutUI Range组件的双滑块范围选择器,通过CSS样式覆盖完全匹配设计稿,支持自定义范围、步长和回调函数。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 🎯 双滑块设计,支持选择范围区间
|
|
||||||
- 🎨 精准还原设计稿的视觉效果
|
|
||||||
- 📱 响应式设计,支持移动端
|
|
||||||
- 🎮 流畅的拖拽交互体验
|
|
||||||
- ⚙️ 可配置的最小值、最大值和步长
|
|
||||||
- 🔒 支持禁用状态
|
|
||||||
- 📊 实时值变化回调
|
|
||||||
|
|
||||||
## 基本用法
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import NtrpRange from '@/components/Range';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const [value, setValue] = useState<[number, number]>([2.0, 4.0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 属性说明
|
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `min` | `number` | `2.0` | 最小值 |
|
|
||||||
| `max` | `number` | `4.0` | 最大值 |
|
|
||||||
| `step` | `number` | `0.5` | 步长 |
|
|
||||||
| `value` | `[number, number]` | `[min, max]` | 当前选择的范围值 |
|
|
||||||
| `onChange` | `(value: [number, number]) => void` | - | 值变化时的回调函数 |
|
|
||||||
| `disabled` | `boolean` | `false` | 是否禁用 |
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
- 基于NutUI Range组件,确保拖拽功能的可靠性
|
|
||||||
- 通过CSS样式覆盖,完全匹配设计稿视觉效果
|
|
||||||
- TypeScript + React Hooks
|
|
||||||
- 响应式设计,支持移动端
|
|
||||||
|
|
||||||
## 设计规范
|
|
||||||
|
|
||||||
组件严格按照设计稿实现,包含以下视觉元素:
|
|
||||||
|
|
||||||
- **网球图标**: 黑色轮廓的网球图标
|
|
||||||
- **标题**: "NTRP水平区间" 文字
|
|
||||||
- **标签**: 左右两端的范围标签(如"2.0及以下"、"4.0及以上")
|
|
||||||
- **滑块轨道**: 圆角矩形容器,带有浅灰色边框和阴影
|
|
||||||
- **滑块手柄**: 两个白色圆形手柄,带有黑色边框和阴影
|
|
||||||
- **轨道填充**: 黑色填充条,显示当前选择的范围
|
|
||||||
- **标记点**: 四个浅灰色圆点,均匀分布在轨道上
|
|
||||||
|
|
||||||
## 样式定制
|
|
||||||
|
|
||||||
组件使用 BEM 命名规范,可以通过 CSS 变量或覆盖样式来自定义外观:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
.ntrp-range {
|
|
||||||
// 自定义样式
|
|
||||||
&__track {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__handle {
|
|
||||||
background: #007bff;
|
|
||||||
border-color: #0056b3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 确保 `min < max`,否则组件可能无法正常工作
|
|
||||||
2. `step` 值应该能够整除 `max - min` 的差值
|
|
||||||
3. 组件内部会确保左右滑块不会重叠,最小间距为 `step` 值
|
|
||||||
4. 拖拽时会自动吸附到最近的步长值
|
|
||||||
|
|
||||||
## 示例
|
|
||||||
|
|
||||||
查看 `example.tsx` 文件获取更多使用示例。
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: juguohong juguohong@flashhold.com
|
|
||||||
* @Date: 2025-08-16 17:59:28
|
|
||||||
* @LastEditors: juguohong juguohong@flashhold.com
|
|
||||||
* @LastEditTime: 2025-08-16 23:48:25
|
|
||||||
* @FilePath: /mini-programs/src/components/Range/example.tsx
|
|
||||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
||||||
*/
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import NtrpRange from './index';
|
|
||||||
|
|
||||||
const RangeExample: React.FC = () => {
|
|
||||||
const [ntrpRange, setNtrpRange] = useState<[number, number]>([2.0, 4.0]);
|
|
||||||
const [customRange, setCustomRange] = useState<[number, number]>([0, 100]);
|
|
||||||
|
|
||||||
const handleNtrpChange = (value: [number, number]) => {
|
|
||||||
console.log('NTRP range changed:', value);
|
|
||||||
setNtrpRange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCustomChange = (value: [number, number]) => {
|
|
||||||
console.log('Custom range changed:', value);
|
|
||||||
setCustomRange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div >
|
|
||||||
<h1>Range 组件示例</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>NTRP 水平区间选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1.0}
|
|
||||||
max={5.0}
|
|
||||||
step={0.5}
|
|
||||||
value={ntrpRange}
|
|
||||||
onChange={handleNtrpChange}
|
|
||||||
/>
|
|
||||||
<div >
|
|
||||||
当前选择范围: {ntrpRange[0]} - {ntrpRange[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>自定义范围选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={10}
|
|
||||||
value={customRange}
|
|
||||||
onChange={handleCustomChange}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
当前选择范围: {customRange[0]} - {customRange[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>禁用状态</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
step={1}
|
|
||||||
value={[3, 7]}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#999' }}>
|
|
||||||
此选择器已被禁用
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>测试说明</h2>
|
|
||||||
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.6' }}>
|
|
||||||
<p>1. 点击并拖拽左右滑块手柄</p>
|
|
||||||
<p>2. 查看控制台日志确认拖拽事件</p>
|
|
||||||
<p>3. 观察滑块位置和值的实时变化</p>
|
|
||||||
<p>4. 检查调试信息显示</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RangeExample;
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import { View, Text, Image } from "@tarojs/components";
|
||||||
import { Range } from "@nutui/nutui-react-taro";
|
import { Range } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import TitleComponent from "../Title";
|
import TitleComponent from "../Title";
|
||||||
|
import img from "../../config/images";
|
||||||
|
|
||||||
interface RangeProps {
|
interface RangeProps {
|
||||||
min?: number;
|
min?: number;
|
||||||
@@ -25,9 +27,8 @@ const NtrpRange: React.FC<RangeProps> = ({
|
|||||||
name,
|
name,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentValue, setCurrentValue] = useState<[number, number]>(value);
|
const [currentValue, setCurrentValue] = useState<[number, number]>(value);
|
||||||
console.log('===currentValue', currentValue)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('===rrr', value)
|
|
||||||
value && setCurrentValue(value);
|
value && setCurrentValue(value);
|
||||||
}, [JSON.stringify(value || [])]);
|
}, [JSON.stringify(value || [])]);
|
||||||
|
|
||||||
@@ -55,7 +56,10 @@ console.log('===currentValue', currentValue)
|
|||||||
return (
|
return (
|
||||||
<div className={`${styles.nutRange} ${className ? className : ""} `}>
|
<div className={`${styles.nutRange} ${className ? className : ""} `}>
|
||||||
<div className={styles.nutRangeHeader}>
|
<div className={styles.nutRangeHeader}>
|
||||||
<TitleComponent title="NTRP水平区间" />
|
<TitleComponent
|
||||||
|
title="NTRP水平区间"
|
||||||
|
icon={<Image src={img.ICON_PLAY} />}
|
||||||
|
/>
|
||||||
<p className={styles.nutRangeHeaderContent}>{rangContent}</p>
|
<p className={styles.nutRangeHeaderContent}>{rangContent}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ console.log('===currentValue', currentValue)
|
|||||||
step={step}
|
step={step}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onEnd={handleChange}
|
onEnd={handleChange}
|
||||||
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
defaultValue={[min, max]}
|
defaultValue={[min, max]}
|
||||||
className={styles.rangeHandle}
|
className={styles.rangeHandle}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import NtrpRange from './index';
|
|
||||||
|
|
||||||
const SimpleTest: React.FC = () => {
|
|
||||||
const [value, setValue] = useState<[number, number]>([2.0, 4.0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h2>简单测试</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
<p>当前值: {value[0]} - {value[1]}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleTest;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import NtrpRange from './index';
|
|
||||||
|
|
||||||
const StyleTest: React.FC = () => {
|
|
||||||
const [value, setValue] = useState<[number, number]>([2.0, 4.0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
|
|
||||||
<h1>NtrpRange 样式测试</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>NTRP 水平区间选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
当前选择范围: {value[0]} - {value[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>自定义范围选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={[20, 80]}
|
|
||||||
onChange={(val) => console.log('Custom range:', val)}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
固定范围: 20 - 80
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>禁用状态</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
step={1}
|
|
||||||
value={[3, 7]}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#999' }}>
|
|
||||||
此选择器已被禁用
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>样式说明</h2>
|
|
||||||
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.6' }}>
|
|
||||||
<p>✅ 网球图标 + "NTRP水平区间"标题</p>
|
|
||||||
<p>✅ 左右范围标签("2.0及以下"、"4.0及以上")</p>
|
|
||||||
<p>✅ 圆角矩形轨道容器,带有边框和阴影</p>
|
|
||||||
<p>✅ 白色圆形滑块手柄,黑色边框和阴影</p>
|
|
||||||
<p>✅ 黑色轨道填充条</p>
|
|
||||||
<p>✅ 五个浅灰色标记点</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StyleTest;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import NtrpRange from './index';
|
|
||||||
|
|
||||||
const TestPage: React.FC = () => {
|
|
||||||
const [value, setValue] = useState<[number, number]>([2.0, 4.0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h1>NtrpRange 组件测试</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
|
||||||
当前值: {value[0].toFixed(1)} - {value[1].toFixed(1)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '20px', fontSize: '12px', color: '#999' }}>
|
|
||||||
<p>测试说明:</p>
|
|
||||||
<p>1. 拖拽左右滑块手柄</p>
|
|
||||||
<p>2. 观察值的变化</p>
|
|
||||||
<p>3. 检查样式是否正确</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestPage;
|
|
||||||
@@ -7,10 +7,14 @@ interface IProps {
|
|||||||
handleFilterIcon: () => void;
|
handleFilterIcon: () => void;
|
||||||
isSelect: boolean;
|
isSelect: boolean;
|
||||||
filterCount: number;
|
filterCount: number;
|
||||||
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBarComponent = (props: IProps) => {
|
const SearchBarComponent = (props: IProps) => {
|
||||||
const { handleFilterIcon, isSelect, filterCount } = props;
|
const { handleFilterIcon, isSelect, filterCount, onChange } = props;
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
onChange && onChange(value);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
@@ -30,11 +34,14 @@ const SearchBarComponent = (props: IProps) => {
|
|||||||
src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER}
|
src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER}
|
||||||
className={styles.filterIcon}
|
className={styles.filterIcon}
|
||||||
/>
|
/>
|
||||||
|
{isSelect && (
|
||||||
<Text className={styles.filterCount}>{filterCount}</Text>
|
<Text className={styles.filterCount}>{filterCount}</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
className={styles.searchBar}
|
className={styles.searchBar}
|
||||||
placeholder="搜索上海的球局和场地"
|
placeholder="搜索上海的球局和场地"
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import styles from "./index.module.scss";
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
}
|
}
|
||||||
const TitleComponent = (props: IProps) => {
|
const TitleComponent = (props: IProps) => {
|
||||||
const { title, className } = props;
|
const { title, className, icon } = props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${styles.titleContainer} ${className ? className : ""} `}
|
className={`${styles.titleContainer} ${className ? className : ""} `}
|
||||||
>
|
>
|
||||||
<div>图</div>
|
<div className={styles.icon}>{icon}</div>
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<h1 className={styles.title}>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,4 +11,10 @@ export default {
|
|||||||
ICON_FILTER: require('@/static/list/icon-filter.svg'),
|
ICON_FILTER: require('@/static/list/icon-filter.svg'),
|
||||||
ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'),
|
ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'),
|
||||||
ICON_SEARCH: require('@/static/list/icon-search.svg'),
|
ICON_SEARCH: require('@/static/list/icon-search.svg'),
|
||||||
|
ICON_PLAY: require('@/static/list/icon-play.svg'),
|
||||||
|
ICON_SITE: require('@/static/list/icon-site.svg'),
|
||||||
|
ICON_ARROW_DOWN: require('@/static/list/icon-arrow-down.svg'),
|
||||||
|
ICON_MENU_ITEM_SELECTED: require('@/static/list/icon-menu-item-selected.svg'),
|
||||||
|
ICON_ARROW_DOWN_WHITE: require('@/static/list/icon-arrow-down-white.svg'),
|
||||||
|
ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'),
|
||||||
}
|
}
|
||||||
@@ -1,60 +1,52 @@
|
|||||||
import { Popup } from "@nutui/nutui-react-taro";
|
import { Popup } from "@nutui/nutui-react-taro";
|
||||||
import Range from "../../components/Range";
|
import Range from "../../components/Range";
|
||||||
import Bubble, { BubbleOption } from "../../components/Bubble";
|
import Bubble from "../../components/Bubble";
|
||||||
import styles from "./filterPopup.module.scss";
|
import styles from "./filterPopup.module.scss";
|
||||||
import TitleComponent from "src/components/Title";
|
import TitleComponent from "src/components/Title";
|
||||||
import { Button } from "@nutui/nutui-react-taro";
|
import { Button } from "@nutui/nutui-react-taro";
|
||||||
|
import { Image } from "@tarojs/components";
|
||||||
|
import img from "../../config/images";
|
||||||
|
import { useListStore } from "src/store/listStore";
|
||||||
|
import {FilterPopupProps} from '../../../types/list/types'
|
||||||
|
|
||||||
const timeOptions: BubbleOption[] = [
|
const FilterPopup = (props: FilterPopupProps) => {
|
||||||
{ id: 1, label: "晨间 6:00-10:00", value: "1" },
|
const {
|
||||||
{ id: 2, label: "上午 10:00-12:00", value: "2" },
|
loading,
|
||||||
{ id: 3, label: "中午 12:00-14:00", value: "3" },
|
onCancel,
|
||||||
{ id: 4, label: "下午 14:00-18:00", value: "4" },
|
onConfirm,
|
||||||
{ id: 5, label: "晚上 18:00-22:00", value: "5" },
|
onChange,
|
||||||
{ id: 6, label: "夜间 22:00-24:00", value: "6" },
|
filterOptions,
|
||||||
];
|
onClear,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const locationOptions: BubbleOption[] = [
|
const store = useListStore() || {};
|
||||||
{ id: 1, label: "室内", value: "1" },
|
const { timeBubbleData, locationOptions } = store;
|
||||||
{ id: 2, label: "室外", value: "2" },
|
|
||||||
{ id: 3, label: "半室外", value: "3" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
onCancel: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onChange: (params: Record<string, string>) => void;
|
|
||||||
loading: boolean;
|
|
||||||
filterOptions: Record<string, any>;
|
|
||||||
onClear: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilterPopup = (props: IProps) => {
|
|
||||||
const { loading, onCancel, onConfirm, onChange, filterOptions, onClear } = props;
|
|
||||||
console.log('===filterOptions', filterOptions)
|
|
||||||
|
|
||||||
const handleFilterChange = (name, value) => {
|
const handleFilterChange = (name, value) => {
|
||||||
onChange({ [name]: value });
|
onChange({ [name]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearFilter = () => {
|
const handleClearFilter = () => {
|
||||||
onClear()
|
onClear();
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup
|
<Popup
|
||||||
visible={true}
|
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
position="top"
|
position="top"
|
||||||
round
|
round
|
||||||
closeOnOverlayClick={true}
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
// closeOnOverlayClick={true}
|
||||||
>
|
>
|
||||||
<div className={styles.filterPopupWrapper}>
|
<div className={styles.filterPopupWrapper}>
|
||||||
{/* 时间气泡选项 */}
|
{/* 时间气泡选项 */}
|
||||||
<Bubble
|
<Bubble
|
||||||
options={timeOptions}
|
options={timeBubbleData}
|
||||||
value={filterOptions?.time}
|
value={filterOptions?.time}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
layout="grid"
|
layout="grid"
|
||||||
@@ -71,12 +63,15 @@ const FilterPopup = (props: IProps) => {
|
|||||||
className={styles.filterPopupRange}
|
className={styles.filterPopupRange}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
value={filterOptions?.ntrp}
|
value={filterOptions?.ntrp}
|
||||||
name='ntrp'
|
name="ntrp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 场次气泡选项 */}
|
{/* 场次气泡选项 */}
|
||||||
<div>
|
<div>
|
||||||
<TitleComponent title="场地类型" />
|
<TitleComponent
|
||||||
|
title="场地类型"
|
||||||
|
icon={<Image src={img.ICON_SITE} />}
|
||||||
|
/>
|
||||||
<Bubble
|
<Bubble
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={filterOptions?.site}
|
value={filterOptions?.site}
|
||||||
@@ -84,12 +79,16 @@ const FilterPopup = (props: IProps) => {
|
|||||||
layout="grid"
|
layout="grid"
|
||||||
size="small"
|
size="small"
|
||||||
columns={3}
|
columns={3}
|
||||||
name='site'
|
name="site"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 按钮 */}
|
{/* 按钮 */}
|
||||||
<div className={styles.filterPopupBtnWrapper}>
|
<div className={styles.filterPopupBtnWrapper}>
|
||||||
<Button className={styles.btn} type="default" onClick={handleClearFilter}>
|
<Button
|
||||||
|
className={styles.btn}
|
||||||
|
type="default"
|
||||||
|
onClick={handleClearFilter}
|
||||||
|
>
|
||||||
清空全部
|
清空全部
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
.listPage {
|
.listPage {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
.listTopSearchWrapper {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.listTopFilterWrapper {
|
.listTopFilterWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 0 10px;
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listContentWrapper {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.menuFilter {
|
.menuFilter {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import styles from "./index.module.scss";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { useListStore } from "../../store/listStore";
|
import { useListStore } from "../../store/listStore";
|
||||||
|
import { View } from "@tarojs/components";
|
||||||
|
|
||||||
const ListPage = () => {
|
const ListPage = () => {
|
||||||
// 从 store 获取数据和方法
|
// 从 store 获取数据和方法
|
||||||
|
const store = useListStore() || {};
|
||||||
|
console.log("===store===", store);
|
||||||
const {
|
const {
|
||||||
isShowFilterPopup,
|
isShowFilterPopup,
|
||||||
error,
|
error,
|
||||||
@@ -24,7 +27,10 @@ const ListPage = () => {
|
|||||||
updateFilterOptions, // 更新筛选条件
|
updateFilterOptions, // 更新筛选条件
|
||||||
filterOptions,
|
filterOptions,
|
||||||
clearFilterOptions,
|
clearFilterOptions,
|
||||||
} = useListStore() || {};
|
distanceData,
|
||||||
|
quickFilterData,
|
||||||
|
distanceQuickFilter,
|
||||||
|
} = store;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 页面加载时获取数据
|
// 页面加载时获取数据
|
||||||
@@ -155,29 +161,34 @@ const ListPage = () => {
|
|||||||
updateState(params);
|
updateState(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cityOptions: BubbleOption[] = [
|
/**
|
||||||
{ id: 0, label: "全城", value: "0" },
|
* @description 更新筛选条件
|
||||||
{ id: 1, label: "3km", value: "3" },
|
* @param {Record<string, any>} params 筛选项
|
||||||
{ id: 2, label: "5km", value: "5" },
|
*/
|
||||||
{ id: 3, label: "10km", value: "10" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ text: "默认排序", value: "a" },
|
|
||||||
{ text: "好评排序", value: "b" },
|
|
||||||
{ text: "销量排序", value: "c" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleUpdateFilterOptions = (params: Record<string, any>) => {
|
const handleUpdateFilterOptions = (params: Record<string, any>) => {
|
||||||
updateFilterOptions(params);
|
updateFilterOptions(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = () => {};
|
||||||
|
|
||||||
|
// 距离筛选
|
||||||
|
const handleDistanceOrQuickChange = (name, value) => {
|
||||||
|
updateState({
|
||||||
|
distanceQuickFilter: {
|
||||||
|
...distanceQuickFilter,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
console.log("===visible", isShowFilterPopup);
|
||||||
return (
|
return (
|
||||||
<div className={styles.listPage}>
|
<View className={styles.listPage}>
|
||||||
|
<View className={styles.listTopSearchWrapper}>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
handleFilterIcon={toggleShowPopup}
|
handleFilterIcon={toggleShowPopup}
|
||||||
isSelect={filterCount > 0}
|
isSelect={filterCount > 0}
|
||||||
filterCount={filterCount}
|
filterCount={filterCount}
|
||||||
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
{/* 综合筛选 */}
|
{/* 综合筛选 */}
|
||||||
{isShowFilterPopup && (
|
{isShowFilterPopup && (
|
||||||
@@ -189,6 +200,8 @@ const ListPage = () => {
|
|||||||
onChange={handleUpdateFilterOptions}
|
onChange={handleUpdateFilterOptions}
|
||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
onClear={clearFilterOptions}
|
onClear={clearFilterOptions}
|
||||||
|
visible={isShowFilterPopup}
|
||||||
|
onClose={toggleShowPopup}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -196,20 +209,24 @@ const ListPage = () => {
|
|||||||
<div className={styles.listTopFilterWrapper}>
|
<div className={styles.listTopFilterWrapper}>
|
||||||
{/* 全城筛选 */}
|
{/* 全城筛选 */}
|
||||||
<CityFilter
|
<CityFilter
|
||||||
options={cityOptions}
|
options={distanceData}
|
||||||
value="1"
|
value={distanceQuickFilter?.distance}
|
||||||
onChange={() => {}}
|
|
||||||
wrapperClassName={styles.menuFilter}
|
wrapperClassName={styles.menuFilter}
|
||||||
|
onChange={handleDistanceOrQuickChange}
|
||||||
|
name="distance"
|
||||||
/>
|
/>
|
||||||
{/* 智能排序 */}
|
{/* 智能排序 */}
|
||||||
<Menu
|
<Menu
|
||||||
options={options}
|
options={quickFilterData}
|
||||||
value="a"
|
value={distanceQuickFilter?.quick}
|
||||||
onChange={() => {}}
|
onChange={handleDistanceOrQuickChange}
|
||||||
wrapperClassName={styles.menuFilter}
|
wrapperClassName={styles.menuFilter}
|
||||||
|
name="quick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className={styles.listContentWrapper}>
|
||||||
{/* 列表内容 */}
|
{/* 列表内容 */}
|
||||||
<List>
|
<List>
|
||||||
{matches.map((match, index) => (
|
{matches.map((match, index) => (
|
||||||
@@ -246,7 +263,8 @@ const ListPage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
3
src/static/list/icon-arrow-down-white.svg
Normal file
3
src/static/list/icon-arrow-down-white.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 6L8 10L4 6" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
3
src/static/list/icon-arrow-down.svg
Normal file
3
src/static/list/icon-arrow-down.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 10L8 6L12 10" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 216 B |
3
src/static/list/icon-list-right-arrow.svg
Normal file
3
src/static/list/icon-list-right-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.33325 4L10.3333 8L6.33325 12" stroke="#3C3C43" stroke-opacity="0.6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 255 B |
3
src/static/list/icon-menu-item-selected.svg
Normal file
3
src/static/list/icon-menu-item-selected.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.16663 9.99999L8.33329 14.1667L16.6666 5.83333" stroke="#333333" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 251 B |
5
src/static/list/icon-play.svg
Normal file
5
src/static/list/icon-play.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.00016 14.6666C11.6821 14.6666 14.6668 11.6819 14.6668 7.99998C14.6668 4.31808 11.6821 1.33331 8.00016 1.33331C4.31826 1.33331 1.3335 4.31808 1.3335 7.99998C1.3335 11.6819 4.31826 14.6666 8.00016 14.6666Z" stroke="black" stroke-width="1.33333"/>
|
||||||
|
<path d="M8.00016 1.33331C7.96653 3.55605 7.4208 5.22318 6.36296 6.33478C5.3051 7.44635 3.6286 8.00235 1.3335 8.00275" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.6561 8.33505C12.4852 8.18615 10.8265 8.65015 9.67983 9.72705C8.53313 10.804 7.97353 12.4505 8.00096 14.6666" stroke="black" stroke-width="1.33333" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 751 B |
6
src/static/list/icon-site.svg
Normal file
6
src/static/list/icon-site.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.6668 3.33331H1.3335V13.3333H14.6668V3.33331Z" stroke="black" stroke-width="1.33333" stroke-linejoin="round"/>
|
||||||
|
<path d="M3.3335 5.33331L12.6668 11.3333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12.6667 5.33331L8 11.6666" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.00016 5.33331L3.3335 11.3333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 608 B |
@@ -1,53 +1,6 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { getTennisMatches } from '../services/listApi'
|
import { getTennisMatches } from '../services/listApi'
|
||||||
|
import {ListActions, IFilterOptions, ListState } from '../../types/list/types'
|
||||||
// 网球比赛数据接口
|
|
||||||
export interface TennisMatch {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
dateTime: string
|
|
||||||
location: string
|
|
||||||
distance: string
|
|
||||||
registeredCount: number
|
|
||||||
maxCount: number
|
|
||||||
skillLevel: string
|
|
||||||
matchType: string
|
|
||||||
images: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store 状态接口
|
|
||||||
interface ListState {
|
|
||||||
matches: TennisMatch[]
|
|
||||||
loading: boolean
|
|
||||||
error: string | null
|
|
||||||
lastRefreshTime: string | null
|
|
||||||
isShowFilterPopup: boolean
|
|
||||||
filterOptions: IFilterOptions
|
|
||||||
filterCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IFilterOptions {
|
|
||||||
location: string
|
|
||||||
time: string
|
|
||||||
ntrp: [number, number]
|
|
||||||
site: string
|
|
||||||
wanfa: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store Actions 接口
|
|
||||||
interface ListActions {
|
|
||||||
fetchMatches: (params?: {
|
|
||||||
page?: number
|
|
||||||
pageSize?: number
|
|
||||||
location?: string
|
|
||||||
skillLevel?: string
|
|
||||||
}) => Promise<void>
|
|
||||||
refreshMatches: () => Promise<void>
|
|
||||||
clearError: () => void
|
|
||||||
updateState: (payload: Record<string, any>) => void
|
|
||||||
updateFilterOptions: (payload: Record<string, any>) => void
|
|
||||||
clearFilterOptions: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完整的 Store 类型
|
// 完整的 Store 类型
|
||||||
type TennisStore = ListState & ListActions
|
type TennisStore = ListState & ListActions
|
||||||
@@ -60,6 +13,8 @@ const defaultFilterOptions: IFilterOptions = {
|
|||||||
wanfa: '', // 玩法
|
wanfa: '', // 玩法
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultDistance = 'all'; // 默认距离
|
||||||
|
|
||||||
// 创建 store
|
// 创建 store
|
||||||
export const useListStore = create<TennisStore>()((set, get) => ({
|
export const useListStore = create<TennisStore>()((set, get) => ({
|
||||||
// 初始状态
|
// 初始状态
|
||||||
@@ -73,6 +28,43 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
filterOptions: defaultFilterOptions,
|
filterOptions: defaultFilterOptions,
|
||||||
// 综合筛选 选择的筛选数量
|
// 综合筛选 选择的筛选数量
|
||||||
filterCount: 0,
|
filterCount: 0,
|
||||||
|
// 距离筛选
|
||||||
|
distance: defaultDistance,
|
||||||
|
// 快捷筛选
|
||||||
|
quickFilter: 1, // 1: 默认 2: 好评 3: 销量
|
||||||
|
// 距离筛选数据
|
||||||
|
distanceData: [
|
||||||
|
{ id: 0, label: "全城", value: "全城" },
|
||||||
|
{ id: 1, label: "3km", value: "3km" },
|
||||||
|
{ id: 2, label: "5km", value: "5km" },
|
||||||
|
{ id: 3, label: "10km", value: "10km" },
|
||||||
|
],
|
||||||
|
// 快捷筛选数据
|
||||||
|
quickFilterData:[
|
||||||
|
{ text: "默认排序", value: "0" },
|
||||||
|
{ text: "好评排序", value: "1" },
|
||||||
|
{ text: "销量排序", value: "2" },
|
||||||
|
],
|
||||||
|
// 距离筛选和快捷筛选
|
||||||
|
distanceQuickFilter: {
|
||||||
|
distance: '全城',
|
||||||
|
quick: '0',
|
||||||
|
},
|
||||||
|
// 时间气泡数据
|
||||||
|
timeBubbleData: [
|
||||||
|
{ id: 1, label: "晨间 6:00-10:00", value: "1" },
|
||||||
|
{ id: 2, label: "上午 10:00-12:00", value: "2" },
|
||||||
|
{ id: 3, label: "中午 12:00-14:00", value: "3" },
|
||||||
|
{ id: 4, label: "下午 14:00-18:00", value: "4" },
|
||||||
|
{ id: 5, label: "晚上 18:00-22:00", value: "5" },
|
||||||
|
{ id: 6, label: "夜间 22:00-24:00", value: "6" },
|
||||||
|
],
|
||||||
|
// 场地类型数据
|
||||||
|
locationOptions: [
|
||||||
|
{ id: 1, label: "室内", value: "1" },
|
||||||
|
{ id: 2, label: "室外", value: "2" },
|
||||||
|
{ id: 3, label: "半室外", value: "3" },
|
||||||
|
],
|
||||||
|
|
||||||
// 获取比赛数据
|
// 获取比赛数据
|
||||||
fetchMatches: async (params) => {
|
fetchMatches: async (params) => {
|
||||||
@@ -87,12 +79,12 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
})
|
})
|
||||||
console.log('Store: 成功获取网球比赛数据:', matches.length, '条')
|
console.log('Store: 成功获取网球比赛数据:', matches.length, '条')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
// const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||||
set({
|
// set({
|
||||||
error: errorMessage,
|
// error: errorMessage,
|
||||||
loading: false
|
// loading: false
|
||||||
})
|
// })
|
||||||
console.error('Store: 获取网球比赛数据失败:', errorMessage)
|
// console.error('Store: 获取网球比赛数据失败:', errorMessage)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,12 +101,12 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
})
|
})
|
||||||
console.log('Store: 成功刷新网球比赛数据:', matches.length, '条')
|
console.log('Store: 成功刷新网球比赛数据:', matches.length, '条')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
// const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||||
set({
|
// set({
|
||||||
error: errorMessage,
|
// error: errorMessage,
|
||||||
loading: false
|
// loading: false
|
||||||
})
|
// })
|
||||||
console.error('Store: 刷新网球比赛数据失败:', errorMessage)
|
// console.error('Store: 刷新网球比赛数据失败:', errorMessage)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
141
types/list/types.ts
Normal file
141
types/list/types.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// 网球比赛数据接口
|
||||||
|
export interface TennisMatch {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
dateTime: string
|
||||||
|
location: string
|
||||||
|
distance: string
|
||||||
|
registeredCount: number
|
||||||
|
maxCount: number
|
||||||
|
skillLevel: string
|
||||||
|
matchType: string
|
||||||
|
images: string[]
|
||||||
|
}
|
||||||
|
export interface IFilterOptions {
|
||||||
|
location: string
|
||||||
|
time: string
|
||||||
|
ntrp: [number, number]
|
||||||
|
site: string
|
||||||
|
wanfa: string
|
||||||
|
}
|
||||||
|
export interface ListState {
|
||||||
|
matches: TennisMatch[]
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
lastRefreshTime: string | null
|
||||||
|
isShowFilterPopup: boolean
|
||||||
|
filterOptions: IFilterOptions
|
||||||
|
filterCount: number
|
||||||
|
distance: string | number
|
||||||
|
quickFilter: string | number
|
||||||
|
distanceData: any[]
|
||||||
|
quickFilterData: any[]
|
||||||
|
distanceQuickFilter: {
|
||||||
|
distance: string
|
||||||
|
quick: string
|
||||||
|
}
|
||||||
|
timeBubbleData: BubbleOption[]
|
||||||
|
locationOptions: BubbleOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListState {
|
||||||
|
matches: TennisMatch[]
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
lastRefreshTime: string | null
|
||||||
|
isShowFilterPopup: boolean
|
||||||
|
filterOptions: IFilterOptions
|
||||||
|
filterCount: number
|
||||||
|
distance: string | number
|
||||||
|
quickFilter: string | number
|
||||||
|
distanceData: any[]
|
||||||
|
quickFilterData: any[]
|
||||||
|
distanceQuickFilter: {
|
||||||
|
distance: string
|
||||||
|
quick: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListActions {
|
||||||
|
fetchMatches: (params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
location?: string
|
||||||
|
skillLevel?: string
|
||||||
|
}) => Promise<void>
|
||||||
|
refreshMatches: () => Promise<void>
|
||||||
|
clearError: () => void
|
||||||
|
updateState: (payload: Record<string, any>) => void
|
||||||
|
updateFilterOptions: (payload: Record<string, any>) => void
|
||||||
|
clearFilterOptions: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷筛选
|
||||||
|
export interface MenuFilterProps {
|
||||||
|
options: { text: string; value: string }[];
|
||||||
|
value: string;
|
||||||
|
onChange: (name: string, value: string) => void;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
itemClassName?: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 距离筛选
|
||||||
|
export interface DistanceFilterProps {
|
||||||
|
options: BubbleOption[];
|
||||||
|
value: string;
|
||||||
|
onChange: (name: string, value: string) => void;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
itemClassName?: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bubble 组件
|
||||||
|
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?: (
|
||||||
|
name: string,
|
||||||
|
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;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BubbleItemProps {
|
||||||
|
option: BubbleOption;
|
||||||
|
isSelected: boolean;
|
||||||
|
size: 'small' | 'medium' | 'large';
|
||||||
|
disabled: boolean;
|
||||||
|
onClick: (option: BubbleOption) => void;
|
||||||
|
itemClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterPopup 组件
|
||||||
|
export interface FilterPopupProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onChange: (params: Record<string, string>) => void;
|
||||||
|
loading: boolean;
|
||||||
|
filterOptions: Record<string, any>;
|
||||||
|
onClear: () => void;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user