Files
mini-programs/src/components/HomeNavbar/index.tsx
张成 9f63a2369a 1
2025-12-13 00:20:26 +08:00

541 lines
19 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 { 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;