refactor(App, navigation): migrate to Expo Router and clean up navigation structure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m27s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h16m45s

- Updated App entry point to utilize Expo Router, simplifying the navigation setup.
- Removed legacy navigation components and services to streamline the codebase.
- Adjusted package.json to reflect the new entry point and updated dependencies for compatibility.
- Enhanced overall application structure by consolidating navigation logic and improving maintainability.
This commit is contained in:
lafay
2026-03-24 14:21:31 +08:00
parent b91e78c921
commit 2ddb9cadd8
111 changed files with 1829 additions and 3214 deletions

View File

@@ -329,18 +329,18 @@ const CommentItem: React.FC<CommentItemProps> = ({
{/* 子评论操作按钮 */}
<View style={styles.subReplyActions}>
<TouchableOpacity
style={styles.actionButton}
style={styles.subActionButton}
onPress={() => onReplyPress?.(reply)}
>
<MaterialCommunityIcons name="reply" size={12} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.actionText}>
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
</Text>
</TouchableOpacity>
{/* 删除按钮 - 子评论作者可见 */}
{isSubReplyAuthor && (
<TouchableOpacity
style={styles.actionButton}
style={styles.subActionButton}
onPress={() => handleSubReplyDelete(reply)}
disabled={isDeleting}
>
@@ -349,7 +349,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
size={12}
color={colors.text.hint}
/>
<Text variant="caption" color={colors.text.hint} style={styles.actionText}>
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
{isDeleting ? '删除中' : '删除'}
</Text>
</TouchableOpacity>
@@ -476,21 +476,26 @@ const CommentItem: React.FC<CommentItemProps> = ({
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingVertical: spacing.sm,
paddingTop: spacing.md,
paddingBottom: spacing.xs,
paddingHorizontal: spacing.lg,
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
backgroundColor: 'transparent',
},
content: {
flex: 1,
marginLeft: spacing.sm,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: spacing.xs,
marginBottom: spacing.sm,
},
userInfo: {
flexDirection: 'row',
@@ -537,7 +542,9 @@ const styles = StyleSheet.create({
backgroundColor: colors.background.default,
paddingHorizontal: spacing.xs,
paddingVertical: 2,
borderRadius: borderRadius.sm,
borderRadius: 999,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
},
floorText: {
fontSize: fontSizes.xs,
@@ -546,7 +553,7 @@ const styles = StyleSheet.create({
marginBottom: spacing.xs,
},
commentContent: {
marginBottom: spacing.xs,
marginBottom: spacing.sm,
},
text: {
lineHeight: 20,
@@ -559,17 +566,24 @@ const styles = StyleSheet.create({
actionButton: {
flexDirection: 'row',
alignItems: 'center',
marginRight: spacing.md,
paddingVertical: spacing.xs,
marginRight: spacing.sm,
paddingHorizontal: spacing.sm,
paddingVertical: 5,
borderRadius: 999,
backgroundColor: colors.background.default,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
},
actionText: {
marginLeft: 2,
marginLeft: 4,
fontSize: fontSizes.xs,
},
subRepliesContainer: {
marginTop: spacing.sm,
backgroundColor: colors.background.default,
borderRadius: borderRadius.md,
borderRadius: borderRadius.lg,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
padding: spacing.sm,
},
subReplyItem: {
@@ -607,6 +621,16 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginTop: spacing.xs,
},
subActionButton: {
flexDirection: 'row',
alignItems: 'center',
marginRight: spacing.sm,
paddingVertical: 2,
},
subActionText: {
marginLeft: 2,
fontSize: fontSizes.xs,
},
});
export default CommentItem;

View File

@@ -9,23 +9,20 @@ import {
Dimensions,
} from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
const { width, height } = Dimensions.get('window');
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
interface QRCodeScannerProps {
visible: boolean;
onClose: () => void;
}
export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }) => {
const navigation = useNavigation<NavigationProp>();
const router = useRouter();
const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false);
@@ -52,7 +49,7 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
const sessionId = extractSessionId(data);
if (sessionId) {
// 跳转到确认页面
navigation.navigate('QRCodeConfirm', { sessionId });
router.push(hrefs.hrefQrLoginConfirm(sessionId));
} else {
Alert.alert('无效的二维码', '无法识别该二维码');
}

View File

@@ -236,22 +236,22 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
{/* 个人信息标签 */}
<View style={styles.metaInfo}>
{user.location && (
{user.location?.trim() ? (
<View style={styles.metaTag}>
<MaterialCommunityIcons name="map-marker-outline" size={12} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} style={styles.metaTagText}>
{user.location}
</Text>
</View>
)}
{user.website && (
) : null}
{user.website?.trim() ? (
<View style={styles.metaTag}>
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
{user.website.replace(/^https?:\/\//, '')}
</Text>
</View>
)}
) : null}
<View style={styles.metaTag}>
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { TouchableOpacity, StyleProp, ViewStyle, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { borderRadius, colors, spacing } from '../../theme';
type AppBackButtonIcon = 'arrow-left' | 'close';
interface AppBackButtonProps {
onPress: () => void;
icon?: AppBackButtonIcon;
style?: StyleProp<ViewStyle>;
iconColor?: string;
backgroundColor?: string;
hitSlop?: number;
testID?: string;
}
const AppBackButton: React.FC<AppBackButtonProps> = ({
onPress,
icon = 'arrow-left',
style,
iconColor = colors.text.primary,
backgroundColor = colors.background.paper,
hitSlop = 8,
testID,
}) => {
return (
<TouchableOpacity
onPress={onPress}
style={[styles.button, { backgroundColor }, style]}
activeOpacity={0.75}
hitSlop={hitSlop}
testID={testID}
accessibilityRole="button"
accessibilityLabel={icon === 'close' ? '关闭' : '返回'}
>
<MaterialCommunityIcons name={icon} size={22} color={iconColor} />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
width: 36,
height: 36,
borderRadius: borderRadius.full,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.xs,
},
});
export default AppBackButton;

View File

@@ -87,6 +87,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isClosingRef = useRef(false);
const currentIndexRef = useRef(initialIndex);
const pendingSwipeDirectionRef = useRef<-1 | 1 | null>(null);
const [currentIndex, setCurrentIndex] = useState(initialIndex);
currentIndexRef.current = currentIndex;
const [showControls, setShowControls] = useState(true);
@@ -173,6 +174,14 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
[validImages.length, onIndexChange]
);
const updateIndexFromSwipe = useCallback(
(newIndex: number, direction: -1 | 1) => {
pendingSwipeDirectionRef.current = direction;
updateIndex(newIndex);
},
[updateIndex]
);
const toggleControls = useCallback(() => {
setShowControls(prev => !prev);
}, []);
@@ -189,18 +198,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
}, 120);
}, [onClose]);
const goToPrev = useCallback(() => {
if (currentIndex > 0) {
updateIndex(currentIndex - 1);
}
}, [currentIndex, updateIndex]);
const goToNext = useCallback(() => {
if (currentIndex < validImages.length - 1) {
updateIndex(currentIndex + 1);
}
}, [currentIndex, validImages.length, updateIndex]);
// 显示短暂提示
const showToast = useCallback((type: 'success' | 'error') => {
setSaveToast(type);
@@ -269,6 +266,38 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 滑动切换图片相关状态
const swipeTranslateX = useSharedValue(0);
// 滑动切图完成后,让新图从目标方向进入,避免旧图先弹回导致前后图闪烁
useEffect(() => {
const direction = pendingSwipeDirectionRef.current;
if (direction == null) {
return;
}
pendingSwipeDirectionRef.current = null;
swipeTranslateX.value = -direction * SCREEN_WIDTH;
swipeTranslateX.value = withTiming(0, { duration: 180 });
}, [currentIndex, swipeTranslateX]);
const switchWithDirection = useCallback(
(targetIndex: number, direction: -1 | 1) => {
swipeTranslateX.value = withTiming(direction * SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(updateIndexFromSwipe)(targetIndex, direction);
});
},
[swipeTranslateX, updateIndexFromSwipe]
);
const goToPrev = useCallback(() => {
if (currentIndex > 0) {
switchWithDirection(currentIndex - 1, 1);
}
}, [currentIndex, switchWithDirection]);
const goToNext = useCallback(() => {
if (currentIndex < validImages.length - 1) {
switchWithDirection(currentIndex + 1, -1);
}
}, [currentIndex, validImages.length, switchWithDirection]);
// 统一的滑动手势:放大时拖动,未放大时切换图片
const panGesture = Gesture.Pan()
.activeOffsetX([-10, 10]) // 水平方向需要移动10pt才激活避免与点击冲突
@@ -309,14 +338,12 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
if (shouldGoNext && currentIndex < validImages.length - 1) {
// 向左滑动,显示下一张
swipeTranslateX.value = withTiming(-SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(updateIndex)(currentIndex + 1);
swipeTranslateX.value = 0;
runOnJS(updateIndexFromSwipe)(currentIndex + 1, -1);
});
} else if (shouldGoPrev && currentIndex > 0) {
// 向右滑动,显示上一张
swipeTranslateX.value = withTiming(SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(updateIndex)(currentIndex - 1);
swipeTranslateX.value = 0;
runOnJS(updateIndexFromSwipe)(currentIndex - 1, 1);
});
} else {
// 回弹到原位

View File

@@ -16,6 +16,7 @@ export { default as ResponsiveContainer } from './ResponsiveContainer';
export { default as ResponsiveGrid } from './ResponsiveGrid';
export { default as ResponsiveStack, HStack, VStack } from './ResponsiveStack';
export { default as AdaptiveLayout, SidebarLayout } from './AdaptiveLayout';
export { default as AppBackButton } from './AppBackButton';
// 图片相关组件
export { default as SmartImage } from './SmartImage';