Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { RootStackParamList } from '../../navigation/types';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { userManager } from '../../stores/userManager';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupRequestDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupRequestDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const { extra_data } = message;
const [submitting, setSubmitting] = useState(false);
const [memberCount, setMemberCount] = useState<number | null>(null);
const [loadingGroup, setLoadingGroup] = useState(false);
const requestType = extra_data?.request_type;
const requestStatus = extra_data?.request_status;
const reviewerName = extra_data?.actor_name || extra_data?.operator_name || '管理员';
const canAction =
requestStatus === 'pending' &&
(message.system_type === 'group_join_apply' || message.system_type === 'group_invite');
const processedText =
requestStatus === 'accepted'
? `${reviewerName}已同意`
: requestStatus === 'rejected'
? `${reviewerName}已拒绝`
: '该请求已处理';
const applicantName = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_name || '被邀请用户';
}
return extra_data?.actor_name || '申请用户';
}, [requestType, extra_data]);
const applicantAvatar = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_avatar || '';
}
return extra_data?.avatar_url || '';
}, [requestType, extra_data]);
const applicantId = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_id;
}
return extra_data?.actor_id_str;
}, [requestType, extra_data]);
useEffect(() => {
const loadGroupInfo = async () => {
if (!extra_data?.group_id) return;
setLoadingGroup(true);
try {
const group = await groupManager.getGroup(extra_data.group_id);
setMemberCount(group.member_count ?? null);
} catch {
setMemberCount(null);
} finally {
setLoadingGroup(false);
}
};
loadGroupInfo();
}, [extra_data?.group_id]);
const handleDecision = async (approve: boolean) => {
const flag = extra_data?.flag;
if (!flag) {
Alert.alert('提示', '缺少请求标识,无法处理');
return;
}
setSubmitting(true);
try {
if (message.system_type === 'group_invite') {
await groupService.respondInvite({
flag,
approve,
});
} else {
await groupService.reviewJoinRequest({
flag,
approve,
});
}
Alert.alert('成功', approve ? '已同意申请' : '已拒绝申请', [
{ text: '确定', onPress: () => navigation.goBack() },
]);
} catch (error) {
Alert.alert('操作失败', '请稍后重试');
} finally {
setSubmitting(false);
}
};
const handleOpenUser = async () => {
if (!applicantId) return;
const user = await userManager.getUserById(applicantId);
if (user?.id) {
navigation.navigate('UserProfile', { userId: String(user.id) });
}
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<GroupInfoSummaryCard
groupName={extra_data?.group_name}
groupAvatar={extra_data?.group_avatar}
groupNo={extra_data?.group_id}
groupDescription={extra_data?.group_description}
memberCountText={loadingGroup ? '人数加载中...' : `${memberCount ?? '-'}`}
/>
<View style={styles.card}>
<Text variant="label" style={styles.sectionTitle}></Text>
<TouchableOpacity style={styles.row} activeOpacity={0.7} onPress={handleOpenUser} disabled={!applicantId}>
<Avatar source={applicantAvatar} size={44} name={applicantName} />
<View style={styles.meta}>
<Text variant="body" style={styles.name}>{applicantName}</Text>
<Text variant="caption" color={colors.text.secondary}>
{requestType === 'invite' ? '被邀请加入' : '申请加入'}
</Text>
</View>
{applicantId ? (
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
) : null}
</TouchableOpacity>
{requestType === 'invite' && (
<Text variant="caption" color={colors.text.secondary} style={styles.subDesc}>
{extra_data?.actor_name || '群成员'}
</Text>
)}
</View>
</ScrollView>
<DecisionFooter
canAction={canAction}
submitting={submitting}
onReject={() => handleDecision(false)}
onApprove={() => handleDecision(true)}
processedText={processedText}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
marginBottom: spacing.md,
...shadows.sm,
},
sectionTitle: {
marginBottom: spacing.sm,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
meta: {
marginLeft: spacing.md,
flex: 1,
},
name: {
marginBottom: 2,
},
subDesc: {
marginTop: spacing.sm,
},
});
export default GroupRequestDetailScreen;