diff --git a/src/components/Bubble/BubbleItem.tsx b/src/components/Bubble/BubbleItem.tsx index 0ede586..62ba0c8 100644 --- a/src/components/Bubble/BubbleItem.tsx +++ b/src/components/Bubble/BubbleItem.tsx @@ -1,16 +1,7 @@ import React from 'react'; -import { BubbleOption } from './index'; +import { BubbleItemProps } from '../../../types/list/types'; 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 = ({ option, isSelected, diff --git a/src/components/Bubble/README.md b/src/components/Bubble/README.md deleted file mode 100644 index 90512d3..0000000 --- a/src/components/Bubble/README.md +++ /dev/null @@ -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(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( - setSelectedLocation(value as string)} - layout="horizontal" - size="medium" - /> - ); -}; -``` - -### 时间选择器示例 - -```tsx -const TimeSelector: React.FC = () => { - const [selectedTime, setSelectedTime] = useState(''); - - 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 ( - setSelectedTime(value as string)} - layout="grid" - columns={3} - size="medium" - /> - ); -}; -``` - -### 多选模式 - -```tsx -const MultiSelectExample: React.FC = () => { - const [selectedValues, setSelectedValues] = useState([]); - - const options: BubbleOption[] = [ - { id: 1, label: '运动', value: 'sports' }, - { id: 2, label: '音乐', value: 'music' }, - { id: 3, label: '阅读', value: 'reading' }, - { id: 4, label: '旅行', value: 'travel' } - ]; - - return ( - setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={2} - size="medium" - /> - ); -}; -``` - -### 带图标和描述 - -```tsx -const IconExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - const options: BubbleOption[] = [ - { - id: 1, - label: '网球', - value: 'tennis', - icon: '🎾', - description: '室内外均可' - }, - { - id: 2, - label: '篮球', - value: 'basketball', - icon: '🏀', - description: '室内场地' - } - ]; - - return ( - 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` 可以是字符串或数字类型 -- 组件会自动处理选中状态的样式变化 -- 支持图标和描述,让选项更加丰富 -- 响应式设计,自动适应不同屏幕尺寸 diff --git a/src/components/Bubble/USAGE.md b/src/components/Bubble/USAGE.md deleted file mode 100644 index afd455f..0000000 --- a/src/components/Bubble/USAGE.md +++ /dev/null @@ -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(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( -
-

选择场地类型

- setSelectedLocation(value as string)} - layout="horizontal" - size="medium" - /> -

您选择的场地类型: {selectedLocation}

-
- ); -}; -``` - -### 2. 时间选择器 - -```tsx -const [selectedTime, setSelectedTime] = useState(''); - -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' } -]; - - setSelectedTime(value as string)} - layout="grid" - columns={3} - size="medium" -/> -``` - -### 3. 多选模式 - -```tsx -const [selectedValues, setSelectedValues] = useState([]); - -const options: BubbleOption[] = [ - { id: 1, label: '运动', value: 'sports' }, - { id: 2, label: '音乐', value: 'music' }, - { id: 3, label: '阅读', value: 'reading' }, - { id: 4, label: '旅行', value: 'travel' } -]; - - setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={2} - size="medium" -/> -``` - -### 4. 带图标和描述 - -```tsx -const [selectedSport, setSelectedSport] = useState(''); - -const sportOptions: BubbleOption[] = [ - { - id: 1, - label: '网球', - value: 'tennis', - icon: '🎾', - description: '室内外均可' - }, - { - id: 2, - label: '篮球', - value: 'basketball', - icon: '🏀', - description: '室内场地' - } -]; - - setSelectedSport(value as string)} - layout="vertical" - size="large" -/> -``` - -### 5. 不同布局方式 - -```tsx -// 水平布局 - 适合选项较少 - - -// 垂直布局 - 适合选项较多 - - -// 网格布局 - 适合需要整齐排列 - -``` - -### 6. 不同尺寸 - -```tsx -// 小尺寸 - 适合紧凑界面 - - -// 中尺寸 - 默认尺寸 - - -// 大尺寸 - 适合触摸设备 - -``` - -## 组件特性 - -- **通用性**: 不局限于特定功能,可用于任何选择场景 -- **灵活布局**: 支持水平、垂直、网格三种布局方式 -- **多尺寸支持**: 小、中、大三种尺寸适应不同场景 -- **丰富内容**: 支持图标和描述,让选项更加丰富 -- **响应式设计**: 自动适应不同屏幕尺寸 -- **状态管理**: 内置选中状态管理,支持单选和多选 -- **类型安全**: 完整的 TypeScript 类型定义 -- **可访问性**: 支持键盘导航和屏幕阅读器 - -## 常见使用场景 - -1. **场地选择**: 室内/室外/半室外 -2. **时间选择**: 时间段、日期范围 -3. **分类选择**: 兴趣爱好、技能标签 -4. **设置选项**: 主题、语言、通知设置 -5. **筛选条件**: 价格范围、评分、距离等 -6. **导航菜单**: 顶部导航、侧边栏菜单 - -## 注意事项 - -1. 确保传入的 `options` 数组不为空 -2. 多选模式下,`value` 应该是数组类型 -3. 单选模式下,`value` 可以是字符串或数字类型 -4. 组件会自动处理选中状态的样式变化 -5. 支持图标和描述,让选项更加丰富 -6. 响应式设计,自动适应不同屏幕尺寸 -7. 可以通过 `disabled` 属性禁用整个组件或单个选项 diff --git a/src/components/Bubble/example.tsx b/src/components/Bubble/example.tsx deleted file mode 100644 index 0ebcdf8..0000000 --- a/src/components/Bubble/example.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React, { useState } from 'react'; -import Bubble, { BubbleOption } from './index'; - -// 室内外选择示例(如UI图所示) -export const LocationSelector: React.FC = () => { - const [selectedLocation, setSelectedLocation] = useState(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( -
-

选择场地类型

- setSelectedLocation(value as string)} - layout="horizontal" - size="small" - /> -

