Merge branch 'master' into feat/liujie

This commit is contained in:
2026-02-07 16:03:34 +08:00
40 changed files with 410 additions and 316 deletions

View File

@@ -149,3 +149,7 @@ src/
## License
MIT
"appid": "wx915ecf6c01bea4ec",
"appid": "wx815b533167eb7b53",

View File

@@ -57,6 +57,7 @@
"@tarojs/shared": "4.1.5",
"@tarojs/taro": "4.1.5",
"babel-plugin-transform-remove-console": "^6.9.4",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"qweather-icons": "^1.8.0",
"react": "^18.0.0",

View File

@@ -3,6 +3,7 @@
"projectname": "playBallTogether",
"description": "playBallTogether",
"appid": "wx915ecf6c01bea4ec",
"setting": {
"urlCheck": true,
"es6": true,

View File

@@ -15,9 +15,10 @@
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": true
"bigPackageSizeSupport": true,
"useIsolateContext": true
}
}

View File

@@ -85,6 +85,10 @@
font-size: 13px;
font-weight: 400;
color: #3c3c43;
display: flex;
flex-direction: row;
align-items: center;
gap:4px;
}
.distanceWrap {

View File

@@ -1,9 +1,14 @@
import { useRef, useState, useEffect } from "react";
import { Menu } from "@nutui/nutui-react-taro";
import { Image, View, ScrollView } from "@tarojs/components";
import Taro from "@tarojs/taro";
import img from "@/config/images";
import Bubble from "../Bubble";
import { useListState } from "@/store/listStore";
import { useListState, useListStore } from "@/store/listStore";
import { getCurrentLocation } from "@/utils/locationUtils";
import { updateUserLocation } from "@/services/userService";
import { useGlobalState } from "@/store/global";
import { useUserActions } from "@/store/userStore";
import "./index.scss";
const DistanceQuickFilterV2 = (props) => {
@@ -19,15 +24,19 @@ const DistanceQuickFilterV2 = (props) => {
quickValue,
districtValue, // 新增:行政区选中值
onMenuVisibleChange, // 菜单展开/收起回调
onRelocate, // 重新定位回调
} = props;
const cityRef = useRef(null);
const quickRef = useRef(null);
const [changePosition, setChangePosition] = useState<number[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [keys, setKeys] = useState(0);
const [isRelocating, setIsRelocating] = useState(false);
// 从 store 获取当前城市信息
const { area } = useListState();
const currentCity = area?.at(-1) || ""; // 获取省份/城市名称
const { updateState } = useGlobalState() || {};
const { fetchUserInfo, updateCache } = useUserActions();
// 全城筛选显示的标题 - 如果选择了行政区,显示行政区名称
const getCityTitle = () => {
@@ -79,6 +88,64 @@ const DistanceQuickFilterV2 = (props) => {
index === 1 && (quickRef.current as any)?.toggle(false);
};
// 重新获取当前位置,调用接口把位置传递后端
const handleRelocate = async () => {
if (isRelocating) return;
setIsRelocating(true);
(Taro as any).showLoading({ title: '定位中...', mask: true });
try {
// 获取当前位置
const location = await getCurrentLocation();
if (location && location.latitude && location.longitude) {
// 更新 store 中的位置信息
updateState?.({ location });
// 调用接口把位置传递给后端,传递一个值代表强制更新
const response = await updateUserLocation(location.latitude, location.longitude, true);
// 如果接口返回成功,重新调用用户信息接口来更新 USER_SELECTED_CITY
if (response?.code === 0) {
// 删除 缓存
(Taro as any).removeStorageSync("USER_SELECTED_CITY");
// 延时一下
await new Promise(resolve => setTimeout(resolve, 600));
// 先清除缓存和 area确保使用最新的用户信息
await updateCache( [ response.data.last_location_province, response.data.last_location_city ]);
}
(Taro as any).showToast({
title: '定位成功',
icon: 'success',
duration: 1500,
});
// 通知父组件位置已更新,可以刷新列表
if (onRelocate) {
onRelocate(location);
}
} else {
throw new Error('获取位置信息失败');
}
} catch (error: any) {
console.error('重新定位失败:', error);
(Taro as any).showToast({
title: error?.message || '定位失败,请检查定位权限',
icon: 'none',
duration: 2000,
});
} finally {
setIsRelocating(false);
(Taro as any).hideLoading();
}
};
// 监听菜单状态变化,通知父组件
useEffect(() => {
onMenuVisibleChange?.(isMenuOpen);
@@ -103,8 +170,11 @@ const DistanceQuickFilterV2 = (props) => {
icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />}
>
<div className="positionWrap">
<p className="title"></p>
<p className="cityName">{currentCity}</p>
<p className="title">{currentCity}</p>
<p className="cityName" onClick={handleRelocate}>
<img src={img.ICON_RELOCATE} style={{ width: '12px', height: "12px" }} />
<span></span>
</p>
</div>
<div className="distanceWrap">
<Bubble

View File

@@ -105,9 +105,9 @@ const HomeNavbar = (props: IProps) => {
const userInfo = useUserInfo();
// 使用用户详情接口中的 last_location 字段
// USER_SELECTED_CITY 第二个值应该是省份/直辖市,不能是区
const lastLocationProvince = (userInfo as any)?.last_location_province || "";
const lastLocationCity = (userInfo as any)?.last_location_city || "";
// 只使用省份/直辖市,不使用城市(城市可能是区)
const detectedLocation = lastLocationProvince;
const detectedLocation = lastLocationCity;
// 检查是否应该显示定位确认弹窗
const should_show_location_dialog = (): boolean => {
@@ -192,7 +192,7 @@ const HomeNavbar = (props: IProps) => {
} else if (detectedLocation) {
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
const newArea: [string, string] = ["中国", detectedLocation];
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedLocation];
updateArea(newArea);
// 保存定位信息到缓存
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -266,7 +266,7 @@ const HomeNavbar = (props: IProps) => {
const { detectedProvince } = locationDialogData;
// 用户选择"切换到",使用用户详情中的位置信息
const newArea: [string, string] = ["中国", detectedProvince];
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedProvince];
updateArea(newArea);
// 更新缓存为新的定位信息
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -481,8 +481,7 @@ const HomeNavbar = (props: IProps) => {
{/* 搜索导航 */}
{!showTitle && (
<View
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
showInput && "visible"
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${showInput && "visible"
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`}
style={navbarStyle}
>

View File

@@ -45,7 +45,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image"
mode="aspectFill"
lazyLoad
defaultSource="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
defaultSource={require("@/static/emptyStatus/publish-empty-card.png")}
/>
);
};

View File

@@ -67,7 +67,7 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
};
const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
const [_, address] = area;
if (address !== '上海') {
if (address !== '上海') {
(Taro as any).showModal({
title: '提示',
content: '仅上海地区开放,您可加入社群或切换城市',

View File

@@ -89,25 +89,15 @@ const RadarChart: React.FC = forwardRef((props, ref) => {
ctx.lineWidth = 1;
ctx.stroke();
// 标签
const offset = 10;
const textX = center.x + (radius + offset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle);
// 标签:沿轴线外侧延伸,文字中心对齐轴线端点
const labelOffset = 28;
const textX = center.x + (radius + labelOffset) * Math.cos(angle);
const textY = center.y + (radius + labelOffset) * Math.sin(angle);
ctx.font = "12px sans-serif";
ctx.fillStyle = "#333";
ctx.textBaseline = "middle";
if (
Math.abs(angle) < 0.01 ||
Math.abs(Math.abs(angle) - Math.PI) < 0.01
) {
ctx.textAlign = "center";
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
ctx.textAlign = "left";
} else {
ctx.textAlign = "right";
}
ctx.fillText(label, textX, textY);
});

View File

@@ -102,26 +102,15 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.lineWidth = 1 * (radarSize / 200); // 根据2倍图调整线宽
ctx.stroke();
// 标签 - 文字显示在圆圈外面
const offset = 10 * (radarSize / 200); // 文字距离圆圈的偏移量2倍图
const textX = center.x + (radius + offset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle);
// 标签:沿轴线外侧延伸,文字中心对齐轴线端点(与 index.tsx 一致)
const labelOffset = 28 * (radarSize / 200); // 文字距离圆圈的偏移量2倍图
const textX = center.x + (radius + labelOffset) * Math.cos(angle);
const textY = center.y + (radius + labelOffset) * Math.sin(angle);
ctx.font = `${12 * (radarSize / 200)}px sans-serif`; // 根据2倍图调整字体大小
ctx.fillStyle = "#333";
ctx.textBaseline = "middle";
// 调整文字对齐方式
if (
Math.abs(angle) < 0.01 ||
Math.abs(Math.abs(angle) - Math.PI) < 0.01
) {
ctx.textAlign = "center";
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
ctx.textAlign = "left";
} else {
ctx.textAlign = "right";
}
ctx.fillText(label, textX, textY);
});

View File

@@ -45,7 +45,7 @@ const SearchBarComponent = (props: IProps) => {
</View>
}
className={styles.searchBar}
placeholder="搜索上海的球局和场地"
placeholder="搜索球局和场地"
onChange={handleChange}
value={value}
onInputClick={onInputClick}

View File

@@ -70,4 +70,5 @@ export default {
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
ICON_RELOCATE: require('@/static/list/icon-relocate.svg'),
}

View File

@@ -10,6 +10,7 @@ import { EvaluateScene } from "@/store/evaluateStore";
import { waitForAuthInit } from "@/utils/authInit";
import "./index.scss";
import { useRef, useEffect, useState, useMemo } from "react";
import { useDictionaryStore } from "@/store/dictionaryStore";
const ListContainer = (props) => {
const {
@@ -28,6 +29,7 @@ const ListContainer = (props) => {
collapse = false,
defaultShowNum,
evaluateFlag,
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
listLoadErrorWrapperHeight,
listLoadErrorWidth,
listLoadErrorHeight,
@@ -44,7 +46,7 @@ const ListContainer = (props) => {
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
const { bannerListImage, bannerDetailImage, bannerListIndex = 0 } = useDictionaryStore((s) => s.bannerDict) || {};
useReachBottom(() => {
// 加载更多方法
if (loading) {
@@ -93,10 +95,10 @@ const ListContainer = (props) => {
};
}, []);
// 获取测试结果,判断最近一个月是否有测试记录
// 获取测试结果,判断最近一个月是否有测试记录(仅首页需要)
useEffect(() => {
const init = async () => {
if (!evaluateFlag) return;
if (!evaluateFlag || !enableHomeCards) return;
// 先等待静默登录完成
await waitForAuthInit();
// 然后再获取用户信息
@@ -111,7 +113,7 @@ const ListContainer = (props) => {
}
};
init();
}, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]);
}, [evaluateFlag, enableHomeCards, userInfo, lastTestResult, fetchLastTestResult]);
// 从全局状态中获取测试状态
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
@@ -130,23 +132,33 @@ const ListContainer = (props) => {
);
};
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
function insertEvaluateCard(list) {
if (!evaluateFlag)
return showNumber !== undefined ? list.slice(0, showNumber) : list;
if (!list || list.length === 0) {
return list;
}
// 如果最近一个月有测试记录,则不插入 card
if (hasTestInLastMonth) {
return showNumber !== undefined ? list.slice(0, showNumber) : list;
// 插入 banner 卡片
function insertBannerCard(list) {
if (!bannerListImage) return list;
if (!list || !Array.isArray(list)) return list ?? [];
return [
...list.slice(0, Number(bannerListIndex)),
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
...list.slice(Number(bannerListIndex))
];
}
if (list.length <= 2) {
return [...list, { type: "evaluateCard" }];
}
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
function insertEvaluateCard(list) {
let result: any[];
if (!evaluateFlag) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (!list || list.length === 0) {
result = list;
} else if (hasTestInLastMonth) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (list.length <= 2) {
result = [...list, { type: "evaluateCard" }];
} else {
const [item1, item2, ...rest] = list;
return [
result = [
item1,
item2,
{ type: "evaluateCard" },
@@ -154,27 +166,22 @@ const ListContainer = (props) => {
];
}
return insertBannerCard(result);
}
const memoizedList = useMemo(
() => insertEvaluateCard(data),
[evaluateFlag, data, hasTestInLastMonth, showNumber]
() => (enableHomeCards ? insertEvaluateCard(data) : data),
[enableHomeCards, evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex]
);
// 渲染 banner 卡片
const renderBanner = (item, index) => {
if (!item?.banner_image_url) return null;
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) {
@@ -183,7 +190,16 @@ const ListContainer = (props) => {
});
}
}}
/>
style={{
height: "100px",
overflow: "hidden",
borderRadius: "12px",
backgroundImage: `url(${item.banner_image_url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
</View>
);
};
@@ -211,12 +227,12 @@ const ListContainer = (props) => {
return (
<>
{memoizedList.map((match, index) => {
if (match.type === "banner") {
if (enableHomeCards && match?.type === "banner") {
return renderBanner(match, index);
}
if (match.type === "evaluateCard") {
if (enableHomeCards && match?.type === "evaluateCard") {
return (
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
<NTRPTestEntryCard key={`evaluate-${index}`} type={EvaluateScene.list} />
);
}
return <ListCard key={match?.id || index} {...match} />;

View File

@@ -128,7 +128,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
Taro.showLoading({ title: "生成中..." });
// Taro.showLoading({ title: "生成中..." });
const qrCodeUrlRes = await DetailService.getQrCodeUrl({
page: "game_pages/detail/index",
scene: `id=${id}`,
@@ -137,6 +137,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
qrCodeUrlRes.data.qr_code_base64
);
await delay(100);
// Taro.showLoading({ title: "生成中..." });
const url = await generatePosterImage({
playType: play_type,
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
@@ -152,7 +153,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
time: `${startTime.format("ah")}${gameLength}`,
qrCodeUrl,
});
Taro.hideLoading();
// Taro.hideLoading();
Taro.showShareImageMenu({
path: url,
});

View File

@@ -131,7 +131,7 @@ const ListSearch = () => {
<View className="topSearch">
<Image className="searchIcon" src={img.ICON_LIST_SEARCH_SEARCH} />
<Input
placeholder="搜索上海的球局和场地"
placeholder="搜索球局和场地"
value={searchValue}
defaultValue={searchValue}
onChange={handleChange}

View File

@@ -62,6 +62,7 @@ function SharePoster(props) {
const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
debugger
await delay(100);
const url = await generatePosterImage({
playType: play_type,

View File

@@ -193,7 +193,7 @@ const LoginPage: React.FC = () => {
/>
</View>
<Text className="button_text">
{is_loading ? "登录中..." : "微信授权登录"}
{is_loading ? "登录中..." : "一键登录"}
</Text>
</Button>
@@ -208,7 +208,7 @@ const LoginPage: React.FC = () => {
src={require("@/static/login/phone_icon.svg")}
/>
</View>
<Text className="button_text"></Text>
<Text className="button_text"></Text>
</Button>
{/* 用户协议复选框 */}
@@ -224,13 +224,13 @@ const LoginPage: React.FC = () => {
className="terms_link"
onClick={() => handle_view_terms("terms")}
>
</Text>
<Text
className="terms_link"
onClick={() => handle_view_terms("binding")}
>
</Text>
<Text
className="terms_link"
@@ -259,13 +259,13 @@ const LoginPage: React.FC = () => {
className="terms_item"
onClick={() => handle_view_terms("terms")}
>
</Text>
<Text
className="terms_item"
onClick={() => handle_view_terms("binding")}
>
</Text>
<Text
className="terms_item"

View File

@@ -1,8 +1,8 @@
# 条款页面 - 场的条款和条件
# 条款页面 - 场的条款和条件
## 功能概述
条款页面展示完整的《场的条款和条件》内容,用户需要仔细阅读并同意后才能继续使用平台服务。
条款页面展示完整的《场的条款和条件》内容,用户需要仔细阅读并同意后才能继续使用平台服务。
## 🎨 设计特点
@@ -54,7 +54,7 @@ TermsPage
## 📋 条款内容
本页面包含完整的《场的条款和条件》,涵盖以下十个主要部分:
本页面包含完整的《场的条款和条件》,涵盖以下十个主要部分:
### 1. 服务内容
- 活动发布、报名、聊天室沟通、活动提醒等服务

View File

@@ -7,7 +7,7 @@ const TermsPage: React.FC = () => {
// 获取页面参数
const [termsType, setTermsType] = React.useState('terms');
const [pageTitle, setPageTitle] = React.useState('条款和条件');
const [termsTitle, setTermsTitle] = React.useState('《场的条款和条件》');
const [termsTitle, setTermsTitle] = React.useState('《场的条款和条件》');
const [termsContent, setTermsContent] = React.useState('');
// 返回上一页
@@ -23,7 +23,7 @@ const TermsPage: React.FC = () => {
switch (type) {
case 'terms':
setPageTitle('条款和条件');
setTermsTitle('《场的条款和条件》');
setTermsTitle('《场的条款和条件》');
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。</span>
一、服务内容
@@ -69,7 +69,7 @@ const TermsPage: React.FC = () => {
break;
case 'binding':
setPageTitle('微信号绑定协议');
setTermsTitle('《场与微信号绑定协议》');
setTermsTitle('《场与微信号绑定协议》');
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。</span>
一、绑定服务说明
@@ -171,7 +171,7 @@ const TermsPage: React.FC = () => {
break;
default:
setPageTitle('条款和条件');
setTermsTitle('《场的条款和条件》');
setTermsTitle('《场的条款和条件》');
setTermsContent('条款内容加载中...');
}
}, []);

View File

@@ -64,7 +64,7 @@ VerificationPage
- **页面跳转**:登录成功后跳转到首页
### 协议支持
- **条款链接**:《场的条款和条件》
- **条款链接**:《场的条款和条件》
- **隐私政策**:《隐私权政策》
- **动态跳转**:支持通过 URL 参数指定协议类型

View File

@@ -66,6 +66,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
gamesNum, // 新增:获取球局数量
} = store;
const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities', ['上海市'])) || [];
const {
isShowFilterPopup,
data: matches,
@@ -92,6 +94,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
// 记录上一次加载数据时的城市,用于检测城市变化
const lastLoadedAreaRef = useRef<[string, string] | null>(null);
const prevIsActiveRef = useRef(isActive);
// 记录是否是进入列表页的第一次调用 updateUserLocation首次传 force: true
const hasUpdatedLocationRef = useRef(false);
// 处理距离筛选显示/隐藏
const handleDistanceFilterVisibleChange = useCallback(
@@ -289,9 +293,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
currentProvince,
});
// 地址发生变化或不一致,重新加载数据和球局数量
// 先调用列表接口,然后在列表接口完成后调用数量接口
(async () => {
// 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动
const delayMs = 280;
const timer = setTimeout(async () => {
try {
if (refreshBothLists) {
await refreshBothLists();
@@ -307,7 +311,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} catch (error) {
console.error("重新加载数据失败:", error);
}
})();
}, delayMs);
prevIsActiveRef.current = isActive;
return () => clearTimeout(timer);
}
}
@@ -364,7 +370,10 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
updateState({ location });
if (location && location.latitude && location.longitude) {
try {
await updateUserLocation(location.latitude, location.longitude);
// 进入列表页的第一次调用传 force: true后续调用传 false
const isFirstCall = !hasUpdatedLocationRef.current;
await updateUserLocation(location.latitude, location.longitude, isFirstCall);
hasUpdatedLocationRef.current = true;
} catch (error) {
console.error("更新用户位置失败:", error);
}
@@ -446,6 +455,17 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
});
};
// 处理重新定位
const handleRelocate = async (location) => {
try {
// 位置已更新到后端,刷新列表数据
await getMatchesData();
await fetchGetGamesCount();
} catch (error) {
console.error("刷新列表失败:", error);
}
};
const handleSearchClick = () => {
navigateTo({
url: "/game_pages/search/index",
@@ -465,7 +485,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
initDictionaryData();
}, []);
// 获取省份名称area 格式: ["中国", "省份"]
const province = area?.at(1) || "上海";
function renderCityQrcode() {
@@ -507,8 +527,12 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}
// 判定是否显示"暂无球局"页面
// 条件:省份不是上海 或 (已加载完成且球局数量为0)
const shouldShowNoGames = province !== "上海";
// 从配置接口 /parameter/many_key 获取 supported_cities格式如 "上海市||北京市"
// 当前省份在有球局城市列表中则显示列表,否则显示暂无球局
const shouldShowNoGames =
supportedCitiesList.length > 0
? !supportedCitiesList.includes(province)
: province !== "上海市"; // 配置未加载时默认按上海判断
return (
<>
@@ -559,6 +583,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
quickValue={distanceQuickFilter?.order}
districtValue={distanceQuickFilter?.district}
onMenuVisibleChange={handleDistanceFilterVisibleChange}
onRelocate={handleRelocate}
/>
</View>
</View>
@@ -602,6 +627,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
evaluateFlag
enableHomeCards
/>
</ScrollView>
</View>

View File

@@ -21,21 +21,17 @@
top: 0;
left: 0;
opacity: 0;
transform: scale(0.98);
transition: opacity 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transition: opacity 0.25s ease-out;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
pointer-events: none;
will-change: opacity, transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
visibility: hidden;
&.active {
opacity: 1;
transform: scale(1);
z-index: 1;
pointer-events: auto;
visibility: visible;
}
}

View File

@@ -67,12 +67,6 @@ const MainPage: React.FC = () => {
try {
await fetchUserInfo();
await checkNicknameChangeStatus();
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
try {
await useDictionaryStore.getState().fetchBannerDictionary();
} catch (e) {
console.error("预取 Banner 字典失败:", e);
}
} catch (error) {
console.error("获取用户信息失败:", error);
}

View File

@@ -1,10 +1,16 @@
.banner_detail_page {
min-height: 100vh;
background: #ffffff;
display: flex;
flex-direction: column;
}
.banner_detail_content {
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.banner_detail_image {
@@ -12,5 +18,3 @@
border-radius: 12px;
display: block;
}

View File

@@ -10,9 +10,8 @@
display: flex;
flex-direction: column;
align-items: center;
height: calc(100vh - 98px);
flex: 1;
position: relative;
overflow: hidden;
}
// 示例消息卡片区域
@@ -163,7 +162,6 @@
&__qr_image {
width: 100%;
height: 100%;
}
&__qr_placeholder {

View File

@@ -21,10 +21,10 @@ const OrderCheck = () => {
//TODO: get order msg from id
const handlePay = async () => {
Taro.showLoading({
title: '支付中...',
mask: true
})
// Taro.showLoading({
// title: '支付中...',
// mask: true
// })
const res = await orderService.createOrder(Number(gameId))
if (res.code === 0) {
const { payment_required, payment_params } = res.data
@@ -37,7 +37,7 @@ const OrderCheck = () => {
signType,
paySign,
success: async () => {
Taro.hideLoading()
// Taro.hideLoading()
Taro.showToast({
title: '支付成功',
icon: 'success'
@@ -48,7 +48,7 @@ const OrderCheck = () => {
})
},
fail: () => {
Taro.hideLoading()
// Taro.hideLoading()
Taro.showToast({
title: '支付失败',
icon: 'none'

View File

@@ -193,7 +193,7 @@ const NewFollow = () => {
<View className="follow-left" onClick={() => handleUserClick(item.user_id)}>
<Image
className="user-avatar" mode="aspectFill"
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
src={item.user_avatar || require("@/static/userInfo/default_avatar.svg")}
/>

View File

@@ -216,8 +216,7 @@ function Intro() {
});
}
Taro.redirectTo({
url: `/other_pages/ntrp-evaluate/index?stage=${type}${
type === StageType.RESULT ? `&id=${id}` : ""
url: `/other_pages/ntrp-evaluate/index?stage=${type}${type === StageType.RESULT ? `&id=${id}` : ""
}`,
});
}
@@ -539,18 +538,21 @@ function Result() {
const res = await evaluateService.getTestResult({ record_id: Number(id) });
if (res.code === 0) {
setResult(res.data);
// delay(1000);
setRadarData(
adjustRadarLabels(
Object.entries(res.data.radar_data.abilities).map(([key, value]) => [
const sortOrder = res.data.sort || [];
const abilities = res.data.radar_data.abilities;
const sortedKeys = sortOrder.filter((k) => k in abilities);
const remainingKeys = Object.keys(abilities).filter((k) => !sortOrder.includes(k));
const allKeys = [...sortedKeys, ...remainingKeys];
let radarData: [string, number][] = allKeys.map((key) => [
key,
Math.min(
100,
Math.floor((value.current_score / value.max_score) * 100)
Math.floor((abilities[key].current_score / abilities[key].max_score) * 100)
),
])
)
);
]);
// 直接使用接口 sort 顺序,不经过 adjustRadarLabels 重新排序
setRadarData(radarData);
updateUserLevel(res.data.record_id, res.data.ntrp_level);
}
}
@@ -698,8 +700,7 @@ function Result() {
}
const currentPage = getCurrentFullPath();
Taro.redirectTo({
url: `/login_pages/index/index${
currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
url: `/login_pages/index/index${currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
}`,
});
}

View File

@@ -51,7 +51,6 @@ class CommonApiService {
data: results.map(result => result.data)
}
} catch (error) {
throw error
} finally {
Taro.hideLoading()
}

View File

@@ -163,7 +163,7 @@ class GameDetailService {
width: number
}>> {
return httpService.post('/user/generate_qrcode', req, {
showLoading: false
showLoading: true
})
}
}

View File

@@ -58,6 +58,7 @@ export interface TestResultData {
level_img?: string; // 等级图片URL
radar_data: RadarData;
answers: Answer[];
sort?: string[]; // 雷达图能力项排序,如 ["正手球质", "正手控制", ...]
}
// 单条测试记录

View File

@@ -129,6 +129,7 @@ class HttpService {
// 隐藏loading支持多个并发请求
private hideLoading(): void {
try {
this.loadingCount = Math.max(0, this.loadingCount - 1)
// 只有所有请求都完成时才隐藏loading
@@ -146,6 +147,12 @@ class HttpService {
this.hideLoadingTimer = null
}, 800)
}
}
catch (e) {
console.warn(e)
}
}
// 处理响应

View File

@@ -134,7 +134,7 @@ export const getCityQrCode = async () => {
}
// 获取行政区列表
export const getDistricts = async (params: { country: string; state: string }) => {
export const getDistricts = async (params: { province: string; city: string }) => {
try {
// 调用HTTP服务获取行政区列表
return httpService.post('/cities/cities', params)

View File

@@ -2,7 +2,7 @@ import { UserInfo } from "@/components/UserInfo";
import { API_CONFIG } from "@/config/api";
import httpService, { ApiResponse } from "./httpService";
import uploadFiles from "./uploadFiles";
import Taro from "@tarojs/taro";
import * as Taro from "@tarojs/taro";
import getCurrentConfig from "@/config/env";
import { clear_login_state } from "@/services/loginService";
@@ -740,12 +740,14 @@ export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
// 更新用户坐标位置
export const updateUserLocation = async (
latitude: number,
longitude: number
longitude: number,
force: boolean = false
) => {
try {
const response = await httpService.post("/user/update_location", {
latitude,
longitude,
force
});
return response;
} catch (error) {

View File

@@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8289_53268)">
<path d="M10.9775 4.82657H8.04862C7.97102 4.82657 7.89658 4.7958 7.84163 4.74101C7.78668 4.68622 7.7557 4.61188 7.75547 4.53428V4.36457C7.75497 4.32525 7.76254 4.28625 7.77773 4.24998C7.79292 4.21371 7.81539 4.18094 7.84376 4.15371L8.88604 3.11143C8.50894 2.72825 8.05949 2.4238 7.56377 2.21574C7.06805 2.00768 6.53594 1.90016 5.99833 1.89943C5.1996 1.90138 4.41883 2.13662 3.75196 2.57623C3.08509 3.01584 2.56116 3.64067 2.24453 4.37397C1.92791 5.10727 1.83238 5.91708 1.9697 6.70392C2.10701 7.49077 2.47118 8.22036 3.01746 8.80307C3.56375 9.38578 4.26835 9.79622 5.0447 9.98397C5.82106 10.1717 6.63535 10.1286 7.38753 9.8599C8.13971 9.59121 8.79702 9.10864 9.2787 8.47149C9.76039 7.83435 10.0455 7.07037 10.0989 6.27343C10.1075 6.11828 10.236 5.99743 10.3912 5.99743H10.9775C11.0574 6.00001 11.1331 6.03387 11.1883 6.09171C11.2415 6.15 11.2698 6.22885 11.2638 6.30771C11.1937 7.51221 10.7124 8.6562 9.90046 9.54861C9.08847 10.441 7.99488 11.0278 6.80233 11.211C5.60978 11.3942 4.39049 11.1627 3.34808 10.5551C2.30568 9.94759 1.50328 9.00078 1.0749 7.87285C0.646012 6.74568 0.616794 5.50548 0.992127 4.35935C1.36746 3.21323 2.12463 2.23056 3.13719 1.57543C4.15001 0.920266 5.35687 0.632222 6.55641 0.759351C7.75595 0.88648 8.87562 1.42109 9.72862 2.274L10.6012 1.40143C10.6279 1.37384 10.6599 1.35189 10.6952 1.33687C10.7305 1.32186 10.7685 1.31408 10.8069 1.314H10.9766C11.0541 1.31422 11.1283 1.34509 11.183 1.39986C11.2378 1.45462 11.2687 1.52883 11.2689 1.60628V4.53428C11.2687 4.61173 11.2378 4.68595 11.183 4.74071C11.1283 4.79548 11.0549 4.82634 10.9775 4.82657Z" fill="#A6A6A6"/>
</g>
<defs>
<clipPath id="clip0_8289_53268">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -20,7 +20,6 @@ interface DictionaryState {
bannerDetailImage: string
bannerListIndex: string
} | null
fetchBannerDictionary: () => Promise<void>
}
// 创建字典Store
@@ -36,20 +35,32 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
set({ isLoading: true, error: null })
try {
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip';
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip,supported_cities,bannerListImage,bannerDetailImage,bannerListIndex';
const response = await commonApi.getDictionaryManyKey(keys)
if (response.code === 0 && response.data) {
const dictionaryData = {};
keys.split(',').forEach(key => {
const list = response.data[key];
const listData = list.split('|');
// supported_cities 格式如 "上海市||北京市",用 || 分割
const listData = key === 'supported_cities'
? (list ? String(list).split('||').map((s) => s.trim()).filter(Boolean) : [])
: (list ? list.split('|') : []);
dictionaryData[key] = listData;
})
set({
dictionaryData: dictionaryData || {},
isLoading: false
})
set({
bannerDict: {
bannerListImage: response.data.bannerListImage || '',
bannerDetailImage: response.data.bannerDetailImage || '',
bannerListIndex: (response.data.bannerListIndex ?? '').toString(),
}
})
console.log('字典数据获取成功:', response.data)
} else {
throw new Error(response.message || '获取字典数据失败')
@@ -64,26 +75,7 @@ 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) => {

View File

@@ -11,8 +11,6 @@ import {
getCityQrCode,
getDistricts,
} from "../services/listApi";
// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取
import { useDictionaryStore } from "./dictionaryStore";
import {
ListActions,
IFilterOptions,
@@ -20,26 +18,6 @@ import {
IPayload,
} 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) {
return dataTree.map((item) => {
const { children, ...rest } = item;
@@ -216,18 +194,17 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const { distanceFilter, order, district } = distanceQuickFilter || {};
// 始终使用 state.area确保所有接口使用一致的城市参数
const areaProvince = state.area?.at(1) || "";
const areaProvince = state.area?.at(0) || "";
const areaCity = state.area?.at(1) || "";
const last_location_province = areaProvince;
// city 参数逻辑:
// 1. 如果选择了行政区district 有值使用行政区的名称label
// 2. 如果是"全城"distanceFilter 为空),不传 city
let city: string | undefined = undefined;
let county: string | undefined = undefined;
if (district) {
// 从 districts 数组中查找对应的行政区名称
const selectedDistrict = state.districts.find(item => item.value === district);
if (selectedDistrict) {
city = selectedDistrict.label; // 传递行政区名称,如"静安"
county = selectedDistrict.label; // 传递行政区名称,如"静安"
}
}
// 如果是"全城"distanceFilter 为空city 保持 undefined不会被传递
@@ -246,11 +223,12 @@ export const useListStore = create<TennisStore>()((set, get) => ({
distanceFilter: distanceFilter,
// 显式设置 province确保始终使用 state.area 中的最新值
province: last_location_province, // 始终使用 state.area 中的 province确保城市参数一致
city: areaCity,
};
// 只在有值时添加 city 参数
if (city) {
searchOption.city = city;
if (county) {
searchOption.county = county;
}
const params = {
@@ -272,14 +250,11 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const currentData = currentPageState?.data || [];
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
// 从字典缓存获取 banner并将其插入到最终列表指定位置全局索引
const dictData = useDictionaryStore.getState().bannerDict;
const processedData = dictData ? insertBannersToRows(newData, dictData) : newData;
state.updateCurrentPageState({
data: processedData,
data: newData,
isHasMoreData,
// 使用插入后的最终数据判断是否显示空状态,避免有 banner 时仍显示空
isShowNoData: processedData?.length === 0,
isShowNoData: newData?.length === 0,
});
set({
@@ -729,18 +704,18 @@ export const useListStore = create<TennisStore>()((set, get) => ({
async getDistricts() {
try {
const state = get();
// 从 area 中获取省份area 格式: ["中国", 省份, 城市]
const country = "中国";
const province = state.area?.at(1) || "上海"; // area[1] 是省份
// 从 area 中获取省份area 格式: [ 省份, 城市]
const province = state.area?.at(0) || "上海";
const cn_city = state.area?.at(1) || "上海"; // area[1] 是省份
const res = await getDistricts({
country,
state: province
province,
city: cn_city
});
if (res.code === 0 && res.data) {
const districts = res.data.map((item) => ({
label: item.cn_city,
label: item.cn_county,
value: item.id.toString(),
id: item.id,
}));

View File

@@ -36,6 +36,7 @@ const getTimeNextDate = (time: string) => {
// 请求锁,避免重复调用
let isFetchingLastTestResult = false;
let isCheckingNicknameStatus = false;
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
export const useUser = create<UserState>()((set) => ({
user: {},
@@ -47,9 +48,11 @@ export const useUser = create<UserState>()((set) => ({
// 优先使用缓存中的城市,不使用用户信息中的位置
// 检查是否有缓存的城市
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");
@@ -60,16 +63,13 @@ export const useUser = create<UserState>()((set) => ({
if (userData?.last_location_province) {
const listStore = useListStore.getState();
const currentArea = listStore.area;
// 只有当 area 不存在时才使用用户信息中的位置
if (!currentArea) {
const newArea: [string, string] = ["中国", userData.last_location_province];
const newArea: [string, string] = [userData.last_location_province||"", userData.last_location_city||""];
listStore.updateArea(newArea);
// 保存到缓存
try {
(Taro as any).setStorageSync?.(CITY_CACHE_KEY, newArea);
} catch (error) {
console.error("保存城市缓存失败:", error);
}
useUser.getState().updateCache(newArea);
}
}
@@ -79,6 +79,16 @@ export const useUser = create<UserState>()((set) => ({
return undefined;
}
},
// 更新缓存
updateCache: async (newArea: [string, string]) => {
try {
(Taro as any).setStorageSync?.(CITY_CACHE_KEY, newArea);
} catch (error) {
console.error("保存城市缓存失败:", error);
}
},
updateUserInfo: async (userInfo: Partial<UserInfoType>) => {
try {
// 先更新后端
@@ -93,7 +103,7 @@ export const useUser = create<UserState>()((set) => ({
const currentArea = listStore.area;
// 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新
if (!currentArea || currentArea[1] !== userInfo.last_location_province) {
const newArea: [string, string] = ["中国", userInfo.last_location_province];
const newArea: [string, string] = [userInfo.last_location_province || "", userInfo.last_location_city || ""];
listStore.updateArea(newArea);
}
}
@@ -195,6 +205,7 @@ export const useNicknameChangeStatus = () =>
export const useUserActions = () =>
useUser((state) => ({
fetchUserInfo: state.fetchUserInfo,
updateCache: state.updateCache,
updateUserInfo: state.updateUserInfo,
checkNicknameChangeStatus: state.checkNicknameChangeStatus,
updateNickname: state.updateNickname,