chore: remove IDE config files and improve web platform compatibility
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m31s
Frontend CI / ota-android (push) Successful in 10m26s
Frontend CI / build-android-apk (push) Successful in 40m43s

- Remove `.idea/` IntelliJ configuration files from version control
- Add web-specific touch handling for swipeable message bubbles and schedule screen
- Fix CSS touch-action rules for better web scrolling behavior
- Add nestedScrollEnabled to ScrollViews for proper gesture handling
- Improve null safety checks in profile screens
- Add horizontal ScrollView wrapper for notification filter tags
- Add hasHeader prop support for embedded profile screens
This commit is contained in:
lafay
2026-04-14 02:12:53 +08:00
parent 57d7c7405c
commit c00b915e5f
18 changed files with 262 additions and 583 deletions

10
.idea/.gitignore generated vendored
View File

@@ -1,10 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Screenshots
screenshots/

9
.idea/frontend.iml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

448
.idea/gradle.xml generated
View File

@@ -1,448 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-application/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-application/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-constants/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-font/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-font/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-image/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linking/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-linking/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-router/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-router/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-updates/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-video/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo-video/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
</set>
</option>
</GradleProjectSettings>
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
<option name="gradleJvm" value="ms-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -53,24 +53,33 @@ if (Platform.OS === 'web' && typeof document !== 'undefined') {
} }
*:focus { outline: none !important; } *:focus { outline: none !important; }
*:focus-visible { outline: none !important; } *:focus-visible { outline: none !important; }
/* 修复移动端 FlatList/ScrollView 滑动问题 */ /* 修复移动端滑动问题 - 仅对根容器限制 */
html, body { html, body {
overscroll-behavior: none; overscroll-behavior: none;
touch-action: pan-y; touch-action: manipulation;
-webkit-overflow-scrolling: touch;
} }
/* React Native Web 生成的滚动容器 */ /* React Native Web 生成的滚动容器 - 允许双向滑动 */
[class*="css-"] { [class*="css-view"] {
touch-action: pan-y !important; touch-action: auto;
-webkit-overflow-scrolling: touch !important; -webkit-overflow-scrolling: touch;
} }
/* 确保所有可滚动元素都可以垂直滑动 */ /* 水平滚动容器 */
div[style*="overflow"] { [data-horizontal-scroll="true"],
touch-action: pan-y !important; div[style*="overflow-x"],
-webkit-overflow-scrolling: touch !important; div[style*="overflow: scroll"],
div[style*="overflow: auto"] {
touch-action: pan-x pan-y;
-webkit-overflow-scrolling: touch;
} }
/* 禁用水平滑动,只允许垂直滑动 */ /* 手势区域 - 允许滑动手势 */
* { [data-gesture-area="true"] {
touch-action: pan-y pinch-zoom; touch-action: pan-x pan-y;
}
/* 图片查看器手势区域 */
.react-native-image-viewer,
[data-image-viewer="true"] {
touch-action: pinch-zoom pan-x pan-y;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);

View File

@@ -361,7 +361,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
onRequestClose={requestClose} onRequestClose={requestClose}
statusBarTranslucent statusBarTranslucent
> >
<GestureHandlerRootView style={styles.root}> <GestureHandlerRootView style={styles.root} data-image-viewer="true">
<View style={[styles.container, { backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity})` }]}> <View style={[styles.container, { backgroundColor: `rgba(0, 0, 0, ${backgroundOpacity})` }]}>
{/* 顶部控制栏 */} {/* 顶部控制栏 */}
{showControls && ( {showControls && (

View File

@@ -676,6 +676,7 @@ export const HomeScreen: React.FC = () => {
ref={capsuleHScrollRef} ref={capsuleHScrollRef}
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
nestedScrollEnabled
contentContainerStyle={styles.capsuleList} contentContainerStyle={styles.capsuleList}
onScroll={onCapsuleHorizontalScroll} onScroll={onCapsuleHorizontalScroll}
scrollEventThrottle={16} scrollEventThrottle={16}

View File

@@ -1182,7 +1182,7 @@ export const PostDetailScreen: React.FC = () => {
)} )}
</View> </View>
{currentUser?.id === post.author?.id && ( {!!currentUser && currentUser.id === post.author?.id && (
<View style={styles.metaActions}> <View style={styles.metaActions}>
<TouchableOpacity <TouchableOpacity
style={styles.editButtonInline} style={styles.editButtonInline}
@@ -1214,8 +1214,8 @@ export const PostDetailScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
{/* 举报按钮 - 只对非帖子作者显示 */} {/* 举报按钮 - 只对已登录且非帖子作者显示 */}
{currentUser?.id !== post.author?.id && ( {!!currentUser && currentUser.id !== post.author?.id && (
<TouchableOpacity <TouchableOpacity
style={styles.reportButtonInline} style={styles.reportButtonInline}
onPress={() => { onPress={() => {

View File

@@ -415,7 +415,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
return ( return (
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}> <SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={[styles.container, { overflow: 'hidden' }]} style={[styles.container, Platform.OS !== 'web' && { overflow: 'hidden' }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
> >

View File

@@ -23,6 +23,7 @@ import {
BackHandler, BackHandler,
Animated, Animated,
StatusBar, StatusBar,
ScrollView,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useIsFocused } from '@react-navigation/native'; import { useIsFocused } from '@react-navigation/native';
@@ -444,6 +445,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
{renderHeader()} {renderHeader()}
{/* 分类筛选 */} {/* 分类筛选 */}
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}> <View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterList}
>
{MESSAGE_TYPES.map(type => { {MESSAGE_TYPES.map(type => {
const count = type.key === 'all' const count = type.key === 'all'
? displayMessages.length ? displayMessages.length
@@ -469,9 +475,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text <Text
variant="body" style={isActive ? { ...styles.filterTagText, ...styles.filterTagTextActive } : styles.filterTagText}
color={isActive ? colors.text.inverse : colors.text.secondary}
style={isActive ? styles.filterTagTextActive : undefined}
> >
{type.title} {type.title}
</Text> </Text>
@@ -479,7 +483,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
<View style={[styles.filterCount, isActive && styles.filterCountActive]}> <View style={[styles.filterCount, isActive && styles.filterCountActive]}>
<Text <Text
variant="caption" variant="caption"
color={isActive ? colors.text.inverse : colors.text.hint} color={isActive ? colors.primary.main : colors.text.hint}
> >
{count} {count}
</Text> </Text>
@@ -488,6 +492,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
</ScrollView>
</View> </View>
{/* 消息列表 */} {/* 消息列表 */}
@@ -526,6 +531,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
{renderHeader()} {renderHeader()}
{/* 分类筛选 */} {/* 分类筛选 */}
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterList}
>
{MESSAGE_TYPES.map(type => { {MESSAGE_TYPES.map(type => {
const count = type.key === 'all' const count = type.key === 'all'
? displayMessages.length ? displayMessages.length
@@ -550,9 +560,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text <Text
variant="body" style={isActive ? { ...styles.filterTagText, ...styles.filterTagTextActive } : styles.filterTagText}
color={isActive ? colors.text.inverse : colors.text.secondary}
style={isActive ? styles.filterTagTextActive : undefined}
> >
{type.title} {type.title}
</Text> </Text>
@@ -560,7 +568,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
<View style={[styles.filterCount, isActive && styles.filterCountActive]}> <View style={[styles.filterCount, isActive && styles.filterCountActive]}>
<Text <Text
variant="caption" variant="caption"
color={isActive ? colors.text.inverse : colors.text.hint} color={isActive ? colors.primary.main : colors.text.hint}
> >
{count} {count}
</Text> </Text>
@@ -569,6 +577,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
</ScrollView>
</View> </View>
{/* 消息列表 */} {/* 消息列表 */}
@@ -677,52 +686,51 @@ function createNotificationsStyles(colors: AppColors) {
}, },
// 分类筛选 - 扁平化风格(分段式设计) // 分类筛选 - 扁平化风格(分段式设计)
filterContainer: { filterContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.divider || '#E5E5EA', borderBottomColor: colors.divider || '#E5E5EA',
}, },
filterContainerWideWeb: { filterContainerWideWeb: {
paddingHorizontal: 32, alignItems: 'center',
paddingVertical: 16, },
justifyContent: 'center', filterList: {
paddingVertical: spacing.md,
gap: spacing.xs,
paddingLeft: spacing.lg,
paddingRight: spacing.lg,
}, },
filterTag: { filterTag: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 14, paddingHorizontal: spacing.md,
paddingVertical: 8, paddingVertical: spacing.xs,
marginRight: 8, borderRadius: borderRadius.full,
borderRadius: 14,
backgroundColor: colors.background.default,
borderWidth: 1,
borderColor: 'transparent',
}, },
filterTagWide: { filterTagWide: {
paddingHorizontal: 18, paddingHorizontal: spacing.lg,
paddingVertical: 10, paddingVertical: spacing.sm,
marginRight: 10,
}, },
filterTagActive: { filterTagActive: {
backgroundColor: colors.primary.main, },
borderColor: colors.primary.main, filterTagText: {
fontSize: 13,
fontWeight: '600',
color: colors.text.secondary,
}, },
filterTagTextActive: { filterTagTextActive: {
fontWeight: '600', color: colors.primary.main,
}, },
filterCount: { filterCount: {
marginLeft: 6, marginLeft: spacing.xs,
backgroundColor: colors.primary.light + '40', backgroundColor: colors.background.default,
paddingHorizontal: 6, paddingHorizontal: spacing.xs,
paddingVertical: 1, paddingVertical: 1,
borderRadius: 8, borderRadius: borderRadius.full,
minWidth: 18, minWidth: 18,
alignItems: 'center', alignItems: 'center',
}, },
filterCountActive: { filterCountActive: {
backgroundColor: 'rgba(255, 255, 255, 0.25)', backgroundColor: colors.primary.light + '30',
}, },
listContent: { listContent: {
flexGrow: 1, flexGrow: 1,

View File

@@ -4,13 +4,12 @@
*/ */
import React, { useRef, useCallback } from 'react'; import React, { useRef, useCallback } from 'react';
import { View, StyleSheet, Animated } from 'react-native'; import { View, StyleSheet, Animated, Platform } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { PanGestureHandler, State } from 'react-native-gesture-handler'; import { PanGestureHandler, State } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { useAppColors } from '../../../../theme'; import { useAppColors } from '../../../../theme';
// 滑动阈值
const SWIPE_THRESHOLD = 30; const SWIPE_THRESHOLD = 30;
const MAX_SWIPE_DISTANCE = 50; const MAX_SWIPE_DISTANCE = 50;
@@ -31,45 +30,39 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
const translateX = useRef(new Animated.Value(0)).current; const translateX = useRef(new Animated.Value(0)).current;
const hasTriggeredRef = useRef(false); const hasTriggeredRef = useRef(false);
// 处理手势事件 - 使用原生驱动,不干预值
const onGestureEvent = Animated.event( const onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: translateX } }], [{ nativeEvent: { translationX: translateX } }],
{ useNativeDriver: true } { useNativeDriver: true }
); );
// 处理手势状态变化
const onHandlerStateChange = useCallback((event: any) => { const onHandlerStateChange = useCallback((event: any) => {
const { nativeEvent } = event; const { nativeEvent } = event;
if (nativeEvent.state === State.END) { if (nativeEvent.state === State.END) {
const translationX = nativeEvent.translationX; const translationX = nativeEvent.translationX;
// 只允许特定方向的滑动
const shouldTrigger = isMe const shouldTrigger = isMe
? translationX < -SWIPE_THRESHOLD // 自己消息只能向左滑 ? translationX < -SWIPE_THRESHOLD
: translationX > SWIPE_THRESHOLD; // 对方消息只能向右滑 : translationX > SWIPE_THRESHOLD;
if (shouldTrigger && !hasTriggeredRef.current) { if (shouldTrigger && !hasTriggeredRef.current) {
hasTriggeredRef.current = true; hasTriggeredRef.current = true;
// 触觉反馈 if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
// 立即触发回复
onReply(); onReply();
// 重置标记
setTimeout(() => { setTimeout(() => {
hasTriggeredRef.current = false; hasTriggeredRef.current = false;
}, 300); }, 300);
} }
// 立即回到原位
translateX.setValue(0); translateX.setValue(0);
} }
}, [isMe, onReply]); }, [isMe, onReply]);
// 计算回复图标的透明度 - 只在允许的方向显示
const iconOpacity = translateX.interpolate({ const iconOpacity = translateX.interpolate({
inputRange: isMe inputRange: isMe
? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10] ? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10]
@@ -78,7 +71,6 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
extrapolate: 'clamp', extrapolate: 'clamp',
}); });
// 计算消息的位移 - 限制在允许的方向
const clampedTranslateX = translateX.interpolate({ const clampedTranslateX = translateX.interpolate({
inputRange: isMe inputRange: isMe
? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10] ? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10]
@@ -89,21 +81,84 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
extrapolate: 'clamp', extrapolate: 'clamp',
}); });
// 如果禁用手势,直接返回子元素 const webTouchStartRef = useRef<{ x: number; time: number } | null>(null);
const webTranslateXRef = useRef(new Animated.Value(0)).current;
const webIconOpacity = webTranslateXRef.interpolate({
inputRange: isMe
? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10]
: [-10, 0, SWIPE_THRESHOLD, MAX_SWIPE_DISTANCE],
outputRange: isMe ? [1, 0.8, 0, 0] : [0, 0, 0.8, 1],
extrapolate: 'clamp',
});
const webClampedTranslateX = webTranslateXRef.interpolate({
inputRange: isMe
? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10]
: [-10, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE + 10],
outputRange: isMe
? [-MAX_SWIPE_DISTANCE, -MAX_SWIPE_DISTANCE, 0, 0]
: [0, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE],
extrapolate: 'clamp',
});
const webLastTouchRef = useRef<{ x: number } | null>(null);
const handleWebTouchStart = useCallback((e: any) => {
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
if (touch && touch.pageX !== undefined) {
webTouchStartRef.current = { x: touch.pageX, time: Date.now() };
webLastTouchRef.current = { x: touch.pageX };
}
}, []);
const handleWebTouchMove = useCallback((e: any) => {
const start = webTouchStartRef.current;
if (!start) return;
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
if (!touch || touch.pageX === undefined) return;
const dx = touch.pageX - start.x;
const allowed = isMe ? dx < 5 : dx > -5;
if (allowed) {
webTranslateXRef.setValue(dx);
webLastTouchRef.current = { x: touch.pageX };
}
}, [isMe]);
const handleWebTouchEnd = useCallback((e: any) => {
const start = webTouchStartRef.current;
if (!start) return;
webTouchStartRef.current = null;
const touch = e.nativeEvent?.changedTouches?.[0] || e.nativeEvent;
if (!touch || touch.pageX === undefined) {
Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start();
return;
}
const dx = touch.pageX - start.x;
const shouldTrigger = isMe
? dx < -SWIPE_THRESHOLD
: dx > SWIPE_THRESHOLD;
if (shouldTrigger && !hasTriggeredRef.current) {
hasTriggeredRef.current = true;
onReply();
setTimeout(() => {
hasTriggeredRef.current = false;
}, 300);
}
Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start();
}, [isMe, onReply]);
if (!enabled) { if (!enabled) {
return <>{children}</>; return <>{children}</>;
} }
if (Platform.OS === 'web') {
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* 回复图标 - 在消息后面 */}
<Animated.View <Animated.View
style={[ style={[
styles.replyIconContainer, styles.replyIconContainer,
isMe ? styles.replyIconLeft : styles.replyIconRight, isMe ? styles.replyIconLeft : styles.replyIconRight,
{ { opacity: webIconOpacity },
opacity: iconOpacity,
},
]} ]}
> >
<MaterialCommunityIcons <MaterialCommunityIcons
@@ -112,8 +167,39 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
color={colors.primary.main} color={colors.primary.main}
/> />
</Animated.View> </Animated.View>
<View
data-gesture-area={true}
onTouchStart={handleWebTouchStart}
onTouchMove={handleWebTouchMove}
onTouchEnd={handleWebTouchEnd}
>
<Animated.View
style={{
transform: [{ translateX: webClampedTranslateX }],
}}
>
{children}
</Animated.View>
</View>
</View>
);
}
{/* 可滑动的消息内容 */} return (
<View style={styles.container}>
<Animated.View
style={[
styles.replyIconContainer,
isMe ? styles.replyIconLeft : styles.replyIconRight,
{ opacity: iconOpacity },
]}
>
<MaterialCommunityIcons
name="reply"
size={20}
color={colors.primary.main}
/>
</Animated.View>
<PanGestureHandler <PanGestureHandler
onGestureEvent={onGestureEvent} onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandlerStateChange} onHandlerStateChange={onHandlerStateChange}

View File

@@ -586,10 +586,10 @@ export const EditProfileScreen: React.FC = () => {
{/* 用户名和简介 */} {/* 用户名和简介 */}
<View style={styles.userInfo}> <View style={styles.userInfo}>
<Text variant="h2" style={styles.nickname}> <Text variant="h2" style={styles.nickname}>
{nickname || currentUser?.nickname} {nickname || currentUser?.nickname || ''}
</Text> </Text>
<Text variant="caption" color={colors.text.secondary} style={styles.username}> <Text variant="caption" color={colors.text.secondary} style={styles.username}>
@{currentUser?.username} @{currentUser?.username || ''}
</Text> </Text>
{bio ? ( {bio ? (
@@ -624,7 +624,7 @@ export const EditProfileScreen: React.FC = () => {
<View style={styles.metaTag}> <View style={styles.metaTag}>
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} /> <MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}> <Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
{website.replace(/^https?:\/\//, '')} {typeof website === 'string' ? website.replace(/^https?:\/\//, '') : ''}
</Text> </Text>
</View> </View>
) : ( ) : (
@@ -638,7 +638,7 @@ export const EditProfileScreen: React.FC = () => {
<View style={styles.metaTag}> <View style={styles.metaTag}>
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} /> <MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}> <Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
{new Date(currentUser?.created_at || Date.now()).getFullYear()} {`加入于 ${new Date(currentUser?.created_at || Date.now()).getFullYear()}`}
</Text> </Text>
</View> </View>
</View> </View>

View File

@@ -186,7 +186,7 @@ const FollowListScreen: React.FC = () => {
</Text> </Text>
) : null} ) : null}
</View> </View>
{!isSelf && ( {isCurrentUser && !isSelf && (
<Button <Button
title={buttonConfig.title} title={buttonConfig.title}
variant={buttonConfig.variant} variant={buttonConfig.variant}
@@ -230,7 +230,7 @@ const FollowListScreen: React.FC = () => {
</Text> </Text>
) : null} ) : null}
</View> </View>
{!isSelf && ( {isCurrentUser && !isSelf && (
<View style={styles.userCardFooter}> <View style={styles.userCardFooter}>
<Button <Button
title={buttonConfig.title} title={buttonConfig.title}
@@ -278,7 +278,9 @@ const FollowListScreen: React.FC = () => {
</View> </View>
</View> </View>
<Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}> <Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}>
{type === 'following' ? '你已关注的用户' : '关注你的用户'} {type === 'following'
? (isCurrentUser ? '你已关注的用户' : 'TA关注的用户')
: (isCurrentUser ? '关注你的用户' : 'TA的粉丝')}
</Text> </Text>
</View> </View>
); );
@@ -298,7 +300,7 @@ const FollowListScreen: React.FC = () => {
// 宽屏使用网格布局 // 宽屏使用网格布局
if (isWideScreen && columnCount > 1) { if (isWideScreen && columnCount > 1) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<ResponsiveContainer maxWidth={1200}> <ResponsiveContainer maxWidth={1200}>
<FlatList <FlatList
data={users} data={users}
@@ -333,7 +335,7 @@ const FollowListScreen: React.FC = () => {
// 移动端使用列表布局 // 移动端使用列表布局
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<FlatList <FlatList
data={users} data={users}
renderItem={renderUserListItem} renderItem={renderUserListItem}

View File

@@ -21,10 +21,11 @@ import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles
interface UserProfileScreenProps { interface UserProfileScreenProps {
mode: ProfileMode; mode: ProfileMode;
userId?: string; // 仅 other 模式需要 userId?: string;
hasHeader?: boolean;
} }
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId }) => { export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, hasHeader = false }) => {
const colors = useAppColors(); const colors = useAppColors();
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]); const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
const { isDesktop, isTablet } = useResponsive(); const { isDesktop, isTablet } = useResponsive();
@@ -148,7 +149,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
// 未登录/用户不存在状态 // 未登录/用户不存在状态
if (mode === 'self' && !currentUser) { if (mode === 'self' && !currentUser) {
return ( return (
<SafeAreaView style={sharedStyles.container} edges={['top', 'bottom']}> <SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
<EmptyState <EmptyState
title="未登录" title="未登录"
description="请先登录" description="请先登录"
@@ -170,10 +171,12 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
); );
} }
const safeAreaEdges = hasHeader ? ['bottom'] : (mode === 'self' ? ['top', 'bottom'] : ['bottom']);
// 桌面端使用双栏布局 // 桌面端使用双栏布局
if (isDesktop || isTablet) { if (isDesktop || isTablet) {
return ( return (
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}> <SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
<ResponsiveContainer maxWidth={1400}> <ResponsiveContainer maxWidth={1400}>
<View style={sharedStyles.desktopContainer}> <View style={sharedStyles.desktopContainer}>
{/* 左侧:用户信息 */} {/* 左侧:用户信息 */}
@@ -211,7 +214,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
// 移动端使用单栏布局 // 移动端使用单栏布局
return ( return (
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}> <SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
<ScrollView <ScrollView
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
refreshControl={ refreshControl={

View File

@@ -19,6 +19,7 @@ export const UserScreen: React.FC = () => {
<UserProfileScreen <UserProfileScreen
mode={isSelfProfile ? 'self' : 'other'} mode={isSelfProfile ? 'self' : 'other'}
userId={isSelfProfile ? undefined : userId} userId={isSelfProfile ? undefined : userId}
hasHeader
/> />
); );
}; };

View File

@@ -302,6 +302,30 @@ export const ScheduleScreen: React.FC = () => {
[weeks.length] [weeks.length]
); );
const webTouchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
const handleWebTouchStart = useCallback((e: any) => {
const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent;
if (touch && touch.pageX !== undefined) {
webTouchStartRef.current = { x: touch.pageX, y: touch.pageY, time: Date.now() };
}
}, []);
const handleWebTouchEnd = useCallback((e: any) => {
const start = webTouchStartRef.current;
if (!start) return;
webTouchStartRef.current = null;
const touch = e.nativeEvent?.changedTouches?.[0] || e.nativeEvent;
if (!touch || touch.pageX === undefined) return;
const dx = touch.pageX - start.x;
const dy = touch.pageY - start.y;
if (Math.abs(dx) < SWIPE_THRESHOLD) return;
if (Math.abs(dx) <= Math.abs(dy) * 0.8) return;
if (dx < 0) {
setCurrentWeek(prev => Math.min(prev + 1, weeks.length));
} else {
setCurrentWeek(prev => Math.max(prev - 1, 1));
}
}, [weeks.length]);
const isSlotOccupied = useCallback( const isSlotOccupied = useCallback(
(dayOfWeek: number, mergedSection: number, weekCourses: Course[]) => { (dayOfWeek: number, mergedSection: number, weekCourses: Course[]) => {
return weekCourses.some(course => { return weekCourses.some(course => {
@@ -713,10 +737,41 @@ export const ScheduleScreen: React.FC = () => {
// 渲染课程表主体 // 渲染课程表主体
const renderScheduleBody = () => { const renderScheduleBody = () => {
// 移动端和电脑端都不添加额外底部间距,让内容填满整个区域 const gestureLayerProps = Platform.OS === 'web'
// 动态计算的高度已经确保第六节课显示在正确位置 ? {
onTouchStart: handleWebTouchStart,
onTouchEnd: handleWebTouchEnd,
'data-gesture-area': true,
}
: {};
return ( return (
Platform.OS === 'web' ? (
<View
style={styles.scheduleBodyGestureLayer}
{...gestureLayerProps}
>
<ScrollView
style={styles.scheduleBody}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scheduleBodyContent}
>
{renderTimeColumn()}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
nestedScrollEnabled
contentContainerStyle={{ flexGrow: 1 }}
>
<View style={styles.daysContainer}>
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1)
)}
</View>
</ScrollView>
</ScrollView>
</View>
) : (
<PanGestureHandler <PanGestureHandler
activeOffsetX={[-8, 8]} activeOffsetX={[-8, 8]}
failOffsetY={[-80, 80]} failOffsetY={[-80, 80]}
@@ -732,6 +787,7 @@ export const ScheduleScreen: React.FC = () => {
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
nestedScrollEnabled
contentContainerStyle={{ flexGrow: 1 }} contentContainerStyle={{ flexGrow: 1 }}
> >
<View style={styles.daysContainer}> <View style={styles.daysContainer}>
@@ -743,6 +799,7 @@ export const ScheduleScreen: React.FC = () => {
</ScrollView> </ScrollView>
</View> </View>
</PanGestureHandler> </PanGestureHandler>
)
); );
}; };