当前选择: {selectedLocation || '未选择'}

-
- ); -}; - -// 时间选择器示例 -export const TimeSelector: React.FC = () => { - const [selectedTime, setSelectedTime] = useState(''); - - 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 ( -
-

选择时间段

- setSelectedTime(value as string)} - layout="grid" - size="small" - columns={3} - /> -

当前选择: {selectedTime || '未选择'}

-
-

选择时间段

- setSelectedTime(value as string)} - layout="grid" - columns={2} - size="small" - /> -

当前选择: {selectedTime || '未选择'}

-
- ); -}; - -// 多选示例 -export const MultiSelectExample: React.FC = () => { - const [selectedValues, setSelectedValues] = useState([]); - - 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 ( -
-

多选示例 - 兴趣爱好

- setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={3} - size="medium" - /> -

当前选择: {selectedValues.join(', ') || '未选择'}

-
- ); -}; - -// 带图标和描述的示例 -export const IconExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - 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 ( -
-

带图标和描述的运动选择

- setSelectedValue(value as string)} - layout="vertical" - size="large" - /> -

当前选择: {selectedValue || '未选择'}

-
- ); -}; - -// 带禁用状态的示例 -export const DisabledExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - 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 ( -
-

带禁用状态的示例

- setSelectedValue(value as string)} - layout="grid" - columns={2} - size="medium" - /> -

当前选择: {selectedValue || '未选择'}

-
- ); -}; - -// 不同尺寸的示例 -export const SizeExample: React.FC = () => { - const [selectedSize, setSelectedSize] = useState(''); - - const sizeOptions: BubbleOption[] = [ - { id: 1, label: '小尺寸', value: 'small' }, - { id: 2, label: '中尺寸', value: 'medium' }, - { id: 3, label: '大尺寸', value: 'large' } - ]; - - return ( -
-

不同尺寸的示例

- -

小尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="small" - /> - -

中尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="medium" - /> - -

大尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="large" - /> - -

当前选择: {selectedSize || '未选择'}

-
- ); -}; - -// 主示例组件 -export const BubbleExamples: React.FC = () => { - return ( -
-

Bubble 通用气泡组件示例

- -
- -
- -
- -
- -
- -
- ); -}; - -export default BubbleExamples; diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx index 59af227..8f07166 100644 --- a/src/components/Bubble/index.tsx +++ b/src/components/Bubble/index.tsx @@ -1,34 +1,7 @@ import React, { useState, useEffect } from "react"; import styles from "./index.module.scss"; import BubbleItem from "./BubbleItem"; - -export interface BubbleOption { - id: string | number; - label: string; - value: string | number; - disabled?: boolean; - icon?: React.ReactNode; - description?: string; -} - -export interface BubbleProps { - options: BubbleOption[]; - value?: string | number | (string | number)[]; - onChange?: ( - 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; -} +import {BubbleProps} from '../../../types/list/types' const Bubble: React.FC = ({ options, @@ -79,6 +52,7 @@ const Bubble: React.FC = ({ ); onChange(name, newSelectedValues, selectedOptions); } else { + console.log('===111', name, option.value) onChange(name, option.value, option); } } diff --git a/src/components/CityFilter/example.tsx b/src/components/CityFilter/example.tsx deleted file mode 100644 index 022297a..0000000 --- a/src/components/CityFilter/example.tsx +++ /dev/null @@ -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 ( - setValue(val)} - /> - ); -} diff --git a/src/components/CityFilter/index.module.scss b/src/components/CityFilter/index.module.scss index ec1ee63..dca674d 100644 --- a/src/components/CityFilter/index.module.scss +++ b/src/components/CityFilter/index.module.scss @@ -1,5 +1,15 @@ .menuWrap { padding: 5px 20px 10px; + $height: 26px; + + .menuIcon { + width: 16px; + height: 16px; + + &.rotate { + transform: rotate(180deg); + } + } .menuItem { width: 100vw; @@ -13,7 +23,7 @@ } &.active { - .nut-menu-bar { + :global(.nut-menu-bar) { background-color: #000000; color: #ffffff; } @@ -21,24 +31,23 @@ :global(.nut-menu-bar) { color: #000000; - line-height: 1; + width: 66px; + height: $height; + border-radius: $height; + line-height: $height; box-shadow: unset; - min-height: 28px; - min-width: 80px; - border-radius: 28px; border: 1px solid #e5e5e5; - line-height: 28px; font-size: 14px; - width: max-content; + font-weight: 600; + } - .nut-menu-title { - color: inherit !important; - font-weight: 600; - } + :global(.nut-menu-title) { + color: inherit !important; + font-weight: 600; + } - .nut-menu-title-text { - padding-left: 0; - } + :global(.nut-menu-title-text) { + --nutui-menu-title-padding: 0 6px 0 0; } .positionWrap { diff --git a/src/components/CityFilter/index.tsx b/src/components/CityFilter/index.tsx index b50c8ca..9784915 100644 --- a/src/components/CityFilter/index.tsx +++ b/src/components/CityFilter/index.tsx @@ -1,28 +1,33 @@ import { Menu } from "@nutui/nutui-react-taro"; import styles from "./index.module.scss"; 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 [iOpen, setIsOpen] = useState(false); const itemRef = useRef(null); - const handleChange = (value: string) => { - console.log("===value", value); + const handleChange = (name: string, value: string) => { setIsChange(true); - onChange && onChange(value); + onChange && onChange(name, value); + (itemRef.current as any)?.toggle(false); }; - + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; return ( { isChange ? styles.active : "" }`} activeColor="#000" + onOpen={handleOpen} + onClose={handleClose} + icon={ + + } > + } >

当前位置

@@ -49,6 +68,7 @@ const MenuComponent = (props: IProps) => { size="small" columns={4} itemClassName={styles.distanceBubbleItem} + name={name} />
diff --git a/src/components/ListItem/index.scss b/src/components/ListItem/index.scss index ded1507..3fc1920 100644 --- a/src/components/ListItem/index.scss +++ b/src/components/ListItem/index.scss @@ -10,26 +10,32 @@ flex: 1; display: flex; flex-direction: column; - gap: 8px; + gap: 6px; +} + +.titleWrapper { + display: flex; + align-items: center; } .title { font-size: 16px; font-weight: 600; - color: #333333; - line-height: 1.4; + color: #000000; + line-height: 24px; } -.date-time { - font-size: 14px; - color: #666666; - line-height: 1.3; +.title-right-arrow { + width: 16px; + height: 16px; } +.date-time, .location { - font-size: 14px; - color: #666666; - line-height: 1.3; + font-size: 12px; + color: #3C3C4399; + font-weight: 400; + line-height: 18px; } .bottom-info { @@ -58,6 +64,8 @@ border: 2px solid #ffffff; margin-left: -8px; overflow: hidden; + box-sizing: border-box; + .avatar-image { width: 100%; height: 100%; @@ -89,8 +97,10 @@ } .tag { - padding: 3px; - border: 1px solid #f5f5f5; + box-sizing: border-box; + padding: 0 6px; + border: 0.5px solid #00000029; + height: 20px; border-radius: 20px; min-width: 38px; display: flex; @@ -98,7 +108,7 @@ justify-content: center; flex-shrink: 0; color: #000000; - font-size: 12px; + font-size: 11px; } .tag-text-max { @@ -112,16 +122,20 @@ display: flex; align-items: center; justify-content: center; + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; .image-container { width: 100%; height: 100%; - padding: 2px; - background: #ffffff; + border: 1.5px solid #ffffff; border-radius: 10px; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); overflow: hidden; position: absolute; + box-sizing: border-box; + .image { border-radius: 10px; } @@ -132,7 +146,10 @@ position: relative; width: 88px; height: 88px; + .image-container { + width: 88px; + height: 88px; transform: rotate(-10deg); } } @@ -151,13 +168,13 @@ &:first-child { z-index: 2; - transform: rotate(-10deg); + transform: translateX(4px) rotate(-10deg); } &:last-child { right: 0; z-index: 1; - transform: rotate(10deg); + transform: translateX(-4px) rotate(10deg); } } } @@ -177,7 +194,7 @@ width: 55px; height: 55px; z-index: 3; - transform: rotate(-10deg); + transform: translateX(4px) rotate(-10deg); } &:nth-child(2) { @@ -204,4 +221,4 @@ width: 100%; height: 100%; object-fit: cover; -} +} \ No newline at end of file diff --git a/src/components/ListItem/index.tsx b/src/components/ListItem/index.tsx index cc0f498..46a6539 100644 --- a/src/components/ListItem/index.tsx +++ b/src/components/ListItem/index.tsx @@ -1,16 +1,17 @@ -import { View, Text, Image } from '@tarojs/components' -import './index.scss' +import { View, Text, Image } from "@tarojs/components"; +import img from "../../config/images"; +import "./index.scss"; interface ListItemProps { - title: string - dateTime: string - location: string - distance: string - registeredCount: number - maxCount: number - skillLevel: string - matchType: string - images: string[] + title: string; + dateTime: string; + location: string; + distance: string; + registeredCount: number; + maxCount: number; + skillLevel: string; + matchType: string; + images: string[]; } const ListItem: React.FC = ({ @@ -22,22 +23,22 @@ const ListItem: React.FC = ({ maxCount, skillLevel, matchType, - images + images, }) => { // 根据图片数量决定展示样式 const renderImages = () => { - if (images.length === 0) return null - + if (images.length === 0) return null; + if (images.length === 1) { return ( - + - ) + ); } - + if (images.length === 2) { return ( @@ -48,9 +49,9 @@ const ListItem: React.FC = ({ - ) + ); } - + // 3张或更多图片 return ( @@ -64,40 +65,54 @@ const ListItem: React.FC = ({ - ) - } + ); + }; return ( {/* 左侧内容区域 */} {/* 标题 */} - {title} - + + {title} + + + {/* 时间信息 */} {dateTime} - + {/* 地点和距离 */} - {location}・{distance} - + + {location}・{distance} + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} - {Array.from({ length: Math.min(registeredCount, 3) }).map((_, index) => ( - - - - ))} + {Array.from({ length: Math.min(registeredCount, 3) }).map( + (_, index) => ( + + + + ) + )} - + - + - 报名人数 {registeredCount}/{maxCount} + 报名人数 {registeredCount}/ + {maxCount} @@ -109,13 +124,11 @@ const ListItem: React.FC = ({ - - {/* 右侧图片区域 */} - - {renderImages()} - - - ) -} -export default ListItem \ No newline at end of file + {/* 右侧图片区域 */} + {renderImages()} + + ); +}; + +export default ListItem; diff --git a/src/components/Menu/example.tsx b/src/components/Menu/example.tsx deleted file mode 100644 index 022297a..0000000 --- a/src/components/Menu/example.tsx +++ /dev/null @@ -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 ( - setValue(val)} - /> - ); -} diff --git a/src/components/Menu/index.module.scss b/src/components/Menu/index.module.scss index 4bebabb..cf1df49 100644 --- a/src/components/Menu/index.module.scss +++ b/src/components/Menu/index.module.scss @@ -1,33 +1,46 @@ .menuWrap { position: static; padding: 5px 20px 10px; + --nutui-menu-title-padding: 0 6px 0 0; + $height: 26px; + .menuItem { width: 100vw; left: 0; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; } + + .menuIcon { + width: 16px; + height: 16px; + + &.rotate { + transform: rotate(180deg); + } + } + + .itemIcon { + width: 20px; + height: 20px; + } + &.active { :global(.nut-menu-bar) { background-color: #000000; color: #ffffff; } } + :global(.nut-menu-bar) { color: #000000; - line-height: 1; box-shadow: unset; - min-height: 28px; - min-width: 94px; - border-radius: 28px; + border-radius: $height; + line-height: $height; + height: $height; border: 1px solid #e5e5e5; - line-height: 28px; font-size: 14px; - width: max-content; - - .nut-menu-title-text { - padding-left: 0; - } + width: 94px; } :global(.nut-menu-title) { @@ -35,13 +48,18 @@ font-weight: 600; } + :global(.nut-menu-title-text) { + --nutui-menu-title-padding: 0 6px 0 4px; + } + :global(.nut-menu-container-item) { color: #3c3c43; font-weight: 600; font-size: 14px; } + :global(.nut-menu-container-item.active) { flex-direction: row-reverse; justify-content: space-between; } -} +} \ No newline at end of file diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 0f3d435..fd7b4db 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -1,34 +1,55 @@ import { Menu } from "@nutui/nutui-react-taro"; -import styles from "./index.module.scss"; 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 { - options: { text: string; value: string }[]; - value: string; - onChange: (value: string) => void; - wrapperClassName?: string; - itemClassName?: string; -} - -const MenuComponent = (props: IProps) => { - const { options, value, onChange, wrapperClassName, itemClassName } = props; +const MenuComponent = (props: MenuFilterProps) => { + const { options, value, onChange, wrapperClassName, itemClassName, name } = + props; const [isChange, setIsChange] = useState(false); + const [isOpen, setIsOpen] = useState(false); - const handleChange = (value: string) => { + const handleChange = (val: Record) => { setIsChange(true); - onChange && onChange(value); + onChange && onChange(name, val.value); + }; + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); }; return ( + } > + } /> ); diff --git a/src/components/Range/README.md b/src/components/Range/README.md deleted file mode 100644 index 5da1a01..0000000 --- a/src/components/Range/README.md +++ /dev/null @@ -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 ( - - ); -}; -``` - -## 属性说明 - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `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` 文件获取更多使用示例。 diff --git a/src/components/Range/example.tsx b/src/components/Range/example.tsx deleted file mode 100644 index a9b65df..0000000 --- a/src/components/Range/example.tsx +++ /dev/null @@ -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 ( -
-

Range 组件示例

- -
-

NTRP 水平区间选择器

- -
- 当前选择范围: {ntrpRange[0]} - {ntrpRange[1]} -
-
- -
-

自定义范围选择器

- -
- 当前选择范围: {customRange[0]} - {customRange[1]} -
-
- -
-

禁用状态

- -
- 此选择器已被禁用 -
-
- -
-

测试说明

-
-

1. 点击并拖拽左右滑块手柄

-

2. 查看控制台日志确认拖拽事件

-

3. 观察滑块位置和值的实时变化

-

4. 检查调试信息显示

-
-
-
- ); -}; - -export default RangeExample; diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx index e4d0f78..3275515 100644 --- a/src/components/Range/index.tsx +++ b/src/components/Range/index.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useMemo } from "react"; +import { View, Text, Image } from "@tarojs/components"; import { Range } from "@nutui/nutui-react-taro"; import styles from "./index.module.scss"; import TitleComponent from "../Title"; +import img from "../../config/images"; interface RangeProps { min?: number; @@ -25,9 +27,8 @@ const NtrpRange: React.FC = ({ name, }) => { const [currentValue, setCurrentValue] = useState<[number, number]>(value); -console.log('===currentValue', currentValue) + useEffect(() => { - console.log('===rrr', value) value && setCurrentValue(value); }, [JSON.stringify(value || [])]); @@ -55,7 +56,10 @@ console.log('===currentValue', currentValue) return (
- + } + />

{rangContent}

@@ -69,6 +73,7 @@ console.log('===currentValue', currentValue) step={step} value={currentValue} onEnd={handleChange} + onChange={handleChange} disabled={disabled} defaultValue={[min, max]} className={styles.rangeHandle} diff --git a/src/components/Range/simple-test.tsx b/src/components/Range/simple-test.tsx deleted file mode 100644 index f874642..0000000 --- a/src/components/Range/simple-test.tsx +++ /dev/null @@ -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 ( -
-

简单测试

- -

当前值: {value[0]} - {value[1]}

-
- ); -}; - -export default SimpleTest; diff --git a/src/components/Range/style-test.tsx b/src/components/Range/style-test.tsx deleted file mode 100644 index 60134d0..0000000 --- a/src/components/Range/style-test.tsx +++ /dev/null @@ -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 ( -
-

NtrpRange 样式测试

- -
-

NTRP 水平区间选择器

- -
- 当前选择范围: {value[0]} - {value[1]} -
-
- -
-

自定义范围选择器

- console.log('Custom range:', val)} - /> -
- 固定范围: 20 - 80 -
-
- -
-

禁用状态

- -
- 此选择器已被禁用 -
-
- -
-

样式说明

-
-

✅ 网球图标 + "NTRP水平区间"标题

-

✅ 左右范围标签("2.0及以下"、"4.0及以上")

-

✅ 圆角矩形轨道容器,带有边框和阴影

-

✅ 白色圆形滑块手柄,黑色边框和阴影

-

✅ 黑色轨道填充条

-

✅ 五个浅灰色标记点

-
-
-
- ); -}; - -export default StyleTest; diff --git a/src/components/Range/test.tsx b/src/components/Range/test.tsx deleted file mode 100644 index 624772c..0000000 --- a/src/components/Range/test.tsx +++ /dev/null @@ -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 ( -
-

NtrpRange 组件测试

- -
- -
- -
- 当前值: {value[0].toFixed(1)} - {value[1].toFixed(1)} -
- -
-

测试说明:

-

1. 拖拽左右滑块手柄

-

2. 观察值的变化

-

3. 检查样式是否正确

-
-
- ); -}; - -export default TestPage; diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index 9d4827d..e4f1fab 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -7,10 +7,14 @@ interface IProps { handleFilterIcon: () => void; isSelect: boolean; filterCount: number; + onChange: (value: string) => void; } const SearchBarComponent = (props: IProps) => { - const { handleFilterIcon, isSelect, filterCount } = props; + const { handleFilterIcon, isSelect, filterCount, onChange } = props; + const handleChange = (value: string) => { + onChange && onChange(value); + }; return ( <> { src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER} className={styles.filterIcon} /> - {filterCount} + {isSelect && ( + {filterCount} + )} } className={styles.searchBar} placeholder="搜索上海的球局和场地" + onChange={handleChange} /> ); diff --git a/src/components/Title/index.module.scss b/src/components/Title/index.module.scss index a731606..c31b552 100644 --- a/src/components/Title/index.module.scss +++ b/src/components/Title/index.module.scss @@ -4,8 +4,19 @@ margin-bottom: 10px; gap: 6px; } + +.icon { + width: 16px; + height: 16px; + + image { + width: 100%; + height: 100%; + } +} + .title { font-weight: 600; font-size: 16px; line-height: 20px; -} +} \ No newline at end of file diff --git a/src/components/Title/index.tsx b/src/components/Title/index.tsx index 2eca985..425c593 100644 --- a/src/components/Title/index.tsx +++ b/src/components/Title/index.tsx @@ -2,15 +2,16 @@ import styles from "./index.module.scss"; interface IProps { title: string; className?: string; + icon?: JSX.Element; } const TitleComponent = (props: IProps) => { - const { title, className } = props; + const { title, className, icon } = props; return ( <>
-
+
{icon}

{title}

diff --git a/src/config/images.js b/src/config/images.js index 9effb04..2eaa335 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -11,4 +11,10 @@ export default { ICON_FILTER: require('@/static/list/icon-filter.svg'), ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.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'), } \ No newline at end of file diff --git a/src/pages/list/FilterPopup.tsx b/src/pages/list/FilterPopup.tsx index 5598593..9af9136 100644 --- a/src/pages/list/FilterPopup.tsx +++ b/src/pages/list/FilterPopup.tsx @@ -1,60 +1,52 @@ import { Popup } from "@nutui/nutui-react-taro"; import Range from "../../components/Range"; -import Bubble, { BubbleOption } from "../../components/Bubble"; +import Bubble from "../../components/Bubble"; import styles from "./filterPopup.module.scss"; import TitleComponent from "src/components/Title"; 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[] = [ - { 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" }, -]; - -const locationOptions: BubbleOption[] = [ - { id: 1, label: "室内", value: "1" }, - { id: 2, label: "室外", value: "2" }, - { id: 3, label: "半室外", value: "3" }, -]; - -interface IProps { - onCancel: () => void; - onConfirm: () => void; - onChange: (params: Record) => void; - loading: boolean; - filterOptions: Record; - onClear: () => void; -} - -const FilterPopup = (props: IProps) => { - const { loading, onCancel, onConfirm, onChange, filterOptions, onClear } = props; - console.log('===filterOptions', filterOptions) +const FilterPopup = (props: FilterPopupProps) => { + const { + loading, + onCancel, + onConfirm, + onChange, + filterOptions, + onClear, + visible, + onClose, + } = props; + + const store = useListStore() || {}; + const { timeBubbleData, locationOptions } = store; const handleFilterChange = (name, value) => { onChange({ [name]: value }); }; const handleClearFilter = () => { - onClear() + onClear(); onCancel(); - } + }; return ( <>
{/* 时间气泡选项 */} { className={styles.filterPopupRange} onChange={handleFilterChange} value={filterOptions?.ntrp} - name='ntrp' + name="ntrp" /> {/* 场次气泡选项 */}
- + } + /> { layout="grid" size="small" columns={3} - name='site' + name="site" />
{/* 按钮 */}
- -
- )} -
+
暂无比赛数据
+ +
+ )} +
+ ); }; diff --git a/src/static/list/icon-arrow-down-white.svg b/src/static/list/icon-arrow-down-white.svg new file mode 100644 index 0000000..31becd1 --- /dev/null +++ b/src/static/list/icon-arrow-down-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-arrow-down.svg b/src/static/list/icon-arrow-down.svg new file mode 100644 index 0000000..1c6953d --- /dev/null +++ b/src/static/list/icon-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-list-right-arrow.svg b/src/static/list/icon-list-right-arrow.svg new file mode 100644 index 0000000..018fb2e --- /dev/null +++ b/src/static/list/icon-list-right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-menu-item-selected.svg b/src/static/list/icon-menu-item-selected.svg new file mode 100644 index 0000000..c79702f --- /dev/null +++ b/src/static/list/icon-menu-item-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-play.svg b/src/static/list/icon-play.svg new file mode 100644 index 0000000..715cd6b --- /dev/null +++ b/src/static/list/icon-play.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/list/icon-site.svg b/src/static/list/icon-site.svg new file mode 100644 index 0000000..8e28d73 --- /dev/null +++ b/src/static/list/icon-site.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/store/listStore.ts b/src/store/listStore.ts index f0b2cd7..9a5ec9a 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -1,53 +1,6 @@ import { create } from 'zustand' import { getTennisMatches } from '../services/listApi' - -// 网球比赛数据接口 -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 - refreshMatches: () => Promise - clearError: () => void - updateState: (payload: Record) => void - updateFilterOptions: (payload: Record) => void - clearFilterOptions: () => void -} +import {ListActions, IFilterOptions, ListState } from '../../types/list/types' // 完整的 Store 类型 type TennisStore = ListState & ListActions @@ -60,6 +13,8 @@ const defaultFilterOptions: IFilterOptions = { wanfa: '', // 玩法 }; +const defaultDistance = 'all'; // 默认距离 + // 创建 store export const useListStore = create()((set, get) => ({ // 初始状态 @@ -73,6 +28,43 @@ export const useListStore = create()((set, get) => ({ filterOptions: defaultFilterOptions, // 综合筛选 选择的筛选数量 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) => { @@ -87,12 +79,12 @@ export const useListStore = create()((set, get) => ({ }) console.log('Store: 成功获取网球比赛数据:', matches.length, '条') } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误' - set({ - error: errorMessage, - loading: false - }) - console.error('Store: 获取网球比赛数据失败:', errorMessage) + // const errorMessage = error instanceof Error ? error.message : '未知错误' + // set({ + // error: errorMessage, + // loading: false + // }) + // console.error('Store: 获取网球比赛数据失败:', errorMessage) } }, @@ -109,12 +101,12 @@ export const useListStore = create()((set, get) => ({ }) console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误' - set({ - error: errorMessage, - loading: false - }) - console.error('Store: 刷新网球比赛数据失败:', errorMessage) + // const errorMessage = error instanceof Error ? error.message : '未知错误' + // set({ + // error: errorMessage, + // loading: false + // }) + // console.error('Store: 刷新网球比赛数据失败:', errorMessage) } }, diff --git a/types/list/types.ts b/types/list/types.ts new file mode 100644 index 0000000..2fc8d71 --- /dev/null +++ b/types/list/types.ts @@ -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 + refreshMatches: () => Promise + clearError: () => void + updateState: (payload: Record) => void + updateFilterOptions: (payload: Record) => 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) => void; + loading: boolean; + filterOptions: Record; + onClear: () => void; + visible: boolean; + onClose: () => void; +} \ No newline at end of file