diff --git a/app/(tabs)/top.tsx b/app/(tabs)/top.tsx index d0923ee..e6d8015 100644 --- a/app/(tabs)/top.tsx +++ b/app/(tabs)/top.tsx @@ -5,14 +5,23 @@ import ClassifyModal from '@/components/owner/classify'; import LocationModal from '@/components/owner/location'; import PodiumComponent from '@/components/owner/podium'; import RankList from '@/components/owner/rankList'; +import { findInnermostElement } from '@/components/owner/utils'; import { ThemedText } from '@/components/ThemedText'; +import { convertRegions } from '@/components/utils/cascaderData'; import { transformData } from '@/components/utils/objectToCascader'; import { fetchApi } from '@/lib/server-api-util'; import { GroupedData, RankingItem, TargetItem } from '@/types/user'; import { useRouter } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { LayoutChangeEvent, StyleSheet, TouchableOpacity, View } from 'react-native'; +import * as SecureStore from 'expo-secure-store'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { LayoutChangeEvent, Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from "react-native-safe-area-context"; +interface LocationData { + id: number; + name: string; + children: LocationData[]; +} + export default function OwnerPage() { const insets = useSafeAreaInsets(); const router = useRouter(); @@ -20,7 +29,8 @@ export default function OwnerPage() { const [locationModalVisible, setLocationModalVisible] = useState(false); // 分类弹窗 const [classifyModalVisible, setClassifyModalVisible] = useState(false); - + // 地区数据 + const [locationData, setLocationData] = useState([]); // 在组件内部添加: const podiumRef = useRef(null); const [podiumPosition, setPodiumPosition] = useState({ x: 0, y: 0, width: 0, height: 0 }); @@ -28,6 +38,7 @@ export default function OwnerPage() { const [classify, setClassify] = useState([]); const getClassify = () => { fetchApi("/title-tags").then((res: GroupedData) => { + setSelectedClassify([transformData(res)?.[0]?.children?.[0]]); setClassify(transformData(res)); }); } @@ -51,7 +62,6 @@ export default function OwnerPage() { }; // 地区选择 const handleLocationChange = useCallback((selectedItems: CascaderItem[]) => { - console.log('SelectedLocation:', selectedItems); if (selectedItems.length > 0) { const lastItem = selectedItems[selectedItems.length - 1]; // 只有当选择完成时才更新状态 @@ -63,7 +73,6 @@ export default function OwnerPage() { // 分类选择 const handleClassifyChange = useCallback((selectedItems: CascaderItem[]) => { - console.log('SelectedClassify:', selectedItems); if (selectedItems.length > 0) { const lastItem = selectedItems[selectedItems.length - 1]; // 只有当选择完成时才更新状态 @@ -72,15 +81,26 @@ export default function OwnerPage() { } } }, []); + // 获取本地存储的地址信息 + const getLocation = async () => { + let location; + if (Platform.OS === 'web') { + location = localStorage.getItem('location'); + } else { + location = await SecureStore.getItemAsync('location'); + + } + return location; + }; // 获取排名信息 const [ranking, setRanking] = useState([]); const getRanking = () => { fetchApi("/title-rank", { method: "POST", body: JSON.stringify({ - "title_tag_id": selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].value : 3, - "area_id": 1 + "title_tag_id": selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].value : null, + "area_id": selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].value : null }) }).then((res) => { setRanking(res); @@ -89,13 +109,61 @@ export default function OwnerPage() { // 当用户选择发生变化时,重新获取排名 useEffect(() => { - getRanking(); + console.log('selectedLocation', selectedLocation); + console.log('selectedClassify', selectedClassify); + if (selectedLocation?.length > 0 && selectedClassify?.length > 0) { + getRanking(); + } }, [selectedLocation, selectedClassify]) // 初始化获取分类 useEffect(() => { - getClassify(); + const start = async () => { + await getClassify(); + } + start(); }, []) + const fetchLocationData = useMemo(() => async () => { + try { + const res = await fetchApi("/area/tree"); + const transformed = convertRegions(res?.children, { + nameKey: 'name', // 源数据中表示"名称"的字段 + valueKey: 'id', // 源数据中作为 value 的字段 + regionsKey: 'children', // 源数据中表示"子级区域"的字段 + childrenKey: 'children' // 输出结构中表示"子级区域"的字段 + }); + return transformed; + } catch (error) { + return []; + } + }, []); + + useEffect(() => { + let isMounted = true; + + const loadLocationData = async () => { + const data = await fetchLocationData(); + if (isMounted) { + setLocationData(data); + // 获取本地存储的地址信息 + const location = await getLocation(); + const xuhuiElement = findInnermostElement(data?.filter((item) => { + return item.name === JSON.parse(location || "").city + }) || [], JSON.parse(location || "").district); + if (location) { + console.log("xuhuiElement", xuhuiElement); + + setSelectedLocation([xuhuiElement]); + } + } + }; + + loadLocationData(); + + return () => { + isMounted = false; + }; + }, [fetchLocationData]); return ( @@ -149,6 +217,7 @@ export default function OwnerPage() { setModalVisible={setLocationModalVisible} podiumPosition={podiumPosition} handleChange={handleLocationChange} + data={locationData} /> {/* 分类选择弹窗 */} { setModalVisible(false)} - swipeDirection="right" // 支持向右滑动关闭 + swipeDirection={['right']} // 改为数组形式 propagateSwipe={true} - animationIn="slideInRight" // 入场动画 - animationOut="slideOutRight" // 出场动画 + animationIn="slideInRight" + animationOut="slideOutRight" backdropOpacity={0.5} onSwipeComplete={() => setModalVisible(false)} - style={{ margin: 0, justifyContent: 'flex-start', marginTop: podiumPosition.height + podiumPosition.y }} + onModalHide={() => { + // 确保动画完全结束后再更新状态 + }} + useNativeDriver={true} // 启用原生驱动 + hideModalContentWhileAnimating={true} // 动画时隐藏内容 + style={{ + margin: 0, + justifyContent: 'flex-start', + marginTop: podiumPosition.height + podiumPosition.y, + }} > diff --git a/components/owner/location.tsx b/components/owner/location.tsx index 22fece8..5f4839e 100644 --- a/components/owner/location.tsx +++ b/components/owner/location.tsx @@ -1,39 +1,41 @@ -import locationData from '@/assets/json/location.json'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Modal from 'react-native-modal'; import Cascader, { CascaderItem } from '../cascader'; -import { convertRegions } from '../utils/cascaderData'; interface LocationModalProps { modalVisible: boolean; setModalVisible: (visible: boolean) => void; podiumPosition: { x: number, y: number, width: number, height: number }; handleChange: (selectedItems: CascaderItem[]) => void; + data: CascaderItem[]; } const LocationModal = React.memo((props: LocationModalProps) => { - const { modalVisible, setModalVisible, podiumPosition, handleChange } = props; - const transformed = convertRegions(locationData, { - nameKey: 'name', // 源数据中表示"名称"的字段 - valueKey: 'name', // 源数据中作为 value 的字段 - regionsKey: 'regions', // 源数据中表示"子级区域"的字段 - childrenKey: 'children' // 输出结构中表示"子级区域"的字段 - }); + const { modalVisible, setModalVisible, podiumPosition, handleChange, data } = props; const { t } = useTranslation(); return ( setModalVisible(false)} - swipeDirection="right" // 支持向右滑动关闭 + swipeDirection={['right']} // 改为数组形式 propagateSwipe={true} - animationIn="slideInRight" // 入场动画 - animationOut="slideOutRight" // 出场动画 + animationIn="slideInRight" + animationOut="slideOutRight" backdropOpacity={0.5} onSwipeComplete={() => setModalVisible(false)} - style={{ margin: 0, justifyContent: 'flex-start', marginTop: podiumPosition.height + podiumPosition.y }} + onModalHide={() => { + // 确保动画完全结束后再更新状态 + }} + useNativeDriver={true} // 启用原生驱动 + hideModalContentWhileAnimating={true} // 动画时隐藏内容 + style={{ + margin: 0, + justifyContent: 'flex-start', + marginTop: podiumPosition.height + podiumPosition.y, + }} > @@ -45,7 +47,7 @@ const LocationModal = React.memo((props: LocationModalProps) => { diff --git a/components/owner/podium.tsx b/components/owner/podium.tsx index 36c5192..228968b 100644 --- a/components/owner/podium.tsx +++ b/components/owner/podium.tsx @@ -3,22 +3,34 @@ import FirstSvg from "@/assets/icons/svg/first.svg"; import SecondSvg from "@/assets/icons/svg/second.svg"; import ThirdSvg from "@/assets/icons/svg/third.svg"; import { RankingItem } from "@/types/user"; +import { useState } from "react"; import { Image, StyleSheet, View } from "react-native"; import { ThemedText } from "../ThemedText"; interface IPodium { data: RankingItem[] } const PodiumComponent = ({ data }: IPodium) => { - return ( { - data[1]?.user_avatar_url - ? - : + (() => { + const [imageError, setImageError] = useState(false); + + if (!data[1]?.user_avatar_url || imageError) { + return ; + } + + return ( + setImageError(true)} + /> + ); + })() } { { - data[0]?.user_avatar_url - ? - : + (() => { + const [imageError, setImageError] = useState(false); + + if (!data[0]?.user_avatar_url || imageError) { + return ; + } + + return ( + setImageError(true)} + /> + ); + })() } { { - data[2]?.user_avatar_url - ? - : + (() => { + const [imageError, setImageError] = useState(false); + + if (!data[2]?.user_avatar_url || imageError) { + return ; + } + + return ( + setImageError(true)} + /> + ); + })() } { {index + 1} {item.user_nick_name} - {item.user_avatar_url ? ( - - ) : ( - - )} + {(() => { + const [imageError, setImageError] = useState(false); + + if (!item.user_avatar_url || imageError) { + return ; + } + + return ( + setImageError(true)} + /> + ); + })()} ))} diff --git a/components/owner/utils.ts b/components/owner/utils.ts index 75f4a2e..626610e 100644 --- a/components/owner/utils.ts +++ b/components/owner/utils.ts @@ -1,10 +1,17 @@ // 地理位置逆编码 +import { fetchApi } from '@/lib/server-api-util'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; import * as Notifications from 'expo-notifications'; import * as SecureStore from 'expo-secure-store'; import { Alert, Linking, Platform } from 'react-native'; +interface Address { + id: number; + name: string; + // Add other address properties as needed +} + // 配置通知处理器 Notifications.setNotificationHandler({ handleNotification: async () => ({ @@ -16,9 +23,11 @@ Notifications.setNotificationHandler({ }), }); +// 逆编码 export const reverseGeocode = async (latitude: number, longitude: number) => { try { - const addressResults = await Location.reverseGeocodeAsync({ latitude, longitude }); + const addressResults = await fetchApi(`/area/gecoding?latitude=${latitude}&longitude=${longitude}`); + console.log('地址:', addressResults); for (let address of addressResults) { console.log('地址:', address); if (Platform.OS === 'web') { @@ -29,7 +38,7 @@ export const reverseGeocode = async (latitude: number, longitude: number) => { return address; } } catch (error) { - console.error('逆地理编码失败:', error); + console.log('逆地理编码失败:', error); } }; @@ -299,4 +308,34 @@ export const sendLocalNotification = async (title: string, body: string, data: R console.error('发送通知时出错:', error); return false; } -}; \ No newline at end of file +}; + + +// 获取定位信息 -- 最子集元素 +export function findInnermostElement(data: any[], targetName: string): { name: string; value: any } | null { + let result: { name: string; value: any } | null = null; + + function search(nodes: any[]): boolean { + for (const node of nodes) { + if (node.name === targetName) { + result = { name: node.name, value: node.value }; + // Keep searching to see if there's a deeper match + let foundDeeper = false; + if (node.children && node.children.length > 0) { + foundDeeper = search(node.children); + } + // If no deeper match was found, this is the innermost one + if (!foundDeeper) { + return true; + } + } else if (node.children && node.children.length > 0) { + const found = search(node.children); + if (found) return true; + } + } + return false; + } + + search(data); + return result; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0e979a4..2d3c8d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "expo-haptics": "~14.1.4", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", + "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.7", "expo-localization": "^16.1.5", "expo-location": "~18.1.5", diff --git a/package.json b/package.json index fcff40b..50c1a34 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "react-redux": "^9.2.0", - "expo-clipboard": "~7.1.5" + "expo-clipboard": "~7.1.5", + "expo-linear-gradient": "~14.1.5" }, "devDependencies": { "@babel/core": "^7.25.2",