diff --git a/README.md b/README.md index f6aa878..e8e5643 100644 --- a/README.md +++ b/README.md @@ -148,4 +148,8 @@ src/ ## License -MIT \ No newline at end of file +MIT + +"appid": "wx915ecf6c01bea4ec", + + "appid": "wx815b533167eb7b53", \ No newline at end of file diff --git a/package.json b/package.json index f5238d4..5143872 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/project.config.json b/project.config.json index fcd025f..c5e1eb5 100644 --- a/project.config.json +++ b/project.config.json @@ -3,6 +3,7 @@ "projectname": "playBallTogether", "description": "playBallTogether", "appid": "wx915ecf6c01bea4ec", + "setting": { "urlCheck": true, "es6": true, diff --git a/project.private.config.json b/project.private.config.json index 0602fbc..ac01ff4 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -15,9 +15,10 @@ "useStaticServer": false, "useLanDebug": false, "showES6CompileOption": false, - "compileHotReLoad": false, + "compileHotReLoad": true, "checkInvalidKey": true, "ignoreDevUnusedFiles": true, - "bigPackageSizeSupport": true + "bigPackageSizeSupport": true, + "useIsolateContext": true } } \ No newline at end of file diff --git a/src/components/DistanceQuickFilterV2/index.scss b/src/components/DistanceQuickFilterV2/index.scss index eb926ee..e6342fb 100644 --- a/src/components/DistanceQuickFilterV2/index.scss +++ b/src/components/DistanceQuickFilterV2/index.scss @@ -85,6 +85,10 @@ font-size: 13px; font-weight: 400; color: #3c3c43; + display: flex; + flex-direction: row; + align-items: center; + gap:4px; } .distanceWrap { diff --git a/src/components/DistanceQuickFilterV2/index.tsx b/src/components/DistanceQuickFilterV2/index.tsx index 3411faf..c6aa7a0 100644 --- a/src/components/DistanceQuickFilterV2/index.tsx +++ b/src/components/DistanceQuickFilterV2/index.tsx @@ -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([]); const [isMenuOpen, setIsMenuOpen] = useState(false); - const [keys, setKeys] = useState(0); + 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={} >
-

当前位置

-

{currentCity}

+

{currentCity}

+

+ + 重新定位 +

