41 Commits

Author SHA1 Message Date
李瑞
49f53d60ed 处理列表请求 2026-02-06 22:37:35 +08:00
李瑞
4c75368fe8 优化banner逻辑 2026-02-01 23:37:31 +08:00
张成
8abf6e6f2b 1 2026-02-01 18:46:30 +08:00
李瑞
0c83aab053 Merge branch 'feature/juguohong/20260101' 2026-02-01 16:12:48 +08:00
李瑞
1cbec87f77 增加banneer占位图 2026-02-01 16:12:04 +08:00
张成
6d57654005 1 2026-01-29 22:56:33 +08:00
筱野
dade2e2491 修改发布页问题 2026-01-27 22:15:28 +08:00
筱野
4a6ac73ad7 修改文本域长度 2026-01-26 22:51:01 +08:00
筱野
de8677c64c 修改样式 2026-01-26 22:13:52 +08:00
筱野
3ab647f7c6 增加发布仅上海地区可发布、发布防抖 2026-01-05 21:26:02 +08:00
张成
fa328f893d 1 2026-01-05 18:34:05 +08:00
李瑞
d7c24ca8b3 显示空状态 2026-01-01 15:37:27 +08:00
0a3cdbedd2 Merge branch 'feat/liujie' 2025-12-31 11:20:53 +08:00
af131f228a style: 修改球局详情页场馆预定截图展示问题、修改截图地址取值问题 2025-12-31 11:20:41 +08:00
b5f9d23615 Merge branch 'master' into feat/liujie 2025-12-31 10:55:58 +08:00
76b105866c 下载账单页面样式优化 2025-12-29 16:13:33 +08:00
86581f3a11 Merge branch 'feat/liujie' 2025-12-29 15:05:41 +08:00
29094c7e6a feat: 发布分享弹窗样式修改,增加预览消息卡片 2025-12-29 15:05:29 +08:00
4578ca0cb1 Merge branch 'feat/liujie' 2025-12-29 11:41:00 +08:00
d5662e5810 feat: 样式调整 2025-12-29 11:40:52 +08:00
9e53f7a9f5 Merge branch 'feat/liujie' 2025-12-29 10:23:25 +08:00
a0f7838895 feat: 增加防抖 2025-12-29 10:23:15 +08:00
c9c19855c3 Merge branch 'master' into feat/liujie 2025-12-29 10:13:13 +08:00
1b67693752 参与者路由跳转到他人页的问题 2025-12-28 19:21:54 +08:00
张成
d90dcb053e 修改 分享的 图标 2025-12-28 13:33:48 +08:00
张成
e7ee8bc1de 兜底 页面 样式问题 2025-12-28 11:05:32 +08:00
5aec62ae85 Merge branch 'master' into feat/liujie 2025-12-16 10:28:01 +08:00
张成
9f63a2369a 1 2025-12-13 00:20:26 +08:00
张成
e560d06106 1 2025-12-12 23:49:10 +08:00
张成
4c5262441c 1 2025-12-12 11:56:41 +08:00
张成
293b9e6eba 1 2025-12-11 15:56:22 +08:00
c42055d2c3 Merge branch 'feat/liujie' 2025-12-11 09:57:09 +08:00
b7efbce737 feat: 修改NTRP上次测试结果文案 2025-12-11 09:56:56 +08:00
筱野
26ab56fd1e Merge branch 'master' of http://git.bimwe.com/tennis/mini-programs 2025-12-10 22:14:15 +08:00
筱野
28201d79b9 修改标题对齐问题 2025-12-10 22:14:08 +08:00
e1c4990ada Merge branch 'feat/liujie' 2025-12-10 20:08:33 +08:00
46a59ba282 feat: refund policy 从订单接口获取、梳理订单操作按钮 2025-12-10 20:08:13 +08:00
张成
7b620210a2 1 2025-12-10 00:35:15 +08:00
张成
e2a8ed4e32 1 2025-12-10 00:14:09 +08:00
张成
85566b448e Merge branch 'master' of http://git.bimwe.com/tennis/mini-programs 2025-12-09 23:39:38 +08:00
张成
0774cf5ae6 1 2025-12-09 23:39:34 +08:00
49 changed files with 1093 additions and 266 deletions

View File

@@ -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,

View File

@@ -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 图片详情页
], ],
}, },
], ],

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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();
// 移动到旋转中心 // 移动到旋转中心

View File

@@ -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[]) => {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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>

View File

@@ -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

View File

@@ -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} />

View File

@@ -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 />
</> </>
); );

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
); );

View File

@@ -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}`
); );

View File

@@ -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 &&

View File

@@ -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]);

View File

@@ -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);
} }

View File

@@ -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>
); );
}; };

View File

@@ -0,0 +1,8 @@
export default definePageConfig({
navigationBarTitleText: '',
navigationStyle: 'custom',
navigationBarBackgroundColor: '#FFFFFF',
backgroundColor: '#FFFFFF',
});

View 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;
}

View 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;

View File

@@ -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({

View File

@@ -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; // 黑色背景下使用白色文字
} }

View File

@@ -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;
} }

View File

@@ -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([]);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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",
});
}
} }
}) })
} }

View File

@@ -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",
});
}
} }
}) })
} }

View File

@@ -1,3 +1,5 @@
export default definePageConfig({ export default definePageConfig({
navigationStyle: 'custom' navigationStyle: 'custom',
}) // 禁止原生页面滚动,改用内部自定义滚动容器,避免顶部/底部回弹后中间无法继续滚动的问题
disableScroll: true,
})

View File

@@ -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);
} }
} }
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -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()

View File

@@ -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({

View File

@@ -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;
} }
}, },
})); }));

View File

@@ -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");
}} }}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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