diff --git a/.gitea/workflows/dev.yaml b/.gitea/workflows/dev.yaml index 1675ae5..a2ae252 100644 --- a/.gitea/workflows/dev.yaml +++ b/.gitea/workflows/dev.yaml @@ -5,7 +5,7 @@ on: push: branches: - main - - v0.4.0_front + - v0.5.0 jobs: Explore-Gitea-Actions: diff --git a/app.json b/app.json index 23f728d..0b3bf6b 100644 --- a/app.json +++ b/app.json @@ -43,6 +43,22 @@ "plugins": [ "expo-router", "expo-secure-store", + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "允许 $(PRODUCT_NAME) 访问您的位置", + "locationAlwaysPermission": "允许 $(PRODUCT_NAME) 访问您的位置", + "locationWhenInUsePermission": "允许 $(PRODUCT_NAME) 访问您的位置" + } + ], + [ + "expo-notifications", + { + "color": "#ffffff", + "defaultChannel": "default", + "enableBackgroundRemoteNotifications": false + } + ], [ "expo-audio", { diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index c3def1f..9db1790 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -100,6 +100,26 @@ export default function TabLayout() { tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 }} /> + {/* owner */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + {/* 排行榜 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> ); } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index b595ae0..c55b375 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -46,9 +46,9 @@ export default function HomeScreen() { } else { token = await SecureStore.getItemAsync('token') || ""; } - + console.log("token111111111", token); if (token) { - router.push('/user-message') + router.push('/ask') } else { router.push('/login') } diff --git a/app/(tabs)/memo-list.tsx b/app/(tabs)/memo-list.tsx index 3f34e24..602f2f1 100644 --- a/app/(tabs)/memo-list.tsx +++ b/app/(tabs)/memo-list.tsx @@ -1,8 +1,7 @@ import ChatSvg from "@/assets/icons/svg/chat.svg"; -import IPSvg from "@/assets/icons/svg/ip.svg"; +import AskNavbar from "@/components/layout/ask"; import { fetchApi } from "@/lib/server-api-util"; import { Chat } from "@/types/ask"; -import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import React, { useEffect } from 'react'; import { FlatList, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -59,7 +58,13 @@ const MemoList = () => { style={styles.memoItem} onPress={() => handleMemoPress(item)} > - + + + { )} /> {/* 底部导航栏 */} - - - - - - { - router.push({ - pathname: '/ask', - params: { - newSession: "true", - } - }); - }} - > - - - - - - - - - + ); }; diff --git a/app/(tabs)/owner.tsx b/app/(tabs)/owner.tsx new file mode 100644 index 0000000..5ab4693 --- /dev/null +++ b/app/(tabs)/owner.tsx @@ -0,0 +1,211 @@ +import ConversationsSvg from '@/assets/icons/svg/conversations.svg'; +import PointsSvg from '@/assets/icons/svg/points.svg'; +import StoriesSvg from '@/assets/icons/svg/stories.svg'; +import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg'; +import AskNavbar from '@/components/layout/ask'; +import AlbumComponent from '@/components/owner/album'; +import CategoryComponent from '@/components/owner/category'; +import CountComponent from '@/components/owner/count'; +import CreateCountComponent from '@/components/owner/createCount'; +import Ranking from '@/components/owner/ranking'; +import ResourceComponent from '@/components/owner/resource'; +import SettingModal from '@/components/owner/setting'; +import UserInfo from '@/components/owner/userName'; +import { formatDuration } from '@/components/utils/time'; +import { fetchApi } from '@/lib/server-api-util'; +import { CountData, UserInfoDetails } from '@/types/user'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +export default function OwnerPage() { + const insets = useSafeAreaInsets(); + const { t } = useTranslation(); + + // 设置弹窗 + const [modalVisible, setModalVisible] = useState(false); + + // 数据统计 + const [countData, setCountData] = useState({} as CountData); + + // 获取数量统计 --- 需要轮询 + const getCountData = () => { + fetchApi("/material/statistics").then((res) => { + setCountData(res as CountData); + }); + + } + + // 获取用户信息 + const [userInfoDetails, setUserInfoDetails] = useState({} as UserInfoDetails); + const getUserInfo = () => { + fetchApi("/membership/personal-center-info").then((res) => { + setUserInfoDetails(res as UserInfoDetails); + }) + } + // 设计轮询获取数量统计 + // useEffect(() => { + // const interval = setInterval(() => { + // getCountData(); + // }, 1000); + // return () => clearInterval(interval); + // }, []); + + // 初始化获取用户信息 + useEffect(() => { + getUserInfo(); + getCountData(); + }, []); + + return ( + + + {/* 用户信息 */} + + + {/* 设置栏 */} + + + {/* 资源数据 */} + + } style={{ flex: 1 }} isFormatBytes={true} /> + } style={{ flex: 1 }} /> + + {/* 数据统计 */} + + + {/* 分类 */} + + + {countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => { + return ( + + ) + })} + + + + {/* 作品数据 */} + + } number={userInfoDetails.stories_count} /> + } number={userInfoDetails.conversations_count} /> + + + {/* 排行榜 */} + + + + + {/* 设置弹窗 */} + + + {/* 导航栏 */} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + paddingBottom: 86, + }, + resourceContainer: { + flexDirection: 'row', + gap: 16 + }, + userInfo: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + }, + medal: { + marginLeft: 16, + }, + backButton: { + padding: 4, + }, + headerTitle: { + fontSize: 18, + fontWeight: '600', + color: '#333', + }, + content: { + flex: 1, + }, + profileSection: { + backgroundColor: '#fff', + padding: 20, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 10, + }, + avatarContainer: { + marginRight: 16, + }, + avatar: { + width: 70, + height: 70, + borderRadius: 35, + borderWidth: 2, + borderColor: '#FFD38D', + }, + profileInfo: { + flex: 1, + }, + userName: { + fontSize: 18, + fontWeight: '600', + color: '#333', + marginBottom: 4, + }, + userPhone: { + fontSize: 14, + color: '#666', + }, + editButton: { + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderWidth: 1, + borderColor: '#eee', + borderRadius: 15, + }, + editButtonText: { + marginLeft: 4, + color: '#666', + fontSize: 14, + }, + menuContainer: { + backgroundColor: '#fff', + paddingHorizontal: 16, + }, + menuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#f5f5f5', + }, + menuItemText: { + flex: 1, + marginLeft: 12, + fontSize: 16, + color: '#333', + }, + arrowIcon: { + marginLeft: 'auto', + }, +}); \ No newline at end of file diff --git a/app/(tabs)/top.tsx b/app/(tabs)/top.tsx new file mode 100644 index 0000000..d0923ee --- /dev/null +++ b/app/(tabs)/top.tsx @@ -0,0 +1,182 @@ +import ArrowSvg from '@/assets/icons/svg/arrow.svg'; +import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg'; +import { CascaderItem } from '@/components/cascader'; +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 { ThemedText } from '@/components/ThemedText'; +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 { useSafeAreaInsets } from "react-native-safe-area-context"; +export default function OwnerPage() { + const insets = useSafeAreaInsets(); + const router = useRouter(); + // 位置弹窗 + const [locationModalVisible, setLocationModalVisible] = useState(false); + // 分类弹窗 + const [classifyModalVisible, setClassifyModalVisible] = useState(false); + + // 在组件内部添加: + const podiumRef = useRef(null); + const [podiumPosition, setPodiumPosition] = useState({ x: 0, y: 0, width: 0, height: 0 }); + // 获取分类 + const [classify, setClassify] = useState([]); + const getClassify = () => { + fetchApi("/title-tags").then((res: GroupedData) => { + setClassify(transformData(res)); + }); + } + + // 选择地区 + const [selectedLocation, setSelectedLocation] = useState(); + // 选择分类 + const [selectedClassify, setSelectedClassify] = useState(); + + const onPodiumLayout = (event: LayoutChangeEvent) => { + if (podiumRef.current) { + podiumRef.current.measure((x, y, width, height, pageX, pageY) => { + setPodiumPosition({ + x: pageX, + y: pageY, + width, + height + }); + }); + } + }; + // 地区选择 + const handleLocationChange = useCallback((selectedItems: CascaderItem[]) => { + console.log('SelectedLocation:', selectedItems); + if (selectedItems.length > 0) { + const lastItem = selectedItems[selectedItems.length - 1]; + // 只有当选择完成时才更新状态 + if (!lastItem.children || lastItem.children.length === 0) { + setSelectedLocation(selectedItems); + } + } + }, []); + + // 分类选择 + const handleClassifyChange = useCallback((selectedItems: CascaderItem[]) => { + console.log('SelectedClassify:', selectedItems); + if (selectedItems.length > 0) { + const lastItem = selectedItems[selectedItems.length - 1]; + // 只有当选择完成时才更新状态 + if (!lastItem.children || lastItem.children.length === 0) { + setSelectedClassify(selectedItems); + } + } + }, []); + + // 获取排名信息 + 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 + }) + }).then((res) => { + setRanking(res); + }); + } + + // 当用户选择发生变化时,重新获取排名 + useEffect(() => { + getRanking(); + }, [selectedLocation, selectedClassify]) + + // 初始化获取分类 + useEffect(() => { + getClassify(); + }, []) + + return ( + + {/* 导航栏 */} + + { router.push('/owner') }} style={{ padding: 16 }}> + + + + Top Memory Makers + + 123 + + + { setLocationModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}> + 0 ? '#FFB645' : '#4C320C' }}> + {selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].name : "地区"} + + { + selectedLocation?.length > 0 + ? + + : + + } + + { setClassifyModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}> + 0 ? '#FFB645' : '#4C320C' }}> + {selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"} + + {selectedClassify?.length > 0 + ? + + : + + } + + + {/* 颁奖台 */} + + {/* 排名区域 */} + + + {/* 地区选择弹窗 */} + + {/* 分类选择弹窗 */} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginVertical: 16, + }, + headerTitle: { + fontSize: 20, + fontWeight: '700', + color: '#4C320C', + } +}); \ No newline at end of file diff --git a/assets/icons/svg/arrow.svg b/assets/icons/svg/arrow.svg new file mode 100644 index 0000000..28a9826 --- /dev/null +++ b/assets/icons/svg/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/askIP.svg b/assets/icons/svg/askIP.svg new file mode 100644 index 0000000..4f49596 --- /dev/null +++ b/assets/icons/svg/askIP.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/chat.svg b/assets/icons/svg/chat.svg index f1919ab..882e4e2 100644 --- a/assets/icons/svg/chat.svg +++ b/assets/icons/svg/chat.svg @@ -1,41 +1,44 @@ - - - + + + + - - - - - - - + + + + + + + - - + + - - + + + + - + - - + + - + - + - - + + - + diff --git a/assets/icons/svg/conversations.svg b/assets/icons/svg/conversations.svg new file mode 100644 index 0000000..d391cb5 --- /dev/null +++ b/assets/icons/svg/conversations.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/delete.svg b/assets/icons/svg/delete.svg new file mode 100644 index 0000000..757492b --- /dev/null +++ b/assets/icons/svg/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/edit.svg b/assets/icons/svg/edit.svg new file mode 100644 index 0000000..9bfe05c --- /dev/null +++ b/assets/icons/svg/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/svg/first.svg b/assets/icons/svg/first.svg new file mode 100644 index 0000000..c082aed --- /dev/null +++ b/assets/icons/svg/first.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/location.svg b/assets/icons/svg/location.svg new file mode 100644 index 0000000..d2ee247 --- /dev/null +++ b/assets/icons/svg/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/svg/logout.svg b/assets/icons/svg/logout.svg new file mode 100644 index 0000000..edb5376 --- /dev/null +++ b/assets/icons/svg/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/medal.svg b/assets/icons/svg/medal.svg new file mode 100644 index 0000000..6c89a64 --- /dev/null +++ b/assets/icons/svg/medal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/svg/navbar.svg b/assets/icons/svg/navbar.svg index c1d27e5..7d03d75 100644 --- a/assets/icons/svg/navbar.svg +++ b/assets/icons/svg/navbar.svg @@ -1,17 +1,3 @@ - - - - - - - - - - - - - - - - + + diff --git a/assets/icons/svg/owner.svg b/assets/icons/svg/owner.svg new file mode 100644 index 0000000..5e90d32 --- /dev/null +++ b/assets/icons/svg/owner.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/points.svg b/assets/icons/svg/points.svg new file mode 100644 index 0000000..ee964f3 --- /dev/null +++ b/assets/icons/svg/points.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/svg/refresh.svg b/assets/icons/svg/refresh.svg new file mode 100644 index 0000000..99d7e6b --- /dev/null +++ b/assets/icons/svg/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/svg/rightArrow.svg b/assets/icons/svg/rightArrow.svg new file mode 100644 index 0000000..3c2445e --- /dev/null +++ b/assets/icons/svg/rightArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/second.svg b/assets/icons/svg/second.svg new file mode 100644 index 0000000..0867062 --- /dev/null +++ b/assets/icons/svg/second.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/setting.svg b/assets/icons/svg/setting.svg new file mode 100644 index 0000000..3066c85 --- /dev/null +++ b/assets/icons/svg/setting.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/svg/stories.svg b/assets/icons/svg/stories.svg new file mode 100644 index 0000000..31062ed --- /dev/null +++ b/assets/icons/svg/stories.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/svg/tag.svg b/assets/icons/svg/tag.svg new file mode 100644 index 0000000..cda7cc7 --- /dev/null +++ b/assets/icons/svg/tag.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/svg/third.svg b/assets/icons/svg/third.svg new file mode 100644 index 0000000..c152553 --- /dev/null +++ b/assets/icons/svg/third.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/usedStorage.svg b/assets/icons/svg/usedStorage.svg new file mode 100644 index 0000000..ab8492a --- /dev/null +++ b/assets/icons/svg/usedStorage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/png/owner/abstract.png b/assets/images/png/owner/abstract.png new file mode 100644 index 0000000..794df11 Binary files /dev/null and b/assets/images/png/owner/abstract.png differ diff --git a/assets/images/png/owner/animals.png b/assets/images/png/owner/animals.png new file mode 100644 index 0000000..0807282 Binary files /dev/null and b/assets/images/png/owner/animals.png differ diff --git a/assets/images/png/owner/life.png b/assets/images/png/owner/life.png new file mode 100644 index 0000000..a7713ae Binary files /dev/null and b/assets/images/png/owner/life.png differ diff --git a/assets/images/png/owner/people.png b/assets/images/png/owner/people.png new file mode 100644 index 0000000..1217485 Binary files /dev/null and b/assets/images/png/owner/people.png differ diff --git a/assets/images/png/owner/scenery.png b/assets/images/png/owner/scenery.png new file mode 100644 index 0000000..eae5c28 Binary files /dev/null and b/assets/images/png/owner/scenery.png differ diff --git a/assets/json/classify.json b/assets/json/classify.json new file mode 100644 index 0000000..cc85832 --- /dev/null +++ b/assets/json/classify.json @@ -0,0 +1,173 @@ +[ + { + "name": "摄影分类", + "children": [ + { + "name": "人物", + "children": [ + { + "name": "男性" + }, + { + "name": "女性" + }, + { + "name": "老人", + "children": [ + { + "name": "数量/时长:" + } + ] + }, + { + "name": "小孩", + "children": [ + { + "name": "数量:上海市第一人类幼崽萌主" + }, + { + "name": "时长:上海市第一成长记录仪MAX" + } + ] + } + ] + }, + { + "name": "风景", + "children": [ + { + "name": "自然风景", + "children": [ + { + "name": "数量:上海市多巴胺风景供应商TOP1" + }, + { + "name": "时长:上海市第一心灵SPA持久包" + } + ] + }, + { + "name": "建筑物", + "children": [ + { + "name": "数量:上海市第一城市景观收藏家" + }, + { + "name": "时长:徐汇区第一光影驻留大师" + } + ] + } + ] + }, + { + "name": "静物", + "children": [ + { + "name": "美食", + "children": [ + { + "name": "数量:上海市第一人间美味种草机" + }, + { + "name": "时长:上海市第一恩格尔系数爆破手" + } + ] + }, + { + "name": "艺术品(雕像)" + }, + { + "name": "花草", + "children": [ + { + "name": "数量:上海市第一莫奈花园造访者" + }, + { + "name": "时长:徐汇区第一春日魔法师" + } + ] + }, + { + "name": "日常物品", + "children": [ + { + "name": "数量:上海市第一闲鱼の神预备役" + }, + { + "name": "时长:徐汇区第一人间杂货铺店长" + } + ] + } + ] + }, + { + "name": "动物", + "children": [ + { + "name": "猫", + "children": [ + { + "name": "数量:全国第一猫猫重度依赖症患者" + }, + { + "name": "时长:上海市第一最佳铲屎官" + } + ] + }, + { + "name": "狗", + "children": [ + { + "name": "数量:徐汇区第一狗狗教教主" + }, + { + "name": "时长:上海市第一修狗快乐永动机" + } + ] + }, + { + "name": "鸟", + "children": [ + { + "name": "数量:上海市第一天籁点唱机" + }, + { + "name": "时长:上海市第一天籁电台24h主播" + } + ] + } + ] + }, + { + "name": "抽象", + "children": [ + { + "name": "游戏动漫", + "children": [ + { + "name": "数量:徐汇区第一次元壁拆迁办" + }, + { + "name": "时长:全国第一最强爆肝王" + } + ] + }, + { + "name": "艺术作品", + "children": [ + { + "name": "数量:徐汇区第一灵感批发商" + }, + { + "name": "时长:上海市第一浪漫创作官" + } + ] + }, + { + "name": "其他" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/assets/json/location.json b/assets/json/location.json new file mode 100644 index 0000000..3a165a2 --- /dev/null +++ b/assets/json/location.json @@ -0,0 +1,552 @@ +[ + { + "name": "北京市", + "shortCode": "BJ", + "regions": [ + { + "name": "东城区", + "shortCode": "Dongcheng" + }, + { + "name": "西城区", + "shortCode": "Xicheng" + }, + { + "name": "朝阳区", + "shortCode": "Chaoyang" + }, + { + "name": "海淀区", + "shortCode": "Haidian" + } + ] + }, + { + "name": "广东省", + "shortCode": "GD", + "regions": [ + { + "name": "广州市", + "shortCode": "Guangzhou" + }, + { + "name": "深圳市", + "shortCode": "Shenzhen" + }, + { + "name": "珠海市", + "shortCode": "Zhuhai" + }, + { + "name": "佛山市", + "shortCode": "Foshan" + } + ] + }, + { + "name": "河南省", + "shortCode": "Henan", + "regions": [ + { + "name": "郑州市", + "shortCode": "Zhengzhou", + "regions": [ + { + "name": "中原区", + "shortCode": "zhongyuan" + }, + { + "name": "二七区", + "shortCode": "erqi" + }, + { + "name": "管城回族区", + "shortCode": "guancheng" + }, + { + "name": "金水区", + "shortCode": "jinsui" + }, + { + "name": "上街区", + "shortCode": "shangjiao" + }, + { + "name": "惠济区", + "shortCode": "huiji" + } + ] + }, + { + "name": "开封市", + "shortCode": "Kaifeng", + "regions": [ + { + "name": "龙亭区", + "shortCode": "longting" + }, + { + "name": "顺河回族区", + "shortCode": "shunhe" + }, + { + "name": "鼓楼区", + "shortCode": "gulou" + }, + { + "name": "禹王台区", + "shortCode": "yudang" + }, + { + "name": "祥符区", + "shortCode": "xiangfu" + } + ] + }, + { + "name": "洛阳市", + "shortCode": "Luoyang", + "regions": [ + { + "name": "老城区", + "shortCode": "laocheng" + }, + { + "name": "西工区", + "shortCode": "xigong" + }, + { + "name": "瀍河回族区", + "shortCode": "chanhe" + }, + { + "name": "涧西区", + "shortCode": "jianxi" + }, + { + "name": "吉利区", + "shortCode": "jili" + }, + { + "name": "洛龙区", + "shortCode": "luorong" + } + ] + }, + { + "name": "平顶山市", + "shortCode": "Pingdingshan", + "regions": [ + { + "name": "新华区", + "shortCode": "xihu" + }, + { + "name": "卫东区", + "shortCode": "weidong" + }, + { + "name": "石龙区", + "shortCode": "shilong" + }, + { + "name": "湛河区", + "shortCode": "zhanhe" + }, + { + "name": "汝州市", + "shortCode": "ruzhou" + }, + { + "name": "舞钢市", + "shortCode": "wugang" + } + ] + }, + { + "name": "安阳市", + "shortCode": "Anyang", + "regions": [ + { + "name": "文峰区", + "shortCode": "wenfeng" + }, + { + "name": "北关区", + "shortCode": "beiguan" + }, + { + "name": "殷都区", + "shortCode": "yindu" + }, + { + "name": "龙安区", + "shortCode": "longan" + }, + { + "name": "林州市", + "shortCode": "linzhou" + } + ] + }, + { + "name": "鹤壁市", + "shortCode": "Hebi", + "regions": [ + { + "name": "鹤山区", + "shortCode": "heshan" + }, + { + "name": "山城区", + "shortCode": "shancheng" + }, + { + "name": "淇滨区", + "shortCode": "qibin" + } + ] + }, + { + "name": "新乡市", + "shortCode": "Xinxiang", + "regions": [ + { + "name": "红旗区", + "shortCode": "hongqi" + }, + { + "name": "卫滨区", + "shortCode": "weibin" + }, + { + "name": "凤泉区", + "shortCode": "fengquan" + }, + { + "name": "牧野区", + "shortCode": "muye" + }, + { + "name": "卫辉市", + "shortCode": "weihui" + }, + { + "name": "辉县市", + "shortCode": "huixian" + } + ] + }, + { + "name": "焦作市", + "shortCode": "Jiaozuo", + "regions": [ + { + "name": "解放区", + "shortCode": "jiefang" + }, + { + "name": "中站区", + "shortCode": "zhongzhan" + }, + { + "name": "马村区", + "shortCode": "macun" + }, + { + "name": "山阳区", + "shortCode": "shanyang" + } + ] + }, + { + "name": "濮阳市", + "shortCode": "Puyang", + "regions": [ + { + "name": "华龙区", + "shortCode": "hualong" + }, + { + "name": "清丰县", + "shortCode": "qingfeng" + }, + { + "name": "南乐县", + "shortCode": "nanle" + }, + { + "name": "范县", + "shortCode": "fan" + }, + { + "name": "台前县", + "shortCode": "taiqian" + }, + { + "name": "濮阳县", + "shortCode": "puyang" + } + ] + }, + { + "name": "许昌市", + "shortCode": "Xuchang", + "regions": [ + { + "name": "魏都区", + "shortCode": "weidu" + }, + { + "name": "建安区", + "shortCode": "jianan" + }, + { + "name": "鄢陵县", + "shortCode": "yanling" + }, + { + "name": "襄城县", + "shortCode": "xiangcheng" + }, + { + "name": "禹州市", + "shortCode": "yuzhoushi" + }, + { + "name": "长葛市", + "shortCode": "changgeshi" + } + ] + }, + { + "name": "漯河市", + "shortCode": "Luohe", + "regions": [ + { + "name": "源汇区", + "shortCode": "yuanhu" + }, + { + "name": "郾城区", + "shortCode": "yancheng" + }, + { + "name": "召陵区", + "shortCode": "zhaoqing" + }, + { + "name": "舞阳县", + "shortCode": "wuyang" + }, + { + "name": "临颍县", + "shortCode": "linyong" + } + ] + }, + { + "name": "三门峡市", + "shortCode": "Sanmenxia", + "regions": [ + { + "name": "湖滨区", + "shortCode": "hubin" + }, + { + "name": "陕州区", + "shortCode": "shanzhou" + }, + { + "name": "渑池县", + "shortCode": "mianchi" + }, + { + "name": "卢氏县", + "shortCode": "lushi" + }, + { + "name": "义马市", + "shortCode": "yima" + }, + { + "name": "灵宝市", + "shortCode": "lingbao" + } + ] + }, + { + "name": "南阳市", + "shortCode": "Nanyang", + "regions": [ + { + "name": "宛城区", + "shortCode": "wancheng" + }, + { + "name": "卧龙区", + "shortCode": "wolong" + }, + { + "name": "南召县", + "shortCode": "nanzhao" + }, + { + "name": "方城县", + "shortCode": "fangcheng" + }, + { + "name": "西峡县", + "shortCode": "xixia" + }, + { + "name": "镇平县", + "shortCode": "zhenping" + } + ] + }, + { + "name": "商丘市", + "shortCode": "Shangqiu", + "regions": [ + { + "name": "梁园区", + "shortCode": "liangyu" + }, + { + "name": "睢阳区", + "shortCode": "suiyang" + }, + { + "name": "民权县", + "shortCode": "minquan" + }, + { + "name": "睢县", + "shortCode": "sui" + }, + { + "name": "宁陵县", + "shortCode": "ningling" + }, + { + "name": "柘城县", + "shortCode": "zhecheng" + } + ] + }, + { + "name": "信阳市", + "shortCode": "Xinyang", + "regions": [ + { + "name": "浉河区", + "shortCode": "buhe" + }, + { + "name": "平桥区", + "shortCode": "pingqiao" + }, + { + "name": "罗山县", + "shortCode": "luoshan" + }, + { + "name": "光山县", + "shortCode": "guangshan" + }, + { + "name": "新县", + "shortCode": "xin" + }, + { + "name": "商城县", + "shortCode": "shangcheng" + } + ] + }, + { + "name": "周口市", + "shortCode": "zhoukou", + "regions": [ + { + "name": "川汇区", + "shortCode": "chuhui" + }, + { + "name": "扶沟县", + "shortCode": "fugou" + }, + { + "name": "西华县", + "shortCode": "xihua" + }, + { + "name": "商水县", + "shortCode": "shangshui" + }, + { + "name": "沈丘县", + "shortCode": "shenqiu" + }, + { + "name": "郸城县", + "shortCode": "dancheng" + } + ] + }, + { + "name": "驻马店市", + "shortCode": "zhumadian", + "regions": [ + { + "name": "驿城区", + "shortCode": "yecheng" + }, + { + "name": "西平县", + "shortCode": "xiping" + }, + { + "name": "上蔡县", + "shortCode": "shangcai" + }, + { + "name": "平舆县", + "shortCode": "pingyu" + }, + { + "name": "正阳县", + "shortCode": "zhengyang" + }, + { + "name": "确山县", + "shortCode": "qieshan" + } + ] + }, + { + "name": "济源市", + "shortCode": "Jiyuan", + "regions": [ + { + "name": "沁园街道", + "shortCode": "qinyuan" + }, + { + "name": "北海街道", + "shortCode": "beihai" + }, + { + "name": "玉泉街道", + "shortCode": "yuquan" + }, + { + "name": "天坛街道", + "shortCode": "tiantan" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/components/auth/index.tsx b/components/auth/index.tsx new file mode 100644 index 0000000..26df4ec --- /dev/null +++ b/components/auth/index.tsx @@ -0,0 +1,149 @@ +import Constants from 'expo-constants'; +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import { useEffect, useState } from 'react'; +import { Button, Platform, Text, View } from 'react-native'; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldPlaySound: false, + shouldSetBadge: false, + shouldShowBanner: true, + shouldShowList: true, + }), +}); + +export default function AuthNotifications({ setNotificationsEnabled, notificationsEnabled }: { setNotificationsEnabled: (value: boolean) => void, notificationsEnabled: boolean }) { + const [expoPushToken, setExpoPushToken] = useState(''); + const [channels, setChannels] = useState([]); + const [notification, setNotification] = useState( + undefined + ); + + useEffect(() => { + console.log('notificationsEnabled', notificationsEnabled); + registerForPushNotificationsAsync().then(token => { + console.log('token', token); + token && setExpoPushToken(token) + }); + + if (Platform.OS === 'android') { + Notifications.getNotificationChannelsAsync().then(value => setChannels(value ?? [])); + } + const notificationListener = Notifications.addNotificationReceivedListener(notification => { + setNotification(notification); + }); + + const responseListener = Notifications.addNotificationResponseReceivedListener(response => { + console.log(response); + }); + + return () => { + notificationListener.remove(); + responseListener.remove(); + }; + }, [notificationsEnabled]); + + return ( + + Your expo push token: {expoPushToken} + {`Channels: ${JSON.stringify( + channels.map(c => c.id), + null, + 2 + )}`} + + Title: {notification && notification.request.content.title} + Body: {notification && notification.request.content.body} + Data: {notification && JSON.stringify(notification.request.content.data)} + + + ); +} + +async function schedulePushNotification() { + await Notifications.scheduleNotificationAsync({ + content: { + title: "You've got mail! 📬", + body: 'Here is the notification body', + data: { data: 'goes here', test: { test1: 'more data' } }, + }, + trigger: { + type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, + seconds: 2, + }, + }); +} + +async function registerForPushNotificationsAsync() { + let token; + + // 1. Android 特定配置 + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('myNotificationChannel', { + name: 'A channel is needed for the permissions prompt to appear', + importance: Notifications.AndroidImportance.MAX, // 最高优先级 + vibrationPattern: [0, 250, 250, 250], // 振动模式 + lightColor: '#FF231F7C', // 通知灯颜色 + }); + } + + // 2. 检查是否在真实设备上运行 + if (Device.isDevice) { + // 3. 检查通知权限状态 + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + + // 4. 如果尚未授予权限,则请求权限 + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + + // 5. 如果权限被拒绝,显示警告并返回 + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + + // 6. 获取推送令牌 + try { + // 获取项目ID(用于Expo推送通知服务) + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? + Constants?.easConfig?.projectId; + + if (!projectId) { + throw new Error('Project ID not found'); + } + + // 获取Expo推送令牌 + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + console.log(token); // 打印令牌,实际应用中应该发送到你的服务器 + + } catch (e) { + token = `${e}`; // 错误处理 + } + } else { + // 7. 如果是在模拟器上运行,显示警告 + alert('Must use physical device for Push Notifications'); + } + + return token; // 返回获取到的令牌 +} \ No newline at end of file diff --git a/components/cascader.tsx b/components/cascader.tsx new file mode 100644 index 0000000..eb804e4 --- /dev/null +++ b/components/cascader.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState } from 'react'; +import { ScrollView, StyleProp, StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native'; +import { ThemedText } from './ThemedText'; + +export type CascaderItem = { + name: string; + [key: string]: any; // 允许其他自定义属性 + children?: CascaderItem[]; +}; + +type CascaderProps = { + data: CascaderItem[]; // 级联数据 + value?: CascaderItem[]; // 选中的值 + onChange?: (value: CascaderItem[]) => void; // 选中项变化时的回调 + displayRender?: (selectedItems: CascaderItem[]) => React.ReactNode; // 自定义显示内容 + style?: StyleProp; // 容器样式 + itemStyle?: StyleProp; // 选项样式 + activeItemStyle?: StyleProp; // 选中项样式 + textStyle?: StyleProp; // 文字样式 + activeTextStyle?: StyleProp; // 选中文字样式 + columnWidth?: number; // 列宽 + showDivider?: boolean; // 是否显示分割线 + dividerColor?: string; // 分割线颜色 + showArrow?: boolean; // 是否显示箭头 +}; + +const CascaderComponent: React.FC = ({ + data, + value = [], + onChange, + displayRender, + style, + activeItemStyle, + textStyle, + activeTextStyle, + columnWidth = 120, + showDivider = true, + dividerColor = '#e0e0e0', + showArrow = false, +}) => { + const [selectedItems, setSelectedItems] = useState(value); + const [allLevelsData, setAllLevelsData] = useState([]); + + // 初始化数据 + useEffect(() => { + setAllLevelsData([data]); + }, [data]); + + // 处理选择 + const handleSelect = (item: CascaderItem, level: number) => { + const newSelectedItems = [...selectedItems.slice(0, level), item]; + setSelectedItems(newSelectedItems); + + // 如果有子项,添加下一级数据 + if (item.children?.length) { + setAllLevelsData(prev => { + const newLevels = [...prev.slice(0, level + 1)]; + // 确保 children 存在且是数组 + if (item.children && Array.isArray(item.children)) { + newLevels.push(item.children); + } + return newLevels; + }); + } else { + setAllLevelsData(prev => prev.slice(0, level + 1)); + } + + // 触发onChange回调 + onChange?.(newSelectedItems); + }; + + // 渲染某一级选项 + const renderLevel = (items: CascaderItem[], level: number) => { + return ( + + {items.map((item, index) => { + const isActive = selectedItems[level]?.name === item.name; + return ( + + + handleSelect(item, level)} + > + + {item.name} + + {showArrow && item.children?.length ? ( + + ) : null} + + + + ); + })} + + ); + }; + + // 渲染所有级联列 + const renderColumns = () => { + return allLevelsData.map((items, level) => ( + + {renderLevel(items, level)} + + )); + }; + + // 自定义显示内容 + const renderDisplay = () => { + if (displayRender) { + return displayRender(selectedItems); + } + return selectedItems.map(item => item.name).join(' / '); + }; + + return ( + + + {renderColumns()} + + {displayRender && ( + + {renderDisplay()} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + scrollContent: { + flexGrow: 1, + }, + column: { + height: '100%', + }, + columnWithDivider: { + borderRightWidth: 1, + }, + levelContainer: { + height: '100%', + }, + item: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + minWidth: '100%', // 确保最小宽度填满容器 + overflow: 'hidden', // 隐藏超出部分 + }, + text: { + fontSize: 15, + color: '#333', + flexShrink: 0, // 禁止收缩 + paddingRight: 4, + }, + itemContent: { + flexDirection: 'row', + alignItems: 'center', + paddingRight: 16, // 确保有足够的右边距 + }, + activeItem: { + backgroundColor: '#F6F6F6', + }, + activeText: { + color: '#AC7E35', + fontWeight: '500', + }, + arrow: { + fontSize: 18, + color: '#999', + marginLeft: 8, + }, + displayContainer: { + padding: 12, + borderTopWidth: 1, + borderTopColor: '#f0f0f0', + }, +}); + +export default CascaderComponent; \ No newline at end of file diff --git a/components/file-upload/files-uploader.tsx b/components/file-upload/files-uploader.tsx index 402ba16..b328f39 100644 --- a/components/file-upload/files-uploader.tsx +++ b/components/file-upload/files-uploader.tsx @@ -1,12 +1,11 @@ import { fetchApi } from '@/lib/server-api-util'; -import { defaultExifData, ExifData, ImagesuploaderProps } from '@/types/upload'; +import { defaultExifData, ExifData, ImagesuploaderProps, UploadUrlResponse } from '@/types/upload'; import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; -import { UploadUrlResponse } from './file-uploader'; import UploadPreview from './preview'; // 在文件顶部添加这些类型 diff --git a/components/file-upload/images-picker.tsx b/components/file-upload/images-picker.tsx index 27c5239..a3b4c7e 100644 --- a/components/file-upload/images-picker.tsx +++ b/components/file-upload/images-picker.tsx @@ -1,5 +1,5 @@ import { fetchApi } from '@/lib/server-api-util'; -import { ConfirmUpload, defaultExifData, ExifData, ImagesPickerProps, UploadResult } from '@/types/upload'; +import { ConfirmUpload, defaultExifData, ExifData, FileStatus, ImagesPickerProps, UploadResult, UploadUrlResponse } from '@/types/upload'; import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; @@ -7,7 +7,6 @@ import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; import * as Progress from 'react-native-progress'; -import { FileStatus, UploadUrlResponse } from './file-uploader'; export const ImagesPicker: React.FC = ({ children, diff --git a/components/layout/ask.tsx b/components/layout/ask.tsx new file mode 100644 index 0000000..80aa069 --- /dev/null +++ b/components/layout/ask.tsx @@ -0,0 +1,81 @@ +import NavbarSvg from "@/assets/icons/svg/navbar.svg"; +import { Ionicons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import React from 'react'; +import { Platform, TouchableOpacity, View } from 'react-native'; +import { Circle, Ellipse, G, Mask, Path, Rect, Svg } from 'react-native-svg'; + +const AskNavbar = () => { + return ( + + + + router.push('/memo-list')} > + + + + { + router.push({ + pathname: '/ask', + params: { newSession: "true" } + }); + }} + className={`${Platform.OS === 'web' ? '-mt-[4rem]' : '-mt-[5rem] ml-[0.8rem]'}`} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/owner')}> + + + {/* */} + + + + + ); +}; + +export default AskNavbar; \ No newline at end of file diff --git a/components/owner/album.tsx b/components/owner/album.tsx new file mode 100644 index 0000000..855faf9 --- /dev/null +++ b/components/owner/album.tsx @@ -0,0 +1,46 @@ +import SettingSvg from '@/assets/icons/svg/setting.svg'; +import { useTranslation } from 'react-i18next'; +import { StyleProp, StyleSheet, TouchableOpacity, View, ViewStyle } from "react-native"; +import { ThemedText } from "../ThemedText"; +interface CategoryProps { + setModalVisible: (visible: boolean) => void; + style?: StyleProp; +} + +const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => { + const { t } = useTranslation(); + return ( + + + {t('generalSetting.album', { ns: 'personal' })} + + + {t('generalSetting.shareProfile', { ns: 'personal' })} + + setModalVisible(true)} style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6 }]}> + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 16 + }, + text: { + fontSize: 12, + fontWeight: '700', + color: '#AC7E35', + borderColor: '#FFD18A', + borderWidth: 1, + borderRadius: 20, + padding: 4, + textAlign: "center", + } +}); + +export default AlbumComponent; diff --git a/components/owner/category.tsx b/components/owner/category.tsx new file mode 100644 index 0000000..06a0a15 --- /dev/null +++ b/components/owner/category.tsx @@ -0,0 +1,82 @@ +import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +import { ThemedText } from "../ThemedText"; + +interface CategoryProps { + title: string; + data: { title: string, number: string | number }[]; + bgSvg: string | null; + style?: StyleProp; +} + +const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => { + return ( + + + + + + + {title} + {data.map((item, index) => ( + + {item.title} + {item.number} + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderRadius: 32, + overflow: 'hidden', + position: 'relative', + }, + backgroundContainer: { + ...StyleSheet.absoluteFillObject, + width: '100%', + height: '100%', + }, + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.4)', // 0% 不透明度的黑色 + }, + content: { + paddingHorizontal: 16, + paddingVertical: 8, + justifyContent: 'space-between', + flex: 1 + }, + title: { + width: '100%', + textAlign: "center", + color: 'white', + fontSize: 16, + fontWeight: '700', + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 1, height: 1 }, + textShadowRadius: 2, + }, + item: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + itemTitle: { + color: 'white', + fontSize: 10, + fontWeight: '700', + }, + itemNumber: { + color: 'white', + fontSize: 10, + } +}); + +export default CategoryComponent; diff --git a/components/owner/classify.tsx b/components/owner/classify.tsx new file mode 100644 index 0000000..4810f3c --- /dev/null +++ b/components/owner/classify.tsx @@ -0,0 +1,171 @@ +import { TargetItem } from '@/types/user'; +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'; + +interface ClassifyModalProps { + modalVisible: boolean; + setModalVisible: (visible: boolean) => void; + podiumPosition: { x: number, y: number, width: number, height: number }; + handleChange: (selectedItems: CascaderItem[]) => void; + data: TargetItem[]; +} +const ClassifyModal = (props: ClassifyModalProps) => { + const { modalVisible, setModalVisible, podiumPosition, handleChange, data } = props; + const { t } = useTranslation(); + return ( + setModalVisible(false)} + swipeDirection="right" // 支持向右滑动关闭 + propagateSwipe={true} + animationIn="slideInRight" // 入场动画 + animationOut="slideOutRight" // 出场动画 + backdropOpacity={0.5} + onSwipeComplete={() => setModalVisible(false)} + style={{ margin: 0, justifyContent: 'flex-start', marginTop: podiumPosition.height + podiumPosition.y }} + > + + + Settings + {t('generalSetting.classify', { ns: 'personal' })} + setModalVisible(false)}> + × + + + + + + { + setModalVisible(false) + }} + activeOpacity={0.8} + > + + {t('generalSetting.confirm', { ns: 'personal' })} + + + + + ); +}; + +const styles = StyleSheet.create({ + modalView: { + width: '100%', + height: '60%', + backgroundColor: 'white', + borderRadius: 24, + paddingVertical: 16, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + borderBottomWidth: 1, + borderBottomColor: '#E5E5E5', + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + closeButton: { + fontSize: 28, + color: '#4C320C', + padding: 10, + }, + modalContent: { + flex: 1, + paddingHorizontal: 16, + }, + modalText: { + fontSize: 16, + color: '#4C320C', + }, + premium: { + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + content: { + flex: 1, + flexDirection: 'column', + gap: 4, + backgroundColor: '#FAF9F6', + borderRadius: 24, + paddingVertical: 8 + }, + item: { + paddingHorizontal: 16, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + itemText: { + fontSize: 14, + fontWeight: '600', + color: '#4C320C', + }, + upgradeButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + }, + upgradeButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: "600" + }, + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 2, + }, + switchOn: { + backgroundColor: '#E2793F', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, + confirmButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + marginHorizontal: 16, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default ClassifyModal; \ No newline at end of file diff --git a/components/owner/count.tsx b/components/owner/count.tsx new file mode 100644 index 0000000..fbfedcc --- /dev/null +++ b/components/owner/count.tsx @@ -0,0 +1,50 @@ +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +import { ThemedText } from "../ThemedText"; + +interface CountProps { + data: { title: string; number: string | number }[]; + style?: StyleProp; +} +const CountComponent = (props: CountProps) => { + return ( + + {props.data?.map((item, index) => ( + + {item.title} + {item.number} + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#FFB645", + padding: 16, + borderRadius: 20, + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + }, + item: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingVertical: 8 + }, + title: { + color: "#4C320C", + fontWeight: "700", + fontSize: 14, + }, + number: { + color: "#fff", + fontWeight: "700", + fontSize: 32, + textAlign: 'right', + flex: 1, + paddingTop: 8 + } +}) +export default CountComponent; \ No newline at end of file diff --git a/components/owner/createCount.tsx b/components/owner/createCount.tsx new file mode 100644 index 0000000..fec95ee --- /dev/null +++ b/components/owner/createCount.tsx @@ -0,0 +1,68 @@ +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +import { ThemedText } from "../ThemedText"; + +interface CreateCountProps { + title: string; + icon: React.ReactNode; + number: number; + style?: StyleProp; +} +const CreateCountComponent = (props: CreateCountProps) => { + return ( + + + {props.title} + + {props.icon} + + + {props.number} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + justifyContent: "space-between", + gap: 8, + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 12, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + header: { + width: "100%", + display: "flex", + flexDirection: "row", + gap: 8, + // 靠左展示 + textAlign: 'left', + }, + title: { + width: "53%", + fontSize: 11, + fontWeight: "700", + // 允许换行 + flexWrap: "wrap", + }, + number: { + fontSize: 32, + fontWeight: "700", + paddingTop: 8, + width: "100%", + // 靠右展示 + textAlign: "right", + }, +}) +export default CreateCountComponent; diff --git a/components/owner/location.tsx b/components/owner/location.tsx new file mode 100644 index 0000000..22fece8 --- /dev/null +++ b/components/owner/location.tsx @@ -0,0 +1,190 @@ +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; +} + +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 { t } = useTranslation(); + + return ( + setModalVisible(false)} + swipeDirection="right" // 支持向右滑动关闭 + propagateSwipe={true} + animationIn="slideInRight" // 入场动画 + animationOut="slideOutRight" // 出场动画 + backdropOpacity={0.5} + onSwipeComplete={() => setModalVisible(false)} + style={{ margin: 0, justifyContent: 'flex-start', marginTop: podiumPosition.height + podiumPosition.y }} + > + + + Settings + {t('generalSetting.location', { ns: 'personal' })} + setModalVisible(false)}> + × + + + + + + { + setModalVisible(false) + }} + activeOpacity={0.8} + > + + {t('generalSetting.confirm', { ns: 'personal' })} + + + + + ); +}, areEqual); + +function areEqual(prevProps: LocationModalProps, nextProps: LocationModalProps) { + // 只有当这些 props 变化时才重新渲染 + return ( + prevProps.modalVisible === nextProps.modalVisible && + prevProps.podiumPosition.x === nextProps.podiumPosition.x && + prevProps.podiumPosition.y === nextProps.podiumPosition.y && + prevProps.podiumPosition.width === nextProps.podiumPosition.width && + prevProps.podiumPosition.height === nextProps.podiumPosition.height + ); +} + +const styles = StyleSheet.create({ + modalView: { + width: '100%', + height: '60%', + backgroundColor: 'white', + borderRadius: 24, + paddingVertical: 16, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + borderBottomWidth: 1, + borderBottomColor: '#E5E5E5', + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + closeButton: { + fontSize: 28, + color: '#4C320C', + padding: 10, + }, + modalContent: { + flex: 1, + paddingHorizontal: 16, + }, + modalText: { + fontSize: 16, + color: '#4C320C', + }, + premium: { + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + content: { + flex: 1, + flexDirection: 'column', + gap: 4, + backgroundColor: '#FAF9F6', + borderRadius: 24, + paddingVertical: 8 + }, + item: { + paddingHorizontal: 16, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + itemText: { + fontSize: 14, + fontWeight: '600', + color: '#4C320C', + }, + upgradeButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + }, + upgradeButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: "600" + }, + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 2, + }, + switchOn: { + backgroundColor: '#E2793F', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, + confirmButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + marginHorizontal: 16, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default LocationModal; \ No newline at end of file diff --git a/components/owner/locationPicker.tsx b/components/owner/locationPicker.tsx new file mode 100644 index 0000000..2efa781 --- /dev/null +++ b/components/owner/locationPicker.tsx @@ -0,0 +1,148 @@ +import locationData from '@/assets/json/location.json'; +import React, { useEffect, useState } from 'react'; +import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { ThemedText } from '../ThemedText'; + +type Region = { + name: string; + regions?: Region[]; +}; + +const GlobalRegionPicker = () => { + const [selectedRegions, setSelectedRegions] = useState([]); + const [allLevelsData, setAllLevelsData] = useState([]); + + // 初始化第一级数据 + useEffect(() => { + setAllLevelsData([locationData as Region[]]); + }, []); + + // 处理地区选择 + const handleSelectRegion = (region: Region, level: number) => { + // 更新选中的地区 + const newSelectedRegions = [...selectedRegions.slice(0, level), region]; + setSelectedRegions(newSelectedRegions); + + // 如果有子区域,添加下一级数据 + if (region.regions && region.regions.length > 0) { + // 创建新的层级数据数组 + const newAllLevelsData = [...allLevelsData.slice(0, level + 1)]; + // 添加新的子区域数据 + newAllLevelsData.push(region.regions); + setAllLevelsData(newAllLevelsData); + } else { + // 如果没有子区域,截断到当前级别 + setAllLevelsData(allLevelsData.slice(0, level + 1)); + } + }; + + // 渲染某一级的地区列表 + const renderLevel = (regions: Region[], level: number) => { + return ( + + {regions.map((region, index) => ( + handleSelectRegion(region, level)} + > + + {region.name} + + + ))} + + ); + }; + + // 递归渲染地区列 + const renderRegionColumns = (data: Region[][], level: number = 0) => { + if (!data[level]) return null; + + return ( + + + {level > 0 && } + {renderLevel(data[level], level)} + + {data[level + 1] && renderRegionColumns(data, level + 1)} + + ); + }; + + return ( + + + {renderRegionColumns(allLevelsData)} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + scrollContent: { + flexGrow: 1, + }, + column: { + width: 120, // 每列固定宽度 + }, + hasNextLevel: { + borderRightWidth: 1, + borderRightColor: '#f0f0f0', + }, + levelContainer: { + width: '100%', + }, + divider: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 1, + backgroundColor: '#e0e0e0', + }, + regionItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 16 + }, + selectedItem: { + backgroundColor: '#F6F6F6', + }, + regionText: { + fontSize: 15, + color: '#333', + }, + selectedText: { + color: '#AC7E35', + fontWeight: '500', + }, + arrow: { + fontSize: 18, + color: '#999', + marginLeft: 8, + }, +}); + +export default GlobalRegionPicker; \ No newline at end of file diff --git a/components/owner/podium.tsx b/components/owner/podium.tsx new file mode 100644 index 0000000..36c5192 --- /dev/null +++ b/components/owner/podium.tsx @@ -0,0 +1,103 @@ +import UserSvg from "@/assets/icons/svg/ataver.svg"; +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 { Image, StyleSheet, View } from "react-native"; +import { ThemedText } from "../ThemedText"; +interface IPodium { + data: RankingItem[] +} +const PodiumComponent = ({ data }: IPodium) => { + + return ( + + + + + { + data[1]?.user_avatar_url + ? + : + } + + {data[1]?.user_nick_name} + + + + + + + { + data[0]?.user_avatar_url + ? + : + } + + {data[0]?.user_nick_name} + + + + + + + { + data[2]?.user_avatar_url + ? + : + } + + {data[2]?.user_nick_name} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + borderBottomWidth: 1, + borderBottomColor: '#B48C64', + }, + item: { + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + title: { + fontSize: 12, + fontWeight: '500', + color: '#fff', + textAlign: 'center', + width: '100%', + paddingHorizontal: 4 + }, + titleContainer: { + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-end', + marginTop: -29, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + width: 95, + paddingHorizontal: 4, + } +}); + +export default PodiumComponent; diff --git a/components/owner/qualification/lcenses.tsx b/components/owner/qualification/lcenses.tsx new file mode 100644 index 0000000..714a230 --- /dev/null +++ b/components/owner/qualification/lcenses.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Image, Modal, Pressable, StyleSheet } from 'react-native'; + +const LcensesModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void }) => { + const { modalVisible, setModalVisible } = props; + + return ( + { + setModalVisible(!modalVisible); + }}> + setModalVisible(false)}> + e.stopPropagation()} + > + + + + + + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + container: { + flex: 1, + }, + modalView: { + width: '100%', + height: '40%', + backgroundColor: 'white', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + closeButton: { + fontSize: 28, + color: '#4C320C', + padding: 10, + }, + modalContent: { + flex: 1, + }, + modalText: { + fontSize: 16, + color: '#4C320C', + }, + premium: { + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + content: { + flex: 1, + flexDirection: 'column', + gap: 4, + backgroundColor: '#FAF9F6', + borderRadius: 24, + paddingVertical: 8 + }, + item: { + paddingHorizontal: 16, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + itemText: { + fontSize: 14, + fontWeight: '600', + color: '#4C320C', + }, + upgradeButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + }, + upgradeButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: "600" + }, + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 2, + }, + switchOn: { + backgroundColor: '#E2793F', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, +}); +export default LcensesModal; \ No newline at end of file diff --git a/components/owner/qualification/privacy.tsx b/components/owner/qualification/privacy.tsx new file mode 100644 index 0000000..75a6078 --- /dev/null +++ b/components/owner/qualification/privacy.tsx @@ -0,0 +1,201 @@ +import { fetchApi } from '@/lib/server-api-util'; +import { Policy } from '@/types/personal-info'; +import React, { useEffect, useState } from 'react'; +import { Modal, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import RenderHtml from 'react-native-render-html'; + +const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, type: string }) => { + const { modalVisible, setModalVisible, type } = props; + const [article, setArticle] = useState({} as Policy); + useEffect(() => { + const loadArticle = async () => { + // ai协议 + if (type === 'ai') { + fetchApi(`/system-config/policy/ai_policy`).then((res: any) => { + setArticle(res) + }).catch((error: any) => { + console.log(error) + }) + } + // 应用协议 + if (type === 'terms') { + fetchApi(`/system-config/policy/terms_of_service`).then((res: any) => { + setArticle(res) + }).catch((error: any) => { + console.log(error) + }) + } + // 隐私协议 + if (type === 'privacy') { + fetchApi(`/system-config/policy/privacy_policy`).then((res: any) => { + setArticle(res) + }).catch((error: any) => { + console.log(error) + }) + } + //用户协议 + if (type === 'user') { + fetchApi(`/system-config/policy/user_agreement`).then((res: any) => { + setArticle(res) + }).catch((error: any) => { + console.log(error) + }) + } + }; + + loadArticle(); + }, []); + + if (!article) { + return ( + + 加载中... + + ); + } + + return ( + { + setModalVisible(!modalVisible); + }}> + setModalVisible(false)}> + e.stopPropagation()}> + + Settings + {type === 'ai' ? 'AI Policy' : type === 'terms' ? 'Terms of Service' : type === 'privacy' ? 'Privacy Policy' : 'User Agreement'} + setModalVisible(false)}> + × + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'flex-end', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + container: { + flex: 1, + }, + modalView: { + width: '100%', + height: '80%', + backgroundColor: 'white', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingHorizontal: 16, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + closeButton: { + fontSize: 28, + color: '#4C320C', + padding: 10, + }, + modalContent: { + flex: 1, + }, + modalText: { + fontSize: 16, + color: '#4C320C', + }, + premium: { + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + content: { + flex: 1, + flexDirection: 'column', + gap: 4, + backgroundColor: '#FAF9F6', + borderRadius: 24, + paddingVertical: 8 + }, + item: { + paddingHorizontal: 16, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + itemText: { + fontSize: 14, + fontWeight: '600', + color: '#4C320C', + }, + upgradeButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + }, + upgradeButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: "600" + }, + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 2, + }, + switchOn: { + backgroundColor: '#E2793F', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, +}); +export default PrivacyModal; \ No newline at end of file diff --git a/components/owner/rankList.tsx b/components/owner/rankList.tsx new file mode 100644 index 0000000..e8d42af --- /dev/null +++ b/components/owner/rankList.tsx @@ -0,0 +1,94 @@ +import AtaverSvg from "@/assets/icons/svg/ataver.svg"; +import OwnerSvg from "@/assets/icons/svg/owner.svg"; +import { RankingItem } from "@/types/user"; +import { Image, ScrollView, StyleSheet, View } from "react-native"; +import { ThemedText } from "../ThemedText"; +interface IRankList { + data: RankingItem[] +} +const RankList = (props: IRankList) => { + + return ( + + + Rank + Username + Profile + + {props.data?.filter((item, index) => index > 2).map((item, index) => ( + + {index === 1 && ( + + + + )} + {index + 1} + {item.user_nick_name} + + {item.user_avatar_url ? ( + + ) : ( + + )} + + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + headerText: { + fontSize: 14, + color: "#4C320C", + fontWeight: "600" + }, + item: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomWidth: 1, + borderBottomColor: '#B48C64', + height: 70, + position: 'relative', + }, + itemRank: { + fontSize: 14, + color: "#4C320C", + fontWeight: "700" + }, + itemName: { + fontSize: 14, + color: "#AC7E35", + }, + self: { + position: 'absolute', + width: '100%', + height: "100%", + left: 0, + top: 0, + bottom: 0, + right: 0, + zIndex: -1, + } +}); +export default RankList; diff --git a/components/owner/ranking.tsx b/components/owner/ranking.tsx new file mode 100644 index 0000000..c4a9166 --- /dev/null +++ b/components/owner/ranking.tsx @@ -0,0 +1,101 @@ +import RightArrowSvg from "@/assets/icons/svg/rightArrow.svg"; +import { TitleRankings } from "@/types/user"; +import { useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { FlatList, StyleSheet, TouchableOpacity, View } from "react-native"; +import { ThemedText } from "../ThemedText"; + +const Ranking = ({ data }: { data: TitleRankings[] }) => { + const router = useRouter(); + const { t } = useTranslation(); + + return ( + + + { + router.push('/top') + }}> + {t('generalSetting.rank', { ns: 'personal' })} + + + + item.display_name} + renderItem={({ item }) => ( + + No.{item.ranking} + {item.region} + {item.display_name} + {item.value} + + )} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: "column", + justifyContent: "space-between", + gap: 8, + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 12, + shadowColor: "#000", + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + header: { + flex: 1, + width: "100%", + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + headerItem: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + headerTitle: { + fontSize: 14, + fontWeight: '700', + color: '#4C320C', + }, + item: { + flexDirection: 'row', + alignItems: 'center', + width: "100%", + justifyContent: 'space-between', + paddingVertical: 8, // 建议加行高 + }, + rank: { + fontSize: 20, + fontWeight: '700', + color: '#4C320C', + minWidth: 80, // 新增 + }, + title: { + fontSize: 16, + fontWeight: '700', + color: '#4C320C', + flex: 1, + marginHorizontal: 8, // 新增 + }, + number: { + fontSize: 16, + fontWeight: '700', + color: '#AC7E35', + minWidth: 60, // 新增 + textAlign: 'right' + }, +}); + +export default Ranking; diff --git a/components/owner/resource.tsx b/components/owner/resource.tsx new file mode 100644 index 0000000..fcfe6b5 --- /dev/null +++ b/components/owner/resource.tsx @@ -0,0 +1,78 @@ +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; +import * as Progress from 'react-native-progress'; +import { ThemedText } from "../ThemedText"; +import { formatBytes } from "../utils/bytes"; + +interface Data { + all: number; + used: number; +} +interface ResourceProps { + title: string; + subtitle?: string; + data: Data + icon: any; + style?: StyleProp; + // 是否要转化单位 + isFormatBytes?: boolean; +} +const ResourceComponent = (props: ResourceProps) => { + return ( + + + + {props.title} + {props.subtitle || " "} + + + {props.icon} + + + + {props.isFormatBytes ? formatBytes(props.data.used) : props.data.used}/{props.isFormatBytes ? formatBytes(props.data.all) : props.data.all} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: "100%", + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 18, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + title: { + fontSize: 12, + fontWeight: "700", + }, + subtitle: { + fontSize: 10, + fontWeight: "400", + }, + dataContainer: { + flexDirection: "column", + }, + dataText: { + fontSize: 12, + width: "100%", + color: "#AC7E35", + textAlign: "right", + }, +}) + +export default ResourceComponent; \ No newline at end of file diff --git a/components/owner/setting.tsx b/components/owner/setting.tsx new file mode 100644 index 0000000..b6ad4c9 --- /dev/null +++ b/components/owner/setting.tsx @@ -0,0 +1,469 @@ +import LogoutSvg from '@/assets/icons/svg/logout.svg'; +import RightArrowSvg from '@/assets/icons/svg/rightArrow.svg'; +import { useAuth } from '@/contexts/auth-context'; +import { fetchApi } from '@/lib/server-api-util'; +import { Address, User } from '@/types/user'; +import * as Location from 'expo-location'; +import { useRouter } from 'expo-router'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Linking, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { ThemedText } from '../ThemedText'; +import LcensesModal from './qualification/lcenses'; +import PrivacyModal from './qualification/privacy'; +import CustomSwitch from './switch'; +import UserInfo from './userInfo'; +import { getLocationPermission, getPermissions, requestLocationPermission, requestMediaLibraryPermission, reverseGeocode } from './utils'; + +const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => { + const { modalVisible, setModalVisible, userInfo } = props; + const { t } = useTranslation(); + const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai'); + // 协议弹窗 + const [privacyModalVisible, setPrivacyModalVisible] = useState(false); + // 许可证弹窗 + const [lcensesModalVisible, setLcensesModalVisible] = useState(false); + + const { logout } = useAuth(); + const router = useRouter(); + // 打开设置 + const openAppSettings = () => { + Linking.openSettings(); + }; + // 通知消息权限开关 + const [notificationsEnabled, setNotificationsEnabled] = useState(false); + const toggleNotifications = () => setNotificationsEnabled(previous => !previous); + + // 相册权限 + const [albumEnabled, setAlbumEnabled] = useState(false); + const toggleAlbum = () => { + if (albumEnabled) { + // 引导去设置关闭权限 + openAppSettings() + } else { + requestMediaLibraryPermission().then((res) => { + setAlbumEnabled(res as boolean); + }) + } + } + + // 位置权限 + const [locationEnabled, setLocationEnabled] = useState(false); + // 位置权限更改 + const toggleLocation = async () => { + if (locationEnabled) { + // 引导去设置关闭权限 + openAppSettings() + } else { + requestLocationPermission().then((res) => { + setLocationEnabled(res as boolean); + }) + } + }; + // 正在获取位置信息 + const [isLoading, setIsLoading] = useState(false); + // 动画开启 + const [isRefreshing, setIsRefreshing] = useState(false); + + // 当前位置状态 + const [currentLocation, setCurrentLocation] = useState
({} as Address); + + // 获取当前位置 + const getCurrentLocation = async () => { + setIsLoading(true); + setIsRefreshing(true); + + try { + // 1. 首先检查当前权限状态 -- 获取当前的位置权限 + let currentStatus = await getLocationPermission(); + console.log('当前权限状态:', currentStatus); + + // 2. 如果没有权限,则请求权限 + if (!currentStatus) { + const newStatus = await requestLocationPermission(); + setLocationEnabled(newStatus); + currentStatus = newStatus; + + if (!currentStatus) { + alert('需要位置权限才能继续'); + return; + } + } + + // 3. 确保位置服务已启用 + const isEnabled = await Location.hasServicesEnabledAsync(); + if (!isEnabled) { + alert('请先启用位置服务'); + return; + } + console.log('位置服务已启用'); + // 4. 获取当前位置 + const location = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.High, // 使用高精度 + timeInterval: 10000, // 可选:最大等待时间(毫秒) + }); + console.log('位置:', location); + + // 地理位置逆编码 + const address = await reverseGeocode(location.coords.latitude, location.coords.longitude); + // 5. 更新位置状态 + setCurrentLocation(address as Address); + + return location; + } catch (error: any) { + if (error.code === 'PERMISSION_DENIED' || error.code === 'PERMISSION_DENIED_ERROR') { + alert('位置权限被拒绝,请在设置中启用位置服务'); + } else if (error.code === 'TIMEOUT') { + alert('获取位置超时,请检查网络和位置服务'); + } else { + alert(`无法获取您的位置: ${error.message || '未知错误'}`); + } + throw error; // 重新抛出错误以便上层处理 + } finally { + setIsLoading(false); + setIsRefreshing(false); + } + }; + + // 退出登录 + + const handleLogout = () => { + fetchApi("/iam/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then(async (res) => { + await logout(); + setModalVisible(false); + router.replace('/login'); + }) + .catch(() => { + console.error("jwt has expired."); + }); + }; + // 检查是否有权限 + useEffect(() => { + if (modalVisible) { + // 位置权限 + getLocationPermission().then((res) => { + setLocationEnabled(res); + }) + // 媒体库权限 + getPermissions().then((res) => { + setAlbumEnabled(res); + }) + } + }, [modalVisible]) + + return ( + { + setModalVisible(!modalVisible); + }}> + setModalVisible(false)}> + e.stopPropagation()}> + + Settings + {t('generalSetting.allTitle', { ns: 'personal' })} + setModalVisible(false)}> + × + + + + {/* 用户信息 */} + + {/* 升级版本 */} + + {t('generalSetting.subscription', { ns: 'personal' })} + + + {t('generalSetting.subscriptionTitle', { ns: 'personal' })} + {t('generalSetting.subscriptionText', { ns: 'personal' })} + + { + + }} + > + + {t('generalSetting.upgrade', { ns: 'personal' })} + + + + + {/* 消息通知 */} + + {t('permission.pushNotification', { ns: 'personal' })} + + + {t('permission.pushNotification', { ns: 'personal' })} + + + + + {/* 权限信息 */} + + {t('permission.permissionManagement', { ns: 'personal' })} + + {/* 相册权限 */} + + {t('permission.galleryAccess', { ns: 'personal' })} + + + {/* 分割线 */} + + {/* 位置权限 */} + + + {t('permission.locationPermission', { ns: 'personal' })} + + + + {/* */} + {/* 相册成片权限 */} + {/* + + Opus Permission + + + */} + + + {/* 账号 */} + {/* + Account + + + + Notifications + + + + + + Delete Account + + + + */} + {/* 协议 */} + + {t('lcenses.title', { ns: 'personal' })} + + { setLcensesModalVisible(true) }} > + {t('lcenses.qualification', { ns: 'personal' })} + + + + Linking.openURL("https://beian.miit.gov.cn/")} > + {t('lcenses.ICP', { ns: 'personal' })}沪ICP备2023032876号-4 + + + + { setModalType('privacy'); setPrivacyModalVisible(true) }} > + {t('lcenses.privacyPolicy', { ns: 'personal' })} + + + + { setModalType('ai'); setPrivacyModalVisible(true) }} > + {t('lcenses.aiPolicy', { ns: 'personal' })} + + + + { setModalType('terms'); setPrivacyModalVisible(true) }} > + {t('lcenses.applyPermission', { ns: 'personal' })} + + + + { setModalType('user'); setPrivacyModalVisible(true) }} > + {t('lcenses.userAgreement', { ns: 'personal' })} + + + + + {/* 其他信息 */} + + {t('generalSetting.otherInformation', { ns: 'personal' })} + + Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} > + {t('generalSetting.contactUs', { ns: 'personal' })} + {/* */} + + + {Platform.OS !== 'ios' && ( + + Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} > + {t('generalSetting.cleanCache', { ns: 'personal' })} + {/* */} + + + + )} + + {t('generalSetting.version', { ns: 'personal' })} + {"0.5.0"} + + + + {/* 退出 */} + + {t('generalSetting.logout', { ns: 'personal' })} + + + + + + {/* 协议弹窗 */} + + {/* 许可证弹窗 */} + + {/* 通知 */} + {/* */} + + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'flex-end', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + modalView: { + width: '100%', + height: '80%', + backgroundColor: 'white', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingHorizontal: 16, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + closeButton: { + fontSize: 28, + color: '#4C320C', + padding: 10, + }, + modalContent: { + flex: 1, + }, + modalText: { + fontSize: 16, + color: '#4C320C', + }, + premium: { + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + content: { + flex: 1, + flexDirection: 'column', + gap: 4, + backgroundColor: '#FAF9F6', + borderRadius: 24, + paddingVertical: 8 + }, + item: { + paddingHorizontal: 16, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + itemText: { + fontSize: 14, + fontWeight: '600', + color: '#4C320C', + }, + upgradeButton: { + backgroundColor: '#E2793F', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + }, + upgradeButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: "600" + }, + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 2, + }, + switchOn: { + backgroundColor: '#E2793F', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, +}); + +const Divider = () => { + return ( + + ) +} +export default SettingModal; \ No newline at end of file diff --git a/components/owner/switch.tsx b/components/owner/switch.tsx new file mode 100644 index 0000000..689876a --- /dev/null +++ b/components/owner/switch.tsx @@ -0,0 +1,42 @@ +import { StyleSheet, TouchableOpacity, View } from "react-native"; + +const CustomSwitch = ({ isEnabled, toggleSwitch }: { isEnabled: boolean, toggleSwitch: () => void }) => ( + + + +); + +const styles = StyleSheet.create({ + switchContainer: { + width: 50, + height: 30, + borderRadius: 15, + justifyContent: 'center', + paddingHorizontal: 4, + }, + switchOn: { + backgroundColor: '#FFB645', + alignItems: 'flex-end', + }, + switchOff: { + backgroundColor: '#E5E5E5', + alignItems: 'flex-start', + }, + switchCircle: { + width: 26, + height: 26, + borderRadius: 13, + }, + switchCircleOn: { + backgroundColor: 'white', + }, + switchCircleOff: { + backgroundColor: '#A5A5A5', + }, +}); + +export default CustomSwitch; \ No newline at end of file diff --git a/components/owner/userInfo.tsx b/components/owner/userInfo.tsx new file mode 100644 index 0000000..5e769a9 --- /dev/null +++ b/components/owner/userInfo.tsx @@ -0,0 +1,189 @@ +import UserSvg from "@/assets/icons/svg/ataver.svg"; +import EditSvg from "@/assets/icons/svg/edit.svg"; +import LocationSvg from "@/assets/icons/svg/location.svg"; +import RefreshSvg from "@/assets/icons/svg/refresh.svg"; +import { Address, User } from "@/types/user"; +import { useRouter } from "expo-router"; +import * as SecureStore from 'expo-secure-store'; +import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Animated, Easing, Image, Platform, StyleSheet, TouchableOpacity, View } from "react-native"; +import { ThemedText } from "../ThemedText"; + +interface UserInfoProps { + userInfo: User; + setModalVisible: (visible: boolean) => void; + modalVisible: boolean; + getCurrentLocation: () => void; + isLoading: boolean; + isRefreshing: boolean; + currentLocation: Address; + setCurrentLocation: (location: Address) => void; +} +const UserInfo = (props: UserInfoProps) => { + const { userInfo, setModalVisible, modalVisible, getCurrentLocation, isLoading, isRefreshing, currentLocation, setCurrentLocation } = props; + const router = useRouter(); + const { t } = useTranslation(); + // 获取本地存储的location + const getLocation = async () => { + if (Platform.OS === 'web') { + const location = localStorage.getItem('location'); + if (location) { + setCurrentLocation(JSON.parse(location)); + } + } else { + const location = await SecureStore.getItemAsync('location'); + if (location) { + setCurrentLocation(JSON.parse(location)); + } + } + }; + // 添加旋转动画值 + const spinValue = useRef(new Animated.Value(0)).current; + + + // 旋转动画 + const startSpin = () => { + spinValue.setValue(0); + Animated.loop( + Animated.timing(spinValue, { + toValue: 1, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }) + ).start(); + }; + + // 停止旋转 + const stopSpin = () => { + spinValue.stopAnimation(); + spinValue.setValue(0); + }; + // 当开始加载时启动旋转 + useEffect(() => { + if (isLoading) { + startSpin(); + } else { + stopSpin(); + } + }, [isLoading]); + + // 在组件挂载时自动获取位置(可选) + useEffect(() => { + if (modalVisible) { + getLocation(); + if (Object.keys(currentLocation).length === 0) { + getCurrentLocation(); + } + } + }, [modalVisible]) + + return ( + + + {userInfo?.nickname} + {t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id} + + + + {currentLocation?.country}-{currentLocation?.city}-{currentLocation?.district} + + + + + + + + + + {userInfo?.avatar_file_url + ? + + : + + } + { + setModalVisible(false); + router.push('/user-message') + }}> + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: "#FAF9F6", + padding: 16, + borderRadius: 24, + }, + info: { + flexDirection: 'column', + gap: 8, + }, + nickname: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + }, + userId: { + fontSize: 12, + color: '#4C320C', + }, + location: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 4, + gap: 4 + }, + refreshContainer: { + width: 24, + height: 24, + justifyContent: 'center', + alignItems: 'center', + marginLeft: 4 + }, + refreshIcon: { + width: 16, + height: 16, + }, + avatar: { + position: 'relative', + }, + edit: { + position: 'absolute', + padding: 8, + backgroundColor: '#fff', + borderRadius: 50, + right: -5, + bottom: -5, + } +}) + +export default UserInfo; diff --git a/components/owner/userName.tsx b/components/owner/userName.tsx new file mode 100644 index 0000000..ef3beff --- /dev/null +++ b/components/owner/userName.tsx @@ -0,0 +1,70 @@ +import UserSvg from '@/assets/icons/svg/ataver.svg'; +import { ThemedText } from '@/components/ThemedText'; +import { UserInfoDetails } from '@/types/user'; +// import { Image } from 'expo-image'; +import { Image, ScrollView, View } from 'react-native'; +export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) { + + return ( + + {/* 用户名 */} + + + + + {userInfo?.user_info?.nickname} + + + { + userInfo?.medal_infos?.map((item, index) => ( + + )) + } + + + + + User ID:{userInfo?.user_info?.user_id} + + + {/* 头像 */} + + {userInfo?.user_info?.avatar_file_url + ? + + : + + } + + + ); +} + diff --git a/components/owner/utils.ts b/components/owner/utils.ts new file mode 100644 index 0000000..78071c1 --- /dev/null +++ b/components/owner/utils.ts @@ -0,0 +1,195 @@ +// 地理位置逆编码 +import * as ImagePicker from 'expo-image-picker'; +import * as Location from 'expo-location'; +import * as SecureStore from 'expo-secure-store'; +import { Alert, Linking, Platform } from 'react-native'; +export const reverseGeocode = async (latitude: number, longitude: number) => { + try { + const addressResults = await Location.reverseGeocodeAsync({ latitude, longitude }); + for (let address of addressResults) { + console.log('地址:', address); + if (Platform.OS === 'web') { + localStorage.setItem('location', JSON.stringify(address)); + } else { + SecureStore.setItemAsync('location', JSON.stringify(address)); + } + return address; + } + } catch (error) { + console.error('逆地理编码失败:', error); + } +}; + +// 获取位置权限 +export const getLocationPermission = async () => { + const { status } = await Location.getForegroundPermissionsAsync(); + if (status !== 'granted') { + // Alert.alert('需要位置权限', '请允许访问位置以继续'); + return false; + } + return true; +}; + +// 请求位置权限 +export const requestLocationPermission = async () => { + try { + // 1. 先检查当前权限状态 + const { status, canAskAgain } = await Location.getForegroundPermissionsAsync(); + console.log('当前权限状态:', { status, canAskAgain }); + console.log("canAskAgain", canAskAgain); + + // 2. 如果已经有权限,直接返回 + if (status === 'granted') { + return true; + } + + // 3. 如果用户之前选择了"拒绝且不再询问" + if (status === 'denied' && !canAskAgain) { + // 显示提示,引导用户去设置 + const openSettings = await new Promise(resolve => { + Alert.alert( + '需要位置权限', + '您之前拒绝了位置权限。要使用此功能,请在设置中启用位置权限。', + [ + { + text: '取消', + style: 'cancel', + onPress: () => resolve(false) + }, + { + text: '去设置', + onPress: () => resolve(true) + } + ] + ); + }); + + if (openSettings) { + // 打开应用设置 + await Linking.openSettings(); + } + return false; + } + + // 4. 如果是第一次请求或可以再次询问,则请求权限 + console.log('请求位置权限...'); + const { status: newStatus } = await Location.requestForegroundPermissionsAsync(); + console.log('新权限状态:', newStatus); + + if (newStatus !== 'granted') { + Alert.alert('需要位置权限', '请允许访问位置以使用此功能'); + return false; + } + + return true; + } catch (error) { + console.error('请求位置权限时出错:', error); + Alert.alert('错误', '请求位置权限时出错'); + return false; + } +}; + +// 获取媒体库权限 +export const getPermissions = async () => { + if (Platform.OS !== 'web') { + const { status: mediaStatus } = await ImagePicker.getMediaLibraryPermissionsAsync(); + if (mediaStatus !== 'granted') { + // Alert.alert('需要媒体库权限', '请允许访问媒体库以继续'); + return false; + } + return true; + } + return true; +}; + +// 请求媒体库权限 +export const requestPermissions = async () => { + if (Platform.OS !== 'web') { + const mediaStatus = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!mediaStatus.granted) { + // Alert.alert('需要媒体库权限', '请允许访问媒体库以继续'); + return false; + } + return true; + } + return true; +}; + +/** + * 检查相册/媒体库权限 + * @returns 返回权限状态对象 + */ +export const checkMediaLibraryPermission = async (): Promise<{ + hasPermission: boolean; + canAskAgain: boolean; + status: ImagePicker.PermissionStatus; +}> => { + if (Platform.OS === 'web') { + return { hasPermission: true, canAskAgain: true, status: 'granted' }; + } + + const { status, canAskAgain } = await ImagePicker.getMediaLibraryPermissionsAsync(); + + return { + hasPermission: status === 'granted', + canAskAgain, + status + }; +}; + +/** + * 请求相册/媒体库权限 + * @param showAlert 是否在无权限时显示提示 + * @returns 返回是否已授权 + */ +export const requestMediaLibraryPermission = async (showAlert: boolean = true): Promise => { + if (Platform.OS === 'web') { + return true; + } + + try { + // 1. 检查当前权限状态 + const { status: existingStatus, canAskAgain } = await checkMediaLibraryPermission(); + + // 2. 如果已经有权限,直接返回 + if (existingStatus === 'granted') { + return true; + } + + // 3. 如果之前被拒绝且不能再次询问 + if (existingStatus === 'denied' && !canAskAgain) { + if (showAlert) { + const openSettings = await new Promise(resolve => { + Alert.alert( + '需要媒体库权限', + '您之前拒绝了媒体库访问权限。要选择照片,请在设置中启用媒体库权限。', + [ + { text: '取消', style: 'cancel', onPress: () => resolve(false) }, + { text: '去设置', onPress: () => resolve(true) } + ] + ); + }); + + if (openSettings) { + await Linking.openSettings(); + } + } + return false; + } + + // 4. 请求权限 + const { status: newStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + + if (newStatus !== 'granted' && showAlert) { + Alert.alert('需要媒体库权限', '请允许访问媒体库以方便后续操作'); + } + + return newStatus === 'granted'; + } catch (error) { + console.error('请求媒体库权限时出错:', error); + if (showAlert) { + Alert.alert('错误', '请求媒体库权限时出错'); + } + return false; + } +}; \ No newline at end of file diff --git a/components/utils/bytes.ts b/components/utils/bytes.ts new file mode 100644 index 0000000..ad1c140 --- /dev/null +++ b/components/utils/bytes.ts @@ -0,0 +1,17 @@ +/** + * 将字节数转换为易读的格式 + * @param bytes 字节数 + * @param decimals 保留的小数位数,默认为2 + * @returns 格式化后的字符串,如 "1.5 GB" + */ +export function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} \ No newline at end of file diff --git a/components/utils/cascaderData.ts b/components/utils/cascaderData.ts new file mode 100644 index 0000000..e24edc8 --- /dev/null +++ b/components/utils/cascaderData.ts @@ -0,0 +1,90 @@ +// 定义源数据类型 +type SourceRegion = { + name: string; + shortCode?: string; + regions?: SourceRegion[]; + [key: string]: any; // 允许其他自定义属性 +}; + +// 定义目标数据类型 +type TargetRegion = { + name: string; + value?: string | number; // 添加 value 字段 + children?: TargetRegion[]; + [key: string]: any; +}; + +/** + * 将任意结构的地区数据转换为统一的嵌套结构 + * @param data 源数据数组 + * @param keys 字段映射配置 + * @returns 转换后的数据 + */ +export function convertRegions( + data: SourceRegion[], + keys: { + nameKey?: string; + valueKey?: string; // 新增:指定 value 字段 + regionsKey?: string; + childrenKey?: string; + } = {} +): TargetRegion[] { + const { + nameKey = 'name', + valueKey = 'shortCode', // 默认使用 shortCode 作为 value + regionsKey = 'regions', + childrenKey = 'children', + } = keys; + + return data.map(item => { + const converted: TargetRegion = { + name: item[nameKey] as string, + }; + + // 如果指定了 valueKey 且源数据中存在该字段,则添加 value + if (valueKey && item[valueKey] !== undefined) { + converted.value = item[valueKey]; + } + + if (item[regionsKey]) { + const children = convertRegions( + item[regionsKey] as SourceRegion[], + keys // 传递 keys 以保持配置一致 + ); + if (children.length > 0) { + converted[childrenKey] = children; + } + } + + return converted; + }); +} + + +/*** + * 使用示例 + * regionsData 是原数据 + * + * + * const transformed = convertRegions(regionsData, { + * nameKey: 'name', // 源数据中表示“名称”的字段 + * regionsKey: 'regions', // 源数据中表示“子级区域”的字段 + * childrenKey: 'children' // 输出结构中表示“子级区域”的字段 + * }); + * + * + * 输出示例: +* [ +* { +* "name": "北京市", +* "value": "BJ", +* "children": [ +* { +* "name": "东城区", +* "value": "Dongcheng" +* }, +* // ... +* ] +* } +* ] + */ \ No newline at end of file diff --git a/components/utils/objectToCascader.ts b/components/utils/objectToCascader.ts new file mode 100644 index 0000000..be00a6f --- /dev/null +++ b/components/utils/objectToCascader.ts @@ -0,0 +1,22 @@ +import { GroupedData, TargetItem } from "@/types/user"; + +export function transformData(data: GroupedData): TargetItem[] { + const result: TargetItem[] = []; + + for (const category in data) { + const items = data[category]; + + // 构建该类别的 children + const children = items.map(item => ({ + name: item.display_name, + value: item.id + })); + + result.push({ + name: category, + children, + }); + } + + return result; +} \ No newline at end of file diff --git a/components/utils/time.ts b/components/utils/time.ts new file mode 100644 index 0000000..775d246 --- /dev/null +++ b/components/utils/time.ts @@ -0,0 +1,28 @@ +/** + * 将秒数转换为更友好的时间格式 + * @param seconds 总秒数 + * @returns 格式化后的时间字符串 + */ +export function formatDuration(seconds: number): string { + if (seconds < 60) { + return `${seconds}s`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + if (minutes < 60) { + return remainingSeconds > 0 + ? `${minutes}min${remainingSeconds}s` + : `${minutes}min`; + } + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + if (remainingMinutes === 0) { + return `${hours}h`; + } + + return `${hours}h${remainingMinutes}min`; +} diff --git a/i18n/locales/en/personal.json b/i18n/locales/en/personal.json index 48bbfa1..8fc1e2f 100644 --- a/i18n/locales/en/personal.json +++ b/i18n/locales/en/personal.json @@ -1,24 +1,25 @@ { "info": { - "photoCount": "Photos", - "videoCount": "Videos", - "memoryData": "Storage" + "photoCount": "Photo Count", + "videoCount": "Video Count", + "memoryData": "Memory Data" }, "pro": { "title": "Subscribe to MemoWark", - "subtitle": "Let the love of family flow again like memories in photos" + "subtitle": "Family love flows again like memories in photos" }, "setting": { "syncThirdPartyData": "Sync WeChat Data", "myGallery": "My Gallery", - "myVideo": "My Created", + "myVideo": "My Creations", "AIVideo": "AI Video", "setting": "General Settings", "recommendMemoWake": "Recommend MemoWake", - "fiveStarReview": "Rate 5 Stars", + "fiveStarReview": "5-Star Rating", "contactUs": "Contact Support", - "qualification": "Certifications", - "otherAgreement": "Other Agreements" + "qualification": "Qualifications", + "otherAgreement": "Other Agreements", + "version": "Version" }, "personalInfo": { "title": "Personal Information", @@ -26,10 +27,61 @@ "nickname": "Nickname", "userId": "User ID", "email": "Email", - "phone": "Phone", + "phone": "Phone Number", "chat": "WeChat", "changeNickname": "Change Nickname", "nicknameSave": "Save", "phoneBind": "Bind Phone Number" + }, + "lcenses": { + "title": "Agreements", + "ICP": "ICP: ", + "licencePhoto": "Business License", + "otherLcenses": "Other Agreements", + "userAgreement": "User Agreement", + "privacyPolicy": "Privacy Policy", + "aiPolicy": "AI Feature Usage Guidelines", + "applyPermission": "Request Permission", + "qualification": "Qualifications" + }, + "permission": { + "permissionManagement": "Permission Settings", + "pushNotification": "Push Notifications", + "galleryAccess": "Photo Library Access", + "locationPermission": "Location Access", + "personalizedRecommendation": "Personalized Recommendations" + }, + "generalSetting": { + "title": "General Settings", + "permissionManagement": "Permission Settings", + "pushNotification": "Push Notifications", + "galleryAccess": "Photo Library Access", + "personalizedRecommendation": "Personalized Recommendations", + "deleteAccount": "Delete Account", + "logout": "Log Out", + "upgrade": "Upgrade", + "subscription": "Subscription", + "subscriptionTitle": "MemoWake Premium", + "subscriptionText": "Unlock more of what you love", + "otherInformation": "Other Information", + "contactUs": "Contact Support", + "version": "Version", + "cleanCache": "Clear Cache", + "allTitle": "Settings", + "album": "Gallery", + "shareProfile": "Share Profile", + "classify": "Categories", + "confirm": "Confirm", + "location": "Location", + "rank": "Top Memory Makers", + "userId": "User ID", + "usedStorage": "Storage Used", + "remainingPoints": "Points Remaining", + "totalVideo": "Total Videos", + "totalPhoto": "Total Photos", + "live": "Live Photos", + "videoLength": "Video Duration", + "storiesCreated": "Stories Created", + "conversationsWithMemo": "Conversations with Memo" } } \ No newline at end of file diff --git a/i18n/locales/zh/personal.json b/i18n/locales/zh/personal.json index 41d70f3..ae05a98 100644 --- a/i18n/locales/zh/personal.json +++ b/i18n/locales/zh/personal.json @@ -34,14 +34,22 @@ "phoneBind": "绑定手机号" }, "lcenses": { - "title": "资质证照", + "title": "协议", "ICP": "ICP备案:", "licencePhoto": "营业执照", "otherLcenses": "其他协议", "userAgreement": "用户协议", "privacyPolicy": "隐私政策", "aiPolicy": "《AI功能使用规范》", - "applyPermission": "申请使用权限" + "applyPermission": "申请使用权限", + "qualification": "资质证照" + }, + "permission": { + "permissionManagement": "权限管理设置", + "pushNotification": "推送权限", + "galleryAccess": "相册权限", + "locationPermission": "位置权限", + "personalizedRecommendation": "个性化推荐设置" }, "generalSetting": { "title": "通用设置", @@ -50,6 +58,30 @@ "galleryAccess": "相册权限", "personalizedRecommendation": "个性化推荐设置", "deleteAccount": "注销账号", - "logout": "退出登录" + "logout": "退出登录", + "upgrade": "升级", + "subscription": "订阅", + "subscriptionTitle": "MemoWake Premium", + "subscriptionText": "Unlock more of what you love", + "otherInformation": "其他信息", + "contactUs": "联系客服", + "version": "版本号", + "cleanCache": "清理缓存", + "allTitle": "设置", + "album": "图库", + "shareProfile": "分享个人资料", + "classify": "分类", + "confirm": "确定", + "location": "地区选择", + "rank": "Top Memory Makers", + "userId": "User ID", + "usedStorage": "已使用存储", + "remainingPoints": "剩余积分", + "totalVideo": "视频总量", + "totalPhoto": "照片总量", + "live": "动图", + "videoLength": "视频时长", + "storiesCreated": "创作视频", + "conversationsWithMemo": "Memo对话" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8c136e8..32c4a54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,16 +19,18 @@ "expo-blur": "~14.1.5", "expo-constants": "~17.1.6", "expo-dev-client": "~5.2.1", + "expo-device": "~7.1.4", "expo-file-system": "~18.1.10", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", - "expo-image": "~2.3.0", + "expo-image": "~2.3.2", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.5", "expo-localization": "^16.1.5", "expo-location": "~18.1.5", "expo-media-library": "~17.1.7", + "expo-notifications": "~0.31.4", "expo-router": "~5.1.0", "expo-secure-store": "~14.2.3", "expo-splash-screen": "~0.30.9", @@ -48,8 +50,11 @@ "react-i18next": "^15.5.3", "react-native": "0.79.4", "react-native-gesture-handler": "~2.24.0", + "react-native-modal": "^14.0.0-rc.1", + "react-native-picker-select": "^9.3.1", "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.17.4", + "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", "react-native-svg": "15.11.2", @@ -2443,9 +2448,9 @@ } }, "node_modules/@expo/env": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-1.0.6.tgz", - "integrity": "sha512-aokrM+EYgyaJNmyo8QhphP3egVi0E7/4PiAx+riW1k39wu26POCg5NBdOSBHoYGmq1NXbhpFepIFDWVaCw1UeA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-1.0.7.tgz", + "integrity": "sha512-qSTEnwvuYJ3umapO9XJtrb1fAqiPlmUUg78N0IZXXGwQRt+bkp0OBls+Y5Mxw/Owj8waAM0Z3huKKskRADR5ow==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -2515,9 +2520,9 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.7.5.tgz", - "integrity": "sha512-92sk+dplZHlZuv4jAWmGBOqWf70hcb0zoObmjQRgxvZbKWXlQ6ifANaOUhoeJKgNWSB9BrLoW6v/mUyrDUdK+A==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.7.6.tgz", + "integrity": "sha512-GKnMqC79+mo/1AFrmAcUcGfbsXXTRqOMNS1umebuevl3aaw+ztsYEFEiuNhHZW7PQ3Xs3URNT513ZxKhznDscw==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -2833,6 +2838,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ide/backoff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3143,6 +3154,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsamr/counter-style": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@jsamr/counter-style/-/counter-style-2.0.2.tgz", + "integrity": "sha512-2mXudGVtSzVxWEA7B9jZLKjoXUeUFYDDtFrQoC0IFX9/Dszz4t1vZOmafi3JSw/FxD+udMQ+4TAFR8Qs0J3URQ==", + "license": "MIT" + }, + "node_modules/@jsamr/react-native-li": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@jsamr/react-native-li/-/react-native-li-2.3.1.tgz", + "integrity": "sha512-Qbo4NEj48SQ4k8FZJHFE2fgZDKTWaUGmVxcIQh3msg5JezLdTMMHuRRDYctfdHI6L0FZGObmEv3haWbIvmol8w==", + "license": "MIT", + "peerDependencies": { + "@jsamr/counter-style": "^1.0.0 || ^2.0.0", + "react": "*", + "react-native": "*" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", @@ -3156,6 +3184,92 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@native-html/css-processor": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@native-html/css-processor/-/css-processor-1.11.0.tgz", + "integrity": "sha512-NnhBEbJX5M2gBGltPKOetiLlKhNf3OHdRafc8//e2ZQxXN8JaSW/Hy8cm94pnIckQxwaMKxrtaNT3x4ZcffoNQ==", + "license": "MIT", + "dependencies": { + "css-to-react-native": "^3.0.0", + "csstype": "^3.0.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*" + } + }, + "node_modules/@native-html/transient-render-engine": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@native-html/transient-render-engine/-/transient-render-engine-11.2.3.tgz", + "integrity": "sha512-zXwgA3gPUEmFs3I3syfnvDvS6WiUHXEE6jY09OBzK+trq7wkweOSFWIoyXiGkbXrozGYG0KY90YgPyr8Tg8Uyg==", + "license": "MIT", + "dependencies": { + "@native-html/css-processor": "1.11.0", + "@types/ramda": "^0.27.44", + "csstype": "^3.0.9", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "htmlparser2": "^7.1.2", + "ramda": "^0.27.2" + }, + "peerDependencies": { + "@types/react-native": "*", + "react-native": "^*" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3244,6 +3358,20 @@ } } }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", + "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "license": "MIT", + "peer": true, + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.4", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz", @@ -3538,6 +3666,20 @@ "integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==", "license": "MIT" }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", + "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@react-navigation/bottom-tabs": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.2.tgz", @@ -4228,6 +4370,15 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/ramda": { + "version": "0.27.66", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz", + "integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==", + "license": "MIT", + "dependencies": { + "ts-toolbelt": "^6.15.1" + } + }, "node_modules/@types/react": { "version": "19.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz", @@ -4237,6 +4388,17 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", + "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/virtualized-lists": "^0.72.4", + "@types/react": "*" + } + }, "node_modules/@types/react-redux": { "version": "7.1.34", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", @@ -4264,6 +4426,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -5253,6 +5421,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5273,7 +5454,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5479,6 +5659,12 @@ "@babel/core": "^7.0.0" } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5688,7 +5874,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -5707,7 +5892,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5721,7 +5905,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5795,6 +5978,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001726", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", @@ -5831,6 +6023,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6287,6 +6499,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -6312,6 +6533,17 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -6521,7 +6753,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -6548,7 +6779,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -6725,7 +6955,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6881,7 +7110,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6891,7 +7119,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6929,7 +7156,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7537,6 +7763,15 @@ } } }, + "node_modules/expo-application": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.1.5.tgz", + "integrity": "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.6.tgz", @@ -7575,13 +7810,13 @@ } }, "node_modules/expo-constants": { - "version": "17.1.6", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.6.tgz", - "integrity": "sha512-q5mLvJiLtPcaZ7t2diSOlQ2AyxIO8YMVEJsEfI/ExkGj15JrflNQ7CALEW6IF/uNae/76qI/XcjEuuAyjdaCNw==", + "version": "17.1.7", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz", + "integrity": "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA==", "license": "MIT", "dependencies": { - "@expo/config": "~11.0.9", - "@expo/env": "~1.0.5" + "@expo/config": "~11.0.12", + "@expo/env": "~1.0.7" }, "peerDependencies": { "expo": "*", @@ -7662,6 +7897,44 @@ "expo": "*" } }, + "node_modules/expo-device": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.1.4.tgz", + "integrity": "sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/expo-file-system": { "version": "18.1.11", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz", @@ -7846,6 +8119,26 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-notifications": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.31.4.tgz", + "integrity": "sha512-NnGKIFGpgZU66qfiFUyjEBYsS77VahURpSSeWEOLt+P1zOaUFlgx2XqS+dxH3/Bn1Vm7TMj04qKsK5KvzR/8Lw==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.7.6", + "@ide/backoff": "^1.0.0", + "abort-controller": "^3.0.0", + "assert": "^2.0.0", + "badgin": "^1.1.5", + "expo-application": "~6.1.5", + "expo-constants": "~17.1.7" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.1.2.tgz", @@ -8268,7 +8561,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -8396,7 +8688,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8430,7 +8721,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8570,7 +8860,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8627,7 +8916,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -8656,7 +8944,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8669,7 +8956,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8750,6 +9036,89 @@ "void-elements": "3.1.0" } }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -8989,6 +9358,22 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9105,7 +9490,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9226,7 +9610,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -9266,6 +9649,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -9309,7 +9708,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9392,7 +9790,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -10171,6 +10568,19 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10344,7 +10754,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11072,11 +11481,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11086,7 +11510,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11602,7 +12025,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12018,6 +12440,12 @@ ], "license": "MIT" }, + "node_modules/ramda": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", + "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12212,6 +12640,15 @@ } } }, + "node_modules/react-native-animatable": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz", + "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + } + }, "node_modules/react-native-css-interop": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz", @@ -12290,6 +12727,32 @@ "react-native": "*" } }, + "node_modules/react-native-modal": { + "version": "14.0.0-rc.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz", + "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==", + "license": "MIT", + "dependencies": { + "react-native-animatable": "1.4.0" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.70.0" + } + }, + "node_modules/react-native-picker-select": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-9.3.1.tgz", + "integrity": "sha512-o621HcsKJfJkpYeP/PZQiZTKbf8W7FT08niLFL0v1pGkIQyak5IfzfinV2t+/l1vktGwAH2Tt29LrP/Hc5fk3A==", + "license": "MIT", + "dependencies": { + "lodash.isequal": "^4.5.0", + "lodash.isobject": "^3.0.2" + }, + "peerDependencies": { + "@react-native-picker/picker": "^2.4.0" + } + }, "node_modules/react-native-progress": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", @@ -12337,6 +12800,27 @@ "react-native": "*" } }, + "node_modules/react-native-render-html": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.4.tgz", + "integrity": "sha512-H2jSMzZjidE+Wo3qCWPUMU1nm98Vs2SGCvQCz/i6xf0P3Y9uVtG/b0sDbG/cYFir2mSYBYCIlS1Dv0WC1LjYig==", + "license": "BSD-2-Clause", + "dependencies": { + "@jsamr/counter-style": "^2.0.1", + "@jsamr/react-native-li": "^2.3.0", + "@native-html/transient-render-engine": "11.2.3", + "@types/ramda": "^0.27.40", + "@types/urijs": "^1.19.15", + "prop-types": "^15.5.7", + "ramda": "^0.27.2", + "stringify-entities": "^3.1.0", + "urijs": "^1.19.6" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz", @@ -13019,7 +13503,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13288,7 +13771,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13841,6 +14323,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -14360,6 +14857,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "license": "Apache-2.0" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -14731,6 +15234,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/use-latest-callback": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz", @@ -14749,6 +15258,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14968,7 +15490,6 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -15191,6 +15712,15 @@ "node": ">=8.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 22395b9..6236ffb 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,18 @@ "expo-blur": "~14.1.5", "expo-constants": "~17.1.6", "expo-dev-client": "~5.2.1", + "expo-device": "~7.1.4", "expo-file-system": "~18.1.10", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", - "expo-image": "~2.3.0", + "expo-image": "~2.3.2", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.5", "expo-localization": "^16.1.5", "expo-location": "~18.1.5", "expo-media-library": "~17.1.7", + "expo-notifications": "~0.31.4", "expo-router": "~5.1.0", "expo-secure-store": "~14.2.3", "expo-splash-screen": "~0.30.9", @@ -53,8 +55,11 @@ "react-i18next": "^15.5.3", "react-native": "0.79.4", "react-native-gesture-handler": "~2.24.0", + "react-native-modal": "^14.0.0-rc.1", + "react-native-picker-select": "^9.3.1", "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.17.4", + "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", "react-native-svg": "15.11.2", diff --git a/scripts/dev_deploy.sh b/scripts/dev_deploy.sh index 363909a..33e951c 100644 --- a/scripts/dev_deploy.sh +++ b/scripts/dev_deploy.sh @@ -13,7 +13,7 @@ fi # 分支名到端口映射 declare -A PORT_MAP -PORT_MAP[v0.4.0_front]="10280:80" +PORT_MAP[v0.5.0]="10280:80" PORT_MAP[main]="10080:80" PORTS=${PORT_MAP[$BRANCH_NAME]} diff --git a/tailwind.config.js b/tailwind.config.js index 79e45fe..8d9e750 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -20,6 +20,10 @@ module.exports = { buttonFill: '#E2793F', aiBubble: '#FFF8DE', }, + width: { + 'owner-card': 'calc((100% - 1rem)/3)', + 'self': 'calc(100% + 32px)', + } }, }, plugins: [], diff --git a/types/upload.ts b/types/upload.ts index 07a3392..ccf5afa 100644 --- a/types/upload.ts +++ b/types/upload.ts @@ -1,8 +1,13 @@ -import { FileStatus } from "@/components/file-upload/file-uploader"; import { MediaType } from "expo-image-picker"; import { ReactNode } from "react"; import { StyleProp, ViewStyle } from "react-native"; +export interface FileStatus { + file: File; + status: 'pending' | 'uploading' | 'success' | 'error'; + progress: number; + error?: string; +} export interface MaterialFile { id: string; file_name: string; diff --git a/types/user.ts b/types/user.ts index 8cd1317..05dd862 100644 --- a/types/user.ts +++ b/types/user.ts @@ -10,4 +10,96 @@ export interface User { user_id?: string refresh_token?: string avatar_file_url?: string +} + +interface UserCountData { + video_count: number, + photo_count: number, + live_count: number, + video_length: number, + cover_url: string | null +} +export interface CountData { + used_bytes: number; + total_bytes: number; + counter: { + user_id: number; + total_count: UserCountData, + category_count: { + [key: string]: UserCountData + } + } +} + +interface Counter { + user_id: number, + total_count: UserCountData, + category_count: { + [key: string]: UserCountData + } +} +export interface TitleRankings { + display_name: string, + ranking: number, + value: number, + material_type: string, + user_id: string, + region: string +} +export interface UserInfoDetails { + material_counter: Counter, + user_info: User, + stories_count: number, + conversations_count: number, + remain_points: number, + total_points: number, + title_rankings: TitleRankings[], + medal_infos: { + "id": number, + "url": string + }[], + membership_level: string +} + + +export type SourceItem = { + id: number; + name: string; + display_name: string; + statistical_granularity: 'Count' | 'Duration'; +}; + +export type GroupedData = { + [key: string]: SourceItem[]; +}; + +export type TargetItem = { + name: string; + children: { name: string, value: number }[]; +}; + +export type RankingItem = { + display_name: string + material_type: string + ranking: number + region: string + user_avatar_url: string + user_id: string + value: number + user_nick_name: string +} + +export interface Address { + city: string; + country: string; + district: string; + formattedAddress: string; + isoCountryCode: string; + name: string; + postalCode: string | null; + region: string; + street: string; + streetNumber: string; + subregion: string | null; + timezone: string | null; } \ No newline at end of file