{ 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 => { try { const current_time = Date.now(); - + // 检查是否在2小时内切换过城市 const city_change_time = (Taro as any).getStorageSync(CITY_CHANGE_TIME_KEY); if (city_change_time) { @@ -127,13 +127,13 @@ const HomeNavbar = (props: IProps) => { (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) { @@ -141,7 +141,7 @@ const HomeNavbar = (props: IProps) => { (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; @@ -158,7 +158,7 @@ const HomeNavbar = (props: IProps) => { console.log('[HomeNavbar] 用户在2小时内已选择"继续浏览"或切换过城市,不显示弹窗'); return; } - + console.log('[HomeNavbar] 准备显示定位确认弹窗,隐藏 GuideBar'); setLocationDialogData({ detectedProvince: detectedLocation, cachedCity }); setLocationDialogVisible(true); @@ -172,13 +172,13 @@ const HomeNavbar = (props: IProps) => { useEffect(() => { // 1. 优先尝试从缓存中读取上次的定位信息 const cachedCity = (Taro as any).getStorageSync(CITY_CACHE_KEY); - + if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) { // 如果有缓存的定位信息,使用缓存 const cachedCityArray = cachedCity as [string, string]; console.log("[HomeNavbar] 使用缓存的定位城市:", cachedCityArray); updateArea(cachedCityArray); - + // 如果用户详情中有位置信息,且与缓存不一致,检查是否需要弹窗 if (detectedLocation && cachedCityArray[1] !== detectedLocation) { // 检查时间缓存,如果没有或过期,则弹出选择框 @@ -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); @@ -263,10 +263,10 @@ const HomeNavbar = (props: IProps) => { // 处理定位弹窗确认 const handleLocationDialogConfirm = () => { if (!locationDialogData) return; - + 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); @@ -279,13 +279,13 @@ const HomeNavbar = (props: IProps) => { console.error('保存城市切换时间失败:', error); } console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince); - + // 关闭弹窗 setLocationDialogVisible(false); setLocationDialogData(null); // 关闭弹窗时显示 GuideBar setShowGuideBar(true); - + // 刷新数据 handleCityChangeWithoutCache(); }; @@ -293,11 +293,11 @@ const HomeNavbar = (props: IProps) => { // 处理定位弹窗取消(用户选择"继续浏览") const handleLocationDialogCancel = () => { if (!locationDialogData) return; - + const { cachedCity } = locationDialogData; // 用户选择"继续浏览",保持缓存的定位城市 console.log("保持缓存的定位城市:", cachedCity[1]); - + // 记录用户选择"继续浏览"的时间戳,2小时内不再提示 try { const current_time = Date.now(); @@ -306,7 +306,7 @@ const HomeNavbar = (props: IProps) => { } catch (error) { console.error('保存定位弹窗关闭时间失败:', error); } - + // 关闭弹窗 setLocationDialogVisible(false); setLocationDialogData(null); @@ -321,7 +321,7 @@ const HomeNavbar = (props: IProps) => { if (cityPopupVisible) { setCityPopupVisible(false); } - + const currentPagePath = getCurrentFullPath(); if (currentPagePath === "/game_pages/searchResult/index") { (Taro as any).navigateBack(); @@ -338,7 +338,7 @@ const HomeNavbar = (props: IProps) => { if (cityPopupVisible) { setCityPopupVisible(false); } - + // 如果当前在列表页,点击后页面回到顶部 if (getCurrentFullPath() === "/main_pages/index") { // 使用父组件传递的滚动方法(适配 ScrollView) @@ -363,7 +363,7 @@ const HomeNavbar = (props: IProps) => { if (cityPopupVisible) { setCityPopupVisible(false); } - + if (leftIconClick) { leftIconClick(); } else { @@ -397,10 +397,10 @@ const HomeNavbar = (props: IProps) => { const handleCityChange = async (_newArea: any) => { // 用户手动选择的城市保存到缓存 console.log("用户手动选择城市,更新缓存:", _newArea); - + // 先更新 area 状态(用于界面显示和接口参数) updateArea(_newArea); - + // 保存城市到缓存 try { (Taro as any).setStorageSync(CITY_CACHE_KEY, _newArea); @@ -411,7 +411,7 @@ const HomeNavbar = (props: IProps) => { } catch (error) { console.error("保存城市缓存失败:", error); } - + // 先调用列表接口(会使用更新后的 state.area) if (refreshBothLists) { await refreshBothLists(); @@ -481,9 +481,8 @@ const HomeNavbar = (props: IProps) => { {/* 搜索导航 */} {!showTitle && ( - 手机号验证码登录 + 手机号快捷登录 {/* 用户协议复选框 */} @@ -224,13 +224,13 @@ const LoginPage: React.FC = () => { className="terms_link" onClick={() => handle_view_terms("terms")} > - 《开场的条款和条件》 + 《有场的条款和条件》 handle_view_terms("binding")} > - 《开场与微信号绑定协议》 + 《有场与微信号绑定协议》 { className="terms_item" onClick={() => handle_view_terms("terms")} > - 《开场的条款和条件》 + 《有场的条款和条件》 handle_view_terms("binding")} > - 《开场与微信号绑定协议》 + 《有场与微信号绑定协议》 { // 获取页面参数 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(`欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。 一、服务内容 @@ -69,7 +69,7 @@ const TermsPage: React.FC = () => { break; case 'binding': setPageTitle('微信号绑定协议'); - setTermsTitle('《开场与微信号绑定协议》'); + setTermsTitle('《有场与微信号绑定协议》'); setTermsContent(`欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。 一、绑定服务说明 @@ -171,7 +171,7 @@ const TermsPage: React.FC = () => { break; default: setPageTitle('条款和条件'); - setTermsTitle('《开场的条款和条件》'); + setTermsTitle('《有场的条款和条件》'); setTermsContent('条款内容加载中...'); } }, []); diff --git a/src/login_pages/verification/README.md b/src/login_pages/verification/README.md index 24d9dd6..69cd099 100644 --- a/src/login_pages/verification/README.md +++ b/src/login_pages/verification/README.md @@ -64,7 +64,7 @@ VerificationPage - **页面跳转**:登录成功后跳转到首页 ### 协议支持 -- **条款链接**:《开场的条款和条件》 +- **条款链接**:《有场的条款和条件》 - **隐私政策**:《隐私权政策》 - **动态跳转**:支持通过 URL 参数指定协议类型 diff --git a/src/main_pages/components/ListPageContent.tsx b/src/main_pages/components/ListPageContent.tsx index 986fbf7..641fa63 100644 --- a/src/main_pages/components/ListPageContent.tsx +++ b/src/main_pages/components/ListPageContent.tsx @@ -66,6 +66,8 @@ const ListPageContent: React.FC = ({ gamesNum, // 新增:获取球局数量 } = store; + const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities', ['上海市'])) || []; + const { isShowFilterPopup, data: matches, @@ -92,6 +94,8 @@ const ListPageContent: React.FC = ({ // 记录上一次加载数据时的城市,用于检测城市变化 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 = ({ currentProvince, }); - // 地址发生变化或不一致,重新加载数据和球局数量 - // 先调用列表接口,然后在列表接口完成后调用数量接口 - (async () => { + // 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动 + const delayMs = 280; + const timer = setTimeout(async () => { try { if (refreshBothLists) { await refreshBothLists(); @@ -307,7 +311,9 @@ const ListPageContent: React.FC = ({ } catch (error) { console.error("重新加载数据失败:", error); } - })(); + }, delayMs); + prevIsActiveRef.current = isActive; + return () => clearTimeout(timer); } } @@ -364,7 +370,10 @@ const ListPageContent: React.FC = ({ 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 = ({ }); }; + // 处理重新定位 + 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 = ({ initDictionaryData(); }, []); - // 获取省份名称(area 格式: ["中国", "省份"]) + const province = area?.at(1) || "上海"; function renderCityQrcode() { @@ -507,8 +527,12 @@ const ListPageContent: React.FC = ({ } // 判定是否显示"暂无球局"页面 - // 条件:省份不是上海 或 (已加载完成且球局数量为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 = ({ quickValue={distanceQuickFilter?.order} districtValue={distanceQuickFilter?.district} onMenuVisibleChange={handleDistanceFilterVisibleChange} + onRelocate={handleRelocate} /> @@ -602,6 +627,7 @@ const ListPageContent: React.FC = ({ reload={refreshMatches} loadMoreMatches={loadMoreMatches} evaluateFlag + enableHomeCards /> diff --git a/src/main_pages/index.scss b/src/main_pages/index.scss index 2e8d1a1..dc19c1e 100644 --- a/src/main_pages/index.scss +++ b/src/main_pages/index.scss @@ -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; } } diff --git a/src/main_pages/index.tsx b/src/main_pages/index.tsx index b71b954..114d516 100644 --- a/src/main_pages/index.tsx +++ b/src/main_pages/index.tsx @@ -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); } diff --git a/src/other_pages/bannerDetail/index.scss b/src/other_pages/bannerDetail/index.scss index e29887d..79a4b85 100644 --- a/src/other_pages/bannerDetail/index.scss +++ b/src/other_pages/bannerDetail/index.scss @@ -1,16 +1,20 @@ .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 { width: 100%; border-radius: 12px; display: block; -} - - +} \ No newline at end of file diff --git a/src/other_pages/enable_notification/index.scss b/src/other_pages/enable_notification/index.scss index 4eada00..b2e1ca5 100644 --- a/src/other_pages/enable_notification/index.scss +++ b/src/other_pages/enable_notification/index.scss @@ -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 { diff --git a/src/other_pages/favorites/index.tsx b/src/other_pages/favorites/index.tsx index 94c8903..db08552 100644 --- a/src/other_pages/favorites/index.tsx +++ b/src/other_pages/favorites/index.tsx @@ -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' diff --git a/src/other_pages/new_follow/index.tsx b/src/other_pages/new_follow/index.tsx index 61eec71..c1932f9 100644 --- a/src/other_pages/new_follow/index.tsx +++ b/src/other_pages/new_follow/index.tsx @@ -193,7 +193,7 @@ const NewFollow = () => { handleUserClick(item.user_id)}> diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx index 38cde35..33303e9 100644 --- a/src/other_pages/ntrp-evaluate/index.tsx +++ b/src/other_pages/ntrp-evaluate/index.tsx @@ -216,9 +216,8 @@ 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]) => [ - key, - Math.min( - 100, - Math.floor((value.current_score / value.max_score) * 100) - ), - ]) - ) - ); + + 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((abilities[key].current_score / abilities[key].max_score) * 100) + ), + ]); + // 直接使用接口 sort 顺序,不经过 adjustRadarLabels 重新排序 + setRadarData(radarData); updateUserLevel(res.data.record_id, res.data.ntrp_level); } } @@ -698,13 +700,12 @@ 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)}` : "" + }`, }); } - function handleGo() {} + function handleGo() { } return ( diff --git a/src/services/commonApi.ts b/src/services/commonApi.ts index d0b4e73..135264c 100644 --- a/src/services/commonApi.ts +++ b/src/services/commonApi.ts @@ -51,7 +51,6 @@ class CommonApiService { data: results.map(result => result.data) } } catch (error) { - throw error } finally { Taro.hideLoading() } diff --git a/src/services/detailService.ts b/src/services/detailService.ts index b5cdd9c..6c5fd6c 100644 --- a/src/services/detailService.ts +++ b/src/services/detailService.ts @@ -163,7 +163,7 @@ class GameDetailService { width: number }>> { return httpService.post('/user/generate_qrcode', req, { - showLoading: false + showLoading: true }) } } diff --git a/src/services/evaluateService.ts b/src/services/evaluateService.ts index dab032d..d7c3778 100644 --- a/src/services/evaluateService.ts +++ b/src/services/evaluateService.ts @@ -58,6 +58,7 @@ export interface TestResultData { level_img?: string; // 等级图片URL radar_data: RadarData; answers: Answer[]; + sort?: string[]; // 雷达图能力项排序,如 ["正手球质", "正手控制", ...] } // 单条测试记录 diff --git a/src/services/httpService.ts b/src/services/httpService.ts index 664f331..d86885d 100644 --- a/src/services/httpService.ts +++ b/src/services/httpService.ts @@ -129,23 +129,30 @@ class HttpService { // 隐藏loading(支持多个并发请求) private hideLoading(): void { - this.loadingCount = Math.max(0, this.loadingCount - 1) + try { + this.loadingCount = Math.max(0, this.loadingCount - 1) - // 只有所有请求都完成时才隐藏loading - if (this.loadingCount === 0) { - // 清除之前的延时器 - if (this.hideLoadingTimer) { - clearTimeout(this.hideLoadingTimer) - this.hideLoadingTimer = null + // 只有所有请求都完成时才隐藏loading + if (this.loadingCount === 0) { + // 清除之前的延时器 + if (this.hideLoadingTimer) { + clearTimeout(this.hideLoadingTimer) + this.hideLoadingTimer = null + } + + // 延时300ms后隐藏loading,避免频繁切换 + this.hideLoadingTimer = setTimeout(() => { + Taro.hideLoading() + this.currentLoadingText = '' + this.hideLoadingTimer = null + }, 800) } - // 延时300ms后隐藏loading,避免频繁切换 - this.hideLoadingTimer = setTimeout(() => { - Taro.hideLoading() - this.currentLoadingText = '' - this.hideLoadingTimer = null - }, 800) } + catch (e) { + console.warn(e) + } + } // 处理响应 @@ -175,7 +182,7 @@ class HttpService { url: '/login_pages/index/index' }) reject(new Error('用户不存在')) - return response.data + return response.data } @@ -187,7 +194,7 @@ class HttpService { } else { reject(response.data) } - return response.data + return response.data } } diff --git a/src/services/listApi.ts b/src/services/listApi.ts index 2210850..7a7cc26 100644 --- a/src/services/listApi.ts +++ b/src/services/listApi.ts @@ -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) diff --git a/src/services/userService.ts b/src/services/userService.ts index 2309e80..e1bc150 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -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) => { // 更新用户坐标位置 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) { diff --git a/src/static/list/icon-relocate.svg b/src/static/list/icon-relocate.svg new file mode 100644 index 0000000..8100302 --- /dev/null +++ b/src/static/list/icon-relocate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/store/dictionaryStore.ts b/src/store/dictionaryStore.ts index 7d8995e..8f361d2 100644 --- a/src/store/dictionaryStore.ts +++ b/src/store/dictionaryStore.ts @@ -20,7 +20,6 @@ interface DictionaryState { bannerDetailImage: string bannerListIndex: string } | null - fetchBannerDictionary: () => Promise } // 创建字典Store @@ -36,20 +35,32 @@ export const useDictionaryStore = create()((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()((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) => { diff --git a/src/store/listStore.ts b/src/store/listStore.ts index 128589e..d5d2392 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -11,8 +11,6 @@ import { getCityQrCode, getDistricts, } from "../services/listApi"; -// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取 -import { useDictionaryStore } from "./dictionaryStore"; import { ListActions, IFilterOptions, @@ -20,40 +18,20 @@ 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; // 只保留两级:国家和省份,去掉第三级(区域) - const processedChildren = children?.length > 0 + const processedChildren = children?.length > 0 ? children.map(child => ({ - ...child, - text: child.name, - label: child.name, - value: child.name, - children: null, // 去掉第三级 - })) + ...child, + text: child.name, + label: child.name, + value: child.name, + children: null, // 去掉第三级 + })) : null; - + return { ...rest, text: rest.name, @@ -214,20 +192,19 @@ export const useListStore = create()((set, get) => ({ // 全城和快捷筛选 const distanceQuickFilter = currentPageState?.distanceQuickFilter || {}; 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,12 +223,13 @@ export const useListStore = create()((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 = { pageOption: currentPageState?.pageOption, @@ -272,14 +250,11 @@ export const useListStore = create()((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({ @@ -374,7 +349,7 @@ export const useListStore = create()((set, get) => ({ try { const searchParams = getSearchParams() || {}; - + // 并发请求:常规列表、智能排序列表 const [listResSettled, integrateResSettled] = await Promise.allSettled([ getGamesList({ @@ -447,7 +422,7 @@ export const useListStore = create()((set, get) => ({ const state = get(); const { getSearchParams } = state; const searchParams = getSearchParams() || {}; - + // 使用和 games/integrate_list 相同的参数构建逻辑 const params = { ...searchParams, @@ -457,7 +432,7 @@ export const useListStore = create()((set, get) => ({ isRefresh: true, // 和 integrate_list 保持一致 }, }; - + console.log("fetchGetGamesCount 参数:", { area: state.area, params: JSON.stringify(params) }); const resData = (await getGamesCount(params)) || {}; const gamesNum = resData?.data?.count || 0; @@ -551,7 +526,7 @@ export const useListStore = create()((set, get) => ({ const state = get(); const { currentPageState } = state.getCurrentPageState(); const filterOptions = { ...currentPageState?.filterOptions, ...payload }; - + // 计算筛选数量:排除 dateRange、ntrp 默认值,以及空数组和空字符串 const filterCount = Object.entries(filterOptions).filter(([key, value]) => { if (key === 'dateRange') return false; // 日期区间不算筛选 @@ -572,7 +547,7 @@ export const useListStore = create()((set, get) => ({ filterCount, pageOption: defaultPageOption, }); - + // 使用 Promise.resolve 确保状态更新后再调用接口 // 先调用列表接口,然后在列表接口完成后调用数量接口 Promise.resolve().then(async () => { @@ -590,7 +565,7 @@ export const useListStore = create()((set, get) => ({ const { currentPageState } = state.getCurrentPageState(); const { distanceQuickFilter } = currentPageState || {}; const newDistanceQuickFilter = { ...distanceQuickFilter, ...payload }; - + // 先更新状态 state.updateCurrentPageState({ distanceQuickFilter: newDistanceQuickFilter, @@ -729,18 +704,18 @@ export const useListStore = create()((set, get) => ({ async getDistricts() { try { const state = get(); - // 从 area 中获取省份,area 格式: ["中国", 省份, 城市] - const country = "中国"; - const province = state.area?.at(1) || "上海"; // area[1] 是省份 - - const res = await getDistricts({ - country, - state: province + // 从 area 中获取省份,area 格式: [ 省份, 城市] + const province = state.area?.at(0) || "上海"; + const cn_city = state.area?.at(1) || "上海市"; // area[1] 是省份 + + const res = await getDistricts({ + 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, })); diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 1b35edd..b7f839f 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -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()((set) => ({ user: {}, @@ -44,41 +45,50 @@ export const useUser = create()((set) => ({ const res = await fetchUserProfile(); const userData = res.data; set({ user: userData }); - + // 优先使用缓存中的城市,不使用用户信息中的位置 // 检查是否有缓存的城市 - const CITY_CACHE_KEY = "USER_SELECTED_CITY"; + const cachedCity = (Taro as any).getStorageSync?.(CITY_CACHE_KEY); - + + + if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) { // 如果有缓存的城市,使用缓存,不更新 area console.log("[userStore] 检测到缓存的城市,使用缓存,不更新 area"); return userData; } - + // 只有当没有缓存时,才使用用户信息中的位置 if (userData?.last_location_province) { const listStore = useListStore.getState(); const currentArea = listStore.area; + // 只有当 area 不存在时才使用用户信息中的位置 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); } } - + return userData; } catch (error) { console.error("获取用户信息失败:", error); 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) => { try { // 先更新后端 @@ -86,18 +96,18 @@ export const useUser = create()((set) => ({ // 然后立即更新本地状态(乐观更新) set((state) => { const newUser = { ...state.user, ...userInfo }; - + // 当 userLastLocationProvince 更新时,同步更新 area if (userInfo.last_location_province) { const listStore = useListStore.getState(); 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); } } - + return { user: newUser }; }); // 不再每次都重新获取完整用户信息,减少请求次数 @@ -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, diff --git a/src/utils/share.ts b/src/utils/share.ts index 1f2cb87..8e60115 100644 --- a/src/utils/share.ts +++ b/src/utils/share.ts @@ -534,23 +534,23 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize) drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000') - try { - const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null - if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') { - wxAny.canvasToTempFilePath({ - canvas: offscreen, - fileType: 'png', - quality: 1, - success: (res: any) => { - console.log('===res666', res) - resolve(res.tempFilePath) - }, - fail: reject - }) - return - } - } catch { } - reject(new Error('无法导出图片(OffscreenCanvas 转文件失败)')) + try { + const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null + if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') { + wxAny.canvasToTempFilePath({ + canvas: offscreen, + fileType: 'png', + quality: 1, + success: (res: any) => { + console.log('===res666', res) + resolve(res.tempFilePath) + }, + fail: reject + }) + return + } + } catch { } + reject(new Error('无法导出图片(OffscreenCanvas 转文件失败)')) console.log('Canvas绘制命令已发送') } catch (error) {