Files
mini-programs/src/store/listStore.ts
2026-02-01 23:37:31 +08:00

766 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { create } from "zustand";
import dayjs from "dayjs";
import {
getGamesList,
getGamesIntegrateList,
getSearchHistory,
clearHistory,
searchSuggestion,
getGamesCount,
getCities,
getCityQrCode,
getDistricts,
} from "../services/listApi";
// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取
import { useDictionaryStore } from "./dictionaryStore";
import {
ListActions,
IFilterOptions,
ListState,
IPayload,
} from "../../types/list/types";
// 将 banner 按索引插入到列表的工具方法0基长度不足则插末尾先移除已存在的 banner
function insertBannersToRows(rows: any[], dictData: any) {
if (!Array.isArray(rows) || !dictData) return rows;
const img = (dictData?.bannerListImage || "").trim();
const indexRaw = (dictData?.bannerListIndex || "").toString().trim();
if (!img) return rows;
const parsed = parseInt(indexRaw, 10);
const normalized = Number.isFinite(parsed) ? parsed : 0;
// 先移除已有的 banner确保列表中仅一条 banner
const resultRows = rows?.filter((item) => item?.type !== "banner") || [];
const target = Math.max(0, Math.min(normalized, resultRows.length));
resultRows.splice(target, 0, {
type: "banner",
id: `banner-${target}`,
banner_image_url: img,
banner_detail_url: (dictData?.bannerDetailImage || "").trim(),
} as any);
return resultRows;
}
function translateCityData(dataTree) {
return dataTree.map((item) => {
const { children, ...rest } = item;
// 只保留两级:国家和省份,去掉第三级(区域)
const processedChildren = children?.length > 0
? children.map(child => ({
...child,
text: child.name,
label: child.name,
value: child.name,
children: null, // 去掉第三级
}))
: null;
return {
...rest,
text: rest.name,
label: rest.name,
value: rest.name,
children: processedChildren,
};
});
}
// 完整的 Store 类型
type TennisStore = ListState & ListActions;
const defaultDateRange: [string, string] = [dayjs().format('YYYY-MM-DD'), dayjs().add(1, 'M').format('YYYY-MM-DD')]
const defaultFilterOptions: IFilterOptions = {
dateRange: defaultDateRange, // 日期区间
timeSlot: [], // 时间段(多选,默认为空数组)
ntrp: [1, 5], // NTRP 水平区间
venueType: [], // 场地类型(多选,默认为空数组)
playType: [], // 玩法(多选,默认为空数组)
};
// const defaultDistance = "all"; // 默认距离
const defaultDistanceQuickFilter = {
distanceFilter: "",
order: "0",
district: "", // 新增:行政区筛选
};
const defaultPageOption = {
page: 1,
pageSize: 20,
};
// 页面状态默认值
const pageStateDefaultValue = {
// 列表数据
data: [],
// 推荐列表
recommendList: [],
// 是否展示综合筛选弹窗
isShowFilterPopup: false,
// 综合筛选项
filterOptions: defaultFilterOptions,
// 距离筛选和快捷筛选
distanceQuickFilter: defaultDistanceQuickFilter,
// 综合筛选 选择的筛选数量
filterCount: 0,
// 分页
pageOption: defaultPageOption,
// 球局数量
gamesNum: 0,
// 是否还有更多数据
isHasMoreData: true,
// 是否展示无数据
isShowNoData: false,
}
// 列表页状态
const listPageStateDefaultValue = {
...pageStateDefaultValue,
// 列表页是否显示搜索框自定义导航
isShowInputCustomerNavBar: false,
}
// 搜索页状态
const searchPageStateDefaultValue = {
...pageStateDefaultValue,
// 搜索结果数据
data: [],
// 联想词
suggestionList: [],
// 是否显示联想词
isShowSuggestion: false,
// 搜索历史数据
searchHistory: [],
// 搜索历史数据默认 Top 15
searchHistoryParams: {
page: 1,
pageSize: 15,
},
}
// const now = new Date();
// 公共属性
const commonStateDefaultValue = {
// 是否是搜索结果页
isSearchResult: false,
// 是否加载中
loading: false,
// 错误信息
error: null,
// 位置
location: {
latitude: 0,
longitude: 0,
},
// 搜索的value
searchValue: "",
// 日期区间
// dateRange: [now, new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)],
// 距离筛选数据
distanceData: [
{ id: 0, label: "全城", value: "" },
{ id: 1, label: "3km", value: "3" },
{ id: 2, label: "5km", value: "5" },
{ id: 3, label: "10km", value: "10" },
],
// 快捷筛选数据
quickFilterData: [
{ label: "智能排序", value: "0" },
{ label: "距离更近", value: "distance" },
{ label: "时间更近", value: "time" },
],
// 气泡日期范围
dateRangeOptions: [
{ id: 1, label: "本周末", value: "1" },
{ id: 2, label: "一周内", value: "2" },
{ id: 3, label: "一月内", value: "3" },
],
// 时间气泡数据
timeBubbleData: [
{ id: 1, label: "晨间 6:00-10:00", value: "6:00-10:00" },
{ id: 2, label: "上午 10:00-12:00", value: "10:00-12:00" },
{ id: 3, label: "中午 12:00-14:00", value: "12:00-14:00" },
{ id: 4, label: "下午 14:00-18:00", value: "14:00-18:00" },
{ id: 5, label: "晚上 18:00-22:00", value: "18:00-22:00" },
{ id: 6, label: "夜间 22:00-24:00", value: "22:00-24:00" },
],
cities: [],
cityQrCode: [],
area: ['', ''] as [string, string], // 改为两级:国家、省份
districts: [], // 新增:行政区列表
}
// 创建 store
export const useListStore = create<TennisStore>()((set, get) => ({
currentPage: "",
// 列表页
listPageState: listPageStateDefaultValue,
// 搜索及搜索结果页
searchPageState: searchPageStateDefaultValue,
...commonStateDefaultValue,
gamesNum: 0,
// 组装搜索数据
// 注意:始终使用 state.area不接收 overrideArea 参数,确保参数一致性
getSearchParams: () => {
const state = get();
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const filterOptions = currentPageState?.filterOptions || {};
// 全城和快捷筛选
const distanceQuickFilter = currentPageState?.distanceQuickFilter || {};
const { distanceFilter, order, district } = distanceQuickFilter || {};
// 始终使用 state.area确保所有接口使用一致的城市参数
const areaProvince = state.area?.at(1) || "";
const last_location_province = areaProvince;
// city 参数逻辑:
// 1. 如果选择了行政区district 有值使用行政区的名称label
// 2. 如果是"全城"distanceFilter 为空),不传 city
let city: string | undefined = undefined;
if (district) {
// 从 districts 数组中查找对应的行政区名称
const selectedDistrict = state.districts.find(item => item.value === district);
if (selectedDistrict) {
city = selectedDistrict.label; // 传递行政区名称,如"静安"
}
}
// 如果是"全城"distanceFilter 为空city 保持 undefined不会被传递
// 使用 filterOptions 中的 dateRange
const dateRange: [string, string] = filterOptions?.dateRange || defaultDateRange;
// 构建 searchOption
// 注意province 必须从 state.area 获取,不能依赖 filterOptions 中可能存在的旧值
const searchOption: any = {
...filterOptions,
title: state.searchValue,
ntrpMin: filterOptions?.ntrp?.[0],
ntrpMax: filterOptions?.ntrp?.[1],
dateRange: dateRange, // 确保始终是两个值的数组
distanceFilter: distanceFilter,
// 显式设置 province确保始终使用 state.area 中的最新值
province: last_location_province, // 始终使用 state.area 中的 province确保城市参数一致
};
// 只在有值时添加 city 参数
if (city) {
searchOption.city = city;
}
const params = {
pageOption: currentPageState?.pageOption,
seachOption: searchOption,
order: order,
lat: state?.location?.latitude,
lng: state?.location?.longitude,
};
return params;
},
// 设置列表结果
setListData: (payload: IPayload & { isAppend?: boolean }) => {
const state = get();
const { error, data, loading, count, isAppend = false } = payload;
const isHasMoreData = count > 0;
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const currentData = currentPageState?.data || [];
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
// 从字典缓存获取 banner并将其插入到最终列表指定位置全局索引
const dictData = useDictionaryStore.getState().bannerDict;
const processedData = dictData ? insertBannersToRows(newData, dictData) : newData;
state.updateCurrentPageState({
data: processedData,
isHasMoreData,
// 使用插入后的最终数据判断是否显示空状态,避免有 banner 时仍显示空
isShowNoData: processedData?.length === 0,
});
set({
error,
loading,
});
},
// 获取列表数据(常规搜索)
fetchMatches: async (params, isFirstLoad = false, isAppend = false) => {
if (get().loading) {
return;
}
set({ loading: true, error: null });
const { getSearchParams, setListData } = get();
try {
const searchParams = getSearchParams() || {};
const reqParams = {
...(searchParams || {}),
...params,
};
// 获取当前页面的距离筛选
const state = get();
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const distanceQuickFilter = currentPageState?.distanceQuickFilter || {};
// 是否选择了智能排序
const isIntegrate = distanceQuickFilter?.order === "0";
let fetchFn = getGamesList;
if (isIntegrate) {
reqParams.order = "";
fetchFn = getGamesIntegrateList;
// 第一次进入页面时传入 isRefresh 参数
if (isFirstLoad) {
reqParams.seachOption.isRefresh = true;
}
}
let resData: any = {};
resData = (await fetchFn(reqParams)) || {};
const { data = {}, code } = resData;
if (code !== 0) {
setListData({
error: "-1",
data: [],
loading: false,
count: 0,
isAppend,
});
return Promise.reject(new Error('获取数据失败'));
}
const { count } = data;
let { rows } = data as any;
setListData({
error: '',
data: rows || [],
loading: false,
count,
isAppend,
});
return Promise.resolve();
} catch (error) {
setListData({
error: "",
data: [],
loading: false,
count: 0,
isAppend,
});
return Promise.reject(error);
}
},
// 获取列表数据
getMatchesData: async () => {
const { fetchMatches } = get();
return await fetchMatches({}, true); // 第一次进入页面,传入 isFirstLoad = true
},
// 同时更新两个列表接口(常规列表和智能排序列表)
// 注意:不再接收 overrideArea 参数,始终使用 state.area
refreshBothLists: async () => {
const state = get();
const { getSearchParams, setListData } = state;
const { getGamesList, getGamesIntegrateList } = await import("../services/listApi");
try {
const searchParams = getSearchParams() || {};
// 并发请求:常规列表、智能排序列表
const [listResSettled, integrateResSettled] = await Promise.allSettled([
getGamesList({
...searchParams,
order: searchParams.order || "distance",
}),
getGamesIntegrateList({
...searchParams,
order: "",
seachOption: {
...searchParams.seachOption,
isRefresh: true,
},
}),
]);
const listRes = listResSettled.status === "fulfilled" ? listResSettled.value : null;
const integrateRes = integrateResSettled.status === "fulfilled" ? integrateResSettled.value : null;
// 根据当前排序方式更新对应的数据
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const distanceQuickFilter = currentPageState?.distanceQuickFilter || {};
const isIntegrate = distanceQuickFilter?.order === "0";
if (listRes?.code === 0 && listRes?.data) {
const { count } = listRes.data;
let { rows } = listRes.data as any;
if (!isIntegrate) {
// 如果当前是常规排序,更新常规列表数据
setListData({
error: '',
data: rows || [],
loading: false,
count,
isAppend: false,
});
}
}
if (integrateRes?.code === 0 && integrateRes?.data) {
const { count } = integrateRes.data;
let { rows, recommendList } = integrateRes.data as any;
if (isIntegrate) {
// 如果当前是智能排序,更新智能排序列表数据
setListData({
error: '',
data: rows || [],
loading: false,
count,
isAppend: false,
});
}
// 无论当前排序方式如何,都更新推荐列表
state.updateCurrentPageState({
recommendList: recommendList || [],
});
}
return Promise.resolve();
} catch (error) {
console.error("更新列表数据失败:", error);
return Promise.reject(error);
}
},
// 获取球局数量
// 注意:必须和 games/integrate_list 使用相同的参数构建逻辑,确保数据一致性
// 不再接收 overrideArea 参数,始终使用 state.area
fetchGetGamesCount: async () => {
const state = get();
const { getSearchParams } = state;
const searchParams = getSearchParams() || {};
// 使用和 games/integrate_list 相同的参数构建逻辑
const params = {
...searchParams,
order: "", // 和 integrate_list 保持一致
seachOption: {
...searchParams.seachOption,
isRefresh: true, // 和 integrate_list 保持一致
},
};
console.log("fetchGetGamesCount 参数:", { area: state.area, params: JSON.stringify(params) });
const resData = (await getGamesCount(params)) || {};
const gamesNum = resData?.data?.count || 0;
console.log("fetchGetGamesCount 结果:", gamesNum);
set({ gamesNum });
},
// 获取历史搜索数据
getSearchHistory: async () => {
try {
const state = get();
const params = state.searchPageState?.searchHistoryParams || {};
const resData = (await getSearchHistory(params)) || {};
const searchHistory = resData?.data?.records || [];
set({
searchPageState: {
...state.searchPageState,
searchHistory,
},
});
} catch (error) { }
},
// 清空历史记录
clearHistory: async () => {
try {
const state = get();
const params = {};
const resData = (await clearHistory(params)) || {};
if (resData?.code === 0) {
set({
searchPageState: {
...state.searchPageState,
searchHistory: [],
},
});
}
} catch (error) { }
},
// 获取联想
searchSuggestion: async (val: string) => {
try {
const state = get();
const resData = (await searchSuggestion({ keyword: val, limit: 10 })) || {};
const recommendations = resData?.data?.recommendations || [];
const total = resData?.data?.total;
set({
searchPageState: {
...state.searchPageState,
suggestionList: recommendations,
isShowSuggestion: total > 0,
},
});
} catch (error) {
const state = get();
set({
searchPageState: {
...state.searchPageState,
suggestionList: [],
isShowSuggestion: true,
},
});
}
},
// 清除错误信息
clearError: () => {
set({ error: null });
},
getCurrentPageState: () => {
const state = get();
return {
currentPageState: state.isSearchResult ? state.searchPageState : state.listPageState,
currentPageKey: state.isSearchResult ? "searchPageState" : "listPageState",
};
},
// 更新当前页面状态
updateCurrentPageState: (payload: Record<string, any>) => {
const state = get();
const { currentPageState, currentPageKey } = state.getCurrentPageState();
set({
[currentPageKey]: { ...currentPageState, ...payload }
});
},
// 更新综合筛选项
updateFilterOptions: (payload: Record<string, any>) => {
const state = get();
const { currentPageState } = state.getCurrentPageState();
const filterOptions = { ...currentPageState?.filterOptions, ...payload };
// 计算筛选数量:排除 dateRange、ntrp 默认值,以及空数组和空字符串
const filterCount = Object.entries(filterOptions).filter(([key, value]) => {
if (key === 'dateRange') return false; // 日期区间不算筛选
if (key === 'ntrp') {
// NTRP 只有不是默认值 [1, 5] 时才算筛选
const ntrp = value as [number, number];
return ntrp && (ntrp[0] !== 1 || ntrp[1] !== 5);
}
// 数组为空数组或字符串为空字符串时不算筛选
if (Array.isArray(value)) return value.length > 0;
if (typeof value === 'string') return value !== '';
return Boolean(value);
}).length;
// 先更新状态
state.updateCurrentPageState({
filterOptions,
filterCount,
pageOption: defaultPageOption,
});
// 使用 Promise.resolve 确保状态更新后再调用接口
// 先调用列表接口,然后在列表接口完成后调用数量接口
Promise.resolve().then(async () => {
const freshState = get(); // 重新获取最新状态
// 先调用列表接口
await freshState.getMatchesData();
// 列表接口完成后,再调用数量接口
await freshState.fetchGetGamesCount();
});
},
// 更新距离和快捷筛选
updateDistanceQuickFilter: (payload: Record<string, any>) => {
const state = get();
const { currentPageState } = state.getCurrentPageState();
const { distanceQuickFilter } = currentPageState || {};
const newDistanceQuickFilter = { ...distanceQuickFilter, ...payload };
// 先更新状态
state.updateCurrentPageState({
distanceQuickFilter: newDistanceQuickFilter,
pageOption: defaultPageOption,
});
// 使用 Promise.resolve 确保状态更新后再调用接口
// 先调用列表接口,然后在列表接口完成后调用数量接口
Promise.resolve().then(async () => {
const freshState = get(); // 重新获取最新状态
// 先调用列表接口
await freshState.getMatchesData();
// 列表接口完成后,再调用数量接口
await freshState.fetchGetGamesCount();
});
},
// 清空综合筛选选项
clearFilterOptions: async () => {
const state = get();
const { getMatchesData, fetchGetGamesCount } = state;
state.updateCurrentPageState({
filterOptions: defaultFilterOptions,
filterCount: 0,
pageOption: defaultPageOption,
});
// 先调用列表接口
await getMatchesData();
// 列表接口完成后,再调用数量接口
await fetchGetGamesCount();
},
// 加载更多数据
loadMoreMatches: async () => {
const state = get();
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const { pageOption, isHasMoreData } = currentPageState || {};
if (!isHasMoreData) {
return Promise.resolve();
}
const newPageOption = {
page: (pageOption?.page || 1) + 1,
pageSize: 20,
};
state.updateCurrentPageState({
pageOption: newPageOption,
});
// 加载更多时追加数据到现有数组
return await state.fetchMatches({}, false, true);
},
// 初始化搜索条件 重新搜索
initialFilterSearch: async (isSearchData = false) => {
const state = get();
const { getMatchesData, fetchGetGamesCount } = state;
if (state.isSearchResult) {
set({
searchPageState: {
...searchPageStateDefaultValue
},
// loading: true,
});
} else {
set({
listPageState: {
...listPageStateDefaultValue
},
// loading: true,
});
}
if (!isSearchData) {
return;
}
// 先调用列表接口
await getMatchesData();
// 列表接口完成后,再调用数量接口
await fetchGetGamesCount();
},
// 更新store数据
updateState: (payload: Record<string, any>) => {
set({
...(payload || {}),
});
},
// 更新列表页状态中的特定字段
updateListPageState: (payload: Record<string, any>) => {
console.log("===更新列表页状态:", payload);
const state = get();
set({
listPageState: {
...state.listPageState,
...payload,
},
});
},
// 更新搜索页状态中的特定字段
updateSearchPageState: (payload: Record<string, any>) => {
const state = get();
set({
searchPageState: {
...state.searchPageState,
...payload,
},
});
console.log("===更新搜索页状态:", state);
},
async getCities() {
const res = await getCities();
const state = get();
set({
...state,
cities: translateCityData(res.data),
})
},
async getCityQrCode() {
const res = await getCityQrCode();
const state = get();
set({
...state,
cityQrCode: res.data,
})
},
// 新增:获取行政区列表
async getDistricts() {
try {
const state = get();
// 从 area 中获取省份area 格式: ["中国", 省份, 城市]
const country = "中国";
const province = state.area?.at(1) || "上海"; // area[1] 是省份
const res = await getDistricts({
country,
state: province
});
if (res.code === 0 && res.data) {
const districts = res.data.map((item) => ({
label: item.cn_city,
value: item.id.toString(),
id: item.id,
}));
set({ districts });
return districts;
}
return [];
} catch (error) {
console.error("获取行政区列表失败:", error);
return [];
}
},
updateArea(payload: [string, string]) {
set({
area: payload,
})
},
}));
// 导出便捷的 hooks
export const useListState = () => useListStore((state) => state);