feat: 地址,逆编码接口联调
This commit is contained in:
parent
fea5af96fa
commit
6bc7d8b362
@ -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<CascaderItem[]>([]);
|
||||
// 在组件内部添加:
|
||||
const podiumRef = useRef<View>(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<TargetItem[]>([]);
|
||||
const getClassify = () => {
|
||||
fetchApi<GroupedData>("/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<RankingItem[]>([]);
|
||||
const getRanking = () => {
|
||||
fetchApi<RankingItem[]>("/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(() => {
|
||||
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<LocationData>("/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 (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
@ -149,6 +217,7 @@ export default function OwnerPage() {
|
||||
setModalVisible={setLocationModalVisible}
|
||||
podiumPosition={podiumPosition}
|
||||
handleChange={handleLocationChange}
|
||||
data={locationData}
|
||||
/>
|
||||
{/* 分类选择弹窗 */}
|
||||
<ClassifyModal
|
||||
|
||||
@ -19,13 +19,22 @@ const ClassifyModal = (props: ClassifyModalProps) => {
|
||||
<Modal
|
||||
isVisible={modalVisible}
|
||||
onBackdropPress={() => 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,
|
||||
}}
|
||||
>
|
||||
<View style={styles.modalView}>
|
||||
<View style={styles.modalHeader}>
|
||||
|
||||
@ -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 (
|
||||
<Modal
|
||||
isVisible={modalVisible}
|
||||
onBackdropPress={() => 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,
|
||||
}}
|
||||
>
|
||||
<View style={styles.modalView}>
|
||||
<View style={styles.modalHeader}>
|
||||
@ -45,7 +47,7 @@ const LocationModal = React.memo((props: LocationModalProps) => {
|
||||
</View>
|
||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||
<Cascader
|
||||
data={transformed}
|
||||
data={data}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.item, { opacity: data[1]?.user_id ? 1 : 0 }]}>
|
||||
<SecondSvg />
|
||||
<View style={[styles.titleContainer, { backgroundColor: '#FFB645', borderTopRightRadius: 0, height: 60 }]}>
|
||||
{
|
||||
data[1]?.user_avatar_url
|
||||
? <Image source={{ uri: data[1]?.user_avatar_url }} style={{ width: 30, height: 30, borderRadius: 30 }} />
|
||||
: <UserSvg width={30} height={30} />
|
||||
(() => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!data[1]?.user_avatar_url || imageError) {
|
||||
return <UserSvg width={30} height={30} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: data[1].user_avatar_url }}
|
||||
style={{ width: 30, height: 30, borderRadius: 30 }}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
<ThemedText
|
||||
numberOfLines={1}
|
||||
@ -33,9 +45,21 @@ const PodiumComponent = ({ data }: IPodium) => {
|
||||
<FirstSvg />
|
||||
<View style={[styles.titleContainer, { backgroundColor: '#E2793F', height: 90 }]}>
|
||||
{
|
||||
data[0]?.user_avatar_url
|
||||
? <Image source={{ uri: data[0]?.user_avatar_url }} style={{ width: 40, height: 40, borderRadius: 40 }} />
|
||||
: <UserSvg width={40} height={40} />
|
||||
(() => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!data[0]?.user_avatar_url || imageError) {
|
||||
return <UserSvg width={40} height={40} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: data[0].user_avatar_url }}
|
||||
style={{ width: 40, height: 40, borderRadius: 40 }}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
<ThemedText
|
||||
numberOfLines={2}
|
||||
@ -50,9 +74,21 @@ const PodiumComponent = ({ data }: IPodium) => {
|
||||
<ThirdSvg />
|
||||
<View style={[styles.titleContainer, { backgroundColor: '#FFD18A', borderTopLeftRadius: 0, height: 50 }]}>
|
||||
{
|
||||
data[2]?.user_avatar_url
|
||||
? <Image source={{ uri: data[2]?.user_avatar_url }} style={{ width: 20, height: 20, borderRadius: 20 }} />
|
||||
: <UserSvg width={20} height={20} />
|
||||
(() => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!data[2]?.user_avatar_url || imageError) {
|
||||
return <UserSvg width={20} height={20} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: data[2].user_avatar_url }}
|
||||
style={{ width: 20, height: 20, borderRadius: 20 }}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
<ThemedText
|
||||
numberOfLines={1}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import AtaverSvg from "@/assets/icons/svg/ataver.svg";
|
||||
import OwnerSvg from "@/assets/icons/svg/owner.svg";
|
||||
import { RankingItem } from "@/types/user";
|
||||
import { useState } from "react";
|
||||
import { Image, ScrollView, StyleSheet, View } from "react-native";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
interface IRankList {
|
||||
@ -37,14 +38,21 @@ const RankList = (props: IRankList) => {
|
||||
<ThemedText style={styles.itemRank}>{index + 1}</ThemedText>
|
||||
<ThemedText style={styles.itemName}>{item.user_nick_name}</ThemedText>
|
||||
<View style={{ opacity: index == 1 ? 0 : 1 }}>
|
||||
{item.user_avatar_url ? (
|
||||
{(() => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
if (!item.user_avatar_url || imageError) {
|
||||
return <AtaverSvg width={40} height={40} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: item.user_avatar_url }}
|
||||
style={{ width: 40, height: 40, borderRadius: 40 }}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
) : (
|
||||
<AtaverSvg width={40} height={40} />
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@ -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<Address[]>(`/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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -300,3 +309,33 @@ export const sendLocalNotification = async (title: string, body: string, data: R
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取定位信息 -- 最子集元素
|
||||
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;
|
||||
}
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user