Compare commits
41 Commits
57d1b9446b
...
feature/ju
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49f53d60ed | ||
|
|
4c75368fe8 | ||
|
|
8abf6e6f2b | ||
|
|
0c83aab053 | ||
|
|
1cbec87f77 | ||
|
|
6d57654005 | ||
|
|
dade2e2491 | ||
|
|
4a6ac73ad7 | ||
|
|
de8677c64c | ||
|
|
3ab647f7c6 | ||
|
|
fa328f893d | ||
|
|
d7c24ca8b3 | ||
| 0a3cdbedd2 | |||
| af131f228a | |||
| b5f9d23615 | |||
| 76b105866c | |||
| 86581f3a11 | |||
| 29094c7e6a | |||
| 4578ca0cb1 | |||
| d5662e5810 | |||
| 9e53f7a9f5 | |||
| a0f7838895 | |||
| c9c19855c3 | |||
| 1b67693752 | |||
|
|
d90dcb053e | ||
|
|
e7ee8bc1de | ||
| 5aec62ae85 | |||
|
|
9f63a2369a | ||
|
|
e560d06106 | ||
|
|
4c5262441c | ||
|
|
293b9e6eba | ||
| c42055d2c3 | |||
| b7efbce737 | |||
|
|
26ab56fd1e | ||
|
|
28201d79b9 | ||
| e1c4990ada | |||
| 46a59ba282 | |||
|
|
7b620210a2 | ||
|
|
e2a8ed4e32 | ||
|
|
85566b448e | ||
|
|
0774cf5ae6 |
@@ -2,7 +2,7 @@
|
|||||||
"miniprogramRoot": "dist/",
|
"miniprogramRoot": "dist/",
|
||||||
"projectname": "playBallTogether",
|
"projectname": "playBallTogether",
|
||||||
"description": "playBallTogether",
|
"description": "playBallTogether",
|
||||||
"appid": "wx815b533167eb7b53",
|
"appid": "wx915ecf6c01bea4ec",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default defineAppConfig({
|
|||||||
"ntrp-evaluate/index", // NTRP评估页
|
"ntrp-evaluate/index", // NTRP评估页
|
||||||
"enable_notification/index", // 开启消息通知
|
"enable_notification/index", // 开启消息通知
|
||||||
"emptyState/index", // 空状态页面
|
"emptyState/index", // 空状态页面
|
||||||
|
"bannerDetail/index", // Banner 图片详情页
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ page {
|
|||||||
font-family: "Quicksand";
|
font-family: "Quicksand";
|
||||||
// 注意:此路径来自 @/config/api.ts 中的 OSS_BASE_URL 配置
|
// 注意:此路径来自 @/config/api.ts 中的 OSS_BASE_URL 配置
|
||||||
// 如需修改,请更新配置文件中的 OSS_BASE_URL
|
// 如需修改,请更新配置文件中的 OSS_BASE_URL
|
||||||
src: url("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
|
src: url("https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
border-radius: 20px 20px 0 0 !important;
|
border-radius: 20px 20px 0 0 !important;
|
||||||
}
|
}
|
||||||
.common-popup__drag-handle-container {
|
.common-popup__drag-handle-container {
|
||||||
position: position;
|
position: relative;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.common-popup__drag-handle {
|
.common-popup__drag-handle {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { View, Text, Image } from "@tarojs/components";
|
import { View, Text, Image } from "@tarojs/components";
|
||||||
import img from "@/config/images";
|
import img from "@/config/images";
|
||||||
import { useGlobalState } from "@/store/global";
|
import { useGlobalState } from "@/store/global";
|
||||||
@@ -13,6 +13,12 @@ import LocationConfirmDialog from "@/components/LocationConfirmDialog";
|
|||||||
|
|
||||||
// 城市缓存 key
|
// 城市缓存 key
|
||||||
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
|
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 {
|
interface IProps {
|
||||||
config?: {
|
config?: {
|
||||||
@@ -90,7 +96,6 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
detectedProvince: string;
|
detectedProvince: string;
|
||||||
cachedCity: [string, string];
|
cachedCity: [string, string];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const hasShownLocationDialog = useRef(false); // 防止重复弹窗
|
|
||||||
|
|
||||||
// 监听城市选择器状态变化,通知父组件
|
// 监听城市选择器状态变化,通知父组件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -104,41 +109,156 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
// 只使用省份/直辖市,不使用城市(城市可能是区)
|
// 只使用省份/直辖市,不使用城市(城市可能是区)
|
||||||
const detectedLocation = lastLocationProvince;
|
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(() => {
|
useEffect(() => {
|
||||||
// 1. 尝试从缓存中读取上次的定位信息
|
// 1. 优先尝试从缓存中读取上次的定位信息
|
||||||
const cachedCity = (Taro as any).getStorageSync(CITY_CACHE_KEY);
|
const cachedCity = (Taro as any).getStorageSync(CITY_CACHE_KEY);
|
||||||
|
|
||||||
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
|
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
|
||||||
// 如果有缓存的定位信息,使用缓存
|
// 如果有缓存的定位信息,使用缓存
|
||||||
const cachedCityArray = cachedCity as [string, string];
|
const cachedCityArray = cachedCity as [string, string];
|
||||||
console.log("使用缓存的定位城市:", cachedCityArray);
|
console.log("[HomeNavbar] 使用缓存的定位城市:", cachedCityArray);
|
||||||
updateArea(cachedCityArray);
|
updateArea(cachedCityArray);
|
||||||
|
|
||||||
// 如果用户详情中有位置信息且与缓存不同,弹窗询问是否切换
|
// 如果用户详情中有位置信息,且与缓存不一致,检查是否需要弹窗
|
||||||
if (detectedLocation && cachedCityArray[1] !== detectedLocation && !hasShownLocationDialog.current) {
|
if (detectedLocation && cachedCityArray[1] !== detectedLocation) {
|
||||||
hasShownLocationDialog.current = true;
|
// 检查时间缓存,如果没有或过期,则弹出选择框
|
||||||
showLocationConfirmDialog(detectedLocation, cachedCityArray);
|
if (should_show_location_dialog()) {
|
||||||
|
console.log("[HomeNavbar] 缓存城市与用户详情位置不一致,且时间过期,弹出选择框");
|
||||||
|
showLocationConfirmDialog(detectedLocation, cachedCityArray);
|
||||||
|
} else {
|
||||||
|
console.log("[HomeNavbar] 缓存城市与用户详情位置不一致,但时间未过期,不弹出选择框");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (detectedLocation) {
|
} else if (detectedLocation) {
|
||||||
// 如果没有缓存但有用户详情中的位置信息,直接使用并保存到缓存
|
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
|
||||||
console.log("没有缓存,使用用户详情中的位置信息:", detectedLocation);
|
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
|
||||||
const newArea: [string, string] = ["中国", detectedLocation];
|
const newArea: [string, string] = ["中国", detectedLocation];
|
||||||
updateArea(newArea);
|
updateArea(newArea);
|
||||||
// 保存定位信息到缓存
|
// 保存定位信息到缓存
|
||||||
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
|
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
|
||||||
}
|
}
|
||||||
}, [detectedLocation]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []); // 空依赖数组,确保只在组件挂载时执行一次
|
||||||
|
|
||||||
// 显示定位确认弹窗
|
// 检查是否在2小时内已选择"继续浏览"或切换过城市(当前不使用,首页重新进入时直接使用缓存中的位置)
|
||||||
const showLocationConfirmDialog = (detectedLocation: string, cachedCity: [string, string]) => {
|
// const should_show_location_dialog = (): boolean => {
|
||||||
console.log('[LocationDialog] 准备显示定位确认弹窗,隐藏 GuideBar');
|
// try {
|
||||||
setLocationDialogData({ detectedProvince: detectedLocation, cachedCity });
|
// // 检查是否在2小时内切换过城市
|
||||||
setLocationDialogVisible(true);
|
// const city_change_time = (Taro as any).getStorageSync(CITY_CHANGE_TIME_KEY);
|
||||||
// 显示弹窗时隐藏 GuideBar
|
// if (city_change_time) {
|
||||||
setShowGuideBar(false);
|
// const current_time = Date.now();
|
||||||
console.log('[LocationDialog] setShowGuideBar(false) 已调用');
|
// 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 = () => {
|
const handleLocationDialogConfirm = () => {
|
||||||
@@ -150,6 +270,14 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
updateArea(newArea);
|
updateArea(newArea);
|
||||||
// 更新缓存为新的定位信息
|
// 更新缓存为新的定位信息
|
||||||
(Taro as any).setStorageSync(CITY_CACHE_KEY, 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);
|
console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince);
|
||||||
|
|
||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
@@ -162,14 +290,23 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
handleCityChangeWithoutCache();
|
handleCityChangeWithoutCache();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理定位弹窗取消
|
// 处理定位弹窗取消(用户选择"继续浏览")
|
||||||
const handleLocationDialogCancel = () => {
|
const handleLocationDialogCancel = () => {
|
||||||
if (!locationDialogData) return;
|
if (!locationDialogData) return;
|
||||||
|
|
||||||
const { cachedCity } = locationDialogData;
|
const { cachedCity } = locationDialogData;
|
||||||
// 用户选择"否",保持缓存的定位城市
|
// 用户选择"继续浏览",保持缓存的定位城市
|
||||||
console.log("保持缓存的定位城市:", cachedCity[1]);
|
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);
|
setLocationDialogVisible(false);
|
||||||
setLocationDialogData(null);
|
setLocationDialogData(null);
|
||||||
@@ -258,12 +395,23 @@ const HomeNavbar = (props: IProps) => {
|
|||||||
|
|
||||||
// 处理城市切换(用户手动选择)
|
// 处理城市切换(用户手动选择)
|
||||||
const handleCityChange = async (_newArea: any) => {
|
const handleCityChange = async (_newArea: any) => {
|
||||||
// 用户手动选择的城市不保存到缓存(临时切换)
|
// 用户手动选择的城市保存到缓存
|
||||||
console.log("用户手动选择城市(不保存缓存):", _newArea);
|
console.log("用户手动选择城市,更新缓存:", _newArea);
|
||||||
|
|
||||||
// 先更新 area 状态(用于界面显示和接口参数)
|
// 先更新 area 状态(用于界面显示和接口参数)
|
||||||
updateArea(_newArea);
|
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)
|
// 先调用列表接口(会使用更新后的 state.area)
|
||||||
if (refreshBothLists) {
|
if (refreshBothLists) {
|
||||||
await refreshBothLists();
|
await refreshBothLists();
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -111,6 +115,8 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -40,10 +40,22 @@ const LocationConfirmDialog: React.FC<LocationConfirmDialogProps> = ({
|
|||||||
<View className="locationDialogContent">
|
<View className="locationDialogContent">
|
||||||
<Text className="locationDialogTitle">定位显示您在{detectedCity}</Text>
|
<Text className="locationDialogTitle">定位显示您在{detectedCity}</Text>
|
||||||
<View className="locationDialogButtons">
|
<View className="locationDialogButtons">
|
||||||
<View className="locationDialogButton primary" onClick={onConfirm}>
|
<View
|
||||||
|
className="locationDialogButton primary"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text className="locationDialogButtonText primary">切换到{detectedCity}</Text>
|
<Text className="locationDialogButtonText primary">切换到{detectedCity}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="locationDialogButton secondary" onClick={onCancel}>
|
<View
|
||||||
|
className="locationDialogButton secondary"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text className="locationDialogButtonText secondary">继续浏览{currentCity}</Text>
|
<Text className="locationDialogButtonText secondary">继续浏览{currentCity}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
EvaluateCallback,
|
EvaluateCallback,
|
||||||
EvaluateScene,
|
EvaluateScene,
|
||||||
} from "@/store/evaluateStore";
|
} from "@/store/evaluateStore";
|
||||||
|
import { useListState } from "@/store/listStore";
|
||||||
|
|
||||||
import { navigateTo, redirectTo, navigateBack } from "@/utils/navigation";
|
import { navigateTo, redirectTo, navigateBack } from "@/utils/navigation";
|
||||||
import { requireLoginWithPhone } from "@/utils/helper";
|
import { requireLoginWithPhone } from "@/utils/helper";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
@@ -24,6 +26,11 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
|
|||||||
const { onVisibleChange } = props;
|
const { onVisibleChange } = props;
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
area
|
||||||
|
} = useListState();
|
||||||
|
|
||||||
|
|
||||||
// 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调
|
// 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onVisibleChange?.(isVisible);
|
onVisibleChange?.(isVisible);
|
||||||
@@ -59,6 +66,16 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
|
const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
|
||||||
|
const [_, address] = area;
|
||||||
|
if (address !== '上海') {
|
||||||
|
(Taro as any).showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '仅上海地区开放,您可加入社群或切换城市',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了'
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!userInfo.ntrp_level) {
|
if (!userInfo.ntrp_level) {
|
||||||
ntrpRef.current.show({
|
ntrpRef.current.show({
|
||||||
type: EvaluateScene.publish,
|
type: EvaluateScene.publish,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Taro from "@tarojs/taro";
|
|||||||
import { View, Canvas } from "@tarojs/components";
|
import { View, Canvas } from "@tarojs/components";
|
||||||
import { forwardRef, useImperativeHandle } from "react";
|
import { forwardRef, useImperativeHandle } from "react";
|
||||||
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png";
|
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";
|
import { OSS_BASE_URL } from "@/config/api";
|
||||||
|
|
||||||
interface RadarChartV2Props {
|
interface RadarChartV2Props {
|
||||||
@@ -315,7 +315,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
|||||||
const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度
|
const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const docCopyImg = await loadImage(canvas, docCopySvg);
|
const docCopyImg = await loadImage(canvas, docCopyPng);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// 移动到旋转中心
|
// 移动到旋转中心
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
|
|||||||
// 处理文本输入变化
|
// 处理文本输入变化
|
||||||
const handleTextChange = useCallback((val: string) => {
|
const handleTextChange = useCallback((val: string) => {
|
||||||
console.log(val,'e.detail.value')
|
console.log(val,'e.detail.value')
|
||||||
onChange({...value, description: val})
|
const maxAllowedLength = Math.floor(maxLength * 1.2)
|
||||||
}, [onChange])
|
const truncatedVal = val.length > maxAllowedLength ? val.slice(0, maxAllowedLength) : val
|
||||||
|
onChange({...value, description: truncatedVal})
|
||||||
|
}, [onChange, maxLength, value])
|
||||||
|
|
||||||
// 处理标签选择变化
|
// 处理标签选择变化
|
||||||
const handleTagChange = useCallback((selectedTags: string[]) => {
|
const handleTagChange = useCallback((selectedTags: string[]) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { View } from '@tarojs/components'
|
import { View } from '@tarojs/components'
|
||||||
import { TextArea } from '@nutui/nutui-react-taro';
|
import { TextArea } from '@nutui/nutui-react-taro';
|
||||||
|
|
||||||
@@ -22,27 +22,49 @@ const TitleTextarea: React.FC<TitleTextareaProps> = ({
|
|||||||
onBlur
|
onBlur
|
||||||
}) => {
|
}) => {
|
||||||
const isOverflow = value.length > maxLength
|
const isOverflow = value.length > maxLength
|
||||||
|
// const [isFocused, setIsFocused] = useState(false)
|
||||||
|
|
||||||
|
// const showPlaceholder = !isFocused && !value
|
||||||
|
|
||||||
const handleChange = useCallback((values) => {
|
const handleChange = useCallback((values) => {
|
||||||
// if (values.length > maxLength ) {
|
// if (values.length > maxLength ) {
|
||||||
// const newValues = values.slice(0, maxLength)
|
// const newValues = values.slice(0, maxLength)
|
||||||
// onChange(newValues)
|
// onChange(newValues)
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
onChange(values)
|
onChange(values)
|
||||||
}, [])
|
}, [onChange])
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
// setIsFocused(true)
|
||||||
|
onFocus?.()
|
||||||
|
}, [onFocus])
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
// setIsFocused(false)
|
||||||
|
onBlur?.()
|
||||||
|
}, [onBlur])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='title-input-wrapper'>
|
<View className='title-input-wrapper'>
|
||||||
<TextArea
|
<View className='title-input-box'>
|
||||||
className='title-input'
|
<TextArea
|
||||||
placeholder={placeholder}
|
className='title-input'
|
||||||
value={value}
|
placeholder={placeholder}
|
||||||
onInput={(e) => handleChange(e.detail.value)}
|
value={value}
|
||||||
// maxlength={maxLength}
|
onInput={(e) => handleChange(e.detail.value)}
|
||||||
autoSize={true}
|
// maxlength={maxLength}
|
||||||
placeholderClass='title-input-placeholder'
|
placeholderClass='title-input-placeholder'
|
||||||
onFocus={onFocus}
|
autoSize={true}
|
||||||
onBlur={onBlur}
|
onFocus={handleFocus}
|
||||||
/>
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
{/* {showPlaceholder && (
|
||||||
|
<View className='title-input-placeholder-custom'>
|
||||||
|
{placeholder}
|
||||||
|
</View>
|
||||||
|
)} */}
|
||||||
|
</View>
|
||||||
<View className={`char-count${isOverflow ? ' char-count--error' : ''}`}>
|
<View className={`char-count${isOverflow ? ' char-count--error' : ''}`}>
|
||||||
{value.length}/{maxLength}
|
{value.length}/{maxLength}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -4,9 +4,22 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px;
|
padding: 10px 12px;
|
||||||
min-height: 36px;
|
min-height: 32px;
|
||||||
box-sizing: border-box;
|
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 {
|
.title-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -17,22 +30,23 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
resize: none;
|
resize: none;
|
||||||
line-height: 26px;
|
line-height: 22px;
|
||||||
min-height: 26px;
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
|
.title-input-placeholder-custom {
|
||||||
// 使用 placeholderClass 来控制 placeholder 样式
|
position: absolute;
|
||||||
.title-input-placeholder {
|
left: 7px;
|
||||||
color: rgba(60, 60, 67, 0.60) !important;
|
top: 50%;
|
||||||
font-size: 16px !important;
|
width: 100%;
|
||||||
font-weight: normal !important;
|
padding: 0;
|
||||||
line-height: 26px !important;
|
color: rgba(60, 60, 67, 0.60);
|
||||||
height: 26px;
|
font-size: 16px;
|
||||||
flex: 1;
|
font-weight: normal;
|
||||||
|
line-height: 22px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.char-count {
|
.char-count {
|
||||||
color: #999;
|
color: #999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async function convert_to_jpg_and_compress(
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.canvasToTempFilePath({
|
Taro.canvasToTempFilePath({
|
||||||
canvas: canvas as unknown as Taro.Canvas,
|
canvas: canvas as unknown as Taro.Canvas,
|
||||||
fileType: "jpg",
|
fileType: "png",
|
||||||
quality: 0.7,
|
quality: 0.7,
|
||||||
success: (res) => resolve(res.tempFilePath),
|
success: (res) => resolve(res.tempFilePath),
|
||||||
fail: reject,
|
fail: reject,
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ function genRefundNotice(refund_policy) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCancelContent(checkOrderInfo) {
|
function renderCancelContent(refund_policy = []) {
|
||||||
const { refund_policy = [] } = checkOrderInfo;
|
|
||||||
const current = dayjs();
|
const current = dayjs();
|
||||||
const policyList = [
|
const policyList = [
|
||||||
{
|
{
|
||||||
@@ -65,7 +64,6 @@ function renderCancelContent(checkOrderInfo) {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
console.log("policyList", policyList);
|
|
||||||
const targetIndex = policyList.findIndex((item) => item.beforeCurrent);
|
const targetIndex = policyList.findIndex((item) => item.beforeCurrent);
|
||||||
const { notice } = genRefundNotice(refund_policy);
|
const { notice } = genRefundNotice(refund_policy);
|
||||||
return (
|
return (
|
||||||
@@ -107,7 +105,7 @@ export type RefundRef = {
|
|||||||
|
|
||||||
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [checkOrderInfo, setCheckOrderInfo] = useState({});
|
const [refundPolicy, setRefundPolicy] = useState([]);
|
||||||
const [orderData, setOrderData] = useState({});
|
const [orderData, setOrderData] = useState({});
|
||||||
const onDown = useRef<((result: boolean) => void) | null>(null);
|
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) {
|
async function onShow(orderItem, onFinish: (result: boolean) => void) {
|
||||||
const { game_info } = orderItem;
|
const { refund_policy } = orderItem;
|
||||||
onDown.current = onFinish;
|
onDown.current = onFinish;
|
||||||
setOrderData(orderItem);
|
setOrderData(orderItem);
|
||||||
const res = await orderService.getCheckOrderInfo(game_info.id);
|
setRefundPolicy(refund_policy);
|
||||||
setCheckOrderInfo(res.data);
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +169,7 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{renderCancelContent(checkOrderInfo)}
|
{renderCancelContent(refundPolicy)}
|
||||||
<Button className={styles.action} onClick={handleConfirmQuit}>
|
<Button className={styles.action} onClick={handleConfirmQuit}>
|
||||||
确认并退出
|
确认并退出
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import envConfig from './env'// API配置
|
import envConfig from './env'// API配置
|
||||||
|
|
||||||
// OSS 基础路径配置
|
// OSS 基础路径配置
|
||||||
export const OSS_BASE_URL = 'https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball'
|
export const OSS_BASE_URL = 'https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball'
|
||||||
|
|
||||||
export const API_CONFIG = {
|
export const API_CONFIG = {
|
||||||
// 基础URL
|
// 基础URL
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ListCard from "@/components/ListCard";
|
|||||||
import ListLoadError from "@/components/ListLoadError";
|
import ListLoadError from "@/components/ListLoadError";
|
||||||
import ListCardSkeleton from "@/components/ListCardSkeleton";
|
import ListCardSkeleton from "@/components/ListCardSkeleton";
|
||||||
import { useReachBottom } from "@tarojs/taro";
|
import { useReachBottom } from "@tarojs/taro";
|
||||||
|
import Taro from "@tarojs/taro";
|
||||||
import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
|
import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
|
||||||
import { NTRPTestEntryCard } from "@/components";
|
import { NTRPTestEntryCard } from "@/components";
|
||||||
import { EvaluateScene } from "@/store/evaluateStore";
|
import { EvaluateScene } from "@/store/evaluateStore";
|
||||||
@@ -158,6 +159,35 @@ const ListContainer = (props) => {
|
|||||||
[evaluateFlag, data, hasTestInLastMonth, showNumber]
|
[evaluateFlag, data, hasTestInLastMonth, showNumber]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 渲染 banner 卡片
|
||||||
|
const renderBanner = (item, index) => {
|
||||||
|
if (!item?.banner_image_url) return null;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={item.id || `banner-${index}`}
|
||||||
|
style={{
|
||||||
|
maxHeight: "122px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={item.banner_image_url}
|
||||||
|
mode="widthFix"
|
||||||
|
style={{ width: "100%", display: "block", maxHeight: "122px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const target = item.banner_detail_url;
|
||||||
|
if (target) {
|
||||||
|
(Taro as any).navigateTo({
|
||||||
|
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染列表
|
// 渲染列表
|
||||||
const renderList = () => {
|
const renderList = () => {
|
||||||
// 请求数据为空
|
// 请求数据为空
|
||||||
@@ -181,6 +211,9 @@ const ListContainer = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{memoizedList.map((match, index) => {
|
{memoizedList.map((match, index) => {
|
||||||
|
if (match.type === "banner") {
|
||||||
|
return renderBanner(match, index);
|
||||||
|
}
|
||||||
if (match.type === "evaluateCard") {
|
if (match.type === "evaluateCard") {
|
||||||
return (
|
return (
|
||||||
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
|
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export default function Participants(props) {
|
|||||||
}>({ show: () => {} });
|
}>({ show: () => {} });
|
||||||
const userInfo = useUserInfo();
|
const userInfo = useUserInfo();
|
||||||
const participants = detail.participants || [];
|
const participants = detail.participants || [];
|
||||||
|
const substitute_members = detail.substitute_members || [];
|
||||||
// const participants = Array(10)
|
// const participants = Array(10)
|
||||||
// .fill(0)
|
// .fill(0)
|
||||||
// .map((_, index) => ({
|
// .map((_, index) => ({
|
||||||
@@ -84,7 +85,7 @@ export default function Participants(props) {
|
|||||||
// id: 18,
|
// id: 18,
|
||||||
// nickname: "小猫开刀削面店往猫毛里面下面条",
|
// nickname: "小猫开刀削面店往猫毛里面下面条",
|
||||||
// avatar_url:
|
// avatar_url:
|
||||||
// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/d284060f-248b-4d58-a153-4d37c0ca77c8.jpg",
|
// "https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball/images/d284060f-248b-4d58-a153-4d37c0ca77c8.jpg",
|
||||||
// phone: "18513125687",
|
// phone: "18513125687",
|
||||||
// ntrp_level: "1.5",
|
// ntrp_level: "1.5",
|
||||||
// },
|
// },
|
||||||
@@ -92,6 +93,8 @@ export default function Participants(props) {
|
|||||||
const {
|
const {
|
||||||
participant_count,
|
participant_count,
|
||||||
max_participants,
|
max_participants,
|
||||||
|
substitute_count,
|
||||||
|
max_substitute_players,
|
||||||
user_action_status = {},
|
user_action_status = {},
|
||||||
start_time,
|
start_time,
|
||||||
price,
|
price,
|
||||||
@@ -293,6 +296,13 @@ export default function Participants(props) {
|
|||||||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||||||
|
|
||||||
const leftCount = max_participants - participant_count;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -389,6 +399,98 @@ export default function Participants(props) {
|
|||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
</View>
|
</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 />
|
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,32 @@
|
|||||||
.shareContainer {
|
.shareContainer {
|
||||||
|
.opacityContainer {
|
||||||
|
height: 140px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.shareImageContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
width: 220px;
|
||||||
|
height: 180px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 16px;
|
||||||
|
transform: rotateZ(-5deg) translateX(-50%);
|
||||||
|
box-shadow: 0 3px 32px 0 rgba(0, 0, 0, 0.16);
|
||||||
|
|
||||||
|
.shareImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentContainer {
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-top-left-radius: 16px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
padding: 20px 20px 16px;
|
padding: 20px 20px 16px;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -12,6 +40,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.publishTitle {
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
.publishText {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeIconWrap {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.publishText {
|
.publishText {
|
||||||
color: #2a4d44;
|
color: #2a4d44;
|
||||||
font-family: "Noto Sans SC";
|
font-family: "Noto Sans SC";
|
||||||
@@ -62,6 +102,21 @@
|
|||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
|
|
||||||
|
.customBtnWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customButton,
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -76,7 +131,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
|
|
||||||
background-color: #fff;
|
// background-color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ dayjs.locale("zh-cn");
|
|||||||
export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [publishFlag, setPublishFlag] = useState(false);
|
const [publishFlag, setPublishFlag] = useState(false);
|
||||||
|
const [shareImageUrl, setShareImageUrl] = useState("");
|
||||||
const { fetchUserInfo } = useUserActions();
|
const { fetchUserInfo } = useUserActions();
|
||||||
// const posterRef = useRef();
|
// const posterRef = useRef();
|
||||||
const { max_participants, participant_count } = detail || {};
|
const { max_participants, participant_count } = detail || {};
|
||||||
@@ -56,13 +57,17 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
show: (publish_flag = false) => {
|
show: async (publish_flag = false) => {
|
||||||
setPublishFlag(publish_flag);
|
setPublishFlag(publish_flag);
|
||||||
|
if (publish_flag) {
|
||||||
|
const url = await generateShareImageUrl();
|
||||||
|
setShareImageUrl(url);
|
||||||
|
}
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useShareAppMessage(async (res) => {
|
async function generateShareImageUrl() {
|
||||||
const {
|
const {
|
||||||
play_type,
|
play_type,
|
||||||
skill_level_max,
|
skill_level_max,
|
||||||
@@ -76,7 +81,6 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
|||||||
const endTime = dayjs(end_time);
|
const endTime = dayjs(end_time);
|
||||||
const dayofWeek = DayOfWeekMap.get(startTime.day());
|
const dayofWeek = DayOfWeekMap.get(startTime.day());
|
||||||
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
|
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
|
||||||
await changeMessageType();
|
|
||||||
const url = await generateShareImage({
|
const url = await generateShareImage({
|
||||||
userAvatar: userInfo.avatar_url,
|
userAvatar: userInfo.avatar_url,
|
||||||
userNickname: userInfo.nickname,
|
userNickname: userInfo.nickname,
|
||||||
@@ -90,6 +94,12 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
|||||||
venueName: location_name,
|
venueName: location_name,
|
||||||
venueImages: image_list ? image_list : [],
|
venueImages: image_list ? image_list : [],
|
||||||
});
|
});
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
useShareAppMessage(async (res) => {
|
||||||
|
await changeMessageType();
|
||||||
|
const url = await generateShareImageUrl();
|
||||||
// console.log(res, "res");
|
// console.log(res, "res");
|
||||||
return {
|
return {
|
||||||
title: detail.title,
|
title: detail.title,
|
||||||
@@ -168,51 +178,96 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
|||||||
showHeader={false}
|
showHeader={false}
|
||||||
hideFooter
|
hideFooter
|
||||||
enableDragToClose={false}
|
enableDragToClose={false}
|
||||||
style={{ minHeight: "100px" }}
|
style={{ minHeight: "100px", background: "unset" }}
|
||||||
zIndex={1000}
|
zIndex={1000}
|
||||||
>
|
>
|
||||||
<View className={styles.shareContainer}>
|
<View className={styles.shareContainer}>
|
||||||
<View catchMove className={styles.title}>
|
|
||||||
{publishFlag ? (
|
|
||||||
<Text className={styles.publishText}>球局发布成功 🎉</Text>
|
|
||||||
) : (
|
|
||||||
<Text>分享至</Text>
|
|
||||||
)}
|
|
||||||
<View className={styles.closeIconWrap} onClick={onClose}>
|
|
||||||
<Image className={styles.closeIcon} src={CrossIcon} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{publishFlag && (
|
{publishFlag && (
|
||||||
<View className={styles.shareTip}>
|
<>
|
||||||
<Text>
|
<View className={styles.opacityContainer} />
|
||||||
还剩
|
<View className={styles.shareImageContainer}>
|
||||||
<Text className={styles.specialCount}>
|
<Image className={styles.shareImage} src={shareImageUrl} />
|
||||||
{" "}
|
</View>
|
||||||
{max_participants - participant_count}{" "}
|
</>
|
||||||
</Text>
|
|
||||||
人加入完成组局, 去邀请好友加入吧~
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
<View className={styles.shareItems}>
|
<View
|
||||||
<Button className={styles.button} openType="share">
|
className={styles.contentContainer}
|
||||||
<View className={classnames(styles.icon, styles.wechatIcon)}>
|
style={{
|
||||||
<Image className={styles.wechat} src={WechatLogo} />
|
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
catchMove
|
||||||
|
className={classnames(
|
||||||
|
styles.title,
|
||||||
|
publishFlag ? styles.publishTitle : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{publishFlag ? (
|
||||||
|
<Text className={styles.publishText}>球局发布成功 🎉</Text>
|
||||||
|
) : (
|
||||||
|
<Text>分享至</Text>
|
||||||
|
)}
|
||||||
|
<View className={styles.closeIconWrap} onClick={onClose}>
|
||||||
|
<Image className={styles.closeIcon} src={CrossIcon} />
|
||||||
</View>
|
</View>
|
||||||
<Text>微信好友</Text>
|
</View>
|
||||||
</Button>
|
{publishFlag && (
|
||||||
<Button className={styles.button} onClick={handlePost}>
|
<View className={styles.shareTip}>
|
||||||
<View className={styles.icon}>
|
<Text>
|
||||||
<Image className={styles.download} src={DownloadIcon} />
|
还剩
|
||||||
|
<Text className={styles.specialCount}>
|
||||||
|
{" "}
|
||||||
|
{max_participants - participant_count}{" "}
|
||||||
|
</Text>
|
||||||
|
人加入完成组局, 去邀请好友加入吧~
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text>生成分享图</Text>
|
)}
|
||||||
</Button>
|
<View className={styles.shareItems}>
|
||||||
<Button className={styles.button}>
|
<View className={styles.customBtnWrapper}>
|
||||||
<View className={styles.icon}>
|
<Button className={styles.button} openType="share">
|
||||||
<Image className={styles.linkIcon} src={LinkIcon} />
|
<View className={classnames(styles.icon, styles.wechatIcon)}>
|
||||||
|
<Image className={styles.wechat} src={WechatLogo} />
|
||||||
|
</View>
|
||||||
|
<Text>微信好友</Text>
|
||||||
|
</Button>
|
||||||
|
<View className={styles.customButton}>
|
||||||
|
<View className={classnames(styles.icon, styles.wechatIcon)}>
|
||||||
|
<Image className={styles.wechat} src={WechatLogo} />
|
||||||
|
</View>
|
||||||
|
<Text>微信好友</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text>复制链接</Text>
|
<View className={styles.customBtnWrapper}>
|
||||||
</Button>
|
<Button className={styles.button} onClick={handlePost}>
|
||||||
|
<View className={styles.icon}>
|
||||||
|
<Image className={styles.download} src={DownloadIcon} />
|
||||||
|
</View>
|
||||||
|
<Text>生成分享图</Text>
|
||||||
|
</Button>
|
||||||
|
<View className={styles.customButton}>
|
||||||
|
<View className={styles.icon}>
|
||||||
|
<Image className={styles.download} src={DownloadIcon} />
|
||||||
|
</View>
|
||||||
|
<Text>生成分享图</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className={styles.customBtnWrapper}>
|
||||||
|
<Button className={styles.button}>
|
||||||
|
<View className={styles.icon}>
|
||||||
|
<Image className={styles.linkIcon} src={LinkIcon} />
|
||||||
|
</View>
|
||||||
|
<Text>复制链接</Text>
|
||||||
|
</Button>
|
||||||
|
<View className={styles.customButton}>
|
||||||
|
<View className={styles.icon}>
|
||||||
|
<Image className={styles.linkIcon} src={LinkIcon} />
|
||||||
|
</View>
|
||||||
|
<Text>复制链接</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</CommonPopup>
|
</CommonPopup>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { debounce } from "@tarojs/runtime";
|
||||||
import { Text, View, Image } from "@tarojs/components";
|
import { Text, View, Image } from "@tarojs/components";
|
||||||
import OrderService from "@/services/orderService";
|
import OrderService from "@/services/orderService";
|
||||||
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
|
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
|
||||||
@@ -348,6 +349,8 @@ export default function StickyButton(props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debounceAction = debounce(action, 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={styles["sticky-bottom-bar"]}>
|
<View className={styles["sticky-bottom-bar"]}>
|
||||||
@@ -390,7 +393,7 @@ export default function StickyButton(props) {
|
|||||||
<View
|
<View
|
||||||
style={is_organizer ? {} : { margin: "auto" }}
|
style={is_organizer ? {} : { margin: "auto" }}
|
||||||
className={styles["sticky-bottom-bar-join-game"]}
|
className={styles["sticky-bottom-bar-join-game"]}
|
||||||
onClick={action}
|
onClick={debounceAction}
|
||||||
>
|
>
|
||||||
<ActionText />
|
<ActionText />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -81,9 +81,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.venue-screenshot-scroll-view {
|
.venue-screenshot-scroll-view {
|
||||||
max-height: calc(100vh - 260px);
|
max-height: calc(100dvh - 260px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.venue-screenshot-image-list {
|
.venue-screenshot-image-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -92,9 +93,11 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 10px 10px;
|
gap: 10px 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.venue-screenshot-image-item {
|
.venue-screenshot-image-item {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
|
min-height: 100%;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ export default function VenueInfo(props) {
|
|||||||
function previewImage(current_url) {
|
function previewImage(current_url) {
|
||||||
Taro.previewImage({
|
Taro.previewImage({
|
||||||
current: current_url,
|
current: current_url,
|
||||||
urls:
|
urls: venue_image_list || [],
|
||||||
venue_image_list?.length > 0 ? venue_image_list.map((c) => c.url) : [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -83,16 +82,17 @@ export default function VenueInfo(props) {
|
|||||||
<ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}>
|
<ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}>
|
||||||
<View className={styles["venue-screenshot-image-list"]}>
|
<View className={styles["venue-screenshot-image-list"]}>
|
||||||
{venue_image_list?.length > 0 &&
|
{venue_image_list?.length > 0 &&
|
||||||
venue_image_list.map((item) => {
|
venue_image_list.map((url, index) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={styles["venue-screenshot-image-item"]}
|
className={styles["venue-screenshot-image-item"]}
|
||||||
onClick={previewImage.bind(null, item.url)}
|
onClick={previewImage.bind(null, url)}
|
||||||
|
key={index}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
className={styles["venue-screenshot-image-item-image"]}
|
className={styles["venue-screenshot-image-item-image"]}
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
src={item.url}
|
src={url}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ function Index() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
|
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
|
||||||
await fetchDetail();
|
// 注意:这里不调用 fetchDetail,避免与 useDidShow 中的调用重复
|
||||||
|
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
|
||||||
if (from === "publish") {
|
if (from === "publish") {
|
||||||
handleShare(true);
|
handleShare(true);
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,7 @@ function Index() {
|
|||||||
|
|
||||||
function handleViewUserInfo(userId) {
|
function handleViewUserInfo(userId) {
|
||||||
navto(
|
navto(
|
||||||
isMyOwn
|
userId === myInfo.id
|
||||||
? "/user_pages/myself/index"
|
? "/user_pages/myself/index"
|
||||||
: `/user_pages/other/index?userid=${userId}`
|
: `/user_pages/other/index?userid=${userId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
area,
|
area,
|
||||||
cityQrCode,
|
cityQrCode,
|
||||||
districts,
|
districts,
|
||||||
|
fetchMatches,
|
||||||
gamesNum, // 新增:获取球局数量
|
gamesNum, // 新增:获取球局数量
|
||||||
} = store;
|
} = store;
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
pageOption,
|
pageOption,
|
||||||
isShowNoData,
|
isShowNoData,
|
||||||
} = listPageState || {};
|
} = listPageState || {};
|
||||||
|
console.log('===matches', matches)
|
||||||
|
|
||||||
const scrollContextRef = useRef(null);
|
const scrollContextRef = useRef(null);
|
||||||
const scrollViewRef = useRef(null);
|
const scrollViewRef = useRef(null);
|
||||||
@@ -92,6 +94,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
// 记录上一次加载数据时的城市,用于检测城市变化
|
// 记录上一次加载数据时的城市,用于检测城市变化
|
||||||
const lastLoadedAreaRef = useRef<[string, string] | null>(null);
|
const lastLoadedAreaRef = useRef<[string, string] | null>(null);
|
||||||
const prevIsActiveRef = useRef(isActive);
|
const prevIsActiveRef = useRef(isActive);
|
||||||
|
// 首次加载标记:避免切回 tab 时使用 isRefresh 导致智能排序顺序抖动
|
||||||
|
const hasLoadedOnceRef = useRef(false);
|
||||||
|
|
||||||
// 处理距离筛选显示/隐藏
|
// 处理距离筛选显示/隐藏
|
||||||
const handleDistanceFilterVisibleChange = useCallback(
|
const handleDistanceFilterVisibleChange = useCallback(
|
||||||
@@ -129,15 +133,37 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
// ScrollView 滚动处理
|
// ScrollView 滚动处理
|
||||||
const handleScrollViewScroll = useCallback(
|
const handleScrollViewScroll = useCallback(
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
|
|
||||||
|
|
||||||
const currentScrollTop = e?.detail?.scrollTop || 0;
|
const currentScrollTop = e?.detail?.scrollTop || 0;
|
||||||
|
const scrollHeight = e?.detail?.scrollHeight || 0;
|
||||||
|
const clientHeight = e?.detail?.clientHeight || 0;
|
||||||
const lastScrollTop = lastScrollTopRef.current;
|
const lastScrollTop = lastScrollTopRef.current;
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
const timeDiff = currentTime - lastScrollTimeRef.current;
|
const timeDiff = currentTime - lastScrollTimeRef.current;
|
||||||
|
|
||||||
if (timeDiff < 100) return;
|
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;
|
const scrollDiff = currentScrollTop - lastScrollTop;
|
||||||
let newDirection = scrollDirectionRef.current;
|
let newDirection = scrollDirectionRef.current;
|
||||||
if (Math.abs(scrollDiff) > 15) {
|
if (Math.abs(scrollDiff) > 15) {
|
||||||
@@ -195,7 +221,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
lastScrollTopRef.current = currentScrollTop;
|
lastScrollTopRef.current = currentScrollTop;
|
||||||
lastScrollTimeRef.current = currentTime;
|
lastScrollTimeRef.current = currentTime;
|
||||||
},
|
},
|
||||||
[updateListPageState, onNavStateChange]
|
[updateListPageState, onNavStateChange, loading, loadMoreMatches, listPageState?.isHasMoreData]
|
||||||
// 移除 showSearchBar 和 isShowInputCustomerNavBar 依赖,使用 ref 获取最新值
|
// 移除 showSearchBar 和 isShowInputCustomerNavBar 依赖,使用 ref 获取最新值
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -208,12 +234,51 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
|
|
||||||
// 只有当页面激活时才加载位置和列表数据
|
// 只有当页面激活时才加载位置和列表数据
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
getLocation().catch((error) => {
|
const firstLoad = !hasLoadedOnceRef.current;
|
||||||
console.error('获取位置信息失败:', error);
|
getLocation(firstLoad)
|
||||||
});
|
.then(() => {
|
||||||
|
if (firstLoad) {
|
||||||
|
hasLoadedOnceRef.current = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('获取位置信息失败:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isActive]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// 如果从非激活状态变为激活状态(切回列表页)
|
// 如果从非激活状态变为激活状态(切回列表页)
|
||||||
@@ -305,7 +370,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getLocation = async () => {
|
const getLocation = async (useRefresh = true) => {
|
||||||
const location = await getCurrentLocationInfo();
|
const location = await getCurrentLocationInfo();
|
||||||
updateState({ location });
|
updateState({ location });
|
||||||
if (location && location.latitude && location.longitude) {
|
if (location && location.latitude && location.longitude) {
|
||||||
@@ -316,7 +381,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 先调用列表接口
|
// 先调用列表接口
|
||||||
await getMatchesData();
|
await fetchMatches({}, useRefresh);
|
||||||
// 列表接口完成后,再调用数量接口
|
// 列表接口完成后,再调用数量接口
|
||||||
await fetchGetGamesCount();
|
await fetchGetGamesCount();
|
||||||
// 初始数据加载完成后,记录当前城市
|
// 初始数据加载完成后,记录当前城市
|
||||||
@@ -331,7 +396,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
|
if (refreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
@@ -518,7 +585,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
|||||||
refresherEnabled={true}
|
refresherEnabled={true}
|
||||||
refresherTriggered={refreshing}
|
refresherTriggered={refreshing}
|
||||||
onRefresherRefresh={handleRefresh}
|
onRefresherRefresh={handleRefresh}
|
||||||
lowerThreshold={100}
|
lowerThreshold={600}
|
||||||
onScrollToLower={async () => {
|
onScrollToLower={async () => {
|
||||||
if (
|
if (
|
||||||
!loading &&
|
!loading &&
|
||||||
|
|||||||
@@ -64,11 +64,17 @@ const MessagePageContent: React.FC<MessagePageContentProps> = ({ isActive = true
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只有当页面激活且未加载过数据时才加载接口
|
// 当切换到消息 tab 时,调用红点信息接口
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive) {
|
||||||
|
fetchReddotInfo();
|
||||||
|
}
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
// 只有当页面激活且未加载过数据时才加载通知列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActive && !hasLoaded) {
|
if (isActive && !hasLoaded) {
|
||||||
getNoticeList();
|
getNoticeList();
|
||||||
fetchReddotInfo();
|
|
||||||
setHasLoaded(true);
|
setHasLoaded(true);
|
||||||
}
|
}
|
||||||
}, [isActive, hasLoaded]);
|
}, [isActive, hasLoaded]);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import MessagePageContent from "./components/MessagePageContent";
|
|||||||
import MyselfPageContent from "./components/MyselfPageContent";
|
import MyselfPageContent from "./components/MyselfPageContent";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import FamilyContext from "@/context";
|
import FamilyContext from "@/context";
|
||||||
|
import { useDictionaryStore } from "@/store/dictionaryStore";
|
||||||
|
|
||||||
type TabType = "list" | "message" | "personal";
|
type TabType = "list" | "message" | "personal";
|
||||||
|
|
||||||
@@ -66,6 +67,12 @@ const MainPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await fetchUserInfo();
|
await fetchUserInfo();
|
||||||
await checkNicknameChangeStatus();
|
await checkNicknameChangeStatus();
|
||||||
|
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
|
||||||
|
try {
|
||||||
|
await useDictionaryStore.getState().fetchBannerDictionary();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("预取 Banner 字典失败:", e);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取用户信息失败:", error);
|
console.error("获取用户信息失败:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import orderService, {
|
|||||||
OrderStatus,
|
OrderStatus,
|
||||||
refundTextMap,
|
refundTextMap,
|
||||||
} from "@/services/orderService";
|
} from "@/services/orderService";
|
||||||
|
import { debounce } from "@tarojs/runtime";
|
||||||
import {
|
import {
|
||||||
payOrder,
|
payOrder,
|
||||||
delay,
|
delay,
|
||||||
@@ -380,8 +381,13 @@ function OrderMsg(props) {
|
|||||||
wechat_contact,
|
wechat_contact,
|
||||||
price,
|
price,
|
||||||
} = detail;
|
} = detail;
|
||||||
const { order_no } = orderDetail;
|
const { order_no, registrant_phone: registrant_phone_from_order } =
|
||||||
const { order_info: { registrant_phone } = {} } = checkOrderInfo;
|
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 startTime = dayjs(start_time);
|
||||||
const endTime = dayjs(end_time);
|
const endTime = dayjs(end_time);
|
||||||
const startDate = startTime.format("YYYY年M月D日");
|
const startDate = startTime.format("YYYY年M月D日");
|
||||||
@@ -402,13 +408,11 @@ function OrderMsg(props) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "报名人电话",
|
title: "报名人电话",
|
||||||
// content: registrant_phone,
|
|
||||||
content: registrant_phone ? (
|
content: registrant_phone ? (
|
||||||
<Text
|
<Text
|
||||||
selectable={true} // 支持长按复制
|
selectable={true} // 支持长按复制
|
||||||
style={{
|
style={{
|
||||||
color: "#007AFF",
|
color: "#007AFF",
|
||||||
// textDecoration: "underline",
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -427,7 +431,6 @@ function OrderMsg(props) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "组织人电话",
|
title: "组织人电话",
|
||||||
// content: wechat_contact,
|
|
||||||
content:
|
content:
|
||||||
wechat_contact && isPhoneNumber(wechat_contact) ? (
|
wechat_contact && isPhoneNumber(wechat_contact) ? (
|
||||||
<Text
|
<Text
|
||||||
@@ -489,8 +492,7 @@ function OrderMsg(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RefundPolicy(props) {
|
function RefundPolicy(props) {
|
||||||
const { checkOrderInfo } = props;
|
const { refund_policy = [] } = props;
|
||||||
const { refund_policy = [] } = checkOrderInfo;
|
|
||||||
const current = dayjs();
|
const current = dayjs();
|
||||||
const policyList = [
|
const policyList = [
|
||||||
{
|
{
|
||||||
@@ -563,7 +565,7 @@ const OrderCheck = () => {
|
|||||||
const [id, gameId] = [Number(stringId), Number(stringGameId)];
|
const [id, gameId] = [Number(stringId), Number(stringGameId)];
|
||||||
const [detail, setDetail] = useState<GameData | {}>({});
|
const [detail, setDetail] = useState<GameData | {}>({});
|
||||||
const [location, setLocation] = useState<number[]>([0, 0]);
|
const [location, setLocation] = useState<number[]>([0, 0]);
|
||||||
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
|
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes>();
|
||||||
const [orderDetail, setOrderDetail] = useState({});
|
const [orderDetail, setOrderDetail] = useState({});
|
||||||
const { paying, setPaying } = useOrder();
|
const { paying, setPaying } = useOrder();
|
||||||
|
|
||||||
@@ -584,11 +586,11 @@ const OrderCheck = () => {
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
gameDetail = res.data;
|
gameDetail = res.data;
|
||||||
}
|
}
|
||||||
|
checkOrder(gameId);
|
||||||
}
|
}
|
||||||
if (gameDetail.id) {
|
setDetail(gameDetail);
|
||||||
setDetail(gameDetail);
|
const location = await getCurrentLocation();
|
||||||
onInit(gameDetail.id);
|
setLocation([location.latitude, location.longitude]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkOrder(gid) {
|
async function checkOrder(gid) {
|
||||||
@@ -596,12 +598,6 @@ const OrderCheck = () => {
|
|||||||
setCheckOrderInfo(orderRes.data);
|
setCheckOrderInfo(orderRes.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onInit(gid) {
|
|
||||||
checkOrder(gid);
|
|
||||||
const location = await getCurrentLocation();
|
|
||||||
setLocation([location.latitude, location.longitude]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPaymentParams() {
|
async function getPaymentParams() {
|
||||||
// 检查登录状态和手机号(创建订单前检查)
|
// 检查登录状态和手机号(创建订单前检查)
|
||||||
if (!requireLoginWithPhone()) {
|
if (!requireLoginWithPhone()) {
|
||||||
@@ -620,16 +616,12 @@ const OrderCheck = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO: get order msg from id
|
//TODO: get order msg from id
|
||||||
const handlePay = async () => {
|
const handlePay = debounce(async () => {
|
||||||
// 检查登录状态和手机号
|
// 检查登录状态和手机号
|
||||||
if (!requireLoginWithPhone()) {
|
if (!requireLoginWithPhone()) {
|
||||||
return; // 未登录或未绑定手机号,已跳转到登录页
|
return; // 未登录或未绑定手机号,已跳转到登录页
|
||||||
}
|
}
|
||||||
setPaying(true);
|
setPaying(true);
|
||||||
Taro.showLoading({
|
|
||||||
title: "支付中...",
|
|
||||||
mask: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let payment_params = {};
|
let payment_params = {};
|
||||||
try {
|
try {
|
||||||
@@ -641,7 +633,6 @@ const OrderCheck = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await payOrder(payment_params);
|
await payOrder(payment_params);
|
||||||
Taro.hideLoading();
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: "支付成功",
|
title: "支付成功",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
@@ -655,7 +646,6 @@ const OrderCheck = () => {
|
|||||||
// delta: 1,
|
// delta: 1,
|
||||||
// });
|
// });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Taro.hideLoading();
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: error.message,
|
title: error.message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -665,7 +655,8 @@ const OrderCheck = () => {
|
|||||||
init();
|
init();
|
||||||
setPaying(false);
|
setPaying(false);
|
||||||
}
|
}
|
||||||
};
|
}, 300);
|
||||||
|
|
||||||
if (!id && !gameId) {
|
if (!id && !gameId) {
|
||||||
return (
|
return (
|
||||||
<View className={styles.errorTip}>
|
<View className={styles.errorTip}>
|
||||||
@@ -712,22 +703,27 @@ const OrderCheck = () => {
|
|||||||
checkOrderInfo={checkOrderInfo}
|
checkOrderInfo={checkOrderInfo}
|
||||||
/>
|
/>
|
||||||
{/* Refund policy */}
|
{/* Refund policy */}
|
||||||
<RefundPolicy checkOrderInfo={checkOrderInfo} />
|
<RefundPolicy
|
||||||
|
refund_policy={
|
||||||
|
checkOrderInfo?.refund_policy || orderDetail?.refund_policy || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/* Disclaimer */}
|
{/* Disclaimer */}
|
||||||
<Disclaimer />
|
<Disclaimer />
|
||||||
{(!id ||
|
{(!id ||
|
||||||
(order_status === OrderStatus.PENDING &&
|
(order_status === OrderStatus.PENDING &&
|
||||||
cancel_type === CancelType.NONE)) &&
|
cancel_type === CancelType.NONE)) && (
|
||||||
!paying && (
|
<Button
|
||||||
<Button
|
className={styles.payButton}
|
||||||
className={styles.payButton}
|
disabled={paying}
|
||||||
disabled={paying}
|
onClick={handlePay}
|
||||||
onClick={handlePay}
|
loading={paying}
|
||||||
>
|
>
|
||||||
{order_status === OrderStatus.PENDING ? "继续" : "确认"}
|
{paying
|
||||||
支付
|
? "支付中..."
|
||||||
</Button>
|
: `${order_status === OrderStatus.PENDING ? "继续" : "确认"}支付`}
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
8
src/other_pages/bannerDetail/index.config.ts
Normal file
8
src/other_pages/bannerDetail/index.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
navigationBarBackgroundColor: '#FFFFFF',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
16
src/other_pages/bannerDetail/index.scss
Normal file
16
src/other_pages/bannerDetail/index.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.banner_detail_page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner_detail_content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner_detail_image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
36
src/other_pages/bannerDetail/index.tsx
Normal file
36
src/other_pages/bannerDetail/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { View, Image } from '@tarojs/components';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { GeneralNavbar } from '@/components';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
function Index() {
|
||||||
|
const [imgUrl, setImgUrl] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const instance = (Taro as any).getCurrentInstance?.();
|
||||||
|
const params = instance?.router?.params || {};
|
||||||
|
const url = params?.img ? decodeURIComponent(params.img) : '';
|
||||||
|
setImgUrl(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="banner_detail_page">
|
||||||
|
<GeneralNavbar title="" showBack={true} />
|
||||||
|
<View className="banner_detail_content">
|
||||||
|
{imgUrl ? (
|
||||||
|
<Image
|
||||||
|
className="banner_detail_image"
|
||||||
|
src={imgUrl}
|
||||||
|
mode="widthFix"
|
||||||
|
showMenuByLongpress
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|
||||||
|
|
||||||
@@ -71,14 +71,12 @@ const CommentReply = () => {
|
|||||||
|
|
||||||
setCommentList(mappedList);
|
setCommentList(mappedList);
|
||||||
|
|
||||||
// 获取未读评论ID并标记已读
|
// 获取所有评论ID列表并标记已读(传入所有ID,包括已读和未读)
|
||||||
const unreadIds = res.data.rows
|
const allCommentIds = res.data.rows.map((item: any) => item.id);
|
||||||
.filter((item: any) => item.is_read === 0)
|
|
||||||
.map((item: any) => item.id);
|
|
||||||
|
|
||||||
if (unreadIds.length > 0) {
|
if (allCommentIds.length > 0) {
|
||||||
// 使用统一接口标记已读
|
// 使用统一接口标记已读,传入所有评论ID
|
||||||
messageService.markAsRead('comment', unreadIds).catch(e => {
|
messageService.markAsRead('comment', allCommentIds).catch(e => {
|
||||||
console.error("标记评论已读失败:", e);
|
console.error("标记评论已读失败:", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -217,6 +215,15 @@ const CommentReply = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setCommentList(mappedList);
|
setCommentList(mappedList);
|
||||||
|
|
||||||
|
// 获取所有评论ID列表并标记已读(传入所有ID)
|
||||||
|
const allCommentIds = res.data.rows.map((item: any) => item.id);
|
||||||
|
|
||||||
|
if (allCommentIds.length > 0) {
|
||||||
|
messageService.markAsRead('comment', allCommentIds).catch(e => {
|
||||||
|
console.error("标记评论已读失败:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: calc(100vh - 98px);
|
height: calc(100vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 空状态图片
|
// 空状态图片
|
||||||
@@ -56,13 +56,13 @@
|
|||||||
// 按钮区域
|
// 按钮区域
|
||||||
&__buttons {
|
&__buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 110px;
|
bottom: 48px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 20px;
|
padding: 8px 20px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 251px;
|
width: 100%;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1),
|
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1),
|
||||||
@@ -87,13 +87,12 @@
|
|||||||
|
|
||||||
// 按钮文字样式
|
// 按钮文字样式
|
||||||
&_text {
|
&_text {
|
||||||
font-family: 'DingTalk JinBuTi';
|
|
||||||
font-weight: 400;
|
font-family: 'PingFang SC';
|
||||||
font-size: 18px;
|
font-style: normal;
|
||||||
line-height: 1.11;
|
font-weight: 600;
|
||||||
letter-spacing: -0.05em;
|
font-size: 16px;
|
||||||
text-align: center;
|
line-height: 22px;
|
||||||
color: #000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主要按钮(去看看其他球局)
|
// 主要按钮(去看看其他球局)
|
||||||
@@ -101,6 +100,8 @@
|
|||||||
background: #000000;
|
background: #000000;
|
||||||
border: 2px solid rgba(0, 0, 0, 0.06);
|
border: 2px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.empty_state_page__button_text {
|
.empty_state_page__button_text {
|
||||||
color: #ffffff; // 黑色背景下使用白色文字
|
color: #ffffff; // 黑色背景下使用白色文字
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ import './index.scss';
|
|||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
|
||||||
const [countdown, setCountdown] = useState(5);
|
const [countdown, setCountdown] = useState(5);
|
||||||
|
|
||||||
// 倒计时自动返回
|
// 倒计时自动返回
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (countdown <= 0) {
|
if (countdown <= 0) {
|
||||||
handle_go_to_home();
|
handle_go_to_home();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,14 +56,12 @@ const NewFollow = () => {
|
|||||||
|
|
||||||
setFollowList(mappedList);
|
setFollowList(mappedList);
|
||||||
|
|
||||||
// 获取未读关注ID并标记已读
|
// 获取所有关注者ID列表并标记已读(传入所有ID,包括已读和未读)
|
||||||
const unreadFanIds = res.list
|
const allFanIds = res.list.map((item: any) => item.id);
|
||||||
.filter((item: any) => item.is_read === 0)
|
|
||||||
.map((item: any) => item.id);
|
|
||||||
|
|
||||||
if (unreadFanIds.length > 0) {
|
if (allFanIds.length > 0) {
|
||||||
// 使用统一接口标记已读
|
// 使用统一接口标记已读,传入所有关注者ID
|
||||||
messageService.markAsRead('follow', unreadFanIds).catch(e => {
|
messageService.markAsRead('follow', allFanIds).catch(e => {
|
||||||
console.error("标记关注已读失败:", e);
|
console.error("标记关注已读失败:", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -164,6 +162,15 @@ const NewFollow = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setFollowList(mappedList);
|
setFollowList(mappedList);
|
||||||
|
|
||||||
|
// 获取所有关注者ID列表并标记已读(传入所有ID)
|
||||||
|
const allFanIds = res.list.map((item: any) => item.id);
|
||||||
|
|
||||||
|
if (allFanIds.length > 0) {
|
||||||
|
messageService.markAsRead('follow', allFanIds).catch(e => {
|
||||||
|
console.error("标记关注已读失败:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有数据,设置为空数组以显示空状态
|
// 如果没有数据,设置为空数组以显示空状态
|
||||||
setFollowList([]);
|
setFollowList([]);
|
||||||
|
|||||||
@@ -146,8 +146,22 @@
|
|||||||
.introContainer {
|
.introContainer {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
|
background-size: cover;
|
||||||
#fafafa;
|
background-repeat: no-repeat;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
|
||||||
|
#fafafa;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
.avatarWrap {
|
.avatarWrap {
|
||||||
@@ -235,6 +249,8 @@
|
|||||||
height: 52px;
|
height: 52px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -258,6 +274,16 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
|
||||||
|
.arrowImage {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,8 +389,23 @@
|
|||||||
.testContainer {
|
.testContainer {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
|
background-repeat: no-repeat;
|
||||||
#fafafa;
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
|
||||||
|
#fafafa;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
margin: 12px 20px 36px;
|
margin: 12px 20px 36px;
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ function Intro() {
|
|||||||
const { setCallback } = useEvaluate();
|
const { setCallback } = useEvaluate();
|
||||||
|
|
||||||
const { last_test_result = null } = ntrpData || {};
|
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
|
const lastTestTime = create_time
|
||||||
? dayjs(create_time).format("YYYY年M月D日")
|
? dayjs(create_time).format("YYYY年M月D日")
|
||||||
: "";
|
: "";
|
||||||
@@ -222,7 +223,12 @@ function Intro() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={styles.introContainer}>
|
<View
|
||||||
|
className={styles.introContainer}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CommonGuideBar />
|
<CommonGuideBar />
|
||||||
{ntrpData?.has_test_record ? (
|
{ntrpData?.has_test_record ? (
|
||||||
<View className={styles.result}>
|
<View className={styles.result}>
|
||||||
@@ -266,11 +272,11 @@ function Intro() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className={styles.slogan}>
|
<View className={styles.slogan}>
|
||||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
<Text>{level_description}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={styles.actions}>
|
<View className={styles.actions}>
|
||||||
<View className={styles.buttonWrap}>
|
<View className={classnames(styles.buttonWrap, styles.customBtn)}>
|
||||||
<Button
|
<Button
|
||||||
className={classnames(styles.button, styles.primary)}
|
className={classnames(styles.button, styles.primary)}
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -279,6 +285,12 @@ function Intro() {
|
|||||||
<Text>再次测试</Text>
|
<Text>再次测试</Text>
|
||||||
<Image className={styles.arrowImage} src={ArrowRight} />
|
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<View
|
||||||
|
className={classnames(styles.customBtnCover, styles.primary)}
|
||||||
|
>
|
||||||
|
<Text>再次测试</Text>
|
||||||
|
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={classnames(styles.buttonWrap, styles.customBtn)}>
|
<View className={classnames(styles.buttonWrap, styles.customBtn)}>
|
||||||
<Button
|
<Button
|
||||||
@@ -413,7 +425,12 @@ function Test() {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View className={styles.testContainer}>
|
<View
|
||||||
|
className={styles.testContainer}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
|
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
|
||||||
<View className={styles.bar}>
|
<View className={styles.bar}>
|
||||||
<View
|
<View
|
||||||
@@ -667,7 +684,7 @@ function Result() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useShareAppMessage(async (res) => {
|
useShareAppMessage(async (res) => {
|
||||||
console.log( "res",result);
|
console.log("res", result);
|
||||||
return {
|
return {
|
||||||
title: "来测一测你的NTRP等级吧",
|
title: "来测一测你的NTRP等级吧",
|
||||||
imageUrl: result?.level_img || undefined,
|
imageUrl: result?.level_img || undefined,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
box-sizing: border-box;
|
||||||
// 搜索区域
|
// 搜索区域
|
||||||
.search-section {
|
.search-section {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
|
|||||||
@@ -106,8 +106,15 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
|||||||
})
|
})
|
||||||
setShowDetail(true)
|
setShowDetail(true)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err: { errMsg: string }) => {
|
||||||
console.error('选择位置失败:', err)
|
console.error('选择位置失败:', err)
|
||||||
|
const { errMsg } = err || {};
|
||||||
|
if (!errMsg.includes('fail cancel')) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: errMsg,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,12 +145,15 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
|||||||
istance: null
|
istance: null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err: { errMsg: string }) => {
|
||||||
console.error('选择位置失败:', err)
|
console.error('选择位置失败:', err)
|
||||||
Taro.showToast({
|
const { errMsg } = err || {};
|
||||||
title: '位置选择失败',
|
if (!errMsg.includes('fail cancel')) {
|
||||||
icon: 'error'
|
Taro.showToast({
|
||||||
})
|
title: errMsg,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom'
|
navigationStyle: 'custom',
|
||||||
})
|
// 禁止原生页面滚动,改用内部自定义滚动容器,避免顶部/底部回弹后中间无法继续滚动的问题
|
||||||
|
disableScroll: true,
|
||||||
|
})
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const PublishBall: React.FC = () => {
|
|||||||
defaultFormData,
|
defaultFormData,
|
||||||
]);
|
]);
|
||||||
const [checked, setChecked] = useState(true);
|
const [checked, setChecked] = useState(true);
|
||||||
|
const [publishLoading, setPublishLoading] = useState(false);
|
||||||
const [titleBar, setTitleBar] = useState("发布球局");
|
const [titleBar, setTitleBar] = useState("发布球局");
|
||||||
// 控制是否响应全局键盘(由具体输入框 focus/blur 控制)
|
// 控制是否响应全局键盘(由具体输入框 focus/blur 控制)
|
||||||
const [shouldReactToKeyboard, setShouldReactToKeyboard] = useState(false);
|
const [shouldReactToKeyboard, setShouldReactToKeyboard] = useState(false);
|
||||||
@@ -372,9 +373,10 @@ const PublishBall: React.FC = () => {
|
|||||||
const { republish } = params || {};
|
const { republish } = params || {};
|
||||||
if (activityType === "individual") {
|
if (activityType === "individual") {
|
||||||
const isValid = validateFormData(formData[0]);
|
const isValid = validateFormData(formData[0]);
|
||||||
if (!isValid) {
|
if (!isValid || publishLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setPublishLoading(true);
|
||||||
const {
|
const {
|
||||||
activityInfo,
|
activityInfo,
|
||||||
descriptionInfo,
|
descriptionInfo,
|
||||||
@@ -435,13 +437,15 @@ const PublishBall: React.FC = () => {
|
|||||||
title: res.message,
|
title: res.message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
|
setPublishLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (activityType === "group") {
|
if (activityType === "group") {
|
||||||
const isValid = formData.every((item) => validateFormData(item));
|
const isValid = formData.every((item) => validateFormData(item));
|
||||||
if (!isValid) {
|
if (!isValid || publishLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setPublishLoading(true);
|
||||||
if (checkAdjacentDataSame(formData)) {
|
if (checkAdjacentDataSame(formData)) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: "信息不可与前序场完全一致",
|
title: "信息不可与前序场完全一致",
|
||||||
@@ -505,6 +509,7 @@ const PublishBall: React.FC = () => {
|
|||||||
title: res.message,
|
title: res.message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
|
setPublishLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
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 |
@@ -14,6 +14,13 @@ interface DictionaryState {
|
|||||||
fetchDictionary: () => Promise<void>
|
fetchDictionary: () => Promise<void>
|
||||||
getDictionaryValue: (key: string, defaultValue?: any) => any
|
getDictionaryValue: (key: string, defaultValue?: any) => any
|
||||||
clearDictionary: () => void
|
clearDictionary: () => void
|
||||||
|
// banner 字典(单独管理,保持原始值)
|
||||||
|
bannerDict: {
|
||||||
|
bannerListImage: string
|
||||||
|
bannerDetailImage: string
|
||||||
|
bannerListIndex: string
|
||||||
|
} | null
|
||||||
|
fetchBannerDictionary: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建字典Store
|
// 创建字典Store
|
||||||
@@ -22,6 +29,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
|||||||
dictionaryData: {},
|
dictionaryData: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
bannerDict: null,
|
||||||
|
|
||||||
// 获取字典数据
|
// 获取字典数据
|
||||||
fetchDictionary: async () => {
|
fetchDictionary: async () => {
|
||||||
@@ -56,6 +64,27 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取 Banner 字典(启动时或手动调用)
|
||||||
|
fetchBannerDictionary: async () => {
|
||||||
|
try {
|
||||||
|
const keys = 'bannerListImage,bannerDetailImage,bannerListIndex';
|
||||||
|
const response = await commonApi.getDictionaryManyKey(keys)
|
||||||
|
if (response.code === 0 && response.data) {
|
||||||
|
const data = response.data || {};
|
||||||
|
set({
|
||||||
|
bannerDict: {
|
||||||
|
bannerListImage: data.bannerListImage || '',
|
||||||
|
bannerDetailImage: data.bannerDetailImage || '',
|
||||||
|
bannerListIndex: (data.bannerListIndex ?? '').toString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 保持静默,避免影响启动流程
|
||||||
|
console.error('获取 Banner 字典失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 获取字典值
|
// 获取字典值
|
||||||
getDictionaryValue: (key: string, defaultValue?: any) => {
|
getDictionaryValue: (key: string, defaultValue?: any) => {
|
||||||
const { dictionaryData } = get()
|
const { dictionaryData } = get()
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
getCityQrCode,
|
getCityQrCode,
|
||||||
getDistricts,
|
getDistricts,
|
||||||
} from "../services/listApi";
|
} from "../services/listApi";
|
||||||
|
// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取
|
||||||
|
import { useDictionaryStore } from "./dictionaryStore";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
ListActions,
|
||||||
IFilterOptions,
|
IFilterOptions,
|
||||||
@@ -18,6 +20,26 @@ import {
|
|||||||
IPayload,
|
IPayload,
|
||||||
} from "../../types/list/types";
|
} 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) {
|
function translateCityData(dataTree) {
|
||||||
return dataTree.map((item) => {
|
return dataTree.map((item) => {
|
||||||
const { children, ...rest } = item;
|
const { children, ...rest } = item;
|
||||||
@@ -250,10 +272,14 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
||||||
const currentData = currentPageState?.data || [];
|
const currentData = currentPageState?.data || [];
|
||||||
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
|
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
|
||||||
|
// 从字典缓存获取 banner,并将其插入到最终列表指定位置(全局索引)
|
||||||
|
const dictData = useDictionaryStore.getState().bannerDict;
|
||||||
|
const processedData = dictData ? insertBannersToRows(newData, dictData) : newData;
|
||||||
state.updateCurrentPageState({
|
state.updateCurrentPageState({
|
||||||
data: newData,
|
data: processedData,
|
||||||
isHasMoreData,
|
isHasMoreData,
|
||||||
isShowNoData: newData?.length === 0,
|
// 使用插入后的最终数据判断是否显示空状态,避免有 banner 时仍显示空
|
||||||
|
isShowNoData: processedData?.length === 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
set({
|
set({
|
||||||
@@ -296,7 +322,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resData = (await fetchFn(reqParams)) || {};
|
let resData: any = {};
|
||||||
|
resData = (await fetchFn(reqParams)) || {};
|
||||||
|
|
||||||
const { data = {}, code } = resData;
|
const { data = {}, code } = resData;
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
setListData({
|
setListData({
|
||||||
@@ -308,7 +336,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
return Promise.reject(new Error('获取数据失败'));
|
return Promise.reject(new Error('获取数据失败'));
|
||||||
}
|
}
|
||||||
const { count, rows } = data;
|
const { count } = data;
|
||||||
|
let { rows } = data as any;
|
||||||
|
|
||||||
setListData({
|
setListData({
|
||||||
error: '',
|
error: '',
|
||||||
data: rows || [],
|
data: rows || [],
|
||||||
@@ -319,7 +349,7 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setListData({
|
setListData({
|
||||||
error: "-1",
|
error: "",
|
||||||
data: [],
|
data: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -345,23 +375,24 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const searchParams = getSearchParams() || {};
|
const searchParams = getSearchParams() || {};
|
||||||
|
|
||||||
// 调用常规列表接口
|
// 并发请求:常规列表、智能排序列表
|
||||||
const listParams = {
|
const [listResSettled, integrateResSettled] = await Promise.allSettled([
|
||||||
...searchParams,
|
getGamesList({
|
||||||
order: searchParams.order || "distance",
|
...searchParams,
|
||||||
};
|
order: searchParams.order || "distance",
|
||||||
const listRes = await getGamesList(listParams);
|
}),
|
||||||
|
getGamesIntegrateList({
|
||||||
// 调用智能排序列表接口
|
...searchParams,
|
||||||
const integrateParams = {
|
order: "",
|
||||||
...searchParams,
|
seachOption: {
|
||||||
order: "",
|
...searchParams.seachOption,
|
||||||
seachOption: {
|
isRefresh: true,
|
||||||
...searchParams.seachOption,
|
},
|
||||||
isRefresh: true,
|
}),
|
||||||
},
|
]);
|
||||||
};
|
|
||||||
const integrateRes = await getGamesIntegrateList(integrateParams);
|
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 currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
|
||||||
@@ -369,7 +400,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
const isIntegrate = distanceQuickFilter?.order === "0";
|
const isIntegrate = distanceQuickFilter?.order === "0";
|
||||||
|
|
||||||
if (listRes?.code === 0 && listRes?.data) {
|
if (listRes?.code === 0 && listRes?.data) {
|
||||||
const { count, rows } = listRes.data;
|
const { count } = listRes.data;
|
||||||
|
let { rows } = listRes.data as any;
|
||||||
if (!isIntegrate) {
|
if (!isIntegrate) {
|
||||||
// 如果当前是常规排序,更新常规列表数据
|
// 如果当前是常规排序,更新常规列表数据
|
||||||
setListData({
|
setListData({
|
||||||
@@ -383,7 +415,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (integrateRes?.code === 0 && integrateRes?.data) {
|
if (integrateRes?.code === 0 && integrateRes?.data) {
|
||||||
const { count, rows, recommendList } = integrateRes.data;
|
const { count } = integrateRes.data;
|
||||||
|
let { rows, recommendList } = integrateRes.data as any;
|
||||||
if (isIntegrate) {
|
if (isIntegrate) {
|
||||||
// 如果当前是智能排序,更新智能排序列表数据
|
// 如果当前是智能排序,更新智能排序列表数据
|
||||||
setListData({
|
setListData({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import Taro from "@tarojs/taro";
|
||||||
import {
|
import {
|
||||||
fetchUserProfile,
|
fetchUserProfile,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
@@ -15,7 +16,7 @@ export interface UserState {
|
|||||||
fetchUserInfo: () => Promise<UserInfoType | undefined>;
|
fetchUserInfo: () => Promise<UserInfoType | undefined>;
|
||||||
updateUserInfo: (userInfo: Partial<UserInfoType>) => void;
|
updateUserInfo: (userInfo: Partial<UserInfoType>) => void;
|
||||||
nicknameChangeStatus: Partial<NicknameChangeStatus>;
|
nicknameChangeStatus: Partial<NicknameChangeStatus>;
|
||||||
checkNicknameChangeStatus: () => void;
|
checkNicknameChangeStatus: (force?: boolean) => void;
|
||||||
updateNickname: (nickname: string) => void;
|
updateNickname: (nickname: string) => void;
|
||||||
// NTRP 测试结果缓存
|
// NTRP 测试结果缓存
|
||||||
lastTestResult: LastTimeTestResult | null;
|
lastTestResult: LastTimeTestResult | null;
|
||||||
@@ -32,6 +33,10 @@ const getTimeNextDate = (time: string) => {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 请求锁,避免重复调用
|
||||||
|
let isFetchingLastTestResult = false;
|
||||||
|
let isCheckingNicknameStatus = false;
|
||||||
|
|
||||||
export const useUser = create<UserState>()((set) => ({
|
export const useUser = create<UserState>()((set) => ({
|
||||||
user: {},
|
user: {},
|
||||||
fetchUserInfo: async () => {
|
fetchUserInfo: async () => {
|
||||||
@@ -40,14 +45,31 @@ export const useUser = create<UserState>()((set) => ({
|
|||||||
const userData = res.data;
|
const userData = res.data;
|
||||||
set({ user: userData });
|
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) {
|
if (userData?.last_location_province) {
|
||||||
const listStore = useListStore.getState();
|
const listStore = useListStore.getState();
|
||||||
const currentArea = listStore.area;
|
const currentArea = listStore.area;
|
||||||
// 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新
|
// 只有当 area 不存在时才使用用户信息中的位置
|
||||||
if (!currentArea || currentArea[1] !== userData.last_location_province) {
|
if (!currentArea) {
|
||||||
const newArea: [string, string] = ["中国", userData.last_location_province];
|
const newArea: [string, string] = ["中国", userData.last_location_province];
|
||||||
listStore.updateArea(newArea);
|
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: {},
|
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 {
|
try {
|
||||||
|
isCheckingNicknameStatus = true;
|
||||||
const res = await checkNicknameChangeStatusApi();
|
const res = await checkNicknameChangeStatusApi();
|
||||||
const { next_period_start_time } = res.data;
|
const { next_period_start_time } = res.data;
|
||||||
set({
|
set({
|
||||||
@@ -99,12 +133,15 @@ export const useUser = create<UserState>()((set) => ({
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("检查昵称变更状态失败:", error);
|
console.error("检查昵称变更状态失败:", error);
|
||||||
|
} finally {
|
||||||
|
isCheckingNicknameStatus = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateNickname: async (nickname) => {
|
updateNickname: async (nickname) => {
|
||||||
try {
|
try {
|
||||||
await updateNicknameApi(nickname);
|
await updateNicknameApi(nickname);
|
||||||
await useUser.getState().checkNicknameChangeStatus();
|
// 更新昵称后强制更新状态
|
||||||
|
await useUser.getState().checkNicknameChangeStatus(true);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
user: { ...state.user, nickname },
|
user: { ...state.user, nickname },
|
||||||
}));
|
}));
|
||||||
@@ -115,7 +152,27 @@ export const useUser = create<UserState>()((set) => ({
|
|||||||
// NTRP 测试结果缓存
|
// NTRP 测试结果缓存
|
||||||
lastTestResult: null,
|
lastTestResult: null,
|
||||||
fetchLastTestResult: async () => {
|
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 {
|
try {
|
||||||
|
isFetchingLastTestResult = true;
|
||||||
const res = await evaluateService.getLastResult();
|
const res = await evaluateService.getLastResult();
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
set({ lastTestResult: res.data });
|
set({ lastTestResult: res.data });
|
||||||
@@ -125,6 +182,8 @@ export const useUser = create<UserState>()((set) => ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取NTRP测试结果失败:", error);
|
console.error("获取NTRP测试结果失败:", error);
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
isFetchingLastTestResult = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -275,10 +275,7 @@ const DownloadBill: React.FC = () => {
|
|||||||
Taro.navigateBack();
|
Taro.navigateBack();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View className="hint_content" style={{ marginTop: `147px` }}>
|
||||||
className="hint_content"
|
|
||||||
style={{ marginTop: `${totalHeight}px` }}
|
|
||||||
>
|
|
||||||
<Text>最长可导出三个月的账单 </Text>
|
<Text>最长可导出三个月的账单 </Text>
|
||||||
<Text className="button_text" onClick={downloadExample}>
|
<Text className="button_text" onClick={downloadExample}>
|
||||||
示例文件
|
示例文件
|
||||||
@@ -322,8 +319,9 @@ const DownloadBill: React.FC = () => {
|
|||||||
近一周
|
近一周
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className={`option_button ${dateType === "month" ? "active" : ""
|
className={`option_button ${
|
||||||
}`}
|
dateType === "month" ? "active" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectDateRange("month");
|
selectDateRange("month");
|
||||||
}}
|
}}
|
||||||
@@ -331,8 +329,9 @@ const DownloadBill: React.FC = () => {
|
|||||||
近一月
|
近一月
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className={`option_button ${dateType === "custom" ? "active" : ""
|
className={`option_button ${
|
||||||
}`}
|
dateType === "custom" ? "active" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectDateRange("custom");
|
selectDateRange("custom");
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 78px;
|
width: 90px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 78px;
|
width: 90px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -6,18 +6,28 @@ export function getOrderStatus(orderData) {
|
|||||||
if (!order_no) {
|
if (!order_no) {
|
||||||
return 'none'
|
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 unPay = order_status === OrderStatus.PENDING && ([CancelType.NONE].includes(cancel_type));
|
||||||
const refund = [RefundStatus.SUCCESS].includes(refund_status);
|
const refund = [RefundStatus.SUCCESS].includes(refund_status);
|
||||||
const refunding = [RefundStatus.PENDING].includes(refund_status);
|
const refunding = [RefundStatus.PENDING].includes(refund_status);
|
||||||
const expired =
|
const expired =
|
||||||
order_status === OrderStatus.FINISHED;
|
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);
|
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
|
// scene: list、detail
|
||||||
|
|||||||
Reference in New Issue
Block a user