diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2f9ff20 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); deps={**d.get\\('dependencies',{}\\), **d.get\\('devDependencies',{}\\)}; [print\\(f'{k}: {v}'\\) for k,v in deps.items\\(\\) if 'navigation' in k or 'tab' in k.lower\\(\\)]\")", + "Bash(npx tsc *)", + "Bash(echo \"EXIT: $?\")" + ] + } +} diff --git a/app.json b/app.json index d84ab6c..dbbc8cb 100644 --- a/app.json +++ b/app.json @@ -7,14 +7,17 @@ "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", "scheme": "withyou", + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, "ios": { "supportsTablet": true, "infoPlist": { "NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话", "NSCameraUsageDescription": "允许威友访问您的相机以进行视频通话", "UIBackgroundModes": [ - "fetch", - "remote-notification", "fetch", "remote-notification", "audio" diff --git a/app/(app)/(tabs)/_layout.tsx b/app/(app)/(tabs)/_layout.tsx index 95c57a0..d4e2844 100644 --- a/app/(app)/(tabs)/_layout.tsx +++ b/app/(app)/(tabs)/_layout.tsx @@ -1,8 +1,9 @@ import { useMemo, useCallback } from 'react'; -import { Platform, useWindowDimensions } from 'react-native'; +import { Platform, Pressable, useWindowDimensions } from 'react-native'; import { Tabs, usePathname } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import type { PressableProps } from 'react-native'; import { useAppColors, shadows } from '../../../src/theme'; import { BREAKPOINTS } from '../../../src/hooks'; @@ -12,6 +13,16 @@ const TAB_BAR_HEIGHT = 56; const TAB_BAR_FLOATING_MARGIN = 12; const TAB_BAR_MARGIN = 20; +const TabBarButton = ({ children, style, ...rest }: PressableProps) => ( + + {children} + +); + export default function TabsLayout() { const colors = useAppColors(); const { width } = useWindowDimensions(); @@ -72,6 +83,7 @@ export default function TabsLayout() { marginHorizontal: 2, paddingVertical: 1, }, + tabBarButton: (props) => , tabBarShowLabel: true, tabBarLabelStyle: { fontSize: 11, fontWeight: '600', marginTop: -1 }, }} diff --git a/src/components/business/BlockEditor/ImageBlockView.tsx b/src/components/business/BlockEditor/ImageBlockView.tsx index b7f1994..aff2f39 100644 --- a/src/components/business/BlockEditor/ImageBlockView.tsx +++ b/src/components/business/BlockEditor/ImageBlockView.tsx @@ -67,7 +67,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, uploadingOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.3)', justifyContent: 'center', alignItems: 'center', diff --git a/src/components/business/VoteCard.tsx b/src/components/business/VoteCard.tsx index 9705da1..df15e76 100644 --- a/src/components/business/VoteCard.tsx +++ b/src/components/business/VoteCard.tsx @@ -159,7 +159,7 @@ function createVoteCardStyles(colors: AppColors) { fontSize: fontSizes.sm, }, loadingOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: colors.background.paper + 'CC', justifyContent: 'center', alignItems: 'center', diff --git a/src/components/call/CallScreen.tsx b/src/components/call/CallScreen.tsx index 1063e49..17d03ea 100644 --- a/src/components/call/CallScreen.tsx +++ b/src/components/call/CallScreen.tsx @@ -267,12 +267,12 @@ const CallScreen: React.FC = () => { const styles = StyleSheet.create({ container: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', zIndex: 9999, }, background: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', }, topBar: { @@ -334,7 +334,7 @@ const styles = StyleSheet.create({ letterSpacing: 0.3, }, fullScreenVideo: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', }, localVideoContainer: { diff --git a/src/components/call/CallScreen.web.tsx b/src/components/call/CallScreen.web.tsx index 1c2b53c..d78610c 100644 --- a/src/components/call/CallScreen.web.tsx +++ b/src/components/call/CallScreen.web.tsx @@ -252,12 +252,12 @@ const CallScreen: React.FC = () => { const styles = StyleSheet.create({ container: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', zIndex: 9999, }, background: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', }, topBar: { @@ -319,7 +319,7 @@ const styles = StyleSheet.create({ letterSpacing: 0.3, }, fullScreenVideo: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: '#1A1A2E', }, localVideoContainer: { diff --git a/src/components/common/AdaptiveLayout.tsx b/src/components/common/AdaptiveLayout.tsx index d6a145a..1bb6a37 100644 --- a/src/components/common/AdaptiveLayout.tsx +++ b/src/components/common/AdaptiveLayout.tsx @@ -346,7 +346,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, overlayTouchable: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, }, overlay: { flex: 1, diff --git a/src/components/common/ImageGallery.tsx b/src/components/common/ImageGallery.tsx index b6d755d..8fd9049 100644 --- a/src/components/common/ImageGallery.tsx +++ b/src/components/common/ImageGallery.tsx @@ -548,7 +548,7 @@ const styles = StyleSheet.create({ height: SCREEN_HEIGHT * 0.8, }, errorContainer: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, justifyContent: 'center', alignItems: 'center', backgroundColor: '#000', diff --git a/src/components/common/ImageGrid.tsx b/src/components/common/ImageGrid.tsx index 402fedc..6ca29a8 100644 --- a/src/components/common/ImageGrid.tsx +++ b/src/components/common/ImageGrid.tsx @@ -148,7 +148,7 @@ function createImageGridStyles(colors: AppColors) { backgroundColor: colors.background.disabled, }, moreOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'center', alignItems: 'center', @@ -390,10 +390,12 @@ export const ImageGrid: React.FC = ({ return ( {displayImages.map((image, index) => { - const previewUrl = usePreview && displayMode + const mainUri = image.uri || image.url || ''; + const previewForSmart = usePreview && displayMode ? getPreviewImageUrl(image as any, displayMode) - : (image.uri || image.url || ''); - + : ''; + const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri); + return ( = ({ ]} > = ({ {displayImages.map((image, index) => { const isLastVisible = index === displayImages.length - 1; const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay; - - const previewUrl = usePreview && displayMode + + const mainUri = image.uri || image.url || ''; + const previewForSmart = usePreview && displayMode ? getPreviewImageUrl(image as any, displayMode) - : (image.uri || image.url || ''); + : ''; + const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri); return ( = ({ ]} > = ({ ? image.width / image.height : 1; const height = itemWidth / aspectRatio; - - const previewUrl = usePreview && displayMode + + const mainUri = image.uri || image.url || ''; + const previewForSmart = usePreview && displayMode ? getPreviewImageUrl(image as any, displayMode) - : (image.uri || image.url || ''); + : ''; + const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri); return ( = ({ ]} > = ({ if (isUpgradingFromPreview.current) { return; } - setLoadState('loading'); + // 已成功加载的图片不再回到loading状态(列表回收/re-render时onLoadStart可能重新触发) + setLoadState(prev => (prev === 'success' ? prev : 'loading')); }, []); // 处理加载完成 diff --git a/src/screens/create/CreatePostScreen.tsx b/src/screens/create/CreatePostScreen.tsx index 7c8cc83..6ee176d 100644 --- a/src/screens/create/CreatePostScreen.tsx +++ b/src/screens/create/CreatePostScreen.tsx @@ -1045,7 +1045,7 @@ function createCreatePostStyles(colors: AppColors) { height: '100%', }, uploadingOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index d7551ae..ba0cfe4 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -902,6 +902,7 @@ export const HomeScreen: React.FC = () => { setShowSearch(false)} + homeTab={homeTab} /> ); diff --git a/src/screens/home/MarketView.tsx b/src/screens/home/MarketView.tsx index aa9ff28..1dfb4c5 100644 --- a/src/screens/home/MarketView.tsx +++ b/src/screens/home/MarketView.tsx @@ -27,7 +27,7 @@ function createMarketStyles(colors: AppColors, gap: number, padding: number) { }, listContent: { paddingHorizontal: padding, - paddingTop: gap / 2, + paddingTop: padding, paddingBottom: 80, }, }); diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index 8d57b50..aee243c 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -18,34 +18,42 @@ import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { Post, User } from '../../types'; +import type { TradeItemDTO } from '../../types/trade'; import { useUserStore } from '../../stores'; import { postService, authService } from '../../services'; +import { tradeService } from '../../services/trade/tradeService'; import { postSyncService } from '@/services/post'; import { PostCard, TabBar, SearchBar } from '../../components/business'; +import { TradeCard } from '../../components/business/TradeCard/TradeCard'; import type { PostCardAction } from '../../components/business/PostCard'; import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common'; import * as hrefs from '../../navigation/hrefs'; import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks'; import { useCursorPagination } from '../../hooks/useCursorPagination'; -const TABS = ['帖子', '用户']; +const SQUARE_TABS = ['帖子', '用户']; +const MARKET_TABS = ['商品', '用户']; const DEFAULT_PAGE_SIZE = 20; // 与 TabsLayout 中的常量保持一致:tabBarHeight 56 + 上下浮动间距 12*2 const FLOATING_TAB_BAR_HEIGHT = 56 + 12 * 2; -type SearchType = 'posts' | 'users'; +type SearchType = 'posts' | 'users' | 'trades'; interface SearchScreenProps { onBack?: () => void; + homeTab?: 'square' | 'market'; } -export const SearchScreen: React.FC = ({ onBack }) => { +export const SearchScreen: React.FC = ({ onBack, homeTab = 'square' }) => { const colors = useAppColors(); const styles = useMemo(() => createSearchScreenStyles(colors), [colors]); const router = useRouter(); const insets = useSafeAreaInsets(); const history = useUserStore((state) => state.searchHistory); const { addSearchHistory, clearSearchHistory } = useUserStore.getState(); + + const isMarket = homeTab === 'market'; + const TABS = isMarket ? MARKET_TABS : SQUARE_TABS; // 使用响应式 hook const { @@ -115,6 +123,10 @@ export const SearchScreen: React.FC = ({ onBack }) => { const [userResults, setUserResults] = useState([]); const [userLoading, setUserLoading] = useState(false); + // 市集搜索结果 + const [tradeResults, setTradeResults] = useState([]); + const [tradeLoading, setTradeLoading] = useState(false); + const refreshRef = useRef(refresh); refreshRef.current = refresh; const resetRef = useRef(reset); @@ -143,14 +155,21 @@ export const SearchScreen: React.FC = ({ onBack }) => { try { const searchType = getSearchType(); - + if (searchType === 'users') { setUserLoading(true); const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20); setUserResults(usersResponse.list || []); + } else if (searchType === 'trades') { + setTradeLoading(true); + const tradeResponse = await tradeService.getTradeItems({ keyword: trimmedKeyword, page_size: DEFAULT_PAGE_SIZE }); + setTradeResults(tradeResponse.list || []); } } catch (error) { console.error('搜索失败:', error); + } finally { + setUserLoading(false); + setTradeLoading(false); } setHasSearched(true); @@ -219,6 +238,13 @@ export const SearchScreen: React.FC = ({ onBack }) => { // 获取当前搜索类型 const getSearchType = (): SearchType => { + if (isMarket) { + switch (activeIndex) { + case 0: return 'trades'; + case 1: return 'users'; + default: return 'trades'; + } + } switch (activeIndex) { case 0: return 'posts'; case 1: return 'users'; @@ -426,18 +452,54 @@ export const SearchScreen: React.FC = ({ onBack }) => { ); }; + // 渲染市集搜索结果 + const renderTradeResults = () => { + if (tradeResults.length === 0 && !tradeLoading) { + return ( + + ); + } + + return ( + ( + + router.push(`/trade/${id}` as any)} + /> + + )} + keyExtractor={item => item.id} + contentContainerStyle={{ paddingVertical: responsiveGap, paddingBottom: listBottomInset }} + showsVerticalScrollIndicator={false} + ListFooterComponent={tradeLoading ? : null} + /> + ); + }; + // 渲染搜索结果 const renderSearchResults = () => { const searchType = getSearchType(); - + if (searchType === 'posts') { return renderPostResults(); } - + if (searchType === 'users') { return renderUserResults(); } - + + if (searchType === 'trades') { + return renderTradeResults(); + } + return null; }; @@ -517,7 +579,7 @@ export const SearchScreen: React.FC = ({ onBack }) => { value={searchText} onChangeText={setSearchText} onSubmit={handleSearch} - placeholder="搜索帖子、用户" + placeholder={isMarket ? "搜索商品、用户" : "搜索帖子、用户"} autoFocus /> @@ -548,7 +610,7 @@ export const SearchScreen: React.FC = ({ onBack }) => { } }} variant="modern" - icons={['file-document-outline', 'account-outline']} + icons={isMarket ? ['shopping-outline', 'account-outline'] : ['file-document-outline', 'account-outline']} /> diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index e432f1e..ac8360a 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -27,11 +27,11 @@ import { Platform, TouchableOpacity, BackHandler, + StatusBar, } from 'react-native'; import { FlashList, ListRenderItem } from '@shopify/flash-list'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useNavigation, useRouter } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { Text, ImageGallery, ImageGridItem } from '../../components/common'; import { useAppColors, useResolvedColorScheme } from '../../theme'; @@ -478,7 +478,7 @@ export const ChatScreen: React.FC = (props) => { behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0} > - {!props.isEmbedded && } + {!props.isEmbedded && } {/* 顶部栏 */} = ({ function createGroupInfoPanelStyles(colors: AppColors) { return StyleSheet.create({ overlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.3)', zIndex: 100, }, diff --git a/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx b/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx index 306eac1..645ffd4 100644 --- a/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx +++ b/src/screens/message/components/ChatScreen/GroupInfoPanel.web.tsx @@ -379,7 +379,7 @@ export const GroupInfoPanel: React.FC = ({ function createGroupInfoPanelStyles(colors: AppColors) { return StyleSheet.create({ overlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.35)', zIndex: 100, }, diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index df4acc2..54c7925 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -551,7 +551,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: { // 输入框@高亮 inputHighlightOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, flexDirection: 'row', flexWrap: 'wrap', paddingTop: 8, @@ -1072,7 +1072,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: { // 遮罩层 overlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.5)', alignItems: 'center', justifyContent: 'center', @@ -1142,7 +1142,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: { justifyContent: 'flex-end', }, manageModalBackdrop: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.4)', }, manageModalHandle: { @@ -1165,7 +1165,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: { borderColor: colors.chat.replyBorder, }, stickerCheckOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(127, 182, 230, 0.14)', alignItems: 'flex-end', justifyContent: 'flex-start', diff --git a/src/screens/profile/EditProfileScreen.tsx b/src/screens/profile/EditProfileScreen.tsx index ed3f333..2de1621 100644 --- a/src/screens/profile/EditProfileScreen.tsx +++ b/src/screens/profile/EditProfileScreen.tsx @@ -68,7 +68,7 @@ function createEditProfileStyles(colors: AppColors) { alignItems: 'center', }, coverOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'center', alignItems: 'center', diff --git a/src/screens/schedule/CourseDetailScreen.tsx b/src/screens/schedule/CourseDetailScreen.tsx index fdaf7da..2d5f48e 100644 --- a/src/screens/schedule/CourseDetailScreen.tsx +++ b/src/screens/schedule/CourseDetailScreen.tsx @@ -198,7 +198,7 @@ const CourseDetailScreen: React.FC = () => { return ( - router.back()} /> + router.back()} /> ); @@ -207,7 +207,7 @@ const CourseDetailScreen: React.FC = () => { return ( - router.back()} /> + router.back()} /> diff --git a/src/screens/trade/CreateTradeScreen.tsx b/src/screens/trade/CreateTradeScreen.tsx index e1a846b..58b7236 100644 --- a/src/screens/trade/CreateTradeScreen.tsx +++ b/src/screens/trade/CreateTradeScreen.tsx @@ -259,7 +259,7 @@ function createStyles(colors: AppColors) { backgroundColor: colors.background.default, }, uploadingOverlay: { - ...StyleSheet.absoluteFillObject, + ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0,0,0,0.3)', alignItems: 'center', justifyContent: 'center', diff --git a/src/stores/message/MessageManager.ts b/src/stores/message/MessageManager.ts index 9a6ebc2..c95666b 100644 --- a/src/stores/message/MessageManager.ts +++ b/src/stores/message/MessageManager.ts @@ -208,7 +208,7 @@ class MessageManager { return this.syncService.loadMoreMessages(conversationId, beforeSeq, limit); } - async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> { + async fetchUnreadCount(): Promise { return this.syncService.fetchUnreadCount(); } diff --git a/src/stores/message/services/MessageSyncService.ts b/src/stores/message/services/MessageSyncService.ts index bb51339..d7da4fb 100644 --- a/src/stores/message/services/MessageSyncService.ts +++ b/src/stores/message/services/MessageSyncService.ts @@ -30,7 +30,7 @@ export class MessageSyncService implements IMessageSyncService { /** 正在加载会话列表下一页 */ private loadingMoreConversations = false; /** fetchUnreadCount 去重:复用 in-flight promise */ - private fetchUnreadCountPromise: Promise<{ totalUnread: number; systemUnread: number }> | null = null; + private fetchUnreadCountPromise: Promise | null = null; constructor( getCurrentUserId: () => string | null, @@ -352,20 +352,19 @@ export class MessageSyncService implements IMessageSyncService { /** * 获取未读数(去重:复用 in-flight promise) */ - async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> { + async fetchUnreadCount(): Promise { if (this.fetchUnreadCountPromise) { return this.fetchUnreadCountPromise; } this.fetchUnreadCountPromise = this.doFetchUnreadCount(); try { - const result = await this.fetchUnreadCountPromise; - return result; + await this.fetchUnreadCountPromise; } finally { this.fetchUnreadCountPromise = null; } } - private async doFetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> { + private async doFetchUnreadCount(): Promise { const store = useMessageStore.getState(); try { @@ -393,10 +392,8 @@ export class MessageSyncService implements IMessageSyncService { // 服务端汇总未读为 0:不主动清零本地未读,等 fetchConversations 提供权威数据 // 避免后端缓存竞态导致的未读数抖动 - return { totalUnread, systemUnread }; } catch (error) { console.error('[MessageSyncService] 获取未读数失败:', error); - return { totalUnread: 0, systemUnread: 0 }; } }