541 lines
19 KiB
TypeScript
541 lines
19 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { View, Text, Image } from "@tarojs/components";
|
||
import img from "@/config/images";
|
||
import { useGlobalState } from "@/store/global";
|
||
import { useUserInfo } from "@/store/userStore";
|
||
import { useListState } from "@/store/listStore";
|
||
import { Input } from "@nutui/nutui-react-taro";
|
||
import Taro from "@tarojs/taro";
|
||
import "./index.scss";
|
||
import { getCurrentFullPath } from "@/utils";
|
||
import { CityPickerV2 as PopupPicker } from "@/components/Picker";
|
||
import LocationConfirmDialog from "@/components/LocationConfirmDialog";
|
||
|
||
// 城市缓存 key
|
||
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
|
||
// 定位弹窗关闭时间缓存 key(用户选择"继续浏览"时记录)
|
||
const LOCATION_DIALOG_DISMISS_TIME_KEY = "LOCATION_DIALOG_DISMISS_TIME";
|
||
// 城市切换时间缓存 key(用户手动切换城市时记录)
|
||
const CITY_CHANGE_TIME_KEY = "CITY_CHANGE_TIME";
|
||
// 2小时的毫秒数
|
||
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
||
|
||
interface IProps {
|
||
config?: {
|
||
showInput?: boolean;
|
||
inputLeftIcon?: string;
|
||
iconPath?: string;
|
||
leftIconClick?: () => void;
|
||
title?: string; // 显示标题(用于"我的"页面等)
|
||
showTitle?: boolean; // 是否显示标题模式
|
||
};
|
||
onCityPickerVisibleChange?: (visible: boolean) => void; // 城市选择器显示/隐藏回调
|
||
onScrollToTop?: () => void; // 滚动到顶部回调
|
||
}
|
||
|
||
function CityPicker(props) {
|
||
const { visible, setVisible, cities, area, setArea, onCityChange } = props;
|
||
console.log(cities, "cities");
|
||
const [value, setValue] = useState(area);
|
||
|
||
function onChange(value: any) {
|
||
console.log(value, "value");
|
||
setValue(value);
|
||
setArea(value);
|
||
// 切换城市时触发接口调用
|
||
if (onCityChange) {
|
||
onCityChange(value);
|
||
}
|
||
}
|
||
return (
|
||
visible && (
|
||
<PopupPicker
|
||
options={cities}
|
||
visible={visible}
|
||
setvisible={setVisible}
|
||
value={value}
|
||
onChange={onChange}
|
||
style={{ zIndex: 9991 }}
|
||
/>
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 首页专用导航栏组件
|
||
* 支持三种模式:
|
||
* 1. Logo + 城市选择 + 球局数量(默认模式)
|
||
* 2. 搜索输入框模式(showInput = true)
|
||
* 3. 标题模式(showTitle = true,用于"我的"页面等)
|
||
*/
|
||
const HomeNavbar = (props: IProps) => {
|
||
const { config, onCityPickerVisibleChange, onScrollToTop } = props;
|
||
const {
|
||
showInput = false,
|
||
inputLeftIcon,
|
||
leftIconClick,
|
||
title,
|
||
showTitle = false,
|
||
} = config || {};
|
||
const { getLocationLoading, statusNavbarHeightInfo, setShowGuideBar } = useGlobalState();
|
||
const {
|
||
gamesNum,
|
||
searchValue,
|
||
cities,
|
||
area,
|
||
updateArea,
|
||
fetchGetGamesCount,
|
||
refreshBothLists,
|
||
} = useListState();
|
||
const { statusBarHeight = 0, navBarHeight = 44 } =
|
||
statusNavbarHeightInfo || {};
|
||
|
||
const [cityPopupVisible, setCityPopupVisible] = useState(false);
|
||
const [locationDialogVisible, setLocationDialogVisible] = useState(false);
|
||
const [locationDialogData, setLocationDialogData] = useState<{
|
||
detectedProvince: string;
|
||
cachedCity: [string, string];
|
||
} | null>(null);
|
||
|
||
// 监听城市选择器状态变化,通知父组件
|
||
useEffect(() => {
|
||
onCityPickerVisibleChange?.(cityPopupVisible);
|
||
}, [cityPopupVisible]);
|
||
|
||
const userInfo = useUserInfo();
|
||
// 使用用户详情接口中的 last_location 字段
|
||
// USER_SELECTED_CITY 第二个值应该是省份/直辖市,不能是区
|
||
const lastLocationProvince = (userInfo as any)?.last_location_province || "";
|
||
// 只使用省份/直辖市,不使用城市(城市可能是区)
|
||
const detectedLocation = lastLocationProvince;
|
||
|
||
// 检查是否应该显示定位确认弹窗
|
||
const should_show_location_dialog = (): boolean => {
|
||
try {
|
||
const current_time = Date.now();
|
||
|
||
// 检查是否在2小时内切换过城市
|
||
const city_change_time = (Taro as any).getStorageSync(CITY_CHANGE_TIME_KEY);
|
||
if (city_change_time) {
|
||
const time_diff = current_time - city_change_time;
|
||
// 如果距离上次切换城市还在2小时内,不显示弹窗
|
||
if (time_diff < TWO_HOURS_MS) {
|
||
console.log(`[HomeNavbar] 距离上次切换城市还不到2小时,剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟,不显示定位弹窗`);
|
||
return false;
|
||
} else {
|
||
// 超过2小时,清除过期记录
|
||
(Taro as any).removeStorageSync(CITY_CHANGE_TIME_KEY);
|
||
}
|
||
}
|
||
|
||
// 检查是否在2小时内已选择"继续浏览"
|
||
const dismiss_time = (Taro as any).getStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY);
|
||
if (!dismiss_time) {
|
||
return true; // 没有记录,可以显示
|
||
}
|
||
|
||
const time_diff = current_time - dismiss_time;
|
||
// 如果距离上次选择"继续浏览"已超过2小时,可以再次显示
|
||
if (time_diff >= TWO_HOURS_MS) {
|
||
// 清除过期记录
|
||
(Taro as any).removeStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY);
|
||
return true;
|
||
}
|
||
|
||
// 在2小时内,不显示弹窗
|
||
console.log(`[HomeNavbar] 距离上次选择"继续浏览"还不到2小时,剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
|
||
return false;
|
||
} catch (error) {
|
||
console.error('[HomeNavbar] 检查定位弹窗显示条件失败:', error);
|
||
return true; // 出错时默认显示
|
||
}
|
||
};
|
||
|
||
// 显示定位确认弹窗
|
||
const showLocationConfirmDialog = (detectedLocation: string, cachedCity: [string, string]) => {
|
||
// 检查是否应该显示弹窗
|
||
if (!should_show_location_dialog()) {
|
||
console.log('[HomeNavbar] 用户在2小时内已选择"继续浏览"或切换过城市,不显示弹窗');
|
||
return;
|
||
}
|
||
|
||
console.log('[HomeNavbar] 准备显示定位确认弹窗,隐藏 GuideBar');
|
||
setLocationDialogData({ detectedProvince: detectedLocation, cachedCity });
|
||
setLocationDialogVisible(true);
|
||
// 显示弹窗时隐藏 GuideBar
|
||
setShowGuideBar(false);
|
||
console.log('[HomeNavbar] setShowGuideBar(false) 已调用');
|
||
};
|
||
|
||
// 初始化城市:优先使用缓存的定位信息,如果缓存城市和用户详情位置不一致,且时间过期,则弹出选择框
|
||
// 只在组件挂载时执行一次,避免重复执行
|
||
useEffect(() => {
|
||
// 1. 优先尝试从缓存中读取上次的定位信息
|
||
const cachedCity = (Taro as any).getStorageSync(CITY_CACHE_KEY);
|
||
|
||
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
|
||
// 如果有缓存的定位信息,使用缓存
|
||
const cachedCityArray = cachedCity as [string, string];
|
||
console.log("[HomeNavbar] 使用缓存的定位城市:", cachedCityArray);
|
||
updateArea(cachedCityArray);
|
||
|
||
// 如果用户详情中有位置信息,且与缓存不一致,检查是否需要弹窗
|
||
if (detectedLocation && cachedCityArray[1] !== detectedLocation) {
|
||
// 检查时间缓存,如果没有或过期,则弹出选择框
|
||
if (should_show_location_dialog()) {
|
||
console.log("[HomeNavbar] 缓存城市与用户详情位置不一致,且时间过期,弹出选择框");
|
||
showLocationConfirmDialog(detectedLocation, cachedCityArray);
|
||
} else {
|
||
console.log("[HomeNavbar] 缓存城市与用户详情位置不一致,但时间未过期,不弹出选择框");
|
||
}
|
||
}
|
||
} else if (detectedLocation) {
|
||
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
|
||
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
|
||
const newArea: [string, string] = ["中国", detectedLocation];
|
||
updateArea(newArea);
|
||
// 保存定位信息到缓存
|
||
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []); // 空依赖数组,确保只在组件挂载时执行一次
|
||
|
||
// 检查是否在2小时内已选择"继续浏览"或切换过城市(当前不使用,首页重新进入时直接使用缓存中的位置)
|
||
// const should_show_location_dialog = (): boolean => {
|
||
// try {
|
||
// // 检查是否在2小时内切换过城市
|
||
// const city_change_time = (Taro as any).getStorageSync(CITY_CHANGE_TIME_KEY);
|
||
// if (city_change_time) {
|
||
// const current_time = Date.now();
|
||
// const time_diff = current_time - city_change_time;
|
||
//
|
||
// // 如果距离上次切换城市还在2小时内,不显示弹窗
|
||
// if (time_diff < TWO_HOURS_MS) {
|
||
// console.log(`距离上次切换城市还不到2小时,剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟,不显示定位弹窗`);
|
||
// return false;
|
||
// } else {
|
||
// // 超过2小时,清除过期记录
|
||
// (Taro as any).removeStorageSync(CITY_CHANGE_TIME_KEY);
|
||
// }
|
||
// }
|
||
//
|
||
// // 检查是否在2小时内已选择"继续浏览"
|
||
// const dismiss_time = (Taro as any).getStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY);
|
||
// if (!dismiss_time) {
|
||
// return true; // 没有记录,可以显示
|
||
// }
|
||
//
|
||
// const current_time = Date.now();
|
||
// const time_diff = current_time - dismiss_time;
|
||
//
|
||
// // 如果距离上次选择"继续浏览"已超过2小时,可以再次显示
|
||
// if (time_diff >= TWO_HOURS_MS) {
|
||
// // 清除过期记录
|
||
// (Taro as any).removeStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY);
|
||
// return true;
|
||
// }
|
||
//
|
||
// // 在2小时内,不显示弹窗
|
||
// console.log(`距离上次选择"继续浏览"还不到2小时,剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
|
||
// return false;
|
||
// } catch (error) {
|
||
// console.error('检查定位弹窗显示条件失败:', error);
|
||
// return true; // 出错时默认显示
|
||
// }
|
||
// };
|
||
|
||
// 显示定位确认弹窗(当前不使用,首页重新进入时直接使用缓存中的位置)
|
||
// const showLocationConfirmDialog = (detectedLocation: string, cachedCity: [string, string]) => {
|
||
// // 检查是否在2小时内已选择"继续浏览"
|
||
// if (!should_show_location_dialog()) {
|
||
// console.log('[LocationDialog] 用户在2小时内已选择"继续浏览",不显示弹窗');
|
||
// return;
|
||
// }
|
||
//
|
||
// console.log('[LocationDialog] 准备显示定位确认弹窗,隐藏 GuideBar');
|
||
// setLocationDialogData({ detectedProvince: detectedLocation, cachedCity });
|
||
// setLocationDialogVisible(true);
|
||
// // 显示弹窗时隐藏 GuideBar
|
||
// setShowGuideBar(false);
|
||
// console.log('[LocationDialog] setShowGuideBar(false) 已调用');
|
||
// };
|
||
|
||
// 处理定位弹窗确认
|
||
const handleLocationDialogConfirm = () => {
|
||
if (!locationDialogData) return;
|
||
|
||
const { detectedProvince } = locationDialogData;
|
||
// 用户选择"切换到",使用用户详情中的位置信息
|
||
const newArea: [string, string] = ["中国", detectedProvince];
|
||
updateArea(newArea);
|
||
// 更新缓存为新的定位信息
|
||
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
|
||
// 记录切换城市的时间戳,2小时内不再提示
|
||
try {
|
||
const current_time = Date.now();
|
||
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
|
||
console.log(`[LocationDialog] 已记录用户切换城市的时间,2小时内不再提示`);
|
||
} catch (error) {
|
||
console.error('保存城市切换时间失败:', error);
|
||
}
|
||
console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince);
|
||
|
||
// 关闭弹窗
|
||
setLocationDialogVisible(false);
|
||
setLocationDialogData(null);
|
||
// 关闭弹窗时显示 GuideBar
|
||
setShowGuideBar(true);
|
||
|
||
// 刷新数据
|
||
handleCityChangeWithoutCache();
|
||
};
|
||
|
||
// 处理定位弹窗取消(用户选择"继续浏览")
|
||
const handleLocationDialogCancel = () => {
|
||
if (!locationDialogData) return;
|
||
|
||
const { cachedCity } = locationDialogData;
|
||
// 用户选择"继续浏览",保持缓存的定位城市
|
||
console.log("保持缓存的定位城市:", cachedCity[1]);
|
||
|
||
// 记录用户选择"继续浏览"的时间戳,2小时内不再提示
|
||
try {
|
||
const current_time = Date.now();
|
||
(Taro as any).setStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY, current_time);
|
||
console.log(`[LocationDialog] 已记录用户选择"继续浏览"的时间,2小时内不再提示`);
|
||
} catch (error) {
|
||
console.error('保存定位弹窗关闭时间失败:', error);
|
||
}
|
||
|
||
// 关闭弹窗
|
||
setLocationDialogVisible(false);
|
||
setLocationDialogData(null);
|
||
// 关闭弹窗时显示 GuideBar
|
||
setShowGuideBar(true);
|
||
};
|
||
|
||
// const currentAddress = city + district;
|
||
|
||
const handleInputClick = () => {
|
||
// 关闭城市选择器
|
||
if (cityPopupVisible) {
|
||
setCityPopupVisible(false);
|
||
}
|
||
|
||
const currentPagePath = getCurrentFullPath();
|
||
if (currentPagePath === "/game_pages/searchResult/index") {
|
||
(Taro as any).navigateBack();
|
||
} else {
|
||
(Taro as any).navigateTo({
|
||
url: "/game_pages/search/index",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 点击logo
|
||
const handleLogoClick = () => {
|
||
// 关闭城市选择器
|
||
if (cityPopupVisible) {
|
||
setCityPopupVisible(false);
|
||
}
|
||
|
||
// 如果当前在列表页,点击后页面回到顶部
|
||
if (getCurrentFullPath() === "/main_pages/index") {
|
||
// 使用父组件传递的滚动方法(适配 ScrollView)
|
||
if (onScrollToTop) {
|
||
onScrollToTop();
|
||
} else {
|
||
// 降级方案:使用页面滚动(兼容旧版本)
|
||
(Taro as any).pageScrollTo({
|
||
scrollTop: 0,
|
||
duration: 300,
|
||
});
|
||
}
|
||
return; // 已经在列表页,只滚动到顶部,不需要跳转
|
||
}
|
||
(Taro as any).redirectTo({
|
||
url: "/main_pages/index", // 列表页
|
||
});
|
||
};
|
||
|
||
const handleInputLeftIconClick = () => {
|
||
// 关闭城市选择器
|
||
if (cityPopupVisible) {
|
||
setCityPopupVisible(false);
|
||
}
|
||
|
||
if (leftIconClick) {
|
||
leftIconClick();
|
||
} else {
|
||
handleLogoClick();
|
||
}
|
||
};
|
||
|
||
const navbarStyle = {
|
||
height: `${navBarHeight}px`,
|
||
};
|
||
|
||
function handleToggleCity() {
|
||
setCityPopupVisible(true);
|
||
}
|
||
|
||
const area_city = area.at(-1);
|
||
|
||
// 处理城市切换(仅刷新数据,不保存缓存)
|
||
const handleCityChangeWithoutCache = async () => {
|
||
// 先调用列表接口
|
||
if (refreshBothLists) {
|
||
await refreshBothLists();
|
||
}
|
||
// 列表接口完成后,再调用数量接口
|
||
if (fetchGetGamesCount) {
|
||
await fetchGetGamesCount();
|
||
}
|
||
};
|
||
|
||
// 处理城市切换(用户手动选择)
|
||
const handleCityChange = async (_newArea: any) => {
|
||
// 用户手动选择的城市保存到缓存
|
||
console.log("用户手动选择城市,更新缓存:", _newArea);
|
||
|
||
// 先更新 area 状态(用于界面显示和接口参数)
|
||
updateArea(_newArea);
|
||
|
||
// 保存城市到缓存
|
||
try {
|
||
(Taro as any).setStorageSync(CITY_CACHE_KEY, _newArea);
|
||
// 记录切换时间,2小时内不再弹出定位弹窗
|
||
const current_time = Date.now();
|
||
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
|
||
console.log("已保存城市到缓存并记录切换时间:", _newArea, current_time);
|
||
} catch (error) {
|
||
console.error("保存城市缓存失败:", error);
|
||
}
|
||
|
||
// 先调用列表接口(会使用更新后的 state.area)
|
||
if (refreshBothLists) {
|
||
await refreshBothLists();
|
||
}
|
||
// 列表接口完成后,再调用数量接口(会使用更新后的 state.area)
|
||
if (fetchGetGamesCount) {
|
||
await fetchGetGamesCount();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<View
|
||
className="homeNavbar"
|
||
style={{
|
||
position: "fixed",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100%",
|
||
height: `${navBarHeight}px`,
|
||
paddingTop: `${statusBarHeight}px`,
|
||
backgroundColor: "transparent",
|
||
zIndex: 99,
|
||
}}
|
||
>
|
||
<View className="listNavWrapper">
|
||
{/* 标题模式(用于"我的"页面等) */}
|
||
{showTitle && (
|
||
<View className="titleNavContainer" style={navbarStyle}>
|
||
<View className="titleNavContent">
|
||
<Text className="titleNavText">{title || "我的"}</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 首页logo 导航*/}
|
||
{!showTitle && (
|
||
<View
|
||
className={`listNavContainer toggleElement firstElement hidden
|
||
${!showInput ? "visible" : ""}`}
|
||
style={navbarStyle}
|
||
>
|
||
<View className="listNavContentWrapper">
|
||
{/* logo */}
|
||
<Image
|
||
src={img.ICON_LOGO}
|
||
className="listNavLogo"
|
||
onClick={handleLogoClick}
|
||
mode="aspectFit"
|
||
/>
|
||
<View className="listNavLine" />
|
||
<View className="listNavContent">
|
||
<View className="listNavCityWrapper" onClick={handleToggleCity}>
|
||
{/* 位置 */}
|
||
<Text className="listNavCity">{area_city}</Text>
|
||
{!getLocationLoading && area_city && (
|
||
<Image src={img.ICON_CHANGE} className="listNavChange" />
|
||
)}
|
||
</View>
|
||
<View className="listNavInfoWrapper">
|
||
<Text className="listNavInfo">附近{gamesNum}场球局</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 搜索导航 */}
|
||
{!showTitle && (
|
||
<View
|
||
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
|
||
showInput && "visible"
|
||
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`}
|
||
style={navbarStyle}
|
||
>
|
||
<View className="navContent">
|
||
{/* logo */}
|
||
<Image
|
||
src={inputLeftIcon || img.ICON_LOGO}
|
||
className="logo"
|
||
mode="aspectFit"
|
||
onClick={handleInputLeftIconClick}
|
||
/>
|
||
{/* 搜索框 */}
|
||
<View className="searchContainer">
|
||
<Image
|
||
className="searchIcon icon16"
|
||
src={img.ICON_LIST_SEARCH_SEARCH}
|
||
/>
|
||
<Input
|
||
placeholder="搜索球局和场地"
|
||
className="navbarInput"
|
||
clearable={false}
|
||
disabled
|
||
value={searchValue}
|
||
onClick={handleInputClick}
|
||
/>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
{cityPopupVisible && !showTitle && (
|
||
<CityPicker
|
||
visible={cityPopupVisible}
|
||
setVisible={setCityPopupVisible}
|
||
cities={cities}
|
||
area={area}
|
||
setArea={updateArea}
|
||
onCityChange={handleCityChange}
|
||
/>
|
||
)}
|
||
{/* 定位确认弹窗 */}
|
||
{locationDialogData && (
|
||
<LocationConfirmDialog
|
||
visible={locationDialogVisible}
|
||
currentCity={locationDialogData.cachedCity[1]}
|
||
detectedCity={locationDialogData.detectedProvince}
|
||
onConfirm={handleLocationDialogConfirm}
|
||
onCancel={handleLocationDialogCancel}
|
||
/>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default HomeNavbar;
|