Compare commits
16 Commits
57d1b9446b
...
d90dcb053e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90dcb053e | ||
|
|
e7ee8bc1de | ||
|
|
9f63a2369a | ||
|
|
e560d06106 | ||
|
|
4c5262441c | ||
|
|
293b9e6eba | ||
| c42055d2c3 | |||
| b7efbce737 | |||
|
|
26ab56fd1e | ||
|
|
28201d79b9 | ||
| e1c4990ada | |||
| 46a59ba282 | |||
|
|
7b620210a2 | ||
|
|
e2a8ed4e32 | ||
|
|
85566b448e | ||
|
|
0774cf5ae6 |
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, Image } from "@tarojs/components";
|
||||
import img from "@/config/images";
|
||||
import { useGlobalState } from "@/store/global";
|
||||
@@ -13,6 +13,12 @@ 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?: {
|
||||
@@ -90,7 +96,6 @@ const HomeNavbar = (props: IProps) => {
|
||||
detectedProvince: string;
|
||||
cachedCity: [string, string];
|
||||
} | null>(null);
|
||||
const hasShownLocationDialog = useRef(false); // 防止重复弹窗
|
||||
|
||||
// 监听城市选择器状态变化,通知父组件
|
||||
useEffect(() => {
|
||||
@@ -104,41 +109,156 @@ const HomeNavbar = (props: IProps) => {
|
||||
// 只使用省份/直辖市,不使用城市(城市可能是区)
|
||||
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. 尝试从缓存中读取上次的定位信息
|
||||
// 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("使用缓存的定位城市:", cachedCityArray);
|
||||
console.log("[HomeNavbar] 使用缓存的定位城市:", cachedCityArray);
|
||||
updateArea(cachedCityArray);
|
||||
|
||||
// 如果用户详情中有位置信息且与缓存不同,弹窗询问是否切换
|
||||
if (detectedLocation && cachedCityArray[1] !== detectedLocation && !hasShownLocationDialog.current) {
|
||||
hasShownLocationDialog.current = true;
|
||||
showLocationConfirmDialog(detectedLocation, 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("没有缓存,使用用户详情中的位置信息:", detectedLocation);
|
||||
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
|
||||
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
|
||||
const newArea: [string, string] = ["中国", detectedLocation];
|
||||
updateArea(newArea);
|
||||
// 保存定位信息到缓存
|
||||
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
|
||||
}
|
||||
}, [detectedLocation]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // 空依赖数组,确保只在组件挂载时执行一次
|
||||
|
||||
// 显示定位确认弹窗
|
||||
const showLocationConfirmDialog = (detectedLocation: string, cachedCity: [string, string]) => {
|
||||
console.log('[LocationDialog] 准备显示定位确认弹窗,隐藏 GuideBar');
|
||||
setLocationDialogData({ detectedProvince: detectedLocation, cachedCity });
|
||||
setLocationDialogVisible(true);
|
||||
// 显示弹窗时隐藏 GuideBar
|
||||
setShowGuideBar(false);
|
||||
console.log('[LocationDialog] setShowGuideBar(false) 已调用');
|
||||
};
|
||||
// 检查是否在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 = () => {
|
||||
@@ -150,6 +270,14 @@ const HomeNavbar = (props: IProps) => {
|
||||
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);
|
||||
|
||||
// 关闭弹窗
|
||||
@@ -162,14 +290,23 @@ const HomeNavbar = (props: IProps) => {
|
||||
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);
|
||||
@@ -258,12 +395,23 @@ const HomeNavbar = (props: IProps) => {
|
||||
|
||||
// 处理城市切换(用户手动选择)
|
||||
const handleCityChange = async (_newArea: any) => {
|
||||
// 用户手动选择的城市不保存到缓存(临时切换)
|
||||
console.log("用户手动选择城市(不保存缓存):", _newArea);
|
||||
// 用户手动选择的城市保存到缓存
|
||||
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();
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
@@ -111,6 +115,8 @@
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
&.primary {
|
||||
color: #ffffff;
|
||||
|
||||
@@ -40,10 +40,22 @@ const LocationConfirmDialog: React.FC<LocationConfirmDialogProps> = ({
|
||||
<View className="locationDialogContent">
|
||||
<Text className="locationDialogTitle">定位显示您在{detectedCity}</Text>
|
||||
<View className="locationDialogButtons">
|
||||
<View className="locationDialogButton primary" onClick={onConfirm}>
|
||||
<View
|
||||
className="locationDialogButton primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
<Text className="locationDialogButtonText primary">切换到{detectedCity}</Text>
|
||||
</View>
|
||||
<View className="locationDialogButton secondary" onClick={onCancel}>
|
||||
<View
|
||||
className="locationDialogButton secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
<Text className="locationDialogButtonText secondary">继续浏览{currentCity}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Taro from "@tarojs/taro";
|
||||
import { View, Canvas } from "@tarojs/components";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png";
|
||||
import docCopySvg from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
import docCopyPng from "@/static/ntrp/ntrp_doc_copy.png";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
|
||||
interface RadarChartV2Props {
|
||||
@@ -315,7 +315,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度
|
||||
|
||||
try {
|
||||
const docCopyImg = await loadImage(canvas, docCopySvg);
|
||||
const docCopyImg = await loadImage(canvas, docCopyPng);
|
||||
ctx.save();
|
||||
|
||||
// 移动到旋转中心
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { View } from '@tarojs/components'
|
||||
import { TextArea } from '@nutui/nutui-react-taro';
|
||||
|
||||
@@ -22,27 +22,49 @@ const TitleTextarea: React.FC<TitleTextareaProps> = ({
|
||||
onBlur
|
||||
}) => {
|
||||
const isOverflow = value.length > maxLength
|
||||
// const [isFocused, setIsFocused] = useState(false)
|
||||
|
||||
// const showPlaceholder = !isFocused && !value
|
||||
|
||||
const handleChange = useCallback((values) => {
|
||||
// if (values.length > maxLength ) {
|
||||
// const newValues = values.slice(0, maxLength)
|
||||
// onChange(newValues)
|
||||
// return;
|
||||
// }
|
||||
onChange(values)
|
||||
}, [])
|
||||
onChange(values)
|
||||
}, [onChange])
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
// setIsFocused(true)
|
||||
onFocus?.()
|
||||
}, [onFocus])
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
// setIsFocused(false)
|
||||
onBlur?.()
|
||||
}, [onBlur])
|
||||
|
||||
return (
|
||||
<View className='title-input-wrapper'>
|
||||
<TextArea
|
||||
className='title-input'
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onInput={(e) => handleChange(e.detail.value)}
|
||||
// maxlength={maxLength}
|
||||
autoSize={true}
|
||||
placeholderClass='title-input-placeholder'
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<View className='title-input-box'>
|
||||
<TextArea
|
||||
className='title-input'
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onInput={(e) => handleChange(e.detail.value)}
|
||||
// maxlength={maxLength}
|
||||
placeholderClass='title-input-placeholder'
|
||||
autoSize={true}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{/* {showPlaceholder && (
|
||||
<View className='title-input-placeholder-custom'>
|
||||
{placeholder}
|
||||
</View>
|
||||
)} */}
|
||||
</View>
|
||||
<View className={`char-count${isOverflow ? ' char-count--error' : ''}`}>
|
||||
{value.length}/{maxLength}
|
||||
</View>
|
||||
|
||||
@@ -4,9 +4,22 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
min-height: 36px;
|
||||
padding: 10px 12px;
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
.title-input-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
.title-input-placeholder {
|
||||
color: rgba(60, 60, 67, 0.60) !important;
|
||||
font-weight: normal !important;
|
||||
position: relative;
|
||||
line-height: 22px !important;
|
||||
height: 22px;
|
||||
flex: 1;
|
||||
}
|
||||
.title-input {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
@@ -17,22 +30,23 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
resize: none;
|
||||
line-height: 26px;
|
||||
min-height: 26px;
|
||||
line-height: 22px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
// 使用 placeholderClass 来控制 placeholder 样式
|
||||
.title-input-placeholder {
|
||||
color: rgba(60, 60, 67, 0.60) !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: normal !important;
|
||||
line-height: 26px !important;
|
||||
height: 26px;
|
||||
flex: 1;
|
||||
.title-input-placeholder-custom {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 22px;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.char-count {
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -46,8 +46,7 @@ function genRefundNotice(refund_policy) {
|
||||
};
|
||||
}
|
||||
|
||||
function renderCancelContent(checkOrderInfo) {
|
||||
const { refund_policy = [] } = checkOrderInfo;
|
||||
function renderCancelContent(refund_policy = []) {
|
||||
const current = dayjs();
|
||||
const policyList = [
|
||||
{
|
||||
@@ -65,7 +64,6 @@ function renderCancelContent(checkOrderInfo) {
|
||||
};
|
||||
}),
|
||||
];
|
||||
console.log("policyList", policyList);
|
||||
const targetIndex = policyList.findIndex((item) => item.beforeCurrent);
|
||||
const { notice } = genRefundNotice(refund_policy);
|
||||
return (
|
||||
@@ -107,7 +105,7 @@ export type RefundRef = {
|
||||
|
||||
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checkOrderInfo, setCheckOrderInfo] = useState({});
|
||||
const [refundPolicy, setRefundPolicy] = useState([]);
|
||||
const [orderData, setOrderData] = useState({});
|
||||
const onDown = useRef<((result: boolean) => void) | null>(null);
|
||||
|
||||
@@ -116,11 +114,10 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
||||
}));
|
||||
|
||||
async function onShow(orderItem, onFinish: (result: boolean) => void) {
|
||||
const { game_info } = orderItem;
|
||||
const { refund_policy } = orderItem;
|
||||
onDown.current = onFinish;
|
||||
setOrderData(orderItem);
|
||||
const res = await orderService.getCheckOrderInfo(game_info.id);
|
||||
setCheckOrderInfo(res.data);
|
||||
setRefundPolicy(refund_policy);
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
@@ -172,7 +169,7 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
||||
onClick={onClose}
|
||||
/>
|
||||
</View>
|
||||
{renderCancelContent(checkOrderInfo)}
|
||||
{renderCancelContent(refundPolicy)}
|
||||
<Button className={styles.action} onClick={handleConfirmQuit}>
|
||||
确认并退出
|
||||
</Button>
|
||||
|
||||
@@ -73,6 +73,7 @@ export default function Participants(props) {
|
||||
}>({ show: () => {} });
|
||||
const userInfo = useUserInfo();
|
||||
const participants = detail.participants || [];
|
||||
const substitute_members = detail.substitute_members || [];
|
||||
// const participants = Array(10)
|
||||
// .fill(0)
|
||||
// .map((_, index) => ({
|
||||
@@ -92,6 +93,8 @@ export default function Participants(props) {
|
||||
const {
|
||||
participant_count,
|
||||
max_participants,
|
||||
substitute_count,
|
||||
max_substitute_players,
|
||||
user_action_status = {},
|
||||
start_time,
|
||||
price,
|
||||
@@ -293,6 +296,13 @@ export default function Participants(props) {
|
||||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||||
|
||||
const leftCount = max_participants - participant_count;
|
||||
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0);
|
||||
const showSubstituteApplicationEntry =
|
||||
[can_pay, can_join, is_substituting, waiting_start].every(
|
||||
(item) => !item
|
||||
) &&
|
||||
can_substitute &&
|
||||
dayjs(start_time).isAfter(dayjs());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -389,6 +399,98 @@ export default function Participants(props) {
|
||||
""
|
||||
)}
|
||||
</View>
|
||||
{/* 候补区域 */}
|
||||
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && (
|
||||
<View className={styles["detail-page-content-participants"]}>
|
||||
<View className={styles["participants-title"]}>
|
||||
<Text>候补</Text>
|
||||
<Text>·</Text>
|
||||
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text>
|
||||
</View>
|
||||
<View className={styles["participants-list"]}>
|
||||
{/* 候补申请入口 */}
|
||||
{showSubstituteApplicationEntry && (
|
||||
<View
|
||||
className={styles["participants-list-application"]}
|
||||
onClick={() => {
|
||||
action?.();
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className={styles["participants-list-application-icon"]}
|
||||
src={img.ICON_DETAIL_APPLICATION_ADD}
|
||||
/>
|
||||
<Text className={styles["participants-list-application-text"]}>
|
||||
申请候补
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{/* 候补成员列表 */}
|
||||
<ScrollView
|
||||
refresherBackground="#FAFAFA"
|
||||
className={classnames(
|
||||
styles["participants-list-scroll"],
|
||||
showSubstituteApplicationEntry ? styles.withApplication : ""
|
||||
)}
|
||||
scrollX
|
||||
>
|
||||
<View
|
||||
className={styles["participants-list-scroll-content"]}
|
||||
style={{
|
||||
width: `${
|
||||
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
{substitute_members.map((substitute) => {
|
||||
const {
|
||||
is_organizer,
|
||||
user: {
|
||||
avatar_url,
|
||||
nickname,
|
||||
level,
|
||||
ntrp_level,
|
||||
id: substitute_user_id,
|
||||
},
|
||||
} = substitute;
|
||||
const role = is_organizer ? "组织者" : "参与者";
|
||||
// 优先使用 ntrp_level,如果没有则使用 level
|
||||
const ntrpValue = ntrp_level || level;
|
||||
// 格式化显示 NTRP,如果没有值则显示"初学者"
|
||||
const displayNtrp = ntrpValue
|
||||
? formatNtrpDisplay(ntrpValue)
|
||||
: "初学者";
|
||||
return (
|
||||
<View
|
||||
key={substitute.id}
|
||||
className={styles["participants-list-item"]}
|
||||
>
|
||||
<Image
|
||||
className={styles["participants-list-item-avatar"]}
|
||||
mode="aspectFill"
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(
|
||||
null,
|
||||
substitute_user_id
|
||||
)}
|
||||
/>
|
||||
<Text className={styles["participants-list-item-name"]}>
|
||||
{nickname || "未知"}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-level"]}>
|
||||
{displayNtrp}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-role"]}>
|
||||
{role}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -79,7 +79,8 @@ function Index() {
|
||||
});
|
||||
|
||||
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
|
||||
await fetchDetail();
|
||||
// 注意:这里不调用 fetchDetail,避免与 useDidShow 中的调用重复
|
||||
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
|
||||
if (from === "publish") {
|
||||
handleShare(true);
|
||||
}
|
||||
|
||||
@@ -129,15 +129,37 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
// ScrollView 滚动处理
|
||||
const handleScrollViewScroll = useCallback(
|
||||
(e: any) => {
|
||||
|
||||
|
||||
const currentScrollTop = e?.detail?.scrollTop || 0;
|
||||
const scrollHeight = e?.detail?.scrollHeight || 0;
|
||||
const clientHeight = e?.detail?.clientHeight || 0;
|
||||
const lastScrollTop = lastScrollTopRef.current;
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - lastScrollTimeRef.current;
|
||||
|
||||
if (timeDiff < 100) return;
|
||||
|
||||
// 计算距离底部的距离,提前加载(距离底部600px时开始加载)
|
||||
// 注意:如果 scrollHeight 或 clientHeight 不可用,则使用 lowerThreshold 触发
|
||||
if (scrollHeight > 0 && clientHeight > 0) {
|
||||
const distanceToBottom = scrollHeight - currentScrollTop - clientHeight;
|
||||
const preloadThreshold = 600; // 提前加载阈值
|
||||
|
||||
// 如果距离底部小于阈值,且正在向下滚动,且有更多数据,则提前加载
|
||||
if (
|
||||
distanceToBottom < preloadThreshold &&
|
||||
distanceToBottom > 0 &&
|
||||
!loading &&
|
||||
!loadingMoreRef.current &&
|
||||
listPageState?.isHasMoreData &&
|
||||
currentScrollTop > lastScrollTop // 向下滚动
|
||||
) {
|
||||
loadingMoreRef.current = true;
|
||||
loadMoreMatches().finally(() => {
|
||||
loadingMoreRef.current = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const scrollDiff = currentScrollTop - lastScrollTop;
|
||||
let newDirection = scrollDirectionRef.current;
|
||||
if (Math.abs(scrollDiff) > 15) {
|
||||
@@ -195,7 +217,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
lastScrollTopRef.current = currentScrollTop;
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
},
|
||||
[updateListPageState, onNavStateChange]
|
||||
[updateListPageState, onNavStateChange, loading, loadMoreMatches, listPageState?.isHasMoreData]
|
||||
// 移除 showSearchBar 和 isShowInputCustomerNavBar 依赖,使用 ref 获取最新值
|
||||
);
|
||||
|
||||
@@ -214,6 +236,38 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
// 记录上一次的城市,用于检测城市变化
|
||||
const prevAreaRef = useRef<[string, string] | null>(null);
|
||||
|
||||
// 监听城市变化,重新获取行政区列表并清空已选择的行政区
|
||||
useEffect(() => {
|
||||
if (area && area.length >= 2) {
|
||||
const currentProvince = area[1];
|
||||
const prevProvince = prevAreaRef.current?.[1];
|
||||
|
||||
// 只有当城市真正改变时才执行(避免初始化时也触发)
|
||||
if (prevProvince && prevProvince !== currentProvince) {
|
||||
console.log("城市改变,重新获取行政区列表:", {
|
||||
prevProvince,
|
||||
currentProvince,
|
||||
});
|
||||
// 城市改变时,重新获取行政区列表
|
||||
getDistricts();
|
||||
// 清空已选择的行政区,避免显示错误的行政区
|
||||
const currentState = useListStore.getState();
|
||||
const currentPageState = currentState.isSearchResult
|
||||
? currentState.searchPageState
|
||||
: currentState.listPageState;
|
||||
if (currentPageState?.distanceQuickFilter?.district) {
|
||||
updateDistanceQuickFilter({ district: undefined });
|
||||
}
|
||||
}
|
||||
// 更新记录的城市
|
||||
prevAreaRef.current = area as [string, string];
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [area?.[1]]); // 只监听省份(area[1])的变化
|
||||
|
||||
// 当页面从非激活状态切换为激活状态时,检查城市是否变化,如果变化则重新加载数据
|
||||
useEffect(() => {
|
||||
// 如果从非激活状态变为激活状态(切回列表页)
|
||||
@@ -518,7 +572,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
refresherEnabled={true}
|
||||
refresherTriggered={refreshing}
|
||||
onRefresherRefresh={handleRefresh}
|
||||
lowerThreshold={100}
|
||||
lowerThreshold={600}
|
||||
onScrollToLower={async () => {
|
||||
if (
|
||||
!loading &&
|
||||
|
||||
@@ -380,8 +380,13 @@ function OrderMsg(props) {
|
||||
wechat_contact,
|
||||
price,
|
||||
} = detail;
|
||||
const { order_no } = orderDetail;
|
||||
const { order_info: { registrant_phone } = {} } = checkOrderInfo;
|
||||
const { order_no, registrant_phone: registrant_phone_from_order } =
|
||||
orderDetail;
|
||||
const {
|
||||
order_info: { registrant_phone: registrant_phone_from_check_order } = {},
|
||||
} = checkOrderInfo || {};
|
||||
const registrant_phone =
|
||||
registrant_phone_from_order || registrant_phone_from_check_order;
|
||||
const startTime = dayjs(start_time);
|
||||
const endTime = dayjs(end_time);
|
||||
const startDate = startTime.format("YYYY年M月D日");
|
||||
@@ -402,13 +407,11 @@ function OrderMsg(props) {
|
||||
},
|
||||
{
|
||||
title: "报名人电话",
|
||||
// content: registrant_phone,
|
||||
content: registrant_phone ? (
|
||||
<Text
|
||||
selectable={true} // 支持长按复制
|
||||
style={{
|
||||
color: "#007AFF",
|
||||
// textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -427,7 +430,6 @@ function OrderMsg(props) {
|
||||
},
|
||||
{
|
||||
title: "组织人电话",
|
||||
// content: wechat_contact,
|
||||
content:
|
||||
wechat_contact && isPhoneNumber(wechat_contact) ? (
|
||||
<Text
|
||||
@@ -489,8 +491,7 @@ function OrderMsg(props) {
|
||||
}
|
||||
|
||||
function RefundPolicy(props) {
|
||||
const { checkOrderInfo } = props;
|
||||
const { refund_policy = [] } = checkOrderInfo;
|
||||
const { refund_policy = [] } = props;
|
||||
const current = dayjs();
|
||||
const policyList = [
|
||||
{
|
||||
@@ -563,7 +564,7 @@ const OrderCheck = () => {
|
||||
const [id, gameId] = [Number(stringId), Number(stringGameId)];
|
||||
const [detail, setDetail] = useState<GameData | {}>({});
|
||||
const [location, setLocation] = useState<number[]>([0, 0]);
|
||||
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
|
||||
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes>();
|
||||
const [orderDetail, setOrderDetail] = useState({});
|
||||
const { paying, setPaying } = useOrder();
|
||||
|
||||
@@ -584,11 +585,11 @@ const OrderCheck = () => {
|
||||
if (res.code === 0) {
|
||||
gameDetail = res.data;
|
||||
}
|
||||
checkOrder(gameId);
|
||||
}
|
||||
if (gameDetail.id) {
|
||||
setDetail(gameDetail);
|
||||
onInit(gameDetail.id);
|
||||
}
|
||||
setDetail(gameDetail);
|
||||
const location = await getCurrentLocation();
|
||||
setLocation([location.latitude, location.longitude]);
|
||||
}
|
||||
|
||||
async function checkOrder(gid) {
|
||||
@@ -596,12 +597,6 @@ const OrderCheck = () => {
|
||||
setCheckOrderInfo(orderRes.data);
|
||||
}
|
||||
|
||||
async function onInit(gid) {
|
||||
checkOrder(gid);
|
||||
const location = await getCurrentLocation();
|
||||
setLocation([location.latitude, location.longitude]);
|
||||
}
|
||||
|
||||
async function getPaymentParams() {
|
||||
// 检查登录状态和手机号(创建订单前检查)
|
||||
if (!requireLoginWithPhone()) {
|
||||
@@ -626,10 +621,6 @@ const OrderCheck = () => {
|
||||
return; // 未登录或未绑定手机号,已跳转到登录页
|
||||
}
|
||||
setPaying(true);
|
||||
Taro.showLoading({
|
||||
title: "支付中...",
|
||||
mask: true,
|
||||
});
|
||||
|
||||
let payment_params = {};
|
||||
try {
|
||||
@@ -641,7 +632,6 @@ const OrderCheck = () => {
|
||||
});
|
||||
}
|
||||
await payOrder(payment_params);
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: "支付成功",
|
||||
icon: "success",
|
||||
@@ -655,7 +645,6 @@ const OrderCheck = () => {
|
||||
// delta: 1,
|
||||
// });
|
||||
} catch (error) {
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: error.message,
|
||||
icon: "none",
|
||||
@@ -712,22 +701,27 @@ const OrderCheck = () => {
|
||||
checkOrderInfo={checkOrderInfo}
|
||||
/>
|
||||
{/* Refund policy */}
|
||||
<RefundPolicy checkOrderInfo={checkOrderInfo} />
|
||||
<RefundPolicy
|
||||
refund_policy={
|
||||
checkOrderInfo?.refund_policy || orderDetail?.refund_policy || []
|
||||
}
|
||||
/>
|
||||
{/* Disclaimer */}
|
||||
<Disclaimer />
|
||||
{(!id ||
|
||||
(order_status === OrderStatus.PENDING &&
|
||||
cancel_type === CancelType.NONE)) &&
|
||||
!paying && (
|
||||
<Button
|
||||
className={styles.payButton}
|
||||
disabled={paying}
|
||||
onClick={handlePay}
|
||||
>
|
||||
{order_status === OrderStatus.PENDING ? "继续" : "确认"}
|
||||
支付
|
||||
</Button>
|
||||
)}
|
||||
cancel_type === CancelType.NONE)) && (
|
||||
<Button
|
||||
className={styles.payButton}
|
||||
disabled={paying}
|
||||
onClick={handlePay}
|
||||
loading={paying}
|
||||
>
|
||||
{paying
|
||||
? "支付中..."
|
||||
: `${order_status === OrderStatus.PENDING ? "继续" : "确认"}支付`}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: calc(100vh - 98px);
|
||||
height: calc(100vh);
|
||||
}
|
||||
|
||||
// 空状态图片
|
||||
@@ -56,13 +56,13 @@
|
||||
// 按钮区域
|
||||
&__buttons {
|
||||
position: absolute;
|
||||
bottom: 110px;
|
||||
bottom: 48px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
padding: 8px 20px;
|
||||
z-index: 1;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 251px;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1),
|
||||
@@ -87,13 +87,12 @@
|
||||
|
||||
// 按钮文字样式
|
||||
&_text {
|
||||
font-family: 'DingTalk JinBuTi';
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 1.11;
|
||||
letter-spacing: -0.05em;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
|
||||
font-family: 'PingFang SC';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
// 主要按钮(去看看其他球局)
|
||||
@@ -101,6 +100,8 @@
|
||||
background: #000000;
|
||||
border: 2px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
|
||||
|
||||
.empty_state_page__button_text {
|
||||
color: #ffffff; // 黑色背景下使用白色文字
|
||||
}
|
||||
|
||||
@@ -12,13 +12,12 @@ import './index.scss';
|
||||
|
||||
function Index() {
|
||||
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
|
||||
// 倒计时自动返回
|
||||
useEffect(() => {
|
||||
if (countdown <= 0) {
|
||||
handle_go_to_home();
|
||||
handle_go_to_home();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,8 @@ function Intro() {
|
||||
const { setCallback } = useEvaluate();
|
||||
|
||||
const { last_test_result = null } = ntrpData || {};
|
||||
const { ntrp_level, create_time, id } = last_test_result || {};
|
||||
const { ntrp_level, create_time, id, level_description } =
|
||||
last_test_result || {};
|
||||
const lastTestTime = create_time
|
||||
? dayjs(create_time).format("YYYY年M月D日")
|
||||
: "";
|
||||
@@ -266,7 +267,7 @@ function Intro() {
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles.slogan}>
|
||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
||||
<Text>{level_description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
@@ -667,7 +668,7 @@ function Result() {
|
||||
}
|
||||
|
||||
useShareAppMessage(async (res) => {
|
||||
console.log( "res",result);
|
||||
console.log("res", result);
|
||||
return {
|
||||
title: "来测一测你的NTRP等级吧",
|
||||
imageUrl: result?.level_img || undefined,
|
||||
|
||||
BIN
src/static/ntrp/ntrp_doc_copy.png
Normal file
BIN
src/static/ntrp/ntrp_doc_copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import Taro from "@tarojs/taro";
|
||||
import {
|
||||
fetchUserProfile,
|
||||
updateUserProfile,
|
||||
@@ -15,7 +16,7 @@ export interface UserState {
|
||||
fetchUserInfo: () => Promise<UserInfoType | undefined>;
|
||||
updateUserInfo: (userInfo: Partial<UserInfoType>) => void;
|
||||
nicknameChangeStatus: Partial<NicknameChangeStatus>;
|
||||
checkNicknameChangeStatus: () => void;
|
||||
checkNicknameChangeStatus: (force?: boolean) => void;
|
||||
updateNickname: (nickname: string) => void;
|
||||
// NTRP 测试结果缓存
|
||||
lastTestResult: LastTimeTestResult | null;
|
||||
@@ -32,6 +33,10 @@ const getTimeNextDate = (time: string) => {
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// 请求锁,避免重复调用
|
||||
let isFetchingLastTestResult = false;
|
||||
let isCheckingNicknameStatus = false;
|
||||
|
||||
export const useUser = create<UserState>()((set) => ({
|
||||
user: {},
|
||||
fetchUserInfo: async () => {
|
||||
@@ -40,14 +45,31 @@ export const useUser = create<UserState>()((set) => ({
|
||||
const userData = res.data;
|
||||
set({ user: userData });
|
||||
|
||||
// 当 userLastLocationProvince 更新时,同步更新 area
|
||||
// 优先使用缓存中的城市,不使用用户信息中的位置
|
||||
// 检查是否有缓存的城市
|
||||
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
|
||||
const cachedCity = (Taro as any).getStorageSync?.(CITY_CACHE_KEY);
|
||||
|
||||
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
|
||||
// 如果有缓存的城市,使用缓存,不更新 area
|
||||
console.log("[userStore] 检测到缓存的城市,使用缓存,不更新 area");
|
||||
return userData;
|
||||
}
|
||||
|
||||
// 只有当没有缓存时,才使用用户信息中的位置
|
||||
if (userData?.last_location_province) {
|
||||
const listStore = useListStore.getState();
|
||||
const currentArea = listStore.area;
|
||||
// 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新
|
||||
if (!currentArea || currentArea[1] !== userData.last_location_province) {
|
||||
// 只有当 area 不存在时才使用用户信息中的位置
|
||||
if (!currentArea) {
|
||||
const newArea: [string, string] = ["中国", userData.last_location_province];
|
||||
listStore.updateArea(newArea);
|
||||
// 保存到缓存
|
||||
try {
|
||||
(Taro as any).setStorageSync?.(CITY_CACHE_KEY, newArea);
|
||||
} catch (error) {
|
||||
console.error("保存城市缓存失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +109,20 @@ export const useUser = create<UserState>()((set) => ({
|
||||
}
|
||||
},
|
||||
nicknameChangeStatus: {},
|
||||
checkNicknameChangeStatus: async () => {
|
||||
checkNicknameChangeStatus: async (force = false) => {
|
||||
// 如果正在请求中,直接返回,避免重复调用
|
||||
if (isCheckingNicknameStatus) {
|
||||
return;
|
||||
}
|
||||
// 如果已经有状态数据且不是强制更新,跳过,避免重复调用
|
||||
if (!force) {
|
||||
const currentState = useUser.getState();
|
||||
if (currentState.nicknameChangeStatus && Object.keys(currentState.nicknameChangeStatus).length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
isCheckingNicknameStatus = true;
|
||||
const res = await checkNicknameChangeStatusApi();
|
||||
const { next_period_start_time } = res.data;
|
||||
set({
|
||||
@@ -99,12 +133,15 @@ export const useUser = create<UserState>()((set) => ({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("检查昵称变更状态失败:", error);
|
||||
} finally {
|
||||
isCheckingNicknameStatus = false;
|
||||
}
|
||||
},
|
||||
updateNickname: async (nickname) => {
|
||||
try {
|
||||
await updateNicknameApi(nickname);
|
||||
await useUser.getState().checkNicknameChangeStatus();
|
||||
// 更新昵称后强制更新状态
|
||||
await useUser.getState().checkNicknameChangeStatus(true);
|
||||
set((state) => ({
|
||||
user: { ...state.user, nickname },
|
||||
}));
|
||||
@@ -115,7 +152,27 @@ export const useUser = create<UserState>()((set) => ({
|
||||
// NTRP 测试结果缓存
|
||||
lastTestResult: null,
|
||||
fetchLastTestResult: async () => {
|
||||
// 如果已经有缓存数据,直接返回,避免重复调用
|
||||
const currentState = useUser.getState();
|
||||
if (currentState.lastTestResult) {
|
||||
return currentState.lastTestResult;
|
||||
}
|
||||
// 如果正在请求中,等待请求完成,避免重复调用
|
||||
if (isFetchingLastTestResult) {
|
||||
// 等待请求完成,最多等待 3 秒
|
||||
let waitCount = 0;
|
||||
while (isFetchingLastTestResult && waitCount < 30) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
waitCount++;
|
||||
const state = useUser.getState();
|
||||
if (state.lastTestResult) {
|
||||
return state.lastTestResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
isFetchingLastTestResult = true;
|
||||
const res = await evaluateService.getLastResult();
|
||||
if (res.code === 0) {
|
||||
set({ lastTestResult: res.data });
|
||||
@@ -125,6 +182,8 @@ export const useUser = create<UserState>()((set) => ({
|
||||
} catch (error) {
|
||||
console.error("获取NTRP测试结果失败:", error);
|
||||
return null;
|
||||
} finally {
|
||||
isFetchingLastTestResult = false;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -6,18 +6,28 @@ export function getOrderStatus(orderData) {
|
||||
if (!order_no) {
|
||||
return 'none'
|
||||
}
|
||||
const { start_time } = game_info
|
||||
const { start_time } = game_info || {}
|
||||
if (!start_time) { console.log('活动开始时间未找到, start_time: ', start_time); }
|
||||
const unPay = order_status === OrderStatus.PENDING && ([CancelType.NONE].includes(cancel_type));
|
||||
const refund = [RefundStatus.SUCCESS].includes(refund_status);
|
||||
const refunding = [RefundStatus.PENDING].includes(refund_status);
|
||||
const expired =
|
||||
order_status === OrderStatus.FINISHED;
|
||||
|
||||
const frozen = dayjs().add(2, 'h').isAfter(dayjs(start_time))
|
||||
const frozen = dayjs().isAfter(dayjs(start_time))
|
||||
|
||||
const canceled = [CancelType.TIMEOUT, CancelType.USER].includes(cancel_type);
|
||||
|
||||
return unPay ? 'unpay' : refund ? 'refund' : canceled ? 'canceled' : expired ? 'expired' : refunding ? 'refunding' : frozen ? 'start' : 'progress'
|
||||
// return unPay ? 'unpay' : refund ? 'refund' : canceled ? 'canceled' : expired ? 'expired' : refunding ? 'refunding' : is_substitute_order ? 'progress' : frozen ? 'start' : 'progress'
|
||||
if (unPay) return 'unpay';
|
||||
if (refund) return 'refund';
|
||||
if (canceled) return 'canceled';
|
||||
if (expired) return 'expired';
|
||||
if (refunding) return 'refunding';
|
||||
if (frozen) return 'start';
|
||||
// if (is_substitute_order) return 'progress';
|
||||
return 'progress';
|
||||
|
||||
}
|
||||
|
||||
// scene: list、detail
|
||||
|
||||
Reference in New Issue
Block a user