列表综合筛选

This commit is contained in:
juguohong
2025-08-23 15:40:19 +08:00
parent 7a7ab85a82
commit e6124131e7
12 changed files with 219 additions and 48 deletions

View File

@@ -15,6 +15,7 @@ export interface BubbleProps {
options: BubbleOption[];
value?: string | number | (string | number)[];
onChange?: (
name: string,
value: string | number | (string | number)[],
option: BubbleOption | BubbleOption[]
) => void;
@@ -26,6 +27,7 @@ export interface BubbleProps {
itemClassName?: string;
style?: React.CSSProperties;
disabled?: boolean;
name: string;
}
const Bubble: React.FC<BubbleProps> = ({
@@ -40,6 +42,7 @@ const Bubble: React.FC<BubbleProps> = ({
itemClassName = "",
style = {},
disabled = false,
name,
}) => {
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
@@ -74,9 +77,9 @@ const Bubble: React.FC<BubbleProps> = ({
const selectedOptions = options.filter((opt) =>
newSelectedValues.includes(opt.value)
);
onChange(newSelectedValues, selectedOptions);
onChange(name, newSelectedValues, selectedOptions);
} else {
onChange(option.value, option);
onChange(name, option.value, option);
}
}
};

View File

@@ -1,17 +1,24 @@
.menuWrap {
padding: 5px 20px 10px;
.menuItem {
width: 100vw;
left: 0;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
}
.menuItem {
position: fixed;
}
&.active {
.nut-menu-bar {
background-color: #000000;
color: #ffffff;
}
}
:global(.nut-menu-bar) {
color: #000000;
line-height: 1;
@@ -23,6 +30,7 @@
line-height: 28px;
font-size: 14px;
width: max-content;
.nut-menu-title {
color: inherit !important;
font-weight: 600;
@@ -49,12 +57,14 @@
.cityName {
font-size: 13px;
font-weight: 400;
color: #3C3C43;
color: #3c3c43;
}
.distanceWrap {
margin-bottom: 16px;
width: 100%;
}
.distanceBubbleItem {
width: auto;
}

View File

@@ -1,6 +1,5 @@
.list {
background: #fafafa;
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 5px;

View File

@@ -8,9 +8,10 @@ interface RangeProps {
max?: number;
step?: number;
value?: [number, number];
onChange?: (value: [number, number]) => void;
onChange?: (name: string, value: [number, number]) => void;
disabled?: boolean;
className?: string;
name: string;
}
const NtrpRange: React.FC<RangeProps> = ({
@@ -21,16 +22,18 @@ const NtrpRange: React.FC<RangeProps> = ({
onChange,
disabled = false,
className,
name,
}) => {
const [currentValue, setCurrentValue] = useState<[number, number]>(value);
console.log('===currentValue', currentValue)
useEffect(() => {
console.log('===rrr', value)
value && setCurrentValue(value);
}, [JSON.stringify(value || [])]);
const handleChange = (val: [number, number]) => {
setCurrentValue(val);
onChange?.(val);
onChange?.(name, val);
};
const marks = useMemo(() => {
@@ -50,13 +53,9 @@ const NtrpRange: React.FC<RangeProps> = ({
}, [JSON.stringify(currentValue || []), min, max]);
return (
<div className={`${styles.nutRange} ${className ? className : ''} `}>
<div className={`${styles.nutRange} ${className ? className : ""} `}>
<div className={styles.nutRangeHeader}>
{/* <div className={styles.nutRangeHeaderLeft}>
<div className="ntrp-range__icon">icon</div>
<h3 className={styles.nutRangeHeaderTitle}>NTRP水平区间</h3>
</div> */}
<TitleComponent title='NTRP水平区间'/>
<TitleComponent title="NTRP水平区间" />
<p className={styles.nutRangeHeaderContent}>{rangContent}</p>
</div>
@@ -68,7 +67,7 @@ const NtrpRange: React.FC<RangeProps> = ({
min={min}
max={max}
step={step}
// value={currentValue}
value={currentValue}
onEnd={handleChange}
disabled={disabled}
defaultValue={[min, max]}

View File

@@ -5,18 +5,45 @@
--nutui-searchbar-content-border-radius: 44px;
--nutui-searchbar-input-text-color: #000000;
--nutui-searchbar-input-padding: 0 0 0 10px;
--nutui-searchbar-padding:0 15px;
--nutui-searchbar-padding: 10px 0 0 0;
:global(.nut-searchbar-content) {
box-shadow: 0 4px 48px #00000014;
}
.searchBarRight {
position: relative;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid #0000000F;
border: 1px solid #0000000f;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: #000000;
}
}
.filterIcon {
width: 20px;
height: 20px;
}
.filterCount {
background-color: #000000;
position: absolute;
width: 18px;
height: 18px;
border: 2px solid #ffffff;
border-radius: 50%;
right: -5px;
bottom: -5px;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
}
.searchIcon {
width: 20px;
height: 20px;
}
}

View File

@@ -1,21 +1,38 @@
import { SearchBar } from "@nutui/nutui-react-taro";
// import {ICON_FILTER} from '../../config/images.js'
import { View, Text, Image } from "@tarojs/components";
import img from "../../config/images";
import styles from "./index.module.scss";
interface IProps {
handleFilterIcon: () => void;
isSelect: boolean;
filterCount: number;
}
const SearchBarComponent = (props: IProps) => {
// console.log('===', ICON_FILTER)
const { handleFilterIcon } = props;
const { handleFilterIcon, isSelect, filterCount } = props;
return (
<>
<SearchBar
// leftIn={
// <div>123</div>
// }
right={
<div className={styles.searchBarRight} onClick={handleFilterIcon}>
leftIn={
<div>
<Image className={styles.searchIcon} src={img.ICON_SEARCH} />
</div>
}
right={
<View
className={`${styles.searchBarRight} ${
isSelect ? styles.active : ""
}`}
onClick={handleFilterIcon}
>
<Image
src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER}
className={styles.filterIcon}
/>
<Text className={styles.filterCount}>{filterCount}</Text>
</View>
}
className={styles.searchBar}
placeholder="搜索上海的球局和场地"
/>

View File

@@ -9,4 +9,6 @@ export default {
ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.svg'),
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'),
}

View File

@@ -4,15 +4,14 @@ import Bubble, { BubbleOption } from "../../components/Bubble";
import styles from "./filterPopup.module.scss";
import TitleComponent from "src/components/Title";
import { Button } from "@nutui/nutui-react-taro";
import { useListStore } from "../../store/listStore";
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" },
{ 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[] = [
@@ -24,11 +23,25 @@ const locationOptions: BubbleOption[] = [
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 } = props;
const { loading, onCancel, onConfirm, onChange, filterOptions, onClear } = props;
console.log('===filterOptions', filterOptions)
const handleFilterChange = (name, value) => {
onChange({ [name]: value });
};
const handleClearFilter = () => {
onClear()
onCancel();
}
return (
<>
<Popup
@@ -36,17 +49,18 @@ const FilterPopup = (props: IProps) => {
destroyOnClose
position="top"
round
closeOnOverlayClick={false}
closeOnOverlayClick={true}
>
<div className={styles.filterPopupWrapper}>
{/* 时间气泡选项 */}
<Bubble
options={timeOptions}
value={(value) => {}}
onChange={(value) => {}}
value={filterOptions?.time}
onChange={handleFilterChange}
layout="grid"
size="small"
columns={3}
name="time"
/>
{/* 范围选择 */}
@@ -55,6 +69,9 @@ const FilterPopup = (props: IProps) => {
max={5.0}
step={0.5}
className={styles.filterPopupRange}
onChange={handleFilterChange}
value={filterOptions?.ntrp}
name='ntrp'
/>
{/* 场次气泡选项 */}
@@ -62,19 +79,25 @@ const FilterPopup = (props: IProps) => {
<TitleComponent title="场地类型" />
<Bubble
options={locationOptions}
value={(value) => {}}
onChange={(value) => {}}
value={filterOptions?.site}
onChange={handleFilterChange}
layout="grid"
size="small"
columns={3}
name='site'
/>
</div>
{/* 按钮 */}
<div className={styles.filterPopupBtnWrapper}>
<Button className={styles.btn} type="default" onClick={onCancel}>
<Button className={styles.btn} type="default" onClick={handleClearFilter}>
</Button>
<Button className={`${styles.btn} ${styles.confirm}`} type="default" loading={loading} onClick={onConfirm}>
<Button
className={`${styles.btn} ${styles.confirm}`}
type="default"
loading={loading}
onClick={onConfirm}
>
332
</Button>
</div>

View File

@@ -20,6 +20,10 @@ const ListPage = () => {
refreshMatches,
clearError,
updateState,
filterCount,
updateFilterOptions, // 更新筛选条件
filterOptions,
clearFilterOptions,
} = useListStore() || {};
useEffect(() => {
@@ -147,6 +151,10 @@ const ListPage = () => {
updateState({ isShowFilterPopup: !isShowFilterPopup });
};
const updateFilterSelect = (params) => {
updateState(params);
};
const cityOptions: BubbleOption[] = [
{ id: 0, label: "全城", value: "0" },
{ id: 1, label: "3km", value: "3" },
@@ -160,9 +168,17 @@ const ListPage = () => {
{ text: "销量排序", value: "c" },
];
const handleUpdateFilterOptions = (params: Record<string, any>) => {
updateFilterOptions(params);
};
return (
<div className={styles.listPage}>
<SearchBar handleFilterIcon={toggleShowPopup} />
<SearchBar
handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0}
filterCount={filterCount}
/>
{/* 综合筛选 */}
{isShowFilterPopup && (
<div>
@@ -170,15 +186,28 @@ const ListPage = () => {
loading={loading}
onCancel={toggleShowPopup}
onConfirm={toggleShowPopup}
onChange={handleUpdateFilterOptions}
filterOptions={filterOptions}
onClear={clearFilterOptions}
/>
</div>
)}
{/* 筛选 */}
<div className={ styles.listTopFilterWrapper}>
<div className={styles.listTopFilterWrapper}>
{/* 全城筛选 */}
<CityFilter options={cityOptions} value="1" onChange={() => { }} wrapperClassName={styles.menuFilter} />
<CityFilter
options={cityOptions}
value="1"
onChange={() => {}}
wrapperClassName={styles.menuFilter}
/>
{/* 智能排序 */}
<Menu options={options} value="a" onChange={() => { }} wrapperClassName={styles.menuFilter} />
<Menu
options={options}
value="a"
onChange={() => {}}
wrapperClassName={styles.menuFilter}
/>
</div>
{/* 列表内容 */}

View File

@@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.2915 4.16666H14.7915" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4585 2.5V5.83333" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4582 4.16666H2.2915" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.62484 10H2.2915" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.9585 8.33333V11.6667" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.1252 10H8.9585" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.2915 15.8333H14.7915" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4585 14.1667V17.5" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.4582 15.8333H2.2915" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.74984 15.8333C12.6618 15.8333 15.8332 12.662 15.8332 8.75C15.8332 4.838 12.6618 1.66666 8.74984 1.66666C4.83784 1.66666 1.6665 4.838 1.6665 8.75C1.6665 12.662 4.83784 15.8333 8.74984 15.8333Z" stroke="black" stroke-width="1.66667" stroke-linejoin="round"/>
<path d="M11.1071 5.97629C10.5039 5.37308 9.67057 5 8.75007 5C7.82961 5 6.99627 5.37308 6.39307 5.97629" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8423 13.8424L17.3778 17.3779" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -22,6 +22,16 @@ interface ListState {
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 接口
@@ -35,11 +45,21 @@ interface ListActions {
refreshMatches: () => Promise<void>
clearError: () => void
updateState: (payload: Record<string, any>) => void
updateFilterOptions: (payload: Record<string, any>) => void
clearFilterOptions: () => void
}
// 完整的 Store 类型
type TennisStore = ListState & ListActions
const defaultFilterOptions: IFilterOptions = {
location: '', // 位置
time: '', // 时间
ntrp: [1.0, 5.0], // NTRP 水平区间
site: '', // 场地类型
wanfa: '', // 玩法
};
// 创建 store
export const useListStore = create<TennisStore>()((set, get) => ({
// 初始状态
@@ -49,6 +69,10 @@ export const useListStore = create<TennisStore>()((set, get) => ({
lastRefreshTime: null,
// 是否展示综合筛选弹窗
isShowFilterPopup: false,
// 综合筛选项
filterOptions: defaultFilterOptions,
// 综合筛选 选择的筛选数量
filterCount: 0,
// 获取比赛数据
fetchMatches: async (params) => {
@@ -98,8 +122,30 @@ export const useListStore = create<TennisStore>()((set, get) => ({
clearError: () => {
set({ error: null })
},
// 更新综合筛选项
updateFilterOptions: (payload: Record<string, any>) => {
const preFilterOptions = get()?.filterOptions || {}
const filterOptions = { ...preFilterOptions, ...payload }
const filterCount = Object.values(filterOptions).filter(Boolean).length
set({
filterOptions,
filterCount
})
},
// 清空综合筛选选项
clearFilterOptions: () => {
set({
filterOptions: defaultFilterOptions,
filterCount: 0
})
},
// 更新store数据
updateState: (payload: Record<string, any>) => {
const state = get();
console.log('Store: 更新数据:', state);
set({
...(payload || {})